feat(agents): add per-role 1M context controls for Claude

This commit is contained in:
lzt404 committed 2026-08-10 18:14:35 +08:00
1 parent 9d7b645c1d
commit 8655f6aae4
14 files changed
+352 -112

No files matched your search

@@ -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" },
+6
View File
@@ -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,
}))
}
+45 -17
View File
@@ -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::<serde_json::Value>::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));
}
+87 -40
View File
@@ -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<u64> {
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<u64, String> {
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<bool, String> {
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, String> {
) -> 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<ClaudeDesktopModelMappings, String> {
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<Vec<(String, String)>, 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),
+1 -24
View File
@@ -11,7 +11,6 @@ struct ClaudeCatalog {
fallback_context_window: u64,
context_windows: HashMap<String, u64>,
display_names: HashMap<String, String>,
claude_code_1m_models: HashSet<String>,
claude_code_effort_levels: HashMap<String, String>,
}
@@ -36,13 +35,6 @@ pub(crate) fn display_name_for(model_name: &str) -> Result<Option<String>, Strin
.cloned())
}
pub(crate) fn supports_claude_code_1m(model_name: &str) -> Result<bool, String> {
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<Option<String>, String> {
let catalog = catalog_state()?;
Ok(catalog
@@ -78,7 +70,6 @@ fn parse_catalog(content: &str) -> Result<ClaudeCatalog, String> {
.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<ClaudeCatalog, String> {
{
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<ClaudeCatalog, String> {
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());
}
}
+9
View File
@@ -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,
}
}
}
+44 -5
View File
@@ -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,
+8 -6
View File
@@ -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': '設定を消去',
+8 -6
View File
@@ -563,14 +563,16 @@ export const en: Record<MessageKey, string> = {
'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',
+8 -6
View File
@@ -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} 个可用模型,首次默认选择第一项',
+46 -2
View File
@@ -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() {
<div className="agent-claude-desktop-mapping-grid">
{claudeMappingRoles.map((role) => (
<div className="agent-claude-desktop-mapping-row" key={role.key}>
<strong>{t(role.labelKey)}</strong>
<div className="agent-claude-mapping-card-heading">
<strong>{t(role.labelKey)}</strong>
<label
className="agent-claude-context-toggle"
title={t('agents.claudeMapping.context1mHint')}
>
<span>{t('agents.claudeMapping.context1m')}</span>
<span className="switch-control">
<input
type="checkbox"
checked={claudeModelMappingsDraft[role.contextKey]}
onChange={(event) => changeClaude1mPreference(
role.contextKey,
event.currentTarget.checked,
)}
disabled={busy || modelLoading}
/>
<span className="switch-track" />
</span>
</label>
</div>
<AgentModelPicker
models={claudeMappingModels}
value={claudeModelMappingsDraft[role.key]}
+7 -1
View File
@@ -15,6 +15,9 @@ export type AgentModelMappings = {
opus: string;
sonnet: string;
haiku: string;
opus1m?: boolean;
sonnet1m?: boolean;
haiku1m?: boolean;
};
type ResolveAgentConfigurationActionOptions = {
@@ -39,7 +42,10 @@ export const sameAgentModelMappings = (
right: AgentModelMappings,
) => 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,
+55 -3
View File
@@ -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) {
+26
View File
@@ -123,6 +123,32 @@ describe('agent configuration update action', () => {
})).toBe('update');
});
test.each<AgentConfigurationClientId>(['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');
});