Merge pull request #116 from router-for-me/dev

Dev
This commit is contained in:
lzt404 authored and GitHub committed 2026-08-11 12:34:40 +08:00
commit 44043a20f4
15 files changed
+690 -172

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_effort_level": "max" },
{ "slug": "deepseek-v4-pro", "display_name": "DeepSeek V4 Pro", "context_window": 1000000, "claude_code_effort_level": "max" },
{ "slug": "deepseek-v4-flash", "display_name": "DeepSeek V4 Flash", "context_window": 1000000},
{ "slug": "deepseek-v4-pro", "display_name": "DeepSeek V4 Pro", "context_window": 1000000},
{ "slug": "deepseek-chat", "display_name": "DeepSeek Chat" },
{ "slug": "deepseek-reasoner", "display_name": "DeepSeek Reasoner" },
{ "slug": "grok-4.5" },
+17
View File
@@ -677,6 +677,9 @@ pub(crate) fn resolve_claude_desktop_model_mappings(
opus_1m: requested.opus_1m,
sonnet_1m: requested.sonnet_1m,
haiku_1m: requested.haiku_1m,
max_context_tokens: requested.max_context_tokens,
auto_compact_pct: requested.auto_compact_pct,
disable_auto_compact: requested.disable_auto_compact,
}))
}
@@ -690,6 +693,17 @@ pub(crate) fn resolve_claude_code_model_mappings(
return Ok(None);
}
let requested = requested.unwrap_or_else(|| ClaudeDesktopModelMappings::all(selected_model));
if !(100_000..=1_000_000).contains(&requested.max_context_tokens) {
return Err("Claude Code 最大窗口必须介于 100000 和 1000000 之间".to_string());
}
if !(1..=100).contains(&requested.auto_compact_pct) {
return Err("Claude Code 触发压缩百分比必须介于 1 和 100 之间".to_string());
}
let max_context_tokens = if requested.opus_1m || requested.sonnet_1m || requested.haiku_1m {
CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW
} else {
requested.max_context_tokens
};
let resolve =
|model: &str| resolve_available_agent_model(models, &validate_agent_model(model)?);
Ok(Some(ClaudeDesktopModelMappings {
@@ -699,6 +713,9 @@ pub(crate) fn resolve_claude_code_model_mappings(
opus_1m: requested.opus_1m,
sonnet_1m: requested.sonnet_1m,
haiku_1m: requested.haiku_1m,
max_context_tokens,
auto_compact_pct: requested.auto_compact_pct,
disable_auto_compact: requested.disable_auto_compact,
}))
}
+18 -5
View File
@@ -220,8 +220,7 @@ pub(crate) fn build_claude_agent_config(
let mappings = mappings
.cloned()
.unwrap_or_else(|| ClaudeDesktopModelMappings::all(model));
let max_context_tokens = claude_code_max_context_tokens(&mappings, models)?;
let use_max_context_override = !mappings.sonnet_1m;
let max_context_tokens = mappings.max_context_tokens;
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)?;
@@ -229,6 +228,8 @@ pub(crate) fn build_claude_agent_config(
env.remove("ANTHROPIC_API_KEY");
env.remove("CLAUDE_CODE_EFFORT_LEVEL");
env.remove(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV);
env.remove(CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV);
env.remove(DISABLE_AUTO_COMPACT_ENV);
for (key, value) in [
("ANTHROPIC_BASE_URL", base_url),
("ANTHROPIC_AUTH_TOKEN", api_key),
@@ -253,10 +254,18 @@ pub(crate) fn build_claude_agent_config(
serde_json::Value::String(value.to_string()),
);
}
if use_max_context_override {
env.insert(
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(),
serde_json::Value::String(max_context_tokens.to_string()),
);
env.insert(
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV.to_string(),
serde_json::Value::String(mappings.auto_compact_pct.to_string()),
);
if mappings.disable_auto_compact {
env.insert(
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(),
serde_json::Value::String(max_context_tokens.to_string()),
DISABLE_AUTO_COMPACT_ENV.to_string(),
serde_json::Value::String("1".to_string()),
);
}
for (key, value) in claude_code_model_presentation_environment(&mappings, models)? {
@@ -647,6 +656,8 @@ pub(crate) fn remove_claude_code_managed_configuration(
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_FABLE_MODEL",
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV,
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV,
DISABLE_AUTO_COMPACT_ENV,
] {
env.remove(key);
}
@@ -1018,6 +1029,8 @@ pub(crate) fn build_restored_claude_code_config(
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_FABLE_MODEL",
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV,
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV,
DISABLE_AUTO_COMPACT_ENV,
] {
restore_json_key(env, original_env, key);
}
+51 -24
View File
@@ -1749,35 +1749,52 @@ pub(crate) fn inspect_claude_code_model_mappings(
.map(str::trim)
.filter(|value| !value.is_empty())
.or(fallback)?;
Some((
strip_claude_code_context_suffix(value).to_string(),
has_claude_code_context_suffix(value),
))
let normalized = strip_claude_code_context_suffix(value);
Some((normalized.to_string(), normalized.len() != value.len()))
};
let Some((opus, opus_1m)) = read_model("ANTHROPIC_DEFAULT_OPUS_MODEL") else {
let Some((opus, opus_had_1m)) = read_model("ANTHROPIC_DEFAULT_OPUS_MODEL") else {
return Ok(None);
};
let Some((sonnet, sonnet_1m)) = read_model("ANTHROPIC_DEFAULT_SONNET_MODEL") else {
let Some((sonnet, sonnet_had_1m)) = read_model("ANTHROPIC_DEFAULT_SONNET_MODEL") else {
return Ok(None);
};
let Some((haiku, haiku_1m)) = read_model("ANTHROPIC_DEFAULT_HAIKU_MODEL") else {
let Some((haiku, haiku_had_1m)) = read_model("ANTHROPIC_DEFAULT_HAIKU_MODEL") else {
return Ok(None);
};
let legacy_1m = opus_had_1m || sonnet_had_1m || haiku_had_1m;
let max_context_tokens = env
.get(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV)
.and_then(serde_json::Value::as_str)
.and_then(|value| value.trim().parse::<u64>().ok())
.unwrap_or_else(|| {
if legacy_1m {
CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW
} else {
default_claude_code_max_context_tokens()
}
});
let auto_compact_pct = env
.get(CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV)
.and_then(serde_json::Value::as_str)
.and_then(|value| value.trim().parse::<u8>().ok())
.unwrap_or_else(default_claude_auto_compact_pct);
let disable_auto_compact = env
.get(DISABLE_AUTO_COMPACT_ENV)
.and_then(serde_json::Value::as_str)
.is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"));
Ok(Some(ClaudeDesktopModelMappings {
opus,
sonnet,
haiku,
opus_1m,
sonnet_1m,
haiku_1m,
opus_1m: opus_had_1m,
sonnet_1m: sonnet_had_1m,
haiku_1m: haiku_had_1m,
max_context_tokens,
auto_compact_pct,
disable_auto_compact,
}))
}
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 {
@@ -1850,15 +1867,10 @@ pub(crate) fn claude_effective_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
}
})
context_window
}
#[allow(dead_code)]
pub(crate) fn claude_code_max_context_tokens(
mappings: &ClaudeDesktopModelMappings,
models: &[AgentModelOption],
@@ -1913,6 +1925,9 @@ pub(crate) fn claude_code_model_settings(
opus_1m: mappings.opus_1m,
sonnet_1m: mappings.sonnet_1m,
haiku_1m: mappings.haiku_1m,
max_context_tokens: mappings.max_context_tokens,
auto_compact_pct: mappings.auto_compact_pct,
disable_auto_compact: mappings.disable_auto_compact,
}
}
@@ -1930,10 +1945,12 @@ pub(crate) fn claude_code_model_presentation(
models: &[AgentModelOption],
model_name: &str,
enable_1m: bool,
context_window_override: Option<u64>,
role: Option<&str>,
) -> (String, String) {
let display_name = agent_model_display_name(models, model_name);
let context_window = claude_effective_context_window(models, model_name, enable_1m)
let context_window = context_window_override
.or_else(|| 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 {
@@ -1952,27 +1969,37 @@ pub(crate) fn claude_code_model_presentation_environment(
models,
&mappings.opus,
mappings.opus_1m,
Some(mappings.max_context_tokens),
Some("Opus mapping"),
);
let sonnet = claude_code_model_presentation(
models,
&mappings.sonnet,
mappings.sonnet_1m,
Some(mappings.max_context_tokens),
Some("Sonnet mapping"),
);
let haiku = claude_code_model_presentation(
models,
&mappings.haiku,
mappings.haiku_1m,
Some(mappings.max_context_tokens),
Some("Haiku mapping"),
);
let fable = claude_code_model_presentation(
models,
&mappings.sonnet,
mappings.sonnet_1m,
Some(mappings.max_context_tokens),
Some("Fable mapping"),
);
let custom = claude_code_model_presentation(models, &mappings.sonnet, mappings.sonnet_1m, None);
let custom = claude_code_model_presentation(
models,
&mappings.sonnet,
mappings.sonnet_1m,
Some(mappings.max_context_tokens),
None,
);
let custom_model = claude_code_model_setting(&mappings.sonnet, mappings.sonnet_1m);
Ok([
("ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", opus.0),
+6 -5
View File
@@ -150,11 +150,12 @@ mod tests {
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_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_effort_levels
.contains_key("deepseek-v4-flash"));
assert!(!catalog
.claude_code_effort_levels
.contains_key("deepseek-v4-pro"));
assert_eq!(
catalog.display_names["deepseek-v4-flash"],
"DeepSeek V4 Flash"
+141 -4
View File
@@ -70,7 +70,20 @@ pub(crate) async fn ensure_claude_desktop_model_aliases(
models: &[AgentModelOption],
) -> Result<(), String> {
let content = fetch_management_config_yaml(config).await?;
let updated = ensure_claude_desktop_model_aliases_in_yaml(&content, mappings, models)?;
let updated = match ensure_claude_desktop_model_aliases_in_yaml(&content, mappings, models) {
Ok(updated) => updated,
Err(source_error) => {
let definitions = fetch_codex_model_definitions(config)
.await
.map_err(|error| format!("{source_error};{error}"))?;
ensure_claude_desktop_model_aliases_with_codex_oauth_in_yaml(
&content,
mappings,
models,
&definitions,
)?
}
};
if updated != content {
put_management_config_yaml(config, &updated).await?;
}
@@ -92,6 +105,15 @@ pub(crate) fn ensure_claude_desktop_model_aliases_in_yaml(
content: &str,
mappings: &ClaudeDesktopModelMappings,
models: &[AgentModelOption],
) -> Result<String, String> {
ensure_claude_desktop_model_aliases_with_codex_oauth_in_yaml(content, mappings, models, &[])
}
pub(crate) fn ensure_claude_desktop_model_aliases_with_codex_oauth_in_yaml(
content: &str,
mappings: &ClaudeDesktopModelMappings,
models: &[AgentModelOption],
codex_oauth_models: &[CodexModelDefinition],
) -> Result<String, String> {
let mut document = yaml_serde_edit::YamlValue::parse(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
@@ -113,7 +135,7 @@ pub(crate) fn ensure_claude_desktop_model_aliases_in_yaml(
remove_managed_claude_model_alias(root, alias)?;
}
} else {
ensure_claude_desktop_model_alias(root, source_model, alias)?;
ensure_claude_desktop_model_alias(root, source_model, alias, codex_oauth_models)?;
}
}
render_updated_core_yaml(&mut document, updated)
@@ -167,6 +189,7 @@ pub(crate) fn remove_managed_claude_model_alias(
changed |= models.len() != before;
}
}
changed |= remove_oauth_claude_model_alias(root, alias, true)?;
Ok(changed)
}
@@ -200,6 +223,51 @@ pub(crate) fn remove_existing_claude_model_alias(
changed |= models.len() != before;
}
}
changed |= remove_oauth_claude_model_alias(root, alias, false)?;
Ok(changed)
}
pub(crate) fn remove_oauth_claude_model_alias(
root: &mut serde_norway::Mapping,
alias: &str,
managed_only: bool,
) -> Result<bool, String> {
let Some(oauth_aliases) = yaml_mapping_value_mut(root, "oauth-model-alias") else {
return Ok(false);
};
let oauth_aliases = oauth_aliases
.as_mapping_mut()
.ok_or_else(|| "oauth-model-alias 必须是 YAML 映射".to_string())?;
let mut changed = false;
let mut empty_channels = Vec::new();
for (channel, entries) in oauth_aliases.iter_mut() {
let channel_name = channel.as_str().unwrap_or("unknown");
let entries = entries
.as_sequence_mut()
.ok_or_else(|| format!("oauth-model-alias.{channel_name} 必须是数组"))?;
let before = entries.len();
entries.retain(|entry| {
if managed_only {
!is_managed_claude_model_alias(entry, alias)
} else {
configured_model_identity(entry)
.is_none_or(|(_, client_model, _)| !client_model.eq_ignore_ascii_case(alias))
}
});
if entries.len() != before {
changed = true;
if entries.is_empty() {
empty_channels.push(channel.clone());
}
}
}
for channel in empty_channels {
oauth_aliases.remove(&channel);
}
let remove_oauth_section = changed && oauth_aliases.is_empty();
if remove_oauth_section {
root.remove(yaml_key("oauth-model-alias"));
}
Ok(changed)
}
@@ -232,7 +300,7 @@ pub(crate) fn configured_managed_claude_alias_exists(
root: &serde_norway::Mapping,
alias: &str,
) -> bool {
MODEL_ALIAS_CONFIG_SECTIONS.iter().any(|section| {
let configured_provider_alias = MODEL_ALIAS_CONFIG_SECTIONS.iter().any(|section| {
yaml_mapping_value(root, section)
.and_then(serde_norway::Value::as_sequence)
.into_iter()
@@ -242,13 +310,22 @@ pub(crate) fn configured_managed_claude_alias_exists(
.filter_map(serde_norway::Value::as_sequence)
.flatten()
.any(|model| is_managed_claude_model_alias(model, alias))
})
});
configured_provider_alias
|| yaml_mapping_value(root, "oauth-model-alias")
.and_then(serde_norway::Value::as_mapping)
.into_iter()
.flat_map(|channels| channels.values())
.filter_map(serde_norway::Value::as_sequence)
.flatten()
.any(|model| is_managed_claude_model_alias(model, alias))
}
pub(crate) fn ensure_claude_desktop_model_alias(
root: &mut serde_norway::Mapping,
source_model: &str,
alias: &str,
codex_oauth_models: &[CodexModelDefinition],
) -> Result<(), String> {
if let Some((existing_source, _)) = configured_model_client_identity(root, alias) {
if existing_source.eq_ignore_ascii_case(source_model)
@@ -261,6 +338,13 @@ pub(crate) fn ensure_claude_desktop_model_alias(
if append_claude_desktop_model_alias(root, source_model, alias)? {
return Ok(());
}
if codex_oauth_models
.iter()
.any(|model| model.id.eq_ignore_ascii_case(source_model))
{
append_managed_codex_oauth_model_alias(root, source_model, alias)?;
return Ok(());
}
Err(format!(
"无法确定模型 {source_model} 的 CPA 配置来源,无法创建 Claude Desktop 别名 {alias}"
))
@@ -296,6 +380,24 @@ pub(crate) fn configured_model_client_identity(
}
}
}
if let Some(oauth_aliases) =
yaml_mapping_value(root, "oauth-model-alias").and_then(serde_norway::Value::as_mapping)
{
for entries in oauth_aliases
.values()
.filter_map(serde_norway::Value::as_sequence)
{
for model in entries {
let Some((source, configured_client_model, _)) = configured_model_identity(model)
else {
continue;
};
if configured_client_model.eq_ignore_ascii_case(client_model) {
return Some((source, configured_client_model));
}
}
}
}
None
}
@@ -359,6 +461,41 @@ pub(crate) fn append_claude_desktop_model_alias(
Ok(false)
}
pub(crate) fn append_managed_codex_oauth_model_alias(
root: &mut serde_norway::Mapping,
source_model: &str,
alias: &str,
) -> Result<(), String> {
let oauth_aliases = root
.entry(yaml_key("oauth-model-alias"))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or_else(|| "oauth-model-alias 必须是 YAML 映射".to_string())?;
let codex_aliases = oauth_aliases
.entry(yaml_key("codex"))
.or_insert_with(|| serde_norway::Value::Sequence(Vec::new()))
.as_sequence_mut()
.ok_or_else(|| "oauth-model-alias.codex 必须是数组".to_string())?;
let display_name = managed_claude_alias_display_name(alias)
.ok_or_else(|| format!("不支持的 Claude 托管别名: {alias}"))?;
let mut alias_mapping = serde_norway::Mapping::new();
alias_mapping.insert(
yaml_key("name"),
serde_norway::Value::String(source_model.to_string()),
);
alias_mapping.insert(
yaml_key("alias"),
serde_norway::Value::String(alias.to_string()),
);
alias_mapping.insert(yaml_key("fork"), serde_norway::Value::Bool(true));
alias_mapping.insert(
yaml_key("display-name"),
serde_norway::Value::String(display_name.to_string()),
);
codex_aliases.push(serde_norway::Value::Mapping(alias_mapping));
Ok(())
}
pub(crate) async fn fetch_codex_model_definitions(
config: &GuiConfigFile,
) -> Result<Vec<CodexModelDefinition>, String> {
+20
View File
@@ -144,6 +144,9 @@ const MANAGED_CLAUDE_HAIKU_ALIAS_DISPLAY_NAME: &str =
const DEFAULT_CLAUDE_CONTEXT_WINDOW: u64 = 200_000;
const CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW: u64 = 1_000_000;
const CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV: &str = "CLAUDE_CODE_MAX_CONTEXT_TOKENS";
const CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV: &str = "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE";
const DISABLE_AUTO_COMPACT_ENV: &str = "DISABLE_AUTO_COMPACT";
const DEFAULT_CLAUDE_AUTO_COMPACT_PCT: u8 = 100;
const MODEL_ALIAS_CONFIG_SECTIONS: &[&str] = &[
"codex-api-key",
"openai-compatibility",
@@ -790,6 +793,20 @@ struct ClaudeDesktopModelMappings {
sonnet_1m: bool,
#[serde(default)]
haiku_1m: bool,
#[serde(default = "default_claude_code_max_context_tokens")]
max_context_tokens: u64,
#[serde(default = "default_claude_auto_compact_pct")]
auto_compact_pct: u8,
#[serde(default)]
disable_auto_compact: bool,
}
fn default_claude_code_max_context_tokens() -> u64 {
DEFAULT_CLAUDE_CONTEXT_WINDOW
}
fn default_claude_auto_compact_pct() -> u8 {
DEFAULT_CLAUDE_AUTO_COMPACT_PCT
}
impl ClaudeDesktopModelMappings {
@@ -801,6 +818,9 @@ impl ClaudeDesktopModelMappings {
opus_1m: false,
sonnet_1m: false,
haiku_1m: false,
max_context_tokens: default_claude_code_max_context_tokens(),
auto_compact_pct: default_claude_auto_compact_pct(),
disable_auto_compact: false,
}
}
}
+138 -119
View File
@@ -22,6 +22,9 @@ fn claude_agent_config_preserves_existing_fields() {
assert_eq!(value["env"]["ANTHROPIC_BASE_URL"], "http://127.0.0.1:8317");
assert_eq!(value["env"]["ANTHROPIC_AUTH_TOKEN"], DEFAULT_API_KEY);
assert_eq!(value["env"]["ANTHROPIC_MODEL"], "claude-test");
assert_eq!(value["env"][CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV], "200000");
assert_eq!(value["env"][CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV], "100");
assert!(value["env"].get(DISABLE_AUTO_COMPACT_ENV).is_none());
assert!(value["env"].get("CLAUDE_CODE_EFFORT_LEVEL").is_none());
assert_eq!(value["env"]["CLAUDE_CODE_SUBAGENT_MODEL"], "claude-test");
assert_eq!(
@@ -32,7 +35,7 @@ fn claude_agent_config_preserves_existing_fields() {
}
#[test]
fn claude_code_inspection_normalizes_1m_suffix() {
fn claude_code_inspection_normalizes_legacy_1m_suffix() {
let directory = agent_test_home("claude-code-1m-inspection");
let path = directory.join("settings.json");
fs::write(
@@ -62,6 +65,9 @@ fn claude_code_inspection_normalizes_1m_suffix() {
assert!(mappings.opus_1m);
assert!(mappings.sonnet_1m);
assert!(!mappings.haiku_1m);
assert_eq!(mappings.max_context_tokens, 1_000_000);
assert_eq!(mappings.auto_compact_pct, 100);
assert!(!mappings.disable_auto_compact);
fs::remove_dir_all(directory).unwrap();
}
@@ -73,6 +79,9 @@ fn claude_mapping_legacy_json_defaults_1m_preferences_off() {
assert!(!mappings.opus_1m);
assert!(!mappings.sonnet_1m);
assert!(!mappings.haiku_1m);
assert_eq!(mappings.max_context_tokens, 200_000);
assert_eq!(mappings.auto_compact_pct, 100);
assert!(!mappings.disable_auto_compact);
}
#[test]
@@ -84,6 +93,9 @@ fn claude_code_role_mappings_drive_settings() {
opus_1m: false,
sonnet_1m: false,
haiku_1m: false,
max_context_tokens: 272_000,
auto_compact_pct: 80,
disable_auto_compact: false,
};
let models = vec![
AgentModelOption {
@@ -133,176 +145,98 @@ fn claude_code_role_mappings_drive_settings() {
}
#[test]
fn claude_code_deepseek_picker_shows_catalog_name_and_1m_context() {
fn claude_code_runtime_settings_keep_per_role_1m_suffixes() {
let mappings = ClaudeDesktopModelMappings {
opus: "deepseek-v4-pro".to_string(),
sonnet: "deepseek-v4-pro".to_string(),
haiku: "deepseek-v4-flash".to_string(),
opus: "custom-pro".to_string(),
sonnet: "custom-pro".to_string(),
haiku: "custom-flash".to_string(),
opus_1m: true,
sonnet_1m: true,
haiku_1m: false,
max_context_tokens: 1_000_000,
auto_compact_pct: 75,
disable_auto_compact: true,
};
let models = vec![
AgentModelOption {
name: "deepseek-v4-pro".to_string(),
alias: Some("DeepSeek V4 Pro".to_string()),
name: "custom-pro".to_string(),
alias: Some("Custom Pro".to_string()),
is_alias: false,
context_window: Some(1_000_000),
context_window: Some(200_000),
},
AgentModelOption {
name: "deepseek-v4-flash".to_string(),
alias: Some("DeepSeek V4 Flash".to_string()),
name: "custom-flash".to_string(),
alias: Some("Custom Flash".to_string()),
is_alias: false,
context_window: Some(1_000_000),
context_window: Some(200_000),
},
];
let rendered = build_claude_agent_config(
None,
"http://127.0.0.1:8317",
"test-key",
"deepseek-v4-pro",
"custom-pro",
&models,
Some(&mappings),
)
.unwrap();
let value: serde_json::Value = serde_json::from_str(&rendered).unwrap();
assert!(value["env"]
.get(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV)
.is_none());
assert_eq!(value["env"]["ANTHROPIC_MODEL"], "deepseek-v4-pro[1m]");
assert_eq!(value["env"][CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV], "1000000");
assert_eq!(value["env"][CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV], "75");
assert_eq!(value["env"][DISABLE_AUTO_COMPACT_ENV], "1");
assert_eq!(value["env"]["ANTHROPIC_MODEL"], "custom-pro[1m]");
assert_eq!(
value["env"]["ANTHROPIC_DEFAULT_OPUS_MODEL"],
"deepseek-v4-pro[1m]"
"custom-pro[1m]"
);
assert_eq!(
value["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"],
"deepseek-v4-pro[1m]"
"custom-pro[1m]"
);
assert_eq!(
value["env"]["ANTHROPIC_DEFAULT_HAIKU_MODEL"],
"deepseek-v4-flash"
"custom-flash"
);
assert_eq!(
value["env"]["ANTHROPIC_DEFAULT_FABLE_MODEL"],
"deepseek-v4-pro[1m]"
"custom-pro[1m]"
);
assert_eq!(
value["env"]["CLAUDE_CODE_SUBAGENT_MODEL"],
"deepseek-v4-flash"
);
assert_eq!(value["env"]["CLAUDE_CODE_EFFORT_LEVEL"], "max");
assert_eq!(value["env"]["CLAUDE_CODE_SUBAGENT_MODEL"], "custom-flash");
assert!(value["env"].get("CLAUDE_CODE_EFFORT_LEVEL").is_none());
assert_eq!(
value["env"]["ANTHROPIC_CUSTOM_MODEL_OPTION"],
"deepseek-v4-pro[1m]"
"custom-pro[1m]"
);
assert_eq!(
value["env"]["ANTHROPIC_CUSTOM_MODEL_OPTION_NAME"],
"DeepSeek V4 Pro (1M context)"
"Custom Pro (1M context)"
);
assert_eq!(
value["env"]["ANTHROPIC_DEFAULT_FABLE_MODEL_NAME"],
"DeepSeek V4 Pro (Fable mapping, 1M context)"
"Custom Pro (Fable mapping, 1M context)"
);
assert_eq!(value["model"], "deepseek-v4-pro[1m]");
assert_eq!(value["model"], "custom-pro[1m]");
}
#[test]
fn claude_code_1m_suffix_follows_user_preference() {
let models = vec![
AgentModelOption {
name: "deepseek-v4-flash".to_string(),
alias: None,
is_alias: false,
context_window: Some(1_000_000),
},
AgentModelOption {
name: "gpt-runtime-1m".to_string(),
alias: None,
is_alias: false,
context_window: Some(1_000_000),
},
];
assert_eq!(
claude_code_model_setting("deepseek-v4-flash", true),
"deepseek-v4-flash[1m]"
);
assert_eq!(
claude_code_model_setting("deepseek-v4-flash", false),
"deepseek-v4-flash"
);
assert_eq!(
claude_code_model_setting("gpt-runtime-1m", true),
"gpt-runtime-1m[1m]"
);
assert_eq!(
claude_code_max_context_tokens(
&ClaudeDesktopModelMappings::all("gpt-runtime-1m"),
&models,
)
.unwrap(),
DEFAULT_CLAUDE_CONTEXT_WINDOW
);
}
#[test]
fn claude_code_context_window_follows_primary_model_alias_source() {
let mappings = ClaudeDesktopModelMappings {
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 {
name: mappings.opus.clone(),
alias: None,
is_alias: false,
context_window: Some(128_000),
},
AgentModelOption {
name: "primary-model".to_string(),
alias: None,
is_alias: false,
context_window: Some(372_000),
},
AgentModelOption {
name: mappings.sonnet.clone(),
alias: Some("primary-model".to_string()),
is_alias: true,
context_window: Some(200_000),
},
AgentModelOption {
name: mappings.haiku.clone(),
alias: None,
is_alias: false,
context_window: Some(1_000_000),
},
];
assert_eq!(
claude_code_max_context_tokens(&mappings, &models).unwrap(),
372_000
);
}
#[test]
fn claude_code_uses_200k_when_cpa_context_metadata_is_missing() {
let mappings = ClaudeDesktopModelMappings::all("custom-model");
fn claude_desktop_keeps_original_context_when_1m_is_off() {
let models = vec![AgentModelOption {
name: "custom-model".to_string(),
name: "runtime-model".to_string(),
alias: None,
is_alias: false,
context_window: None,
context_window: Some(1_000_000),
}];
assert_eq!(
claude_code_max_context_tokens(&mappings, &models).unwrap(),
DEFAULT_CLAUDE_CONTEXT_WINDOW
let entry = claude_desktop_inference_model(
CLAUDE_DESKTOP_SONNET_MODEL_ID,
"runtime-model",
false,
&models,
);
assert_eq!(entry["contextWindow"], 1_000_000);
assert!(entry.get("supports1m").is_none());
assert!(entry.get("prefer1m").is_none());
}
#[test]
@@ -532,6 +466,7 @@ fn claude_desktop_profile_keeps_non_claude_models_internal() {
opus_1m: true,
sonnet_1m: false,
haiku_1m: true,
..ClaudeDesktopModelMappings::all("")
};
let models = vec![
AgentModelOption {
@@ -858,6 +793,7 @@ fn claude_desktop_aliases_expose_role_routes_only() {
opus_1m: false,
sonnet_1m: false,
haiku_1m: false,
..ClaudeDesktopModelMappings::all("")
};
let available_models = test_agent_models(&["gpt-5.6-sol"]);
let rendered =
@@ -909,6 +845,66 @@ fn claude_desktop_aliases_expose_role_routes_only() {
);
}
#[test]
fn claude_desktop_aliases_support_codex_oauth_models() {
let input = "port: 8317\n";
let mappings = ClaudeDesktopModelMappings::all("gpt-5.6-sol");
let available_models = test_agent_models(&["gpt-5.6-sol"]);
let definitions = vec![CodexModelDefinition {
id: "gpt-5.6-sol".to_string(),
display_name: None,
description: None,
context_window: Some(372_000),
reasoning_levels: vec!["high".to_string()],
supports_tools: Some(true),
}];
let rendered = ensure_claude_desktop_model_aliases_with_codex_oauth_in_yaml(
input,
&mappings,
&available_models,
&definitions,
)
.unwrap();
let value: serde_norway::Value = serde_norway::from_str(&rendered).unwrap();
let root = value.as_mapping().unwrap();
let aliases = yaml_mapping_value(root, "oauth-model-alias")
.and_then(serde_norway::Value::as_mapping)
.and_then(|channels| yaml_mapping_value(channels, "codex"))
.and_then(serde_norway::Value::as_sequence)
.unwrap();
assert_eq!(aliases.len(), 3);
assert!(aliases.iter().all(|entry| {
configured_model_identity(entry).is_some_and(|(source, _, _)| source == "gpt-5.6-sol")
}));
assert!(aliases.iter().all(|entry| {
entry
.as_mapping()
.and_then(|entry| yaml_mapping_value(entry, "fork"))
== Some(&serde_norway::Value::Bool(true))
}));
assert_eq!(
ensure_claude_desktop_model_aliases_with_codex_oauth_in_yaml(
&rendered,
&mappings,
&available_models,
&definitions,
)
.unwrap(),
rendered
);
let cleaned = remove_managed_claude_model_aliases_in_yaml(&rendered).unwrap();
let cleaned: serde_norway::Value = serde_norway::from_str(&cleaned).unwrap();
let cleaned = cleaned.as_mapping().unwrap();
assert!(yaml_mapping_value(cleaned, "oauth-model-alias").is_none());
assert_eq!(
yaml_mapping_value(cleaned, "port").and_then(serde_norway::Value::as_i64),
Some(8317)
);
}
#[test]
fn claude_managed_aliases_expand_flow_yaml_and_are_removed_selectively() {
let compact = "# keep this comment\ncodex-api-key: [{api-key: test, models: [{name: gpt-5.6-sol, alias: ''}]}]\ncredential-concurrency: {max-limit: 1000000, cleanup-interval: 5s}\n";
@@ -960,6 +956,27 @@ fn claude_alias_cleanup_does_not_delete_user_owned_routes() {
assert!(configured_model_client_identity(root, CLAUDE_DESKTOP_SONNET_MODEL_ID).is_none());
}
#[test]
fn claude_alias_cleanup_preserves_user_owned_oauth_routes() {
let input = format!(
"oauth-model-alias:\n codex:\n - name: user-opus\n alias: {opus}\n fork: true\n - name: app-sonnet\n alias: {sonnet}\n fork: true\n display-name: {managed_sonnet}\n",
opus = CLAUDE_DESKTOP_OPUS_MODEL_ID,
sonnet = CLAUDE_DESKTOP_SONNET_MODEL_ID,
managed_sonnet = MANAGED_CLAUDE_SONNET_ALIAS_DISPLAY_NAME,
);
let cleaned = remove_managed_claude_model_aliases_in_yaml(&input).unwrap();
let value: serde_norway::Value = serde_norway::from_str(&cleaned).unwrap();
let root = value.as_mapping().unwrap();
assert_eq!(
configured_model_client_identity(root, CLAUDE_DESKTOP_OPUS_MODEL_ID)
.unwrap()
.0,
"user-opus"
);
assert!(configured_model_client_identity(root, CLAUDE_DESKTOP_SONNET_MODEL_ID).is_none());
}
#[test]
fn claude_legacy_aliases_are_adopted_and_moved_to_the_selected_model() {
let input = format!(
@@ -991,6 +1008,7 @@ fn claude_desktop_uses_selected_alias_directly_with_original_context() {
opus_1m: false,
sonnet_1m: true,
haiku_1m: false,
..ClaudeDesktopModelMappings::all("")
};
let models = vec![
AgentModelOption {
@@ -1058,6 +1076,7 @@ fn claude_desktop_role_mappings_can_use_different_available_models() {
opus_1m: true,
sonnet_1m: false,
haiku_1m: true,
..ClaudeDesktopModelMappings::all("")
}),
)
.unwrap()
+9 -1
View File
@@ -476,6 +476,7 @@ export const jaOverrides = {
'agents.error.noModels': '選択可能なモデルがないため、設定変更を適用できません',
'agents.error.selectionGone': '選択中のモデルは利用可能一覧にありません。更新後に選び直してください',
'agents.error.mappingSelectionGone': 'Claude Desktop のモデルマッピングに利用できないモデルがあります。モデルを更新して選び直してください',
'agents.error.claudeCodeRuntimeSettingsInvalid': '最大コンテキストは 100000~1000000、自動圧縮率は 1~100 にしてください',
'agents.error.applyFirst': '選択したモデルはまだ適用されていません。先に「設定変更を適用」をクリックしてください',
'agents.status.detecting': '検出中',
'agents.status.unsupported': 'プラットフォーム非対応',
@@ -570,7 +571,14 @@ export const jaOverrides = {
'agents.claudeDesktopMapping.title': 'Claude Desktop モデル → 上流モデル',
'agents.claudeDesktopMapping.description': '役割ごとに上流モデルとコンテキスト機能を選択します。対応モデルでのみ 1M を有効にしてください。',
'agents.claudeCodeMapping.title': 'Claude Code モデル → 上流モデル',
'agents.claudeCodeMapping.description': 'タスク役割ごとに上流モデルとコンテキスト機能を選択します。対応モデルでのみ 1M を有効にしてください。',
'agents.claudeCodeMapping.description': 'タスク役割ごとに上流モデルを選び、共通のコンテキストと自動圧縮ポリシーを設定します。',
'agents.claudeCodeRuntime.maxContextTokens': '最大コンテキスト',
'agents.claudeCodeRuntime.maxContextTokensHint': 'Claude Code の既定値は 200000 です。上流モデルの上限を超えないでください。',
'agents.claudeCodeRuntime.context1mHint': 'このマッピングで [1m] モデルを使用し、最大コンテキストを 1000000 にします。3 つすべてを無効にすると 200000 に戻ります。',
'agents.claudeCodeRuntime.autoCompactPct': '自動圧縮の開始率',
'agents.claudeCodeRuntime.autoCompactPctHint': '既定値は 100% です。小さい値ほど早く圧縮します。',
'agents.claudeCodeRuntime.disableAutoCompact': '自動圧縮を無効化',
'agents.claudeCodeRuntime.disableAutoCompactHint': '有効時は /compact による手動圧縮のみ利用できます。',
'agents.claudeDesktopMapping.customMapping': 'モデルエイリアスを使用',
'agents.claudeDesktopMapping.customMappingHint': '有効にするとエイリアスモデルのみ、無効にするとエイリアス未設定の元モデルのみ表示します。',
'agents.claudeDesktopMapping.opus': 'Claude Opus 5',
+9 -1
View File
@@ -476,6 +476,7 @@ export const en: Record<MessageKey, string> = {
'agents.error.noModels': 'No models are available, so configuration changes cannot be applied',
'agents.error.selectionGone': 'The selected model is no longer available. Refresh and select it again.',
'agents.error.mappingSelectionGone': 'A Claude Desktop mapping is no longer available. Refresh the models and select it again.',
'agents.error.claudeCodeRuntimeSettingsInvalid': 'Maximum context must be 100000–1000000 and the auto-compact percentage must be 1–100.',
'agents.error.applyFirst': 'The selected model has not been applied. Click “Apply Configuration Changes” first.',
'agents.status.detecting': 'Detecting',
'agents.status.unsupported': 'Unsupported Platform',
@@ -565,7 +566,14 @@ export const en: Record<MessageKey, string> = {
'agents.claudeDesktopMapping.title': 'Claude Desktop Model → Upstream Model',
'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 and context capability for each task role. Turn on 1M only for supported models.',
'agents.claudeCodeMapping.description': 'Choose the upstream model for each task role and configure one shared context and auto-compact policy.',
'agents.claudeCodeRuntime.maxContextTokens': 'Maximum context',
'agents.claudeCodeRuntime.maxContextTokensHint': 'Claude Code defaults to 200000. Do not exceed the upstream model limit.',
'agents.claudeCodeRuntime.context1mHint': 'Uses the [1m] model for this mapping and sets the maximum context to 1000000. It returns to 200000 after all three mappings are off.',
'agents.claudeCodeRuntime.autoCompactPct': 'Auto-compact trigger',
'agents.claudeCodeRuntime.autoCompactPctHint': 'Defaults to 100%. Lower values compact earlier.',
'agents.claudeCodeRuntime.disableAutoCompact': 'Disable auto-compaction',
'agents.claudeCodeRuntime.disableAutoCompactHint': 'Only manual /compact remains available when enabled.',
'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': 'Claude Opus 5',
+9 -1
View File
@@ -474,6 +474,7 @@ export const zhCN = {
'agents.error.noModels': '当前没有可选模型,无法应用配置修改',
'agents.error.selectionGone': '当前选择已不在可用模型列表中,请刷新模型后重新选择',
'agents.error.mappingSelectionGone': 'Claude Desktop 模型映射包含不可用模型,请刷新模型后重新选择',
'agents.error.claudeCodeRuntimeSettingsInvalid': '最大窗口必须为 100000–1000000,触发压缩百分比必须为 1–100',
'agents.error.applyFirst': '模型选择尚未应用,请先点击“应用配置修改”',
'agents.status.detecting': '检测中',
'agents.status.unsupported': '平台不支持',
@@ -563,7 +564,14 @@ export const zhCN = {
'agents.claudeDesktopMapping.title': 'Claude Desktop 模型 → 上游模型',
'agents.claudeDesktopMapping.description': '分别选择上游模型和上下文能力;仅为确认支持的模型打开 1M。',
'agents.claudeCodeMapping.title': 'Claude Code 模型 → 上游模型',
'agents.claudeCodeMapping.description': '分别为不同任务角色选择上游模型和上下文能力;仅为确认支持的模型打开 1M。',
'agents.claudeCodeMapping.description': '分别为不同任务角色选择上游模型,并统一配置上下文窗口与自动压缩。',
'agents.claudeCodeRuntime.maxContextTokens': '最大窗口',
'agents.claudeCodeRuntime.maxContextTokensHint': 'Claude Code 默认 200000;请勿超过上游模型实际支持的窗口。',
'agents.claudeCodeRuntime.context1mHint': '为此映射使用 [1m] 模型并将最大窗口设为 1000000;三个映射均关闭后恢复为 200000。',
'agents.claudeCodeRuntime.autoCompactPct': '触发压缩百分比',
'agents.claudeCodeRuntime.autoCompactPctHint': '默认 100;调低后会更早自动压缩。',
'agents.claudeCodeRuntime.disableAutoCompact': '禁止自动压缩',
'agents.claudeCodeRuntime.disableAutoCompactHint': '开启后仅允许用户通过 /compact 手动压缩。',
'agents.claudeDesktopMapping.customMapping': '使用模型别名',
'agents.claudeDesktopMapping.customMappingHint': '开启后只显示别名模型;关闭后只显示未设置别名的原始模型。',
'agents.claudeDesktopMapping.opus': 'Claude Opus 5',
+125 -3
View File
@@ -104,9 +104,14 @@ type ClaudeModelMappings = {
opus1m: boolean;
sonnet1m: boolean;
haiku1m: boolean;
maxContextTokens: number;
autoCompactPct: number;
disableAutoCompact: boolean;
};
const CODEX_OAUTH_LOGIN_REQUIRED_ERROR = 'CODEX_OAUTH_LOGIN_REQUIRED';
const DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS = 200_000;
const DEFAULT_CLAUDE_AUTO_COMPACT_PCT = 100;
const createClaudeModelMappings = (model: string): ClaudeModelMappings => ({
opus: model,
@@ -115,6 +120,9 @@ const createClaudeModelMappings = (model: string): ClaudeModelMappings => ({
opus1m: false,
sonnet1m: false,
haiku1m: false,
maxContextTokens: DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS,
autoCompactPct: DEFAULT_CLAUDE_AUTO_COMPACT_PCT,
disableAutoCompact: false,
});
const claudeMappingRoles = [
@@ -731,6 +739,9 @@ export function AgentsPage() {
opus1m: Boolean(source.opus1m),
sonnet1m: Boolean(source.sonnet1m),
haiku1m: Boolean(source.haiku1m),
maxContextTokens: source.maxContextTokens ?? DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS,
autoCompactPct: source.autoCompactPct ?? DEFAULT_CLAUDE_AUTO_COMPACT_PCT,
disableAutoCompact: Boolean(source.disableAutoCompact),
};
return sameAgentModelMappings(current, next) ? current : next;
});
@@ -757,6 +768,12 @@ export function AgentsPage() {
|| claudeMappingRoles.every((role) =>
Boolean(findAgentModel(models, claudeModelMappingsDraft[role.key])),
);
const claudeCodeRuntimeSettingsReady = selected !== 'claude-code' || (
claudeModelMappingsDraft.maxContextTokens >= 100_000
&& claudeModelMappingsDraft.maxContextTokens <= 1_000_000
&& claudeModelMappingsDraft.autoCompactPct >= 1
&& claudeModelMappingsDraft.autoCompactPct <= 100
);
const claudeMappingDraftChanged = isClaudeModelMappingClient
&& activeStatus?.modificationState === 'applied'
&& !sameAgentModelMappings(
@@ -781,7 +798,7 @@ export function AgentsPage() {
&& activeStatus.installed
&& !modelLoading
&& (isClaudeModelMappingClient
? claudeMappingsReady
? claudeMappingsReady && claudeCodeRuntimeSettingsReady
: selectedModelOption),
);
const modelHint = modelSelectionError
@@ -836,7 +853,32 @@ export function AgentsPage() {
enabled: boolean,
) => {
claudeModelMappingsDirtyRef.current = true;
setClaudeModelMappingsDraft((current) => ({ ...current, [preference]: enabled }));
setClaudeModelMappingsDraft((current) => {
const next = { ...current, [preference]: enabled };
if (selected === 'claude-code') {
const any1mEnabled = next.opus1m || next.sonnet1m || next.haiku1m;
next.maxContextTokens = any1mEnabled
? 1_000_000
: DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS;
}
return next;
});
};
const changeClaudeCodeRuntimeSetting = (
key: 'maxContextTokens' | 'autoCompactPct',
value: number,
) => {
claudeModelMappingsDirtyRef.current = true;
setClaudeModelMappingsDraft((current) => ({ ...current, [key]: value }));
};
const changeClaudeCodeAutoCompactDisabled = (disabled: boolean) => {
claudeModelMappingsDirtyRef.current = true;
setClaudeModelMappingsDraft((current) => ({
...current,
disableAutoCompact: disabled,
}));
};
const changeClaudeCustomMapping = (enabled: boolean) => {
@@ -850,6 +892,9 @@ export function AgentsPage() {
opus1m: current.opus1m,
sonnet1m: current.sonnet1m,
haiku1m: current.haiku1m,
maxContextTokens: current.maxContextTokens,
autoCompactPct: current.autoCompactPct,
disableAutoCompact: current.disableAutoCompact,
};
if (!sameAgentModelMappings(current, next)) {
claudeModelMappingsDirtyRef.current = true;
@@ -888,6 +933,15 @@ export function AgentsPage() {
resolved[role.key] = model.name;
resolved[role.contextKey] = claudeModelMappingsDraft[role.contextKey];
}
if (selected === 'claude-code') {
if (!claudeCodeRuntimeSettingsReady) {
setModelSelectionError(t('agents.error.claudeCodeRuntimeSettingsInvalid'));
return null;
}
}
resolved.maxContextTokens = claudeModelMappingsDraft.maxContextTokens;
resolved.autoCompactPct = claudeModelMappingsDraft.autoCompactPct;
resolved.disableAutoCompact = claudeModelMappingsDraft.disableAutoCompact;
return resolved;
};
@@ -1291,6 +1345,72 @@ export function AgentsPage() {
</label>
</div>
</div>
{selected === 'claude-code' ? (
<div className="agent-claude-code-runtime-settings">
<div className="agent-claude-code-number-field">
<span>{t('agents.claudeCodeRuntime.maxContextTokens')}</span>
<input
type="number"
min={100_000}
max={1_000_000}
step={1_000}
value={claudeModelMappingsDraft.maxContextTokens}
onChange={(event) => changeClaudeCodeRuntimeSetting(
'maxContextTokens',
Math.trunc(event.currentTarget.valueAsNumber || 0),
)}
disabled={busy || modelLoading}
aria-label={t('agents.claudeCodeRuntime.maxContextTokens')}
aria-describedby="claude-code-max-context-hint"
/>
<small id="claude-code-max-context-hint">
{t('agents.claudeCodeRuntime.maxContextTokensHint')}
</small>
</div>
<label className="agent-claude-code-number-field">
<span>{t('agents.claudeCodeRuntime.autoCompactPct')}</span>
<div className="agent-claude-code-percent-input">
<input
type="number"
min={1}
max={100}
step={1}
value={claudeModelMappingsDraft.autoCompactPct}
onChange={(event) => changeClaudeCodeRuntimeSetting(
'autoCompactPct',
Math.trunc(event.currentTarget.valueAsNumber || 0),
)}
disabled={busy || modelLoading || claudeModelMappingsDraft.disableAutoCompact}
aria-describedby="claude-code-auto-compact-hint"
/>
<span>%</span>
</div>
<small id="claude-code-auto-compact-hint">
{t('agents.claudeCodeRuntime.autoCompactPctHint')}
</small>
</label>
<label
className="agent-claude-code-disable-compact"
title={t('agents.claudeCodeRuntime.disableAutoCompactHint')}
>
<span>
<strong>{t('agents.claudeCodeRuntime.disableAutoCompact')}</strong>
<small>{t('agents.claudeCodeRuntime.disableAutoCompactHint')}</small>
</span>
<span className="switch-control">
<input
type="checkbox"
checked={claudeModelMappingsDraft.disableAutoCompact}
onChange={(event) => changeClaudeCodeAutoCompactDisabled(
event.currentTarget.checked,
)}
disabled={busy || modelLoading}
/>
<span className="switch-track" />
</span>
</label>
</div>
) : null}
<div className="agent-claude-desktop-mapping-grid">
{claudeMappingRoles.map((role) => (
<div className="agent-claude-desktop-mapping-row" key={role.key}>
@@ -1298,7 +1418,9 @@ export function AgentsPage() {
<strong>{t(role.labelKey)}</strong>
<label
className="agent-claude-context-toggle"
title={t('agents.claudeMapping.context1mHint')}
title={t(selected === 'claude-code'
? 'agents.claudeCodeRuntime.context1mHint'
: 'agents.claudeMapping.context1mHint')}
>
<span>{t('agents.claudeMapping.context1m')}</span>
<span className="switch-control">
+7 -1
View File
@@ -18,6 +18,9 @@ export type AgentModelMappings = {
opus1m?: boolean;
sonnet1m?: boolean;
haiku1m?: boolean;
maxContextTokens?: number;
autoCompactPct?: number;
disableAutoCompact?: boolean;
};
type ResolveAgentConfigurationActionOptions = {
@@ -45,7 +48,10 @@ export const sameAgentModelMappings = (
&& sameAgentModel(left.haiku, right.haiku)
&& Boolean(left.opus1m) === Boolean(right.opus1m)
&& Boolean(left.sonnet1m) === Boolean(right.sonnet1m)
&& Boolean(left.haiku1m) === Boolean(right.haiku1m);
&& Boolean(left.haiku1m) === Boolean(right.haiku1m)
&& (left.maxContextTokens ?? 200_000) === (right.maxContextTokens ?? 200_000)
&& (left.autoCompactPct ?? 100) === (right.autoCompactPct ?? 100)
&& Boolean(left.disableAutoCompact) === Boolean(right.disableAutoCompact);
export function resolveAgentConfigurationAction({
client,
+108
View File
@@ -5519,6 +5519,98 @@ textarea:focus-visible {
gap: 12px;
}
.agent-claude-code-runtime-settings {
display: grid;
grid-template-columns: minmax(180px, 1fr) minmax(180px, 1fr) minmax(240px, 1.2fr);
gap: 12px;
padding: 12px;
border: 1px solid var(--theme-e3e1db);
border-radius: 9px;
background: var(--theme-faf9f5);
}
.agent-claude-code-number-field {
display: grid;
align-content: start;
gap: 6px;
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
font-weight: 700;
}
.agent-claude-code-number-field > input,
.agent-claude-code-percent-input input {
width: 100%;
min-width: 0;
padding: 8px 10px;
border: 1px solid var(--theme-d3d0c9);
border-radius: 7px;
background: var(--theme-fff);
color: var(--theme-2f2c28);
font: inherit;
font-variant-numeric: tabular-nums;
}
.agent-claude-code-number-field > input:focus,
.agent-claude-code-percent-input input:focus {
border-color: var(--theme-8069ad);
outline: 2px solid rgba(128, 105, 173, 0.14);
}
.agent-claude-code-number-field > input:disabled,
.agent-claude-code-percent-input input:disabled {
cursor: not-allowed;
opacity: 0.62;
}
.agent-claude-code-number-field small,
.agent-claude-code-disable-compact small {
color: var(--theme-817b72);
font-size: var(--font-size-caption);
font-weight: 500;
line-height: 1.4;
}
.agent-claude-code-percent-input {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 7px;
}
.agent-claude-code-percent-input > span {
color: var(--theme-817b72);
font-weight: 700;
}
.agent-claude-code-disable-compact {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--theme-e3e1db);
border-radius: 8px;
background: var(--theme-fff);
color: var(--theme-5f5a53);
cursor: pointer;
}
.agent-claude-code-disable-compact > span:first-child {
display: grid;
gap: 4px;
}
.agent-claude-code-disable-compact:has(input:checked) {
border-color: rgba(174, 92, 82, 0.48);
background: rgba(174, 92, 82, 0.06);
}
.agent-claude-code-disable-compact:has(input:disabled) {
cursor: not-allowed;
opacity: 0.62;
}
.agent-claude-desktop-mapping-row {
display: grid;
min-width: 0;
@@ -7264,6 +7356,14 @@ textarea:focus-visible {
}
@container agent-panel (max-width: 760px) {
.agent-claude-code-runtime-settings {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.agent-claude-code-disable-compact {
grid-column: 1 / -1;
}
.agent-claude-desktop-mapping-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -7283,6 +7383,14 @@ textarea:focus-visible {
grid-template-columns: 1fr;
}
.agent-claude-code-runtime-settings {
grid-template-columns: 1fr;
}
.agent-claude-code-disable-compact {
grid-column: auto;
}
.agent-codex-options {
grid-template-columns: 1fr;
}
+30 -6
View File
@@ -123,11 +123,9 @@ describe('agent configuration update action', () => {
})).toBe('update');
});
test.each<AgentConfigurationClientId>(['claude-code', 'claude-desktop'])(
'%s updates when only a 1M preference changes',
(client) => {
test('Claude Desktop updates when only a 1M preference changes', () => {
expect(resolveAgentConfigurationAction({
client,
client: 'claude-desktop',
modificationState: 'applied',
selectedModel: 'model-a',
appliedModel: 'model-a',
@@ -146,8 +144,34 @@ describe('agent configuration update action', () => {
opus1m: false,
},
})).toBe('update');
},
);
});
test('Claude Code updates when its shared context policy changes', () => {
expect(resolveAgentConfigurationAction({
client: 'claude-code',
modificationState: 'applied',
selectedModel: 'model-a',
appliedModel: 'model-a',
oauthConfiguration: false,
appliedOauthConfiguration: false,
modelMappings: {
opus: 'model-a',
sonnet: 'model-a',
haiku: 'model-a',
maxContextTokens: 372000,
autoCompactPct: 80,
disableAutoCompact: true,
},
appliedModelMappings: {
opus: 'model-a',
sonnet: 'model-a',
haiku: 'model-a',
maxContextTokens: 200000,
autoCompactPct: 100,
disableAutoCompact: false,
},
})).toBe('update');
});
test('Pi never exposes the model update action', () => {
expect(appliedConfiguration('pi', 'model-b')).toBe('close');