mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat: add Claude subagent model config (#4830)
* feat: add Claude subagent takeover config * feat: add Claude subagent model field * i18n: add Claude subagent model labels * fix(proxy): preserve configured subagent model mapping * fix(providers): exclude subagent model from Claude common config * style: format rust code
This commit is contained in:
@@ -12,6 +12,7 @@ pub struct ModelMapping {
|
||||
pub sonnet_model: Option<String>,
|
||||
pub opus_model: Option<String>,
|
||||
pub fable_model: Option<String>,
|
||||
pub subagent_model: Option<String>,
|
||||
pub default_model: Option<String>,
|
||||
}
|
||||
|
||||
@@ -41,6 +42,11 @@ impl ModelMapping {
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
subagent_model: env
|
||||
.and_then(|e| e.get("CLAUDE_CODE_SUBAGENT_MODEL"))
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
default_model: env
|
||||
.and_then(|e| e.get("ANTHROPIC_MODEL"))
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -55,6 +61,7 @@ impl ModelMapping {
|
||||
|| self.sonnet_model.is_some()
|
||||
|| self.opus_model.is_some()
|
||||
|| self.fable_model.is_some()
|
||||
|| self.subagent_model.is_some()
|
||||
|| self.default_model.is_some()
|
||||
}
|
||||
|
||||
@@ -89,6 +96,13 @@ impl ModelMapping {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref m) = self.subagent_model {
|
||||
if strip_one_m_suffix_for_upstream(original_model) == strip_one_m_suffix_for_upstream(m)
|
||||
{
|
||||
return original_model.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 默认模型
|
||||
if let Some(ref m) = self.default_model {
|
||||
return m.clone();
|
||||
@@ -327,6 +341,42 @@ mod tests {
|
||||
assert_eq!(mapped, Some("default-model".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subagent_model_preserved_before_default_fallback() {
|
||||
let mut provider = create_provider_with_mapping();
|
||||
provider.settings_config = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_MODEL": "default-model",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "gpt-5.4-mini"
|
||||
}
|
||||
});
|
||||
|
||||
let body = json!({"model": "gpt-5.4-mini"});
|
||||
let (result, original, mapped) = apply_model_mapping(body, &provider);
|
||||
|
||||
assert_eq!(result["model"], "gpt-5.4-mini");
|
||||
assert_eq!(original, Some("gpt-5.4-mini".to_string()));
|
||||
assert!(mapped.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subagent_model_preserved_with_one_m_suffix_before_default_fallback() {
|
||||
let mut provider = create_provider_with_mapping();
|
||||
provider.settings_config = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_MODEL": "default-model",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "gpt-5.4-mini"
|
||||
}
|
||||
});
|
||||
|
||||
let body = json!({"model": "gpt-5.4-mini[1M]"});
|
||||
let (result, original, mapped) = apply_model_mapping(body, &provider);
|
||||
|
||||
assert_eq!(result["model"], "gpt-5.4-mini[1M]");
|
||||
assert_eq!(original, Some("gpt-5.4-mini[1M]".to_string()));
|
||||
assert!(mapped.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_mapping_configured() {
|
||||
let provider = create_provider_without_mapping();
|
||||
|
||||
@@ -672,6 +672,7 @@ mod tests {
|
||||
"AWS_BEARER_TOKEN_BEDROCK": "bedrock-tok",
|
||||
"ANTHROPIC_BASE_URL": "https://example.com",
|
||||
"ANTHROPIC_MODEL": "claude-x",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "gpt-5.4-mini",
|
||||
// 可共享、非机密配置(复数 _TOKENS 不应被误剥)
|
||||
"ENABLE_TOOL_SEARCH": "true",
|
||||
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "8192"
|
||||
@@ -714,6 +715,9 @@ mod tests {
|
||||
// 端点/模型(provider-specific 非机密)也应剥掉
|
||||
assert!(env.and_then(|e| e.get("ANTHROPIC_BASE_URL")).is_none());
|
||||
assert!(env.and_then(|e| e.get("ANTHROPIC_MODEL")).is_none());
|
||||
assert!(env
|
||||
.and_then(|e| e.get("CLAUDE_CODE_SUBAGENT_MODEL"))
|
||||
.is_none());
|
||||
|
||||
// 可共享的非机密配置必须保留(含复数 _TOKENS 不被误剥)
|
||||
assert_eq!(
|
||||
@@ -2635,6 +2639,7 @@ impl ProviderService {
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL",
|
||||
"ANTHROPIC_BASE_URL",
|
||||
];
|
||||
|
||||
|
||||
@@ -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; 9] = [
|
||||
const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 10] = [
|
||||
"ANTHROPIC_MODEL",
|
||||
"ANTHROPIC_REASONING_MODEL", // legacy: 已废弃,但旧配置可能残留
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
||||
@@ -35,8 +35,8 @@ const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 9] = [
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
||||
// Legacy key (已废弃):历史版本使用该字段区分 small/fast 模型
|
||||
"ANTHROPIC_SMALL_FAST_MODEL",
|
||||
"ANTHROPIC_SMALL_FAST_MODEL", // Legacy key (已废弃):历史版本使用该字段区分 small/fast 模型
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL",
|
||||
];
|
||||
|
||||
const CLAUDE_TAKEOVER_HAIKU_MODEL: &str = "claude-haiku-4-5";
|
||||
@@ -222,7 +222,9 @@ impl ProxyService {
|
||||
.or(default_model)
|
||||
.or(small_fast_model);
|
||||
|
||||
let mut fields = Vec::with_capacity(6);
|
||||
let subagent_model = Self::claude_env_string(env, "CLAUDE_CODE_SUBAGENT_MODEL");
|
||||
|
||||
let mut fields = Vec::with_capacity(7);
|
||||
Self::push_claude_takeover_role_fields(
|
||||
&mut fields,
|
||||
env,
|
||||
@@ -250,6 +252,9 @@ impl ProxyService {
|
||||
true,
|
||||
opus_model,
|
||||
);
|
||||
if let Some(subagent_model) = subagent_model {
|
||||
fields.push(("CLAUDE_CODE_SUBAGENT_MODEL", subagent_model.to_string()));
|
||||
}
|
||||
fields
|
||||
}
|
||||
|
||||
@@ -2857,7 +2862,8 @@ mod tests {
|
||||
"ANTHROPIC_MODEL": "claude-sonnet-4.6",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4.5",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4.6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-sonnet-4.6"
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-sonnet-4.6",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4.6[1M]"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
@@ -2877,7 +2883,8 @@ mod tests {
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "stale-sonnet",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Stale Sonnet",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "stale-opus",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "Stale Opus"
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "Stale Opus",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "stale-subagent"
|
||||
}
|
||||
});
|
||||
ProxyService::apply_claude_takeover_fields_for_provider(
|
||||
@@ -2917,10 +2924,53 @@ mod tests {
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
||||
Some("claude-sonnet-4.6"),
|
||||
);
|
||||
assert_env_str(
|
||||
env,
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL",
|
||||
Some("claude-sonnet-4.6[1M]"),
|
||||
);
|
||||
assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
|
||||
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_account_claude_takeover_removes_stale_subagent_model_when_provider_omits_it() {
|
||||
let mut provider = Provider::with_id(
|
||||
"codex".to_string(),
|
||||
"Codex".to_string(),
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://chatgpt.com/backend-api/codex",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "provider-sonnet"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
);
|
||||
provider.meta = Some(ProviderMeta {
|
||||
provider_type: Some("codex_oauth".to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut live_config = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://stale.example.com",
|
||||
"ANTHROPIC_API_KEY": "stale-key",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "stale-subagent"
|
||||
}
|
||||
});
|
||||
ProxyService::apply_claude_takeover_fields_for_provider(
|
||||
&mut live_config,
|
||||
"http://127.0.0.1:15721",
|
||||
&provider,
|
||||
);
|
||||
|
||||
let env = live_config
|
||||
.get("env")
|
||||
.and_then(|value| value.as_object())
|
||||
.expect("env should exist");
|
||||
assert_env_str(env, "CLAUDE_CODE_SUBAGENT_MODEL", None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_account_claude_takeover_sources_codex_models_from_provider() {
|
||||
let mut provider = Provider::with_id(
|
||||
@@ -4564,7 +4614,8 @@ model = "gpt-5.1-codex"
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME": "DeepSeek V4 Flash",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-pro[1M]",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "DeepSeek V4 Pro",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-ultra [1m]"
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-ultra [1m]",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "deepseek-v4-pro[1M]"
|
||||
},
|
||||
"permissions": { "allow": ["Read"] }
|
||||
}),
|
||||
@@ -4591,7 +4642,8 @@ model = "gpt-5.1-codex"
|
||||
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721",
|
||||
"ANTHROPIC_API_KEY": PROXY_TOKEN_PLACEHOLDER,
|
||||
"ANTHROPIC_MODEL": "stale-model",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Stale Sonnet"
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Stale Sonnet",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "stale-subagent"
|
||||
},
|
||||
"permissions": { "allow": ["Bash"] }
|
||||
}))
|
||||
@@ -4674,6 +4726,13 @@ model = "gpt-5.1-codex"
|
||||
Some("deepseek-v4-ultra"),
|
||||
"implicit display names should strip the local 1M marker"
|
||||
);
|
||||
assert_eq!(
|
||||
live_env
|
||||
.get("CLAUDE_CODE_SUBAGENT_MODEL")
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("deepseek-v4-pro[1M]"),
|
||||
"subagent model should follow the target provider during hot switch"
|
||||
);
|
||||
|
||||
let backup = db
|
||||
.get_live_backup("claude")
|
||||
|
||||
Reference in New Issue
Block a user