diff --git a/src-tauri/src/agents/commands.rs b/src-tauri/src/agents/commands.rs index eb472ef..2eb0345 100644 --- a/src-tauri/src/agents/commands.rs +++ b/src-tauri/src/agents/commands.rs @@ -1040,6 +1040,8 @@ pub(crate) fn resolve_claude_desktop_model_mappings( max_context_tokens: requested.max_context_tokens, auto_compact_pct: requested.auto_compact_pct, disable_auto_compact: requested.disable_auto_compact, + manage_default_model: requested.manage_default_model, + manage_subagent_model: requested.manage_subagent_model, })) } @@ -1077,6 +1079,8 @@ pub(crate) fn resolve_claude_code_model_mappings( max_context_tokens, auto_compact_pct: requested.auto_compact_pct, disable_auto_compact: requested.disable_auto_compact, + manage_default_model: requested.manage_default_model, + manage_subagent_model: requested.manage_subagent_model, })) } diff --git a/src-tauri/src/agents/configuration.rs b/src-tauri/src/agents/configuration.rs index ff235f9..c17dcba 100644 --- a/src-tauri/src/agents/configuration.rs +++ b/src-tauri/src/agents/configuration.rs @@ -578,7 +578,6 @@ pub(crate) fn build_claude_agent_config( for (key, value) in [ ("ANTHROPIC_BASE_URL", base_url), ("ANTHROPIC_AUTH_TOKEN", api_key), - ("ANTHROPIC_MODEL", model_settings.sonnet.as_str()), ( "ANTHROPIC_DEFAULT_HAIKU_MODEL", model_settings.haiku.as_str(), @@ -598,8 +597,36 @@ pub(crate) fn build_claude_agent_config( serde_json::Value::String(value.to_string()), ); } - env.entry("CLAUDE_CODE_SUBAGENT_MODEL".to_string()) - .or_insert_with(|| serde_json::Value::String(subagent_model)); + env.insert( + "EASYCLIPROXY_MANAGE_CLAUDE_CODE_DEFAULT_MODEL".to_string(), + serde_json::Value::String(if mappings.manage_default_model { "1" } else { "0" }.to_string()), + ); + env.insert( + "EASYCLIPROXY_MANAGE_CLAUDE_CODE_SUBAGENT_MODEL".to_string(), + serde_json::Value::String(if mappings.manage_subagent_model { "1" } else { "0" }.to_string()), + ); + if mappings.manage_default_model { + env.insert( + "ANTHROPIC_MODEL".to_string(), + serde_json::Value::String(model_settings.sonnet.clone()), + ); + } else if env + .get("ANTHROPIC_MODEL") + .and_then(serde_json::Value::as_str) + .is_some_and(|value| value == model_settings.opus || value == model_settings.sonnet || value == model_settings.haiku) + { + env.remove("ANTHROPIC_MODEL"); + } + if mappings.manage_subagent_model { + env.entry("CLAUDE_CODE_SUBAGENT_MODEL".to_string()) + .or_insert_with(|| serde_json::Value::String(subagent_model)); + } else if env + .get("CLAUDE_CODE_SUBAGENT_MODEL") + .and_then(serde_json::Value::as_str) + .is_some_and(|value| value == model_settings.opus || value == model_settings.sonnet || value == model_settings.haiku) + { + env.remove("CLAUDE_CODE_SUBAGENT_MODEL"); + } env.insert( CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(), serde_json::Value::String(max_context_tokens.to_string()), @@ -631,10 +658,18 @@ pub(crate) fn build_claude_agent_config( serde_json::Value::String(effort_level), ); } - root.insert( - "model".to_string(), - serde_json::Value::String(model_settings.sonnet), - ); + if mappings.manage_default_model { + root.insert( + "model".to_string(), + serde_json::Value::String(model_settings.sonnet), + ); + } else if root + .get("model") + .and_then(serde_json::Value::as_str) + .is_some_and(|value| value == model_settings.opus || value == model_settings.sonnet || value == model_settings.haiku) + { + root.remove("model"); + } let mut rendered = serde_json::to_string_pretty(&serde_json::Value::Object(root.clone())) .map_err(|error| format!("Failed to generate Claude Code configuration: {error}"))?; rendered.push('\n'); @@ -1144,6 +1179,8 @@ pub(crate) fn prepare_claude_code_managed_removal( "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_FABLE_MODEL", + "EASYCLIPROXY_MANAGE_CLAUDE_CODE_DEFAULT_MODEL", + "EASYCLIPROXY_MANAGE_CLAUDE_CODE_SUBAGENT_MODEL", CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV, CLAUDE_CODE_AUTO_MODE_SERVER_ENV, @@ -1774,6 +1811,8 @@ pub(crate) fn build_restored_claude_code_config( "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_FABLE_MODEL", + "EASYCLIPROXY_MANAGE_CLAUDE_CODE_DEFAULT_MODEL", + "EASYCLIPROXY_MANAGE_CLAUDE_CODE_SUBAGENT_MODEL", CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV, CLAUDE_CODE_AUTO_MODE_SERVER_ENV, diff --git a/src-tauri/src/agents/discovery.rs b/src-tauri/src/agents/discovery.rs index 5c21d30..0b3e117 100644 --- a/src-tauri/src/agents/discovery.rs +++ b/src-tauri/src/agents/discovery.rs @@ -2963,6 +2963,16 @@ pub(crate) fn inspect_claude_code_model_mappings( max_context_tokens, auto_compact_pct, disable_auto_compact, + manage_default_model: env + .get("EASYCLIPROXY_MANAGE_CLAUDE_CODE_DEFAULT_MODEL") + .and_then(serde_json::Value::as_str) + .map(|value| value != "0" && !value.eq_ignore_ascii_case("false")) + .unwrap_or(true), + manage_subagent_model: env + .get("EASYCLIPROXY_MANAGE_CLAUDE_CODE_SUBAGENT_MODEL") + .and_then(serde_json::Value::as_str) + .map(|value| value != "0" && !value.eq_ignore_ascii_case("false")) + .unwrap_or(true), })) } @@ -3089,6 +3099,8 @@ pub(crate) fn claude_code_model_settings( max_context_tokens: mappings.max_context_tokens, auto_compact_pct: mappings.auto_compact_pct, disable_auto_compact: mappings.disable_auto_compact, + manage_default_model: mappings.manage_default_model, + manage_subagent_model: mappings.manage_subagent_model, } } diff --git a/src-tauri/src/agents/transactions.rs b/src-tauri/src/agents/transactions.rs index c85b2c9..a3d18a5 100644 --- a/src-tauri/src/agents/transactions.rs +++ b/src-tauri/src/agents/transactions.rs @@ -844,7 +844,10 @@ fn validate_unmanaged_preserved( env.retain(|key, _| { !matches!( key.as_str(), - "CLAUDE_CODE_SUBAGENT_MODEL" | "CLAUDE_CODE_EFFORT_LEVEL" + "CLAUDE_CODE_SUBAGENT_MODEL" + | "CLAUDE_CODE_EFFORT_LEVEL" + | "EASYCLIPROXY_MANAGE_CLAUDE_CODE_DEFAULT_MODEL" + | "EASYCLIPROXY_MANAGE_CLAUDE_CODE_SUBAGENT_MODEL" ) && !key.starts_with("ANTHROPIC_DEFAULT_") && !key.starts_with("ANTHROPIC_MODEL_") && !key.starts_with("ANTHROPIC_CUSTOM_MODEL_OPTION") diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 63fae1d..2546a9e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1150,6 +1150,14 @@ struct ClaudeDesktopModelMappings { auto_compact_pct: u8, #[serde(default)] disable_auto_compact: bool, + /// Keep Claude Code's session/default model selection under CPA control. + /// Defaults to true for backwards compatibility with existing profiles. + #[serde(default = "default_true")] + manage_default_model: bool, + /// Set CLAUDE_CODE_SUBAGENT_MODEL when enabled; otherwise allow inheritance. + /// Defaults to true for backwards compatibility with existing profiles. + #[serde(default = "default_true")] + manage_subagent_model: bool, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] @@ -1195,6 +1203,10 @@ fn default_claude_auto_compact_pct() -> u8 { DEFAULT_CLAUDE_AUTO_COMPACT_PCT } +fn default_true() -> bool { + true +} + impl ClaudeDesktopModelMappings { fn all(model: &str) -> Self { Self { @@ -1208,6 +1220,8 @@ impl ClaudeDesktopModelMappings { max_context_tokens: default_claude_code_max_context_tokens(), auto_compact_pct: default_claude_auto_compact_pct(), disable_auto_compact: false, + manage_default_model: true, + manage_subagent_model: true, } } } diff --git a/src-tauri/src/tests/agent_configuration.rs b/src-tauri/src/tests/agent_configuration.rs index df42a98..770829c 100644 --- a/src-tauri/src/tests/agent_configuration.rs +++ b/src-tauri/src/tests/agent_configuration.rs @@ -154,6 +154,8 @@ fn claude_code_role_mappings_drive_settings() { max_context_tokens: 272_000, auto_compact_pct: 80, disable_auto_compact: false, + manage_default_model: true, + manage_subagent_model: true, }; let models = vec![ AgentModelOption { @@ -210,6 +212,53 @@ fn claude_code_role_mappings_drive_settings() { ); } +#[test] +fn claude_code_can_leave_session_model_and_subagents_to_claude_code() { + let mappings = ClaudeDesktopModelMappings { + manage_default_model: false, + manage_subagent_model: false, + ..ClaudeDesktopModelMappings::all("gpt-sonnet") + }; + let rendered = build_claude_agent_config( + Some(r#"{"model":"user-selected","env":{"ANTHROPIC_MODEL":"user-selected","CLAUDE_CODE_SUBAGENT_MODEL":"user-subagent"}}"#), + "http://127.0.0.1:8317", + "test-key", + "gpt-sonnet", + &test_agent_models(&["gpt-sonnet"]), + Some(&mappings), + ) + .unwrap(); + let value: serde_json::Value = serde_json::from_str(&rendered).unwrap(); + assert_eq!(value["model"], "user-selected"); + assert_eq!(value["env"]["ANTHROPIC_MODEL"], "user-selected"); + assert_eq!(value["env"]["CLAUDE_CODE_SUBAGENT_MODEL"], "user-subagent"); + assert_eq!(value["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"], "gpt-sonnet"); + assert_eq!(value["env"]["EASYCLIPROXY_MANAGE_CLAUDE_CODE_DEFAULT_MODEL"], "0"); + assert_eq!(value["env"]["EASYCLIPROXY_MANAGE_CLAUDE_CODE_SUBAGENT_MODEL"], "0"); +} + +#[test] +fn claude_code_unmanaged_mode_removes_previous_cpa_overrides() { + let mappings = ClaudeDesktopModelMappings { + manage_default_model: false, + manage_subagent_model: false, + ..ClaudeDesktopModelMappings::all("gpt-sonnet") + }; + let rendered = build_claude_agent_config( + Some(r#"{"model":"gpt-sonnet","env":{"ANTHROPIC_MODEL":"gpt-sonnet","CLAUDE_CODE_SUBAGENT_MODEL":"gpt-sonnet"}}"#), + "http://127.0.0.1:8317", + "test-key", + "gpt-sonnet", + &test_agent_models(&["gpt-sonnet"]), + Some(&mappings), + ) + .unwrap(); + let value: serde_json::Value = serde_json::from_str(&rendered).unwrap(); + assert!(value.get("model").is_none()); + assert!(value["env"].get("ANTHROPIC_MODEL").is_none()); + assert!(value["env"].get("CLAUDE_CODE_SUBAGENT_MODEL").is_none()); +} + #[test] fn claude_code_runtime_settings_keep_per_role_1m_suffixes() { let mappings = ClaudeDesktopModelMappings { @@ -223,6 +272,8 @@ fn claude_code_runtime_settings_keep_per_role_1m_suffixes() { max_context_tokens: 1_000_000, auto_compact_pct: 75, disable_auto_compact: true, + manage_default_model: true, + manage_subagent_model: true, }; let models = vec![ AgentModelOption { diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 5bd08bc..27413e8 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -1460,6 +1460,10 @@ export const jaOverrides = { 'agents.claudeCodeRuntime.autoCompactPctHint': '既定値は 90% です。小さい値ほど早く圧縮します。', 'agents.claudeCodeRuntime.disableAutoCompact': '自動圧縮を無効化', 'agents.claudeCodeRuntime.disableAutoCompactHint': '有効時は /compact による手動圧縮のみ利用できます。', + 'agents.claudeCodeRuntime.manageDefaultModel': '起動モデルを CPA が管理', + 'agents.claudeCodeRuntime.manageDefaultModelHint': '無効にすると ANTHROPIC_MODEL と model を上書きせず、/model で既定モデルを保存できます。', + 'agents.claudeCodeRuntime.manageSubagentModel': 'Subagent モデルを CPA が固定', + 'agents.claudeCodeRuntime.manageSubagentModelHint': '無効にすると CLAUDE_CODE_SUBAGENT_MODEL を設定せず、未指定の Subagent はメインモデルを継承します。', 'agents.claudeDesktopMapping.customMapping': 'モデルエイリアスを使用', 'agents.claudeDesktopMapping.customMappingHint': '有効にするとエイリアスモデルのみ、無効にするとエイリアス未設定の元モデルのみ表示します。', 'agents.claudeDesktopMapping.opus': 'Opus', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 7f58bc1..4e1f1fd 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1426,6 +1426,10 @@ export const en: Record = { 'agents.claudeCodeRuntime.autoCompactPctHint': 'Defaults to 90%. Lower values compact earlier.', 'agents.claudeCodeRuntime.disableAutoCompact': 'Disable auto-compaction', 'agents.claudeCodeRuntime.disableAutoCompactHint': 'Only manual /compact remains available when enabled.', + 'agents.claudeCodeRuntime.manageDefaultModel': 'Let CPA manage the startup model', + 'agents.claudeCodeRuntime.manageDefaultModelHint': 'When off, CPA leaves ANTHROPIC_MODEL and the root model untouched so /model can save the default.', + 'agents.claudeCodeRuntime.manageSubagentModel': 'Let CPA pin the Subagent model', + 'agents.claudeCodeRuntime.manageSubagentModelHint': 'When off, CPA leaves CLAUDE_CODE_SUBAGENT_MODEL unset so unspecified Subagents inherit the main model.', 'agents.claudeDesktopMapping.customMapping': 'Use Model Aliases', 'agents.claudeDesktopMapping.customMappingHint': 'When enabled, only aliased models are shown; when disabled, only original models without aliases are shown.', 'agents.claudeDesktopMapping.opus': 'Opus', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 7a600e8..2a95248 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -1425,6 +1425,10 @@ export const zhCN = { 'agents.claudeCodeRuntime.autoCompactPctHint': '默认 90%;调低后会更早自动压缩。', 'agents.claudeCodeRuntime.disableAutoCompact': '禁止自动压缩', 'agents.claudeCodeRuntime.disableAutoCompactHint': '开启后仅允许用户通过 /compact 手动压缩。', + 'agents.claudeCodeRuntime.manageDefaultModel': '由 CPA 管理启动模型', + 'agents.claudeCodeRuntime.manageDefaultModelHint': '关闭后不再写入 ANTHROPIC_MODEL 或根级 model,Claude Code 可通过 /model 保存默认模型。', + 'agents.claudeCodeRuntime.manageSubagentModel': '由 CPA 固定 Subagent 模型', + 'agents.claudeCodeRuntime.manageSubagentModelHint': '关闭后不再写入 CLAUDE_CODE_SUBAGENT_MODEL,未指定模型的 Subagent 继承主会话模型。', 'agents.claudeDesktopMapping.customMapping': '使用模型别名', 'agents.claudeDesktopMapping.customMappingHint': '开启后只显示别名模型;关闭后只显示未设置别名的原始模型。', 'agents.claudeDesktopMapping.opus': 'Opus', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index 96fe48b..8f98222 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -156,6 +156,8 @@ type ClaudeModelMappings = { maxContextTokens: number; autoCompactPct: number; disableAutoCompact: boolean; + manageDefaultModel: boolean; + manageSubagentModel: boolean; }; type AgentFormValues = { @@ -186,6 +188,8 @@ const createClaudeModelMappings = (model: string): ClaudeModelMappings => ({ maxContextTokens: DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS, autoCompactPct: DEFAULT_CLAUDE_AUTO_COMPACT_PCT, disableAutoCompact: false, + manageDefaultModel: true, + manageSubagentModel: true, }); const createClaudeModelMappingsByClient = (): Record< @@ -888,6 +892,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP maxContextTokens: source.maxContextTokens ?? DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS, autoCompactPct: source.autoCompactPct ?? DEFAULT_CLAUDE_AUTO_COMPACT_PCT, disableAutoCompact: Boolean(source.disableAutoCompact), + manageDefaultModel: source.manageDefaultModel !== false, + manageSubagentModel: source.manageSubagentModel !== false, }; return sameAgentModelMappings(currentClientDraft, next) ? current @@ -1156,6 +1162,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP resolved.maxContextTokens = claudeModelMappingsDraft.maxContextTokens; resolved.autoCompactPct = claudeModelMappingsDraft.autoCompactPct; resolved.disableAutoCompact = claudeModelMappingsDraft.disableAutoCompact; + resolved.manageDefaultModel = claudeModelMappingsDraft.manageDefaultModel; + resolved.manageSubagentModel = claudeModelMappingsDraft.manageSubagentModel; return resolved; }; @@ -2095,6 +2103,48 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP + + ) : null}
diff --git a/src/services/agentConfigurationDraft.ts b/src/services/agentConfigurationDraft.ts index 9aa1d63..3e1a685 100644 --- a/src/services/agentConfigurationDraft.ts +++ b/src/services/agentConfigurationDraft.ts @@ -28,6 +28,8 @@ export type AgentModelMappings = { maxContextTokens?: number; autoCompactPct?: number; disableAutoCompact?: boolean; + manageDefaultModel?: boolean; + manageSubagentModel?: boolean; }; type ResolveAgentConfigurationActionOptions = { @@ -67,7 +69,9 @@ export const sameAgentModelMappings = ( && Boolean(left.haiku1m) === Boolean(right.haiku1m) && (left.maxContextTokens ?? 200_000) === (right.maxContextTokens ?? 200_000) && (left.autoCompactPct ?? 90) === (right.autoCompactPct ?? 90) - && Boolean(left.disableAutoCompact) === Boolean(right.disableAutoCompact); + && Boolean(left.disableAutoCompact) === Boolean(right.disableAutoCompact) + && (left.manageDefaultModel ?? true) === (right.manageDefaultModel ?? true) + && (left.manageSubagentModel ?? true) === (right.manageSubagentModel ?? true); export const resolveAgentModelMappingsDraftSource = ( current: T,