diff --git a/src-tauri/src/proxy/model_mapper.rs b/src-tauri/src/proxy/model_mapper.rs index debe155ab..dbbd81987 100644 --- a/src-tauri/src/proxy/model_mapper.rs +++ b/src-tauri/src/proxy/model_mapper.rs @@ -54,6 +54,7 @@ impl ModelMapping { || self.sonnet_model.is_some() || self.opus_model.is_some() || self.default_model.is_some() + || self.reasoning_model.is_some() } /// 根据原始模型名称获取映射后的模型 @@ -182,6 +183,27 @@ mod tests { } } + fn create_provider_with_reasoning_only() -> Provider { + Provider { + id: "test".to_string(), + name: "Test".to_string(), + settings_config: json!({ + "env": { + "ANTHROPIC_REASONING_MODEL": "reasoning-only-model" + } + }), + website_url: None, + category: None, + created_at: None, + sort_index: None, + notes: None, + meta: None, + icon: None, + icon_color: None, + in_failover_queue: false, + } + } + #[test] fn test_sonnet_mapping() { let provider = create_provider_with_mapping(); @@ -222,6 +244,31 @@ mod tests { assert_eq!(mapped, Some("reasoning-model".to_string())); } + #[test] + fn test_reasoning_only_mapping_in_thinking_mode() { + let provider = create_provider_with_reasoning_only(); + let body = json!({ + "model": "claude-sonnet-4-5", + "thinking": {"type": "enabled"} + }); + let (result, _, mapped) = apply_model_mapping(body, &provider); + assert_eq!(result["model"], "reasoning-only-model"); + assert_eq!(mapped, Some("reasoning-only-model".to_string())); + } + + #[test] + fn test_reasoning_only_mapping_does_not_affect_non_thinking() { + let provider = create_provider_with_reasoning_only(); + let body = json!({ + "model": "claude-sonnet-4-5", + "thinking": {"type": "disabled"} + }); + let (result, original, mapped) = apply_model_mapping(body, &provider); + assert_eq!(result["model"], "claude-sonnet-4-5"); + assert_eq!(original, Some("claude-sonnet-4-5".to_string())); + assert!(mapped.is_none()); + } + #[test] fn test_thinking_disabled() { let provider = create_provider_with_mapping(); diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index 04fb4a3e9..c80dfab9f 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -292,6 +292,14 @@ impl ProviderService { ) .map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?; + // 关键修复:接管模式下切换供应商不会写回 Live 配置, + // 需要主动清理 Claude Live 中的“模型覆盖”字段,避免仍以旧模型名发起请求。 + if matches!(app_type, AppType::Claude) { + if let Err(e) = state.proxy_service.cleanup_claude_model_overrides_in_live() { + log::warn!("清理 Claude Live 模型字段失败(不影响切换结果): {e}"); + } + } + // Note: No Live config write, no MCP sync // The proxy server will route requests to the new provider via is_current return Ok(()); diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 4865f7ede..71b470e0e 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -17,6 +17,20 @@ use tokio::sync::RwLock; /// 用于接管 Live 配置时的占位符(避免客户端提示缺少 key,同时不泄露真实 Token) const PROXY_TOKEN_PLACEHOLDER: &str = "PROXY_MANAGED"; +/// 代理接管模式下需要从 Claude Live 配置中移除的“模型覆盖”字段。 +/// +/// 原因:接管模式切换供应商时不会写回 Live 配置,如果保留这些字段, +/// Claude Code 会继续以旧模型名发起请求,导致新供应商不支持时失败。 +const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 6] = [ + "ANTHROPIC_MODEL", + "ANTHROPIC_REASONING_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + // Legacy key (已废弃):历史版本使用该字段区分 small/fast 模型 + "ANTHROPIC_SMALL_FAST_MODEL", +]; + #[derive(Clone)] pub struct ProxyService { db: Arc, @@ -34,6 +48,31 @@ impl ProxyService { } } + /// 清理接管模式下 Claude Live 配置中的模型覆盖字段。 + /// + /// 这可以避免“接管开启后切换供应商仍使用旧模型”的问题。 + /// 注意:此方法不会修改 Token/Base URL 的接管占位符,仅移除模型字段。 + pub fn cleanup_claude_model_overrides_in_live(&self) -> Result<(), String> { + let mut config = self.read_claude_live()?; + + let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) else { + return Ok(()); + }; + + let mut changed = false; + for key in CLAUDE_MODEL_OVERRIDE_ENV_KEYS { + if env.remove(key).is_some() { + changed = true; + } + } + + if changed { + self.write_claude_live(&config)?; + } + + Ok(()) + } + /// 设置 AppHandle(在应用初始化时调用) pub fn set_app_handle(&self, handle: tauri::AppHandle) { futures::executor::block_on(async { @@ -740,6 +779,10 @@ impl ProxyService { if let Ok(mut live_config) = self.read_claude_live() { if let Some(env) = live_config.get_mut("env").and_then(|v| v.as_object_mut()) { env.insert("ANTHROPIC_BASE_URL".to_string(), json!(&proxy_url)); + // 关键:接管模式下移除模型覆盖字段,避免切换供应商后仍用旧模型名发起请求 + for key in CLAUDE_MODEL_OVERRIDE_ENV_KEYS { + env.remove(key); + } // 仅覆盖已存在的 Token 字段,避免新增字段导致用户困惑; // 若完全没有 Token 字段,则写入 ANTHROPIC_AUTH_TOKEN 占位符用于避免客户端警告。 let token_keys = [ @@ -820,6 +863,10 @@ impl ProxyService { let mut live_config = self.read_claude_live()?; if let Some(env) = live_config.get_mut("env").and_then(|v| v.as_object_mut()) { env.insert("ANTHROPIC_BASE_URL".to_string(), json!(&proxy_url)); + // 关键:接管模式下移除模型覆盖字段,避免切换供应商后仍用旧模型名发起请求 + for key in CLAUDE_MODEL_OVERRIDE_ENV_KEYS { + env.remove(key); + } let token_keys = [ "ANTHROPIC_AUTH_TOKEN", @@ -899,6 +946,10 @@ impl ProxyService { if let Ok(mut live_config) = self.read_claude_live() { if let Some(env) = live_config.get_mut("env").and_then(|v| v.as_object_mut()) { env.insert("ANTHROPIC_BASE_URL".to_string(), json!(&proxy_url)); + // 关键:接管模式下移除模型覆盖字段,避免切换供应商后仍用旧模型名发起请求 + for key in CLAUDE_MODEL_OVERRIDE_ENV_KEYS { + env.remove(key); + } let token_keys = [ "ANTHROPIC_AUTH_TOKEN",