diff --git a/src-tauri/src/proxy/forwarder.rs b/src-tauri/src/proxy/forwarder.rs index 13c95bdff..d24784f7f 100644 --- a/src-tauri/src/proxy/forwarder.rs +++ b/src-tauri/src/proxy/forwarder.rs @@ -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, diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index 33b2df292..ae8123d9e 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -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) { + 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!({ diff --git a/src-tauri/src/proxy/providers/mod.rs b/src-tauri/src/proxy/providers/mod.rs index 8b5720c50..e9dae2487 100644 --- a/src-tauri/src/proxy/providers/mod.rs +++ b/src-tauri/src/proxy/providers/mod.rs @@ -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::{ diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index fddffae82..0a58961b2 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -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\ diff --git a/src-tauri/src/session_manager/providers/openclaw.rs b/src-tauri/src/session_manager/providers/openclaw.rs index d0a4d92d5..3a1e58511 100644 --- a/src-tauri/src/session_manager/providers/openclaw.rs +++ b/src-tauri/src/session_manager/providers/openclaw.rs @@ -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");