diff --git a/src-tauri/resources/claude_models/model-catalog.json b/src-tauri/resources/claude_models/model-catalog.json index 06d9d70..7af4c33 100644 --- a/src-tauri/resources/claude_models/model-catalog.json +++ b/src-tauri/resources/claude_models/model-catalog.json @@ -54,8 +54,8 @@ { "slug": "gemini-2.5-pro" }, { "slug": "gemini-2.5-flash" }, { "slug": "gemini-2.5-flash-lite" }, - { "slug": "deepseek-v4-flash", "display_name": "DeepSeek V4 Flash", "context_window": 1000000, "claude_code_1m": true, "claude_code_effort_level": "max" }, - { "slug": "deepseek-v4-pro", "display_name": "DeepSeek V4 Pro", "context_window": 1000000, "claude_code_1m": true, "claude_code_effort_level": "max" }, + { "slug": "deepseek-v4-flash", "display_name": "DeepSeek V4 Flash", "context_window": 1000000, "claude_code_effort_level": "max" }, + { "slug": "deepseek-v4-pro", "display_name": "DeepSeek V4 Pro", "context_window": 1000000, "claude_code_effort_level": "max" }, { "slug": "deepseek-chat", "display_name": "DeepSeek Chat" }, { "slug": "deepseek-reasoner", "display_name": "DeepSeek Reasoner" }, { "slug": "grok-4.5" }, diff --git a/src-tauri/src/agents/commands.rs b/src-tauri/src/agents/commands.rs index 35cf13a..1b54388 100644 --- a/src-tauri/src/agents/commands.rs +++ b/src-tauri/src/agents/commands.rs @@ -674,6 +674,9 @@ pub(crate) fn resolve_claude_desktop_model_mappings( opus: resolve(&requested.opus)?, sonnet: resolve(&requested.sonnet)?, haiku: resolve(&requested.haiku)?, + opus_1m: requested.opus_1m, + sonnet_1m: requested.sonnet_1m, + haiku_1m: requested.haiku_1m, })) } @@ -693,6 +696,9 @@ pub(crate) fn resolve_claude_code_model_mappings( opus: resolve(&requested.opus)?, sonnet: resolve(&requested.sonnet)?, haiku: resolve(&requested.haiku)?, + opus_1m: requested.opus_1m, + sonnet_1m: requested.sonnet_1m, + haiku_1m: requested.haiku_1m, })) } diff --git a/src-tauri/src/agents/configuration.rs b/src-tauri/src/agents/configuration.rs index bf4b7cf..0979917 100644 --- a/src-tauri/src/agents/configuration.rs +++ b/src-tauri/src/agents/configuration.rs @@ -221,9 +221,9 @@ pub(crate) fn build_claude_agent_config( .cloned() .unwrap_or_else(|| ClaudeDesktopModelMappings::all(model)); let max_context_tokens = claude_code_max_context_tokens(&mappings, models)?; - let use_max_context_override = !claude_code_model_supports_1m(models, &mappings.sonnet)?; - let model_settings = claude_code_model_settings(&mappings, models)?; - let subagent_model = strip_claude_code_context_suffix(&mappings.haiku).to_string(); + let use_max_context_override = !mappings.sonnet_1m; + let model_settings = claude_code_model_settings(&mappings); + let subagent_model = model_settings.haiku.clone(); let effort_level = claude_code_model_effort_level(models, &mappings.sonnet)?; let env = ensure_json_object_entry(root, "env"); env.remove("ANTHROPIC_API_KEY"); @@ -398,21 +398,48 @@ pub(crate) fn build_claude_desktop_profile( let mappings = mappings .cloned() .unwrap_or_else(|| ClaudeDesktopModelMappings::all(model)); - let mut inference_models = vec![ - claude_desktop_inference_model(CLAUDE_DESKTOP_OPUS_MODEL_ID, &mappings.opus, models), - claude_desktop_inference_model(CLAUDE_DESKTOP_SONNET_MODEL_ID, &mappings.sonnet, models), - claude_desktop_inference_model(CLAUDE_DESKTOP_HAIKU_MODEL_ID, &mappings.haiku, models), + let inference_models = vec![ + claude_desktop_inference_model( + CLAUDE_DESKTOP_OPUS_MODEL_ID, + &mappings.opus, + mappings.opus_1m, + models, + ), + claude_desktop_inference_model( + CLAUDE_DESKTOP_SONNET_MODEL_ID, + &mappings.sonnet, + mappings.sonnet_1m, + models, + ), + claude_desktop_inference_model( + CLAUDE_DESKTOP_HAIKU_MODEL_ID, + &mappings.haiku, + mappings.haiku_1m, + models, + ), ]; - let mut seen_models = HashSet::new(); - inference_models.retain(|entry| { - entry - .get("name") - .and_then(serde_json::Value::as_str) - .is_some_and(|name| seen_models.insert(name.to_ascii_lowercase())) - }); + let mut deduplicated_models = Vec::::with_capacity(inference_models.len()); + for entry in inference_models { + let Some(name) = entry.get("name").and_then(serde_json::Value::as_str) else { + continue; + }; + let existing = deduplicated_models.iter_mut().find(|existing| { + existing + .get("name") + .and_then(serde_json::Value::as_str) + .is_some_and(|existing_name| existing_name.eq_ignore_ascii_case(name)) + }); + if let Some(existing) = existing { + if entry.get("supports1m").and_then(serde_json::Value::as_bool) == Some(true) { + *existing = entry; + } + } else { + deduplicated_models.push(entry); + } + } root.insert( "inferenceModels".to_string(), - serde_json::Value::Array(inference_models), + serde_json::Value::Array(deduplicated_models), ); render_agent_json(root, "Claude Desktop 网关配置") } @@ -420,13 +447,14 @@ pub(crate) fn build_claude_desktop_profile( pub(crate) fn claude_desktop_inference_model( route_model: &str, source_model: &str, + enable_1m: bool, models: &[AgentModelOption], ) -> serde_json::Value { let selected = models .iter() .find(|model| model.name.eq_ignore_ascii_case(source_model)); let direct_alias = selected.is_some_and(|model| model.is_alias); - let context_window = agent_model_context_window(models, source_model); + let context_window = claude_effective_context_window(models, source_model, enable_1m); let mut entry = serde_json::Map::new(); entry.insert( "name".to_string(), @@ -441,7 +469,7 @@ pub(crate) fn claude_desktop_inference_model( "contextWindow".to_string(), serde_json::json!(context_window), ); - if context_window >= CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW { + if enable_1m { entry.insert("supports1m".to_string(), serde_json::json!(true)); entry.insert("prefer1m".to_string(), serde_json::json!(true)); } diff --git a/src-tauri/src/agents/discovery.rs b/src-tauri/src/agents/discovery.rs index 1617127..6b2923e 100644 --- a/src-tauri/src/agents/discovery.rs +++ b/src-tauri/src/agents/discovery.rs @@ -1743,30 +1743,40 @@ pub(crate) fn inspect_claude_code_model_mappings( .map(str::trim) .filter(|value| !value.is_empty()); let read_model = |key: &str| { - env.get(key) + let value = env.get(key) .and_then(serde_json::Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) - .or(fallback) - .map(strip_claude_code_context_suffix) - .map(str::to_string) + .or(fallback)?; + Some(( + strip_claude_code_context_suffix(value).to_string(), + has_claude_code_context_suffix(value), + )) }; - let Some(opus) = read_model("ANTHROPIC_DEFAULT_OPUS_MODEL") else { + let Some((opus, opus_1m)) = read_model("ANTHROPIC_DEFAULT_OPUS_MODEL") else { return Ok(None); }; - let Some(sonnet) = read_model("ANTHROPIC_DEFAULT_SONNET_MODEL") else { + let Some((sonnet, sonnet_1m)) = read_model("ANTHROPIC_DEFAULT_SONNET_MODEL") else { return Ok(None); }; - let Some(haiku) = read_model("ANTHROPIC_DEFAULT_HAIKU_MODEL") else { + let Some((haiku, haiku_1m)) = read_model("ANTHROPIC_DEFAULT_HAIKU_MODEL") else { return Ok(None); }; Ok(Some(ClaudeDesktopModelMappings { opus, sonnet, haiku, + opus_1m, + sonnet_1m, + haiku_1m, })) } +pub(crate) fn has_claude_code_context_suffix(value: &str) -> bool { + let value = value.trim(); + value.len() >= 4 && value[value.len() - 4..].eq_ignore_ascii_case("[1m]") +} + pub(crate) fn strip_claude_code_context_suffix(value: &str) -> &str { let mut value = value.trim(); loop { @@ -1826,18 +1836,35 @@ pub(crate) fn agent_model_context_window( .or(Some(DEFAULT_CLAUDE_CONTEXT_WINDOW)) } +pub(crate) fn claude_effective_context_window( + models: &[AgentModelOption], + model_name: &str, + enable_1m: bool, +) -> Option { + let context_window = agent_model_context_window(models, model_name); + if enable_1m { + return Some( + context_window + .unwrap_or(DEFAULT_CLAUDE_CONTEXT_WINDOW) + .max(CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW), + ); + } + context_window.map(|context_window| { + if context_window >= CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW { + DEFAULT_CLAUDE_CONTEXT_WINDOW + } else { + context_window + } + }) +} + pub(crate) fn claude_code_max_context_tokens( mappings: &ClaudeDesktopModelMappings, models: &[AgentModelOption], ) -> Result { let model = mappings.sonnet.as_str(); - let context_window = agent_model_context_window(models, model) + let context_window = claude_effective_context_window(models, model, mappings.sonnet_1m) .ok_or_else(|| format!("CPA 模型 API 未返回 Claude Code 主模型 {model} 的上下文窗口"))?; - if context_window >= CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW - && !claude_code_model_supports_1m(models, model)? - { - return Ok(DEFAULT_CLAUDE_CONTEXT_WINDOW); - } Ok(context_window) } @@ -1859,13 +1886,6 @@ pub(crate) fn agent_model_display_name<'a>( } } -pub(crate) fn claude_code_model_supports_1m( - models: &[AgentModelOption], - model_name: &str, -) -> Result { - claude_catalog::supports_claude_code_1m(agent_model_source_name(models, model_name)) -} - pub(crate) fn claude_code_model_effort_level( models: &[AgentModelOption], model_name: &str, @@ -1874,27 +1894,28 @@ pub(crate) fn claude_code_model_effort_level( } pub(crate) fn claude_code_model_setting( - models: &[AgentModelOption], model_name: &str, enable_1m_variant: bool, -) -> Result { +) -> String { let model_name = strip_claude_code_context_suffix(model_name); - if enable_1m_variant && claude_code_model_supports_1m(models, model_name)? { - Ok(format!("{model_name}[1m]")) + if enable_1m_variant { + format!("{model_name}[1m]") } else { - Ok(model_name.to_string()) + model_name.to_string() } } pub(crate) fn claude_code_model_settings( mappings: &ClaudeDesktopModelMappings, - models: &[AgentModelOption], -) -> Result { - Ok(ClaudeDesktopModelMappings { - opus: claude_code_model_setting(models, &mappings.opus, true)?, - sonnet: claude_code_model_setting(models, &mappings.sonnet, true)?, - haiku: claude_code_model_setting(models, &mappings.haiku, false)?, - }) +) -> ClaudeDesktopModelMappings { + ClaudeDesktopModelMappings { + opus: claude_code_model_setting(&mappings.opus, mappings.opus_1m), + sonnet: claude_code_model_setting(&mappings.sonnet, mappings.sonnet_1m), + haiku: claude_code_model_setting(&mappings.haiku, mappings.haiku_1m), + opus_1m: mappings.opus_1m, + sonnet_1m: mappings.sonnet_1m, + haiku_1m: mappings.haiku_1m, + } } pub(crate) fn format_context_window(context_window: u64) -> String { @@ -1910,11 +1931,12 @@ pub(crate) fn format_context_window(context_window: u64) -> String { pub(crate) fn claude_code_model_presentation( models: &[AgentModelOption], model_name: &str, + enable_1m: bool, role: Option<&str>, ) -> (String, String) { let display_name = agent_model_display_name(models, model_name); - let context_window = - agent_model_context_window(models, model_name).unwrap_or(DEFAULT_CLAUDE_CONTEXT_WINDOW); + let context_window = claude_effective_context_window(models, model_name, enable_1m) + .unwrap_or(DEFAULT_CLAUDE_CONTEXT_WINDOW); let context_label = format_context_window(context_window); let name = match role { Some(role) => format!("{display_name} ({role}, {context_label} context)"), @@ -1928,12 +1950,37 @@ pub(crate) fn claude_code_model_presentation_environment( mappings: &ClaudeDesktopModelMappings, models: &[AgentModelOption], ) -> Result, String> { - let opus = claude_code_model_presentation(models, &mappings.opus, Some("Opus mapping")); - let sonnet = claude_code_model_presentation(models, &mappings.sonnet, Some("Sonnet mapping")); - let haiku = claude_code_model_presentation(models, &mappings.haiku, Some("Haiku mapping")); - let fable = claude_code_model_presentation(models, &mappings.sonnet, Some("Fable mapping")); - let custom = claude_code_model_presentation(models, &mappings.sonnet, None); - let custom_model = claude_code_model_setting(models, &mappings.sonnet, true)?; + let opus = claude_code_model_presentation( + models, + &mappings.opus, + mappings.opus_1m, + Some("Opus mapping"), + ); + let sonnet = claude_code_model_presentation( + models, + &mappings.sonnet, + mappings.sonnet_1m, + Some("Sonnet mapping"), + ); + let haiku = claude_code_model_presentation( + models, + &mappings.haiku, + mappings.haiku_1m, + Some("Haiku mapping"), + ); + let fable = claude_code_model_presentation( + models, + &mappings.sonnet, + mappings.sonnet_1m, + Some("Fable mapping"), + ); + let custom = claude_code_model_presentation( + models, + &mappings.sonnet, + mappings.sonnet_1m, + None, + ); + let custom_model = claude_code_model_setting(&mappings.sonnet, mappings.sonnet_1m); Ok([ ("ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", opus.0), ("ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION", opus.1), diff --git a/src-tauri/src/claude_catalog.rs b/src-tauri/src/claude_catalog.rs index 9952e7b..03472fa 100644 --- a/src-tauri/src/claude_catalog.rs +++ b/src-tauri/src/claude_catalog.rs @@ -11,7 +11,6 @@ struct ClaudeCatalog { fallback_context_window: u64, context_windows: HashMap, display_names: HashMap, - claude_code_1m_models: HashSet, claude_code_effort_levels: HashMap, } @@ -36,13 +35,6 @@ pub(crate) fn display_name_for(model_name: &str) -> Result, Strin .cloned()) } -pub(crate) fn supports_claude_code_1m(model_name: &str) -> Result { - let catalog = catalog_state()?; - Ok(catalog - .claude_code_1m_models - .contains(&normalize_id(model_name))) -} - pub(crate) fn claude_code_effort_level_for(model_name: &str) -> Result, String> { let catalog = catalog_state()?; Ok(catalog @@ -78,7 +70,6 @@ fn parse_catalog(content: &str) -> Result { .ok_or_else(|| "内置 Claude model-catalog.json 必须包含 models 数组".to_string())?; let mut context_windows = HashMap::with_capacity(models.len()); let mut display_names = HashMap::with_capacity(models.len()); - let mut claude_code_1m_models = HashSet::with_capacity(models.len()); let mut claude_code_effort_levels = HashMap::with_capacity(models.len()); let mut model_ids = HashSet::with_capacity(models.len()); for (index, model) in models.iter().enumerate() { @@ -116,13 +107,6 @@ fn parse_catalog(content: &str) -> Result { { display_names.insert(key.clone(), display_name.to_string()); } - if model - .get("claude_code_1m") - .and_then(Value::as_bool) - .unwrap_or(false) - { - claude_code_1m_models.insert(key.clone()); - } if let Some(effort_level) = model .get("claude_code_effort_level") .and_then(Value::as_str) @@ -136,7 +120,6 @@ fn parse_catalog(content: &str) -> Result { fallback_context_window, context_windows, display_names, - claude_code_1m_models, claude_code_effort_levels, }) } @@ -163,18 +146,15 @@ mod tests { use super::{parse_catalog, MODEL_CATALOG_JSON}; #[test] - fn bundled_catalog_marks_deepseek_v4_as_1m() { + fn bundled_catalog_describes_deepseek_v4() { let catalog = parse_catalog(MODEL_CATALOG_JSON).unwrap(); assert_eq!(catalog.context_windows["deepseek-v4-flash"], 1_000_000); assert_eq!(catalog.context_windows["deepseek-v4-pro"], 1_000_000); - assert!(catalog.claude_code_1m_models.contains("deepseek-v4-flash")); - assert!(catalog.claude_code_1m_models.contains("deepseek-v4-pro")); assert_eq!( catalog.claude_code_effort_levels["deepseek-v4-flash"], "max" ); assert_eq!(catalog.claude_code_effort_levels["deepseek-v4-pro"], "max"); - assert!(!catalog.claude_code_1m_models.contains("gpt-5.6-luna")); assert_eq!( catalog.display_names["deepseek-v4-flash"], "DeepSeek V4 Flash" @@ -191,7 +171,6 @@ mod tests { "slug": "gpt-test", "display_name": "GPT Test", "context_window": 272000, - "claude_code_1m": true, "claude_code_effort_level": "max" }] }"#, @@ -199,7 +178,6 @@ mod tests { .unwrap(); assert_eq!(catalog.context_windows["gpt-test"], 272000); assert_eq!(catalog.display_names["gpt-test"], "GPT Test"); - assert!(catalog.claude_code_1m_models.contains("gpt-test")); assert_eq!(catalog.claude_code_effort_levels["gpt-test"], "max"); } @@ -229,7 +207,6 @@ mod tests { .unwrap(); assert!(catalog.context_windows.is_empty()); assert!(catalog.display_names.is_empty()); - assert!(catalog.claude_code_1m_models.is_empty()); assert!(catalog.claude_code_effort_levels.is_empty()); } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ef4cd70..b815395 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -782,6 +782,12 @@ struct ClaudeDesktopModelMappings { opus: String, sonnet: String, haiku: String, + #[serde(default)] + opus_1m: bool, + #[serde(default)] + sonnet_1m: bool, + #[serde(default)] + haiku_1m: bool, } impl ClaudeDesktopModelMappings { @@ -790,6 +796,9 @@ impl ClaudeDesktopModelMappings { opus: model.to_string(), sonnet: model.to_string(), haiku: model.to_string(), + opus_1m: false, + sonnet_1m: false, + haiku_1m: false, } } } diff --git a/src-tauri/src/tests/agent_configuration.rs b/src-tauri/src/tests/agent_configuration.rs index 9b37d71..659187b 100644 --- a/src-tauri/src/tests/agent_configuration.rs +++ b/src-tauri/src/tests/agent_configuration.rs @@ -59,15 +59,33 @@ fn claude_code_inspection_normalizes_1m_suffix() { assert_eq!(mappings.opus, "deepseek-v4-pro"); assert_eq!(mappings.sonnet, "deepseek-v4-pro"); assert_eq!(mappings.haiku, "deepseek-v4-flash"); + assert!(mappings.opus_1m); + assert!(mappings.sonnet_1m); + assert!(!mappings.haiku_1m); fs::remove_dir_all(directory).unwrap(); } +#[test] +fn claude_mapping_legacy_json_defaults_1m_preferences_off() { + let mappings: ClaudeDesktopModelMappings = serde_json::from_str( + r#"{"opus":"model-a","sonnet":"model-b","haiku":"model-c"}"#, + ) + .unwrap(); + + assert!(!mappings.opus_1m); + assert!(!mappings.sonnet_1m); + assert!(!mappings.haiku_1m); +} + #[test] fn claude_code_role_mappings_drive_settings() { let mappings = ClaudeDesktopModelMappings { opus: "gpt-opus".to_string(), sonnet: "gpt-sonnet".to_string(), haiku: "gpt-haiku".to_string(), + opus_1m: false, + sonnet_1m: false, + haiku_1m: false, }; let models = vec![ AgentModelOption { @@ -122,6 +140,9 @@ fn claude_code_deepseek_picker_shows_catalog_name_and_1m_context() { opus: "deepseek-v4-pro".to_string(), sonnet: "deepseek-v4-pro".to_string(), haiku: "deepseek-v4-flash".to_string(), + opus_1m: true, + sonnet_1m: true, + haiku_1m: false, }; let models = vec![ AgentModelOption { @@ -189,7 +210,7 @@ fn claude_code_deepseek_picker_shows_catalog_name_and_1m_context() { } #[test] -fn claude_code_1m_suffix_requires_explicit_catalog_support() { +fn claude_code_1m_suffix_follows_user_preference() { let models = vec![ AgentModelOption { name: "deepseek-v4-flash".to_string(), @@ -206,16 +227,16 @@ fn claude_code_1m_suffix_requires_explicit_catalog_support() { ]; assert_eq!( - claude_code_model_setting(&models, "deepseek-v4-flash", true).unwrap(), + claude_code_model_setting("deepseek-v4-flash", true), "deepseek-v4-flash[1m]" ); assert_eq!( - claude_code_model_setting(&models, "deepseek-v4-flash", false).unwrap(), + claude_code_model_setting("deepseek-v4-flash", false), "deepseek-v4-flash" ); assert_eq!( - claude_code_model_setting(&models, "gpt-runtime-1m", true).unwrap(), - "gpt-runtime-1m" + claude_code_model_setting("gpt-runtime-1m", true), + "gpt-runtime-1m[1m]" ); assert_eq!( claude_code_max_context_tokens( @@ -233,6 +254,9 @@ fn claude_code_context_window_follows_primary_model_alias_source() { opus: "small-model".to_string(), sonnet: "primary-alias".to_string(), haiku: "large-model".to_string(), + opus_1m: false, + sonnet_1m: false, + haiku_1m: false, }; let models = vec![ AgentModelOption { @@ -507,6 +531,9 @@ fn claude_desktop_profile_keeps_non_claude_models_internal() { opus: "gpt-5.6-sol".to_string(), sonnet: "gpt-5.6".to_string(), haiku: "gpt-5.6-mini".to_string(), + opus_1m: true, + sonnet_1m: false, + haiku_1m: true, }; let models = vec![ AgentModelOption { @@ -830,6 +857,9 @@ fn claude_desktop_aliases_expose_role_routes_only() { opus: "gpt-5.6-sol".to_string(), sonnet: "gpt-5.6-sol".to_string(), haiku: "gpt-5.6-sol".to_string(), + opus_1m: false, + sonnet_1m: false, + haiku_1m: false, }; let available_models = test_agent_models(&["gpt-5.6-sol"]); let rendered = @@ -960,6 +990,9 @@ fn claude_desktop_uses_selected_alias_directly_with_original_context() { opus: "gpt-high".to_string(), sonnet: "gpt-high".to_string(), haiku: "gpt-high".to_string(), + opus_1m: false, + sonnet_1m: true, + haiku_1m: false, }; let models = vec![ AgentModelOption { @@ -1024,6 +1057,9 @@ fn claude_desktop_role_mappings_can_use_different_available_models() { opus: "gpt-5.6-sol".to_string(), sonnet: "deepseek-chat".to_string(), haiku: "gemini-3-pro".to_string(), + opus_1m: true, + sonnet_1m: false, + haiku_1m: true, }), ) .unwrap() @@ -1032,6 +1068,9 @@ fn claude_desktop_role_mappings_can_use_different_available_models() { assert_eq!(mappings.opus, "gpt-5.6-sol"); assert_eq!(mappings.sonnet, "deepseek-chat"); assert_eq!(mappings.haiku, "gemini-3-pro"); + assert!(mappings.opus_1m); + assert!(!mappings.sonnet_1m); + assert!(mappings.haiku_1m); assert!(resolve_claude_desktop_model_mappings( AgentClient::ClaudeCode, &models, diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 4478bf3..8fbecaa 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -568,14 +568,16 @@ export const jaOverrides = { 'agents.modify.title': '設定変更', 'agents.modify.oauthConfiguration': 'OAuth ログイン設定方式', 'agents.claudeDesktopMapping.title': 'Claude Desktop モデル → 上流モデル', - 'agents.claudeDesktopMapping.description': '各 Claude Desktop モデルが実際に呼び出す上流モデルを選択します。', + 'agents.claudeDesktopMapping.description': '役割ごとに上流モデルとコンテキスト機能を選択します。対応モデルでのみ 1M を有効にしてください。', 'agents.claudeCodeMapping.title': 'Claude Code モデル → 上流モデル', - 'agents.claudeCodeMapping.description': '各タスク役割に対して使用する上流モデルを選択します。', - 'agents.claudeDesktopMapping.customMapping': 'カスタムマッピング', + 'agents.claudeCodeMapping.description': 'タスク役割ごとに上流モデルとコンテキスト機能を選択します。対応モデルでのみ 1M を有効にしてください。', + 'agents.claudeDesktopMapping.customMapping': 'モデルエイリアスを使用', 'agents.claudeDesktopMapping.customMappingHint': '有効にするとエイリアスモデルのみ、無効にするとエイリアス未設定の元モデルのみ表示します。', - 'agents.claudeDesktopMapping.opus': 'Claude Opus 5 のマッピング先', - 'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6 のマッピング先', - 'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5 のマッピング先', + 'agents.claudeDesktopMapping.opus': 'Claude Opus 5', + 'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6', + 'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5', + 'agents.claudeMapping.context1m': '1M', + 'agents.claudeMapping.context1mHint': '1M コンテキストとして設定します。選択した上流モデルが対応していることを確認してください。', 'agents.oauthLoginRequired.title': 'Codex へのログインが必要です', 'agents.oauthLoginRequired.enableDescription': '有効な Codex のログイン認証情報が見つからないため、OAuth 設定方式を有効にできません。Codex アプリで ChatGPT アカウントにログインしてください。Codex アプリでログインを開始できない場合は、先に', 'agents.oauthLoginRequired.enableClearConfiguration': '設定を消去', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 44be16c..e39efc9 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -563,14 +563,16 @@ export const en: Record = { 'agents.useModel': 'Model', 'agents.pending': 'Not Applied', 'agents.claudeDesktopMapping.title': 'Claude Desktop Model → Upstream Model', - 'agents.claudeDesktopMapping.description': 'Choose the upstream model each Claude Desktop model actually calls.', + 'agents.claudeDesktopMapping.description': 'Choose the upstream model and context capability for each role. Turn on 1M only for supported models.', 'agents.claudeCodeMapping.title': 'Claude Code Model → Upstream Model', - 'agents.claudeCodeMapping.description': 'Choose the upstream model used for each task role.', - 'agents.claudeDesktopMapping.customMapping': 'Custom Mapping', + 'agents.claudeCodeMapping.description': 'Choose the upstream model and context capability for each task role. Turn on 1M only for supported models.', + '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': 'Map Claude Opus 5 to', - 'agents.claudeDesktopMapping.sonnet': 'Map Claude Sonnet 4.6 to', - 'agents.claudeDesktopMapping.haiku': 'Map Claude Haiku 4.5 to', + 'agents.claudeDesktopMapping.opus': 'Claude Opus 5', + 'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6', + 'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5', + 'agents.claudeMapping.context1m': '1M', + 'agents.claudeMapping.context1mHint': 'Configures a 1M context window. Make sure the selected upstream model supports it.', 'agents.model.readingAvailable': 'Loading available models', 'agents.model.current': 'Currently applied: {model}', 'agents.model.firstSelection': '{count} models available; the first is selected by default', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index c2f41fe..9ecde0d 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -561,14 +561,16 @@ export const zhCN = { 'agents.useModel': '使用模型', 'agents.pending': '尚未应用', 'agents.claudeDesktopMapping.title': 'Claude Desktop 模型 → 上游模型', - 'agents.claudeDesktopMapping.description': '选择每个 Claude Desktop 模型实际调用的上游模型。', + 'agents.claudeDesktopMapping.description': '分别选择上游模型和上下文能力;仅为确认支持的模型打开 1M。', 'agents.claudeCodeMapping.title': 'Claude Code 模型 → 上游模型', - 'agents.claudeCodeMapping.description': '为不同任务角色选择使用的上游模型。', - 'agents.claudeDesktopMapping.customMapping': '自定义映射', + 'agents.claudeCodeMapping.description': '分别为不同任务角色选择上游模型和上下文能力;仅为确认支持的模型打开 1M。', + 'agents.claudeDesktopMapping.customMapping': '使用模型别名', 'agents.claudeDesktopMapping.customMappingHint': '开启后只显示别名模型;关闭后只显示未设置别名的原始模型。', - 'agents.claudeDesktopMapping.opus': 'Claude Opus 5 映射到', - 'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6 映射到', - 'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5 映射到', + 'agents.claudeDesktopMapping.opus': 'Claude Opus 5', + 'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6', + 'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5', + 'agents.claudeMapping.context1m': '1M', + 'agents.claudeMapping.context1mHint': '打开后按 1M 上下文配置,请确认所选上游模型支持。', 'agents.model.readingAvailable': '正在读取可用模型', 'agents.model.current': '当前已应用:{model}', 'agents.model.firstSelection': '{count} 个可用模型,首次默认选择第一项', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index e7ac7f6..372faec 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -101,6 +101,9 @@ type ClaudeModelMappings = { opus: string; sonnet: string; haiku: string; + opus1m: boolean; + sonnet1m: boolean; + haiku1m: boolean; }; const CODEX_OAUTH_LOGIN_REQUIRED_ERROR = 'CODEX_OAUTH_LOGIN_REQUIRED'; @@ -109,19 +112,25 @@ const createClaudeModelMappings = (model: string): ClaudeModelMappings => ({ opus: model, sonnet: model, haiku: model, + opus1m: false, + sonnet1m: false, + haiku1m: false, }); const claudeMappingRoles = [ { key: 'opus', + contextKey: 'opus1m', labelKey: 'agents.claudeDesktopMapping.opus', }, { key: 'sonnet', + contextKey: 'sonnet1m', labelKey: 'agents.claudeDesktopMapping.sonnet', }, { key: 'haiku', + contextKey: 'haiku1m', labelKey: 'agents.claudeDesktopMapping.haiku', }, ] as const; @@ -719,6 +728,9 @@ export function AgentsPage() { opus: findAgentModel(models, source.opus)?.name ?? selectedModel, sonnet: findAgentModel(models, source.sonnet)?.name ?? selectedModel, haiku: findAgentModel(models, source.haiku)?.name ?? selectedModel, + opus1m: Boolean(source.opus1m), + sonnet1m: Boolean(source.sonnet1m), + haiku1m: Boolean(source.haiku1m), }; return sameAgentModelMappings(current, next) ? current : next; }); @@ -809,7 +821,7 @@ export function AgentsPage() { }; const selectClaudeModelMapping = ( - role: keyof ClaudeModelMappings, + role: 'opus' | 'sonnet' | 'haiku', value: string, ) => { const model = findAgentModel(models, value); @@ -819,6 +831,14 @@ export function AgentsPage() { setClaudeModelMappingsDraft((current) => ({ ...current, [role]: model.name })); }; + const changeClaude1mPreference = ( + preference: 'opus1m' | 'sonnet1m' | 'haiku1m', + enabled: boolean, + ) => { + claudeModelMappingsDirtyRef.current = true; + setClaudeModelMappingsDraft((current) => ({ ...current, [preference]: enabled })); + }; + const changeClaudeCustomMapping = (enabled: boolean) => { setClaudeCustomMapping(enabled); setModelSelectionError(''); @@ -827,6 +847,9 @@ export function AgentsPage() { opus: resolveAgentModelForAliasMode(models, current.opus, enabled), sonnet: resolveAgentModelForAliasMode(models, current.sonnet, enabled), haiku: resolveAgentModelForAliasMode(models, current.haiku, enabled), + opus1m: current.opus1m, + sonnet1m: current.sonnet1m, + haiku1m: current.haiku1m, }; if (!sameAgentModelMappings(current, next)) { claudeModelMappingsDirtyRef.current = true; @@ -863,6 +886,7 @@ export function AgentsPage() { return null; } resolved[role.key] = model.name; + resolved[role.contextKey] = claudeModelMappingsDraft[role.contextKey]; } return resolved; }; @@ -1270,7 +1294,27 @@ export function AgentsPage() {
{claudeMappingRoles.map((role) => (
- {t(role.labelKey)} +
+ {t(role.labelKey)} + +
sameAgentModel(left.opus, right.opus) && sameAgentModel(left.sonnet, right.sonnet) - && sameAgentModel(left.haiku, right.haiku); + && sameAgentModel(left.haiku, right.haiku) + && Boolean(left.opus1m) === Boolean(right.opus1m) + && Boolean(left.sonnet1m) === Boolean(right.sonnet1m) + && Boolean(left.haiku1m) === Boolean(right.haiku1m); export function resolveAgentConfigurationAction({ client, diff --git a/src/styles.css b/src/styles.css index 56a90c5..c8950ab 100644 --- a/src/styles.css +++ b/src/styles.css @@ -5516,18 +5516,67 @@ textarea:focus-visible { .agent-claude-desktop-mapping-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 10px; + gap: 12px; } .agent-claude-desktop-mapping-row { display: grid; min-width: 0; - gap: 7px; + align-content: start; + gap: 10px; + padding: 12px; + border: 1px solid var(--theme-e3e1db); + border-radius: 9px; + background: var(--theme-fff); + box-shadow: 0 1px 2px rgba(45, 42, 38, 0.04); } -.agent-claude-desktop-mapping-row > strong { +.agent-claude-desktop-mapping-row:has(.agent-claude-context-toggle input:checked) { + border-color: rgba(128, 105, 173, 0.48); + box-shadow: 0 0 0 2px rgba(128, 105, 173, 0.08); +} + +.agent-claude-mapping-card-heading { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.agent-claude-mapping-card-heading > strong { + min-width: 0; + overflow: hidden; color: var(--theme-5f5a53); font-size: var(--font-size-label); + text-overflow: ellipsis; + white-space: nowrap; +} + +.agent-claude-context-toggle { + display: flex; + flex: none; + align-items: center; + gap: 6px; + padding: 4px 7px; + border: 1px solid var(--theme-e3e1db); + border-radius: 999px; + background: var(--theme-faf9f5); + color: var(--theme-5f5a53); + cursor: pointer; + font-size: var(--font-size-label); + font-weight: 700; +} + +.agent-claude-context-toggle:has(input:checked) { + border-color: var(--theme-8069ad); + background: var(--theme-f1ecfb); + color: var(--theme-554075); +} + +.agent-claude-context-toggle:has(input:disabled) { + cursor: not-allowed; + opacity: 0.62; } .agent-section-heading-actions { @@ -7215,6 +7264,9 @@ textarea:focus-visible { } @container agent-panel (max-width: 760px) { + .agent-claude-desktop-mapping-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } @container agent-panel (max-width: 640px) { diff --git a/tests/agentModelPicker.test.ts b/tests/agentModelPicker.test.ts index da36f6d..4f6fcb7 100644 --- a/tests/agentModelPicker.test.ts +++ b/tests/agentModelPicker.test.ts @@ -123,6 +123,32 @@ describe('agent configuration update action', () => { })).toBe('update'); }); + test.each(['claude-code', 'claude-desktop'])( + '%s updates when only a 1M preference changes', + (client) => { + expect(resolveAgentConfigurationAction({ + client, + modificationState: 'applied', + selectedModel: 'model-a', + appliedModel: 'model-a', + oauthConfiguration: false, + appliedOauthConfiguration: false, + modelMappings: { + opus: 'model-a', + sonnet: 'model-a', + haiku: 'model-a', + opus1m: true, + }, + appliedModelMappings: { + opus: 'model-a', + sonnet: 'model-a', + haiku: 'model-a', + opus1m: false, + }, + })).toBe('update'); + }, + ); + test('Pi never exposes the model update action', () => { expect(appliedConfiguration('pi', 'model-b')).toBe('close'); });