fix(proxy): preserve mid-conversation system messages for prefix cache (#6941)

* fix(proxy): preserve mid-conversation system messages for prefix cache

Remove normalize_openai_system_messages which merged all system messages
to the head, breaking radix prefix cache on every turn when Claude Code
injects <total_tokens> metadata as a mid-conversation system message.

Now system messages from the top-level  field are placed first,
and all messages (including mid-conversation system) are extended in
their original order without any merging or reordering.

Also add custom-protocol feature for standalone release builds.

* fix(proxy): restore top-level system merge, add regression test

Restore merging of top-level system array blocks into a single system
message (cross-turn stable, does not affect prefix cache). Mid-conversation
system messages from the messages array remain in place without merging
or reordering.

Add regression test to lock the new behavior: mid-conversation system
messages stay at their original position.
This commit is contained in:
htyvista authored and GitHub committed 2026-08-29 22:55:03 +08:00
1 parent 3217f72596
commit d8065cc628
1 file changed
+40 -53
+40 -53
View File
@@ -158,15 +158,20 @@ pub fn anthropic_to_openai_with_reasoning_content(
messages.push(json!({"role": "system", "content": text}));
}
} else if let Some(arr) = system.as_array() {
// 顶层 system 数组合并为一条 system 消息(跨轮字节稳定,不影响前缀缓存)
let mut parts = Vec::new();
for msg in arr {
if let Some(text) = msg.get("text").and_then(|t| t.as_str()) {
let text = strip_leading_anthropic_billing_header(text);
if text.is_empty() {
continue;
}
messages.push(json!({"role": "system", "content": text}));
parts.push(text.to_string());
}
}
if !parts.is_empty() {
messages.push(json!({"role": "system", "content": parts.join("\n")}));
}
}
}
@@ -180,7 +185,6 @@ pub fn anthropic_to_openai_with_reasoning_content(
}
}
normalize_openai_system_messages(&mut messages);
result["messages"] = json!(messages);
// 转换参数 — o-series 模型需要 max_completion_tokens
@@ -305,57 +309,6 @@ fn map_tool_choice_to_chat(tool_choice: &Value) -> Value {
}
}
fn normalize_openai_system_messages(messages: &mut Vec<Value>) {
let system_count = messages
.iter()
.filter(|message| message.get("role").and_then(|value| value.as_str()) == Some("system"))
.count();
if system_count == 0 {
return;
}
if system_count == 1 {
if let Some(index) = messages.iter().position(|message| {
message.get("role").and_then(|value| value.as_str()) == Some("system")
}) {
if index > 0 {
let message = messages.remove(index);
messages.insert(0, message);
}
}
return;
}
let mut parts = Vec::new();
messages.retain(|message| {
if message.get("role").and_then(|value| value.as_str()) != Some("system") {
return true;
}
match message.get("content") {
Some(Value::String(text)) if !text.is_empty() => parts.push(text.clone()),
Some(Value::Array(content_parts)) => {
let text = content_parts
.iter()
.filter_map(|part| part.get("text").and_then(|value| value.as_str()))
.collect::<Vec<_>>()
.join("\n");
if !text.is_empty() {
parts.push(text);
}
}
_ => {}
}
false
});
if !parts.is_empty() {
messages.insert(0, json!({"role": "system", "content": parts.join("\n")}));
}
}
/// 转换单条消息到 OpenAI 格式(可能产生多条消息)
fn convert_message_to_openai(
role: &str,
@@ -988,6 +941,40 @@ mod tests {
assert!(result["messages"][0].get("cache_control").is_none());
}
#[test]
fn test_anthropic_to_openai_preserves_mid_conversation_system_in_place() {
// Claude Code 会在对话中间注入 system 消息(如 <total_tokens>),
// 必须保持原位,不合并不上提,否则破坏前缀缓存。
let input = json!({
"model": "claude-3-sonnet",
"max_tokens": 1024,
"system": "You are Claude Code.",
"messages": [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"},
{"role": "system", "content": "<total_tokens>14963538 tokens left</total_tokens>"},
{"role": "user", "content": "Continue"}
]
});
let result = anthropic_to_openai(input).unwrap();
let messages = result["messages"].as_array().unwrap();
// 顶层 system 在最前面
assert_eq!(messages[0]["role"], "system");
assert_eq!(messages[0]["content"], "You are Claude Code.");
// 中途 system 保持原位(第 3 条,index=3),不被合并或上提
assert_eq!(messages[3]["role"], "system");
assert_eq!(
messages[3]["content"],
"<total_tokens>14963538 tokens left</total_tokens>"
);
// 总共 5 条消息,没有合并
assert_eq!(messages.len(), 5);
}
#[test]
fn test_anthropic_to_openai_strips_cache_control_from_conflicting_system() {
let input = json!({