Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions src-tauri/src/proxy/providers/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ fn normalize_openai_system_messages(messages: &mut Vec<Value>) {
}

let mut parts = Vec::new();
let mut inherited_cache_control: Option<Value> = None;
let mut cache_control_conflict = false;
messages.retain(|message| {
if message.get("role").and_then(|value| value.as_str()) != Some("system") {
return true;
Expand All @@ -226,11 +228,25 @@ fn normalize_openai_system_messages(messages: &mut Vec<Value>) {
_ => {}
}

if let Some(cache_control) = message.get("cache_control") {
match &inherited_cache_control {
None => inherited_cache_control = Some(cache_control.clone()),
Some(existing) if existing == cache_control => {}
Some(_) => cache_control_conflict = true,
Comment on lines +233 to +238
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat missing cache_control as merge conflict

When system fragments are merged, this branch only marks a conflict if two present cache_control values differ, but it ignores fragments where cache_control is absent. That means one cached fragment plus one uncached fragment will still produce a merged message with cache_control on the entire combined prompt. In mixed-prefix scenarios (static cached prefix + dynamic uncached suffix), this changes cache scope and can cache content that was intentionally left uncached; treating present/absent mixes as ambiguous and dropping cache_control would preserve original intent more safely.

Useful? React with 👍 / 👎.

}
}

false
});

if !parts.is_empty() {
messages.insert(0, json!({"role": "system", "content": parts.join("\n")}));
let mut merged = json!({"role": "system", "content": parts.join("\n")});
if !cache_control_conflict {
if let Some(cache_control) = inherited_cache_control {
merged["cache_control"] = cache_control;
}
}
messages.insert(0, merged);
}
}

Expand Down Expand Up @@ -618,7 +634,7 @@ mod tests {
"model": "claude-3-sonnet",
"max_tokens": 1024,
"system": [
{"type": "text", "text": "You are Claude Code."},
{"type": "text", "text": "You are Claude Code.", "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": "Be concise."}
],
"messages": [
Expand All @@ -634,9 +650,28 @@ mod tests {
result["messages"][0]["content"],
"You are Claude Code.\nBe concise.\nFollow repo conventions."
);
assert_eq!(result["messages"][0]["cache_control"]["type"], "ephemeral");
assert_eq!(result["messages"][1]["role"], "user");
}

#[test]
fn test_anthropic_to_openai_drops_conflicting_system_cache_control_when_merging() {
let input = json!({
"model": "claude-3-sonnet",
"max_tokens": 1024,
"system": [
{"type": "text", "text": "You are Claude Code.", "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": "Be concise.", "cache_control": {"type": "ephemeral", "ttl": "5m"}}
],
"messages": [{"role": "user", "content": "Hello"}]
});

let result = anthropic_to_openai(input, None).unwrap();
assert_eq!(result["messages"][0]["role"], "system");
assert_eq!(result["messages"][0]["content"], "You are Claude Code.\nBe concise.");
assert!(result["messages"][0].get("cache_control").is_none());
}

#[test]
fn test_anthropic_to_openai_tool_use() {
let input = json!({
Expand Down
Loading