mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
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:
1 parent
3217f72596
commit
d8065cc628
1 file changed
+40
-53
@@ -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!({
|
||||
|
||||
Reference in new issue
Block a user