feat(agents): add per-role 1M context controls for Claude
This commit is contained in:
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" },
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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': '設定を消去',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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} 个可用模型,首次默认选择第一项',
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user