From 50270d5e42c7f7c0e816ad8f33103bd4e12b72dd Mon Sep 17 00:00:00 2001 From: Dawn Date: Sat, 11 Jul 2026 10:11:57 +0800 Subject: [PATCH] fix: exclude Fable model env from Claude common config (#4272) (#5206) * Add test for Fable model env key exclusion Add regression test to ensure Fable model env keys are excluded from common config. * Add Fable model to CLAUDE_MODEL_OVERRIDE_ENV_KEYS * Add files via upload * Delete .github/workflows/build-windows-unsigned.yml --- src-tauri/src/services/provider/mod.rs | 54 ++++++++++++++++++++++++++ src-tauri/src/services/proxy.rs | 18 ++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index f95d463a9..507686655 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -775,6 +775,56 @@ mod tests { assert_eq!(value.get("includeCoAuthoredBy"), Some(&json!(false))); } + /// Regression for issue #4272: Fable tier env keys must not enter the shared + /// Claude common-config snippet (same class as haiku/sonnet/opus model pins). + #[test] + fn extract_claude_common_config_strips_fable_model_env_keys() { + let settings = json!({ + "env": { + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "haiku-mapped", + "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME": "Haiku Mapped", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "sonnet-mapped[1M]", + "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Sonnet Mapped", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "opus-mapped[1M]", + "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "Opus Mapped", + "ANTHROPIC_DEFAULT_FABLE_MODEL": "deepseek-v4-flash[1M]", + "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME": "deepseek-v4-flash", + "ANTHROPIC_MODEL": "default-mapped", + "ENABLE_TOOL_SEARCH": "true" + }, + "theme": "dark" + }); + + let snippet = ProviderService::extract_claude_common_config(&settings) + .expect("extract should succeed"); + let value: Value = serde_json::from_str(&snippet).expect("snippet is valid JSON"); + let env = value.get("env"); + + for stripped in [ + "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", + "ANTHROPIC_DEFAULT_FABLE_MODEL", + "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME", + "ANTHROPIC_MODEL", + ] { + assert!( + env.and_then(|e| e.get(stripped)).is_none(), + "provider-specific model key {stripped} must not enter common config" + ); + } + + assert_eq!( + env.and_then(|e| e.get("ENABLE_TOOL_SEARCH")) + .and_then(|v| v.as_str()), + Some("true") + ); + assert_eq!(value.get("theme").and_then(|v| v.as_str()), Some("dark")); + } + #[test] fn validate_provider_settings_rejects_negative_cost_multiplier() { let mut provider = Provider::with_id( @@ -2869,6 +2919,10 @@ impl ProviderService { "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME", + // Fable 是 v3.16.3 新增的第四档模型映射,与 haiku/sonnet/opus 同属供应商专属, + // 不得进入通用配置片段,否则会污染其它供应商(issue #4272)。 + "ANTHROPIC_DEFAULT_FABLE_MODEL", + "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME", "CLAUDE_CODE_SUBAGENT_MODEL", "ANTHROPIC_BASE_URL", ]; diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 72c1e8228..53276d276 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -26,7 +26,7 @@ const PROXY_TOKEN_PLACEHOLDER: &str = "PROXY_MANAGED"; /// 原因:接管模式下 `*_MODEL` 必须由 CC Switch 写成稳定的 Claude 角色别名, /// 再由本地代理映射到当前供应商真实模型;`*_MODEL_NAME` 也需要同步接管, /// 否则 Claude Code 模型菜单会残留上一个供应商的显示名称。 -const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 10] = [ +const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 12] = [ "ANTHROPIC_MODEL", "ANTHROPIC_REASONING_MODEL", // legacy: 已废弃,但旧配置可能残留 "ANTHROPIC_DEFAULT_HAIKU_MODEL", @@ -35,6 +35,8 @@ const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 10] = [ "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", + "ANTHROPIC_DEFAULT_FABLE_MODEL", + "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME", "ANTHROPIC_SMALL_FAST_MODEL", // Legacy key (已废弃):历史版本使用该字段区分 small/fast 模型 "CLAUDE_CODE_SUBAGENT_MODEL", ]; @@ -42,6 +44,7 @@ const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 10] = [ const CLAUDE_TAKEOVER_HAIKU_MODEL: &str = "claude-haiku-4-5"; const CLAUDE_TAKEOVER_SONNET_MODEL: &str = "claude-sonnet-4-6"; const CLAUDE_TAKEOVER_OPUS_MODEL: &str = "claude-opus-4-8"; +const CLAUDE_TAKEOVER_FABLE_MODEL: &str = "claude-fable-5"; // 写给 Claude Code 时沿用文档示例的大写形式;解析侧大小写不敏感。 const CLAUDE_ONE_M_MARKER_FOR_CLIENT: &str = "[1M]"; @@ -221,10 +224,12 @@ impl ProxyService { let opus_model = Self::claude_env_string(env, "ANTHROPIC_DEFAULT_OPUS_MODEL") .or(default_model) .or(small_fast_model); + // Fable 未配置时不写稳定别名;映射侧会 fable→opus 降级(与官方一致)。 + let fable_model = Self::claude_env_string(env, "ANTHROPIC_DEFAULT_FABLE_MODEL"); let subagent_model = Self::claude_env_string(env, "CLAUDE_CODE_SUBAGENT_MODEL"); - let mut fields = Vec::with_capacity(7); + let mut fields = Vec::with_capacity(9); Self::push_claude_takeover_role_fields( &mut fields, env, @@ -252,6 +257,15 @@ impl ProxyService { true, opus_model, ); + Self::push_claude_takeover_role_fields( + &mut fields, + env, + "ANTHROPIC_DEFAULT_FABLE_MODEL", + "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME", + CLAUDE_TAKEOVER_FABLE_MODEL, + true, + fable_model, + ); if let Some(subagent_model) = subagent_model { fields.push(("CLAUDE_CODE_SUBAGENT_MODEL", subagent_model.to_string())); }