mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix(proxy): 规范化 Anthropic system 消息 (#3775)
This commit is contained in:
@@ -1272,7 +1272,7 @@ impl RequestForwarder {
|
||||
};
|
||||
if adapter.name() == "Claude" {
|
||||
if let Some(api_format) = resolved_claude_api_format.as_deref() {
|
||||
super::providers::normalize_anthropic_tool_thinking_history_for_provider(
|
||||
super::providers::normalize_anthropic_messages_for_provider(
|
||||
&mut mapped_body,
|
||||
provider,
|
||||
api_format,
|
||||
|
||||
@@ -145,6 +145,86 @@ pub fn normalize_anthropic_tool_thinking_history_for_provider(
|
||||
normalize_anthropic_tool_thinking_history(body)
|
||||
}
|
||||
|
||||
pub fn normalize_anthropic_messages_for_provider(
|
||||
body: &mut Value,
|
||||
provider: &Provider,
|
||||
api_format: &str,
|
||||
) -> bool {
|
||||
if api_format.trim() != "anthropic" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut changed = normalize_anthropic_system_role_messages(body);
|
||||
changed |= normalize_anthropic_tool_thinking_history_for_provider(body, provider, api_format);
|
||||
changed
|
||||
}
|
||||
|
||||
fn normalize_anthropic_system_role_messages(body: &mut Value) -> bool {
|
||||
let mut system_parts = Vec::new();
|
||||
let changed = {
|
||||
let Some(messages) = body.get_mut("messages").and_then(Value::as_array_mut) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let original_len = messages.len();
|
||||
let mut kept_messages = Vec::with_capacity(messages.len());
|
||||
for message in std::mem::take(messages) {
|
||||
if message.get("role").and_then(Value::as_str) == Some("system") {
|
||||
if let Some(content) = message.get("content") {
|
||||
append_anthropic_system_parts(content, &mut system_parts);
|
||||
}
|
||||
} else {
|
||||
kept_messages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
let changed = kept_messages.len() != original_len;
|
||||
*messages = kept_messages;
|
||||
changed
|
||||
};
|
||||
|
||||
if !changed || system_parts.is_empty() {
|
||||
return changed;
|
||||
}
|
||||
|
||||
let mut merged_parts = Vec::new();
|
||||
if let Some(existing) = body.get("system") {
|
||||
append_anthropic_system_parts(existing, &mut merged_parts);
|
||||
}
|
||||
merged_parts.extend(system_parts);
|
||||
|
||||
if !merged_parts.is_empty() {
|
||||
body["system"] = Value::Array(merged_parts);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn append_anthropic_system_parts(content: &Value, parts: &mut Vec<Value>) {
|
||||
match content {
|
||||
Value::String(text) if !text.trim().is_empty() => {
|
||||
parts.push(json!({
|
||||
"type": "text",
|
||||
"text": text
|
||||
}));
|
||||
}
|
||||
Value::Array(items) => {
|
||||
for item in items {
|
||||
append_anthropic_system_parts(item, parts);
|
||||
}
|
||||
}
|
||||
Value::Object(obj)
|
||||
if obj
|
||||
.get("text")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|text| !text.trim().is_empty()) =>
|
||||
{
|
||||
parts.push(Value::Object(obj.clone()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_anthropic_tool_thinking_history(body: &mut Value) -> bool {
|
||||
let Some(messages) = body.get_mut("messages").and_then(Value::as_array_mut) else {
|
||||
return false;
|
||||
@@ -1995,6 +2075,64 @@ mod tests {
|
||||
assert_eq!(content[2]["type"], "tool_use");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_system_role_messages_move_to_top_level_system() {
|
||||
let provider = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic",
|
||||
"ANTHROPIC_API_KEY": "test-key"
|
||||
}
|
||||
}));
|
||||
let mut body = json!({
|
||||
"system": "Existing top-level system.",
|
||||
"model": "deepseek-v4-pro",
|
||||
"messages": [
|
||||
{ "role": "system", "content": "Message system one." },
|
||||
{ "role": "user", "content": "hello" },
|
||||
{
|
||||
"role": "system",
|
||||
"content": [{ "type": "text", "text": "Message system two." }]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let changed = normalize_anthropic_messages_for_provider(&mut body, &provider, "anthropic");
|
||||
|
||||
assert!(changed);
|
||||
let messages = body["messages"].as_array().unwrap();
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0]["role"], "user");
|
||||
|
||||
let system = body["system"].as_array().unwrap();
|
||||
assert_eq!(system[0]["text"], "Existing top-level system.");
|
||||
assert_eq!(system[1]["text"], "Message system one.");
|
||||
assert_eq!(system[2]["text"], "Message system two.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_system_role_messages_skip_non_anthropic_format() {
|
||||
let provider = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.deepseek.com/v1",
|
||||
"ANTHROPIC_API_KEY": "test-key"
|
||||
}
|
||||
}));
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"messages": [
|
||||
{ "role": "system", "content": "Keep in messages." },
|
||||
{ "role": "user", "content": "hello" }
|
||||
]
|
||||
});
|
||||
|
||||
let changed =
|
||||
normalize_anthropic_messages_for_provider(&mut body, &provider, "openai_chat");
|
||||
|
||||
assert!(!changed);
|
||||
assert!(body.get("system").is_none());
|
||||
assert_eq!(body["messages"][0]["role"], "system");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kimi_anthropic_tool_history_injects_missing_thinking() {
|
||||
let provider = create_provider(json!({
|
||||
|
||||
@@ -42,8 +42,8 @@ pub use adapter::ProviderAdapter;
|
||||
pub use auth::{AuthInfo, AuthStrategy};
|
||||
pub use claude::{
|
||||
claude_api_format_needs_transform, get_claude_api_format,
|
||||
normalize_anthropic_tool_thinking_history_for_provider,
|
||||
transform_claude_request_for_api_format, ClaudeAdapter,
|
||||
normalize_anthropic_messages_for_provider, transform_claude_request_for_api_format,
|
||||
ClaudeAdapter,
|
||||
};
|
||||
pub use codex::CodexAdapter;
|
||||
pub use codex::{
|
||||
|
||||
@@ -5411,7 +5411,7 @@ requires_openai_auth = true
|
||||
)
|
||||
.expect("seed generated catalog file");
|
||||
|
||||
let pointer = catalog_path.to_string_lossy().to_string();
|
||||
let pointer = catalog_path.to_string_lossy().replace('\\', "/");
|
||||
let backup_config = format!(
|
||||
"model_provider = \"custom\"\n\
|
||||
model = \"deepseek-v4-flash\"\n\
|
||||
|
||||
@@ -446,20 +446,17 @@ mod tests {
|
||||
.expect("write session");
|
||||
std::fs::write(
|
||||
sessions_dir.join("sessions.json"),
|
||||
format!(
|
||||
r#"{{
|
||||
"agent:main:main": {{
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"agent:main:main": {
|
||||
"sessionId": "session-123",
|
||||
"sessionFile": "{}"
|
||||
}},
|
||||
"agent:main:other": {{
|
||||
"sessionFile": session_path.to_string_lossy(),
|
||||
},
|
||||
"agent:main:other": {
|
||||
"sessionId": "session-456",
|
||||
"sessionFile": "{}/session-456.jsonl"
|
||||
}}
|
||||
}}"#,
|
||||
session_path.display(),
|
||||
sessions_dir.display()
|
||||
),
|
||||
"sessionFile": sessions_dir.join("session-456.jsonl").to_string_lossy(),
|
||||
},
|
||||
}))
|
||||
.expect("serialize index"),
|
||||
)
|
||||
.expect("write index");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user