feat(agents): add catalog-driven Claude model context configuration

- add explicit Claude model metadata and 1M capability flags
- configure Claude Code model mappings and environment variables
- apply [1m] only to catalog-approved models
- prevent global context overrides from affecting smaller models
- improve Claude Desktop context window configuration
This commit is contained in:
lzt404 committed 2026-08-04 17:14:04 +08:00
1 parent cc2636d815
commit 03cf1167f7
5 files changed
+813 -55

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ src-tauri/target/
bin-work/
cpa-core/
agent_docs
agent_docs/
*.log
*.tmp
@@ -0,0 +1,69 @@
{
"fallback_model": {
"context_window": 200000
},
"models": [
{ "slug": "gpt-5.6" },
{ "slug": "gpt-5.6-sol", "context_window": 372000 },
{ "slug": "gpt-5.6-terra", "context_window": 372000 },
{ "slug": "gpt-5.6-luna", "context_window": 372000 },
{ "slug": "gpt-5.5", "context_window": 272000 },
{ "slug": "gpt-5.5-pro" },
{ "slug": "gpt-5.4", "context_window": 272000 },
{ "slug": "gpt-5.4-pro" },
{ "slug": "gpt-5.4-mini", "context_window": 272000 },
{ "slug": "gpt-5.4-nano" },
{ "slug": "gpt-5.3-codex" },
{ "slug": "gpt-5.3-codex-spark", "context_window": 128000 },
{ "slug": "gpt-5.2" },
{ "slug": "gpt-5.2-pro" },
{ "slug": "gpt-5.2-codex" },
{ "slug": "gpt-5.1" },
{ "slug": "gpt-5.1-codex" },
{ "slug": "gpt-5.1-codex-mini" },
{ "slug": "gpt-5" },
{ "slug": "gpt-5-pro" },
{ "slug": "gpt-5-mini" },
{ "slug": "gpt-5-nano" },
{ "slug": "gpt-4.1" },
{ "slug": "gpt-4.1-mini" },
{ "slug": "gpt-4.1-nano" },
{ "slug": "o1" },
{ "slug": "o1-pro" },
{ "slug": "o3" },
{ "slug": "o3-mini" },
{ "slug": "o3-pro" },
{ "slug": "o4-mini" },
{ "slug": "codex-auto-review", "context_window": 272000 },
{ "slug": "claude-opus-5" },
{ "slug": "claude-opus-4-8" },
{ "slug": "claude-opus-4-7" },
{ "slug": "claude-opus-4-6" },
{ "slug": "claude-opus-4-5" },
{ "slug": "claude-opus-4-1" },
{ "slug": "claude-sonnet-4-6" },
{ "slug": "claude-sonnet-4-5" },
{ "slug": "claude-sonnet-4" },
{ "slug": "claude-haiku-4-5" },
{ "slug": "gemini-3.6-flash" },
{ "slug": "gemini-3.5-flash" },
{ "slug": "gemini-3.1-pro-preview" },
{ "slug": "gemini-3.1-flash-lite" },
{ "slug": "gemini-3-pro-preview" },
{ "slug": "gemini-3-flash-preview" },
{ "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-chat", "display_name": "DeepSeek Chat" },
{ "slug": "deepseek-reasoner", "display_name": "DeepSeek Reasoner" },
{ "slug": "grok-4.5" },
{ "slug": "grok-4.3" },
{ "slug": "grok-4.20" },
{ "slug": "grok-4" },
{ "slug": "grok-4-1-fast" },
{ "slug": "grok-3" },
{ "slug": "grok-3-mini" }
]
}
+235
View File
@@ -0,0 +1,235 @@
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
const MODEL_CATALOG_JSON: &str = include_str!("../resources/claude_models/model-catalog.json");
static CATALOG_STATE: OnceLock<Result<ClaudeCatalog, String>> = OnceLock::new();
#[derive(Clone, Debug)]
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>,
}
pub(crate) fn context_window_for(
model_name: &str,
runtime_context_window: Option<u64>,
) -> Result<u64, String> {
let catalog = catalog_state()?;
Ok(catalog
.context_windows
.get(&normalize_id(model_name))
.copied()
.or(runtime_context_window)
.unwrap_or(catalog.fallback_context_window))
}
pub(crate) fn display_name_for(model_name: &str) -> Result<Option<String>, String> {
let catalog = catalog_state()?;
Ok(catalog
.display_names
.get(&normalize_id(model_name))
.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
.claude_code_effort_levels
.get(&normalize_id(model_name))
.cloned())
}
fn catalog_state() -> Result<&'static ClaudeCatalog, String> {
CATALOG_STATE
.get_or_init(|| parse_catalog(MODEL_CATALOG_JSON))
.as_ref()
.map_err(Clone::clone)
}
fn parse_catalog(content: &str) -> Result<ClaudeCatalog, String> {
let root: Value = serde_json::from_str(content)
.map_err(|error| format!("解析内置 Claude model-catalog.json 失败: {error}"))?;
let root = root
.as_object()
.ok_or_else(|| "内置 Claude model-catalog.json 根节点必须是对象".to_string())?;
let fallback_context_window = root
.get("fallback_model")
.and_then(Value::as_object)
.and_then(|model| model.get("context_window"))
.and_then(positive_u64)
.ok_or_else(|| {
"内置 Claude model-catalog.json 缺少有效的 fallback_model.context_window".to_string()
})?;
let models = root
.get("models")
.and_then(Value::as_array)
.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() {
let model = model.as_object().ok_or_else(|| {
format!(
"内置 Claude model-catalog.json 第 {} 个模型必须是对象",
index + 1
)
})?;
let slug = model
.get("slug")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
format!(
"内置 Claude model-catalog.json 第 {} 个模型的 slug 不能为空",
index + 1
)
})?;
let key = normalize_id(slug);
if !model_ids.insert(key.clone()) {
return Err(format!(
"内置 Claude model-catalog.json 模型 slug 大小写重复: {slug}"
));
}
if let Some(context_window) = model.get("context_window").and_then(positive_u64) {
context_windows.insert(key.clone(), context_window);
}
if let Some(display_name) = model
.get("display_name")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty() && !value.eq_ignore_ascii_case(slug))
{
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)
.map(str::trim)
.filter(|value| !value.is_empty())
{
claude_code_effort_levels.insert(key, effort_level.to_string());
}
}
Ok(ClaudeCatalog {
fallback_context_window,
context_windows,
display_names,
claude_code_1m_models,
claude_code_effort_levels,
})
}
fn positive_u64(value: &Value) -> Option<u64> {
value
.as_u64()
.or_else(|| {
value
.as_i64()
.filter(|value| *value > 0)
.map(|value| value as u64)
})
.or_else(|| value.as_str()?.trim().parse::<u64>().ok())
.filter(|value| *value > 0)
}
fn normalize_id(value: &str) -> String {
value.trim().to_ascii_lowercase()
}
#[cfg(test)]
mod tests {
use super::{parse_catalog, MODEL_CATALOG_JSON};
#[test]
fn bundled_catalog_marks_deepseek_v4_as_1m() {
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"
);
assert_eq!(catalog.display_names["deepseek-v4-pro"], "DeepSeek V4 Pro");
}
#[test]
fn catalog_prefers_file_override_over_runtime_metadata() {
let catalog = parse_catalog(
r#"{
"fallback_model": {"context_window": 200000},
"models": [{
"slug": "gpt-test",
"display_name": "GPT Test",
"context_window": 272000,
"claude_code_1m": true,
"claude_code_effort_level": "max"
}]
}"#,
)
.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");
}
#[test]
fn catalog_rejects_duplicate_model_ids() {
let error = parse_catalog(
r#"{
"fallback_model": {"context_window": 200000},
"models": [
{"slug": "gpt-test", "context_window": 272000},
{"slug": "GPT-TEST", "context_window": 128000}
]
}"#,
)
.unwrap_err();
assert!(error.contains("slug 大小写重复"));
}
#[test]
fn catalog_allows_model_entries_without_context_override() {
let catalog = parse_catalog(
r#"{
"fallback_model": {"context_window": 200000},
"models": [{"slug": "runtime-model"}]
}"#,
)
.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());
}
}
+507 -53
View File
@@ -1,5 +1,6 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod claude_catalog;
mod codex_catalog;
mod codex_sessions;
mod usage;
@@ -2287,6 +2288,17 @@ async fn fetch_prepared_agent_models(
if matches!(client, AgentClient::ClaudeCode | AgentClient::ClaudeDesktop) {
let content = fetch_management_config_yaml(config).await?;
mark_configured_agent_model_aliases(&mut models, &content)?;
for model in &mut models {
model.context_window = Some(claude_catalog::context_window_for(
&model.name,
model.context_window,
)?);
if !model.is_alias {
if let Some(display_name) = claude_catalog::display_name_for(&model.name)? {
model.alias = Some(display_name);
}
}
}
}
Ok(PreparedAgentModels {
models,
@@ -2754,24 +2766,38 @@ async fn launch_agent(
.ok_or_else(|| "Claude Code model environment is not configured".to_string())?;
let prepared = fetch_prepared_agent_models(client, &config).await?;
let max_context_tokens = claude_code_max_context_tokens(&mappings, &prepared.models)?;
let max_context_override =
(!claude_code_model_supports_1m(&prepared.models, &mappings.sonnet)?)
.then_some(max_context_tokens);
{
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "Claude Code configuration lock is poisoned".to_string())?;
update_claude_code_context_window(&claude_config_path, max_context_tokens)?;
update_claude_code_context_window(&claude_config_path, max_context_override)?;
}
let environment = claude_code_launch_environment(
&format!("http://127.0.0.1:{port}"),
effective_agent_api_key(&config),
&mappings,
&prepared.models,
max_context_tokens,
);
)?;
let mut environment_to_remove = vec!["ANTHROPIC_API_KEY"];
if !environment
.iter()
.any(|(key, _)| key == "CLAUDE_CODE_EFFORT_LEVEL")
{
environment_to_remove.push("CLAUDE_CODE_EFFORT_LEVEL");
}
if max_context_override.is_none() {
environment_to_remove.push(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV);
}
return launch_cli_agent(
&executable,
client.name(),
&selected_working_directory,
&environment,
&["ANTHROPIC_API_KEY"],
&environment_to_remove,
);
}
launch_cli_agent(&executable, client.name(), &home, &[], &[])
@@ -5051,6 +5077,7 @@ fn inspect_claude_agent_config(
.or_else(|| root.get("model").and_then(serde_json::Value::as_str))
.map(str::trim)
.filter(|value| !value.is_empty())
.map(strip_claude_code_context_suffix)
.map(str::to_string);
Ok((configured, model))
}
@@ -5080,6 +5107,7 @@ fn inspect_claude_code_model_mappings(
.map(str::trim)
.filter(|value| !value.is_empty())
.or(fallback)
.map(strip_claude_code_context_suffix)
.map(str::to_string)
};
let Some(opus) = read_model("ANTHROPIC_DEFAULT_OPUS_MODEL") else {
@@ -5098,7 +5126,36 @@ fn inspect_claude_code_model_mappings(
}))
}
fn strip_claude_code_context_suffix(value: &str) -> &str {
let mut value = value.trim();
loop {
let Some(suffix_start) = value.len().checked_sub(4) else {
return value;
};
if !value[suffix_start..].eq_ignore_ascii_case("[1m]") {
return value;
}
value = value[..suffix_start].trim_end();
}
}
fn agent_model_source_name<'a>(models: &'a [AgentModelOption], model_name: &'a str) -> &'a str {
let model_name = strip_claude_code_context_suffix(model_name);
let Some(model) = models
.iter()
.find(|model| model.name.eq_ignore_ascii_case(model_name))
else {
return model_name;
};
if model.is_alias {
model.alias.as_deref().unwrap_or(model.name.as_str())
} else {
model.name.as_str()
}
}
fn agent_model_context_window(models: &[AgentModelOption], model_name: &str) -> Option<u64> {
let model_name = strip_claude_code_context_suffix(model_name);
let selected = models
.iter()
.find(|model| model.name.eq_ignore_ascii_case(model_name))?;
@@ -5126,46 +5183,169 @@ fn claude_code_max_context_tokens(
mappings: &ClaudeDesktopModelMappings,
models: &[AgentModelOption],
) -> Result<u64, String> {
[
("Opus", mappings.opus.as_str()),
("Sonnet", mappings.sonnet.as_str()),
("Haiku", mappings.haiku.as_str()),
let model = mappings.sonnet.as_str();
let context_window = agent_model_context_window(models, model)
.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)
}
fn agent_model_display_name<'a>(models: &'a [AgentModelOption], model_name: &'a str) -> &'a str {
let model_name = strip_claude_code_context_suffix(model_name);
let Some(model) = models
.iter()
.find(|model| model.name.eq_ignore_ascii_case(model_name))
else {
return model_name;
};
if model.is_alias {
model.name.as_str()
} else {
model.alias.as_deref().unwrap_or(model.name.as_str())
}
}
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))
}
fn claude_code_model_effort_level(
models: &[AgentModelOption],
model_name: &str,
) -> Result<Option<String>, String> {
claude_catalog::claude_code_effort_level_for(agent_model_source_name(models, model_name))
}
fn claude_code_model_setting(
models: &[AgentModelOption],
model_name: &str,
enable_1m_variant: bool,
) -> Result<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]"))
} else {
Ok(model_name.to_string())
}
}
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)?,
})
}
fn format_context_window(context_window: u64) -> String {
if context_window % 1_000_000 == 0 {
format!("{}M", context_window / 1_000_000)
} else if context_window % 1_000 == 0 {
format!("{}K", context_window / 1_000)
} else {
context_window.to_string()
}
}
fn claude_code_model_presentation(
models: &[AgentModelOption],
model_name: &str,
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_label = format_context_window(context_window);
let name = match role {
Some(role) => format!("{display_name} ({role}, {context_label} context)"),
None => format!("{display_name} ({context_label} context)"),
};
let description = format!("CPA model {model_name} - {context_label} context window");
(name, description)
}
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)?;
Ok([
("ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", opus.0),
("ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION", opus.1),
("ANTHROPIC_DEFAULT_SONNET_MODEL_NAME", sonnet.0),
("ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION", sonnet.1),
("ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME", haiku.0),
("ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION", haiku.1),
("ANTHROPIC_DEFAULT_FABLE_MODEL_NAME", fable.0),
("ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION", fable.1),
("ANTHROPIC_CUSTOM_MODEL_OPTION", custom_model),
("ANTHROPIC_CUSTOM_MODEL_OPTION_NAME", custom.0),
("ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION", custom.1),
]
.into_iter()
.map(|(role, model)| {
agent_model_context_window(models, model).ok_or_else(|| {
format!("CPA 模型 API 未返回 Claude Code {role} 映射模型 {model} 的上下文窗口")
})
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.min()
.ok_or_else(|| "CPA 模型 API 未返回 Claude Code 上下文窗口".to_string())
.map(|(key, value)| (key.to_string(), value))
.collect())
}
fn claude_code_launch_environment(
base_url: &str,
api_key: &str,
mappings: &ClaudeDesktopModelMappings,
models: &[AgentModelOption],
max_context_tokens: u64,
) -> Vec<(String, String)> {
) -> Result<Vec<(String, String)>, String> {
let model_settings = claude_code_model_settings(mappings, models)?;
let subagent_model = strip_claude_code_context_suffix(&mappings.haiku).to_string();
let mut environment = [
("ANTHROPIC_BASE_URL", base_url),
("ANTHROPIC_AUTH_TOKEN", api_key),
("ANTHROPIC_MODEL", mappings.sonnet.as_str()),
("ANTHROPIC_DEFAULT_HAIKU_MODEL", mappings.haiku.as_str()),
("ANTHROPIC_DEFAULT_SONNET_MODEL", mappings.sonnet.as_str()),
("ANTHROPIC_DEFAULT_OPUS_MODEL", mappings.opus.as_str()),
("ANTHROPIC_DEFAULT_FABLE_MODEL", mappings.sonnet.as_str()),
("ANTHROPIC_MODEL", model_settings.sonnet.as_str()),
(
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
model_settings.haiku.as_str(),
),
(
"ANTHROPIC_DEFAULT_SONNET_MODEL",
model_settings.sonnet.as_str(),
),
("ANTHROPIC_DEFAULT_OPUS_MODEL", model_settings.opus.as_str()),
(
"ANTHROPIC_DEFAULT_FABLE_MODEL",
model_settings.sonnet.as_str(),
),
("CLAUDE_CODE_SUBAGENT_MODEL", subagent_model.as_str()),
]
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect::<Vec<_>>();
environment.push((
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(),
max_context_tokens.to_string(),
));
environment
environment.extend(claude_code_model_presentation_environment(
mappings, models,
)?);
if let Some(effort_level) = claude_code_model_effort_level(models, &mappings.sonnet)? {
environment.push(("CLAUDE_CODE_EFFORT_LEVEL".to_string(), effort_level));
}
if !claude_code_model_supports_1m(models, &mappings.sonnet)? {
environment.push((
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(),
max_context_tokens.to_string(),
));
}
Ok(environment)
}
fn inspect_claude_desktop_agent_config(
@@ -5645,29 +5825,56 @@ 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 effort_level = claude_code_model_effort_level(models, &mappings.sonnet)?;
let env = ensure_json_object_entry(root, "env");
env.remove("ANTHROPIC_API_KEY");
env.remove("CLAUDE_CODE_EFFORT_LEVEL");
env.remove(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV);
for (key, value) in [
("ANTHROPIC_BASE_URL", base_url),
("ANTHROPIC_AUTH_TOKEN", api_key),
("ANTHROPIC_MODEL", mappings.sonnet.as_str()),
("ANTHROPIC_DEFAULT_HAIKU_MODEL", mappings.haiku.as_str()),
("ANTHROPIC_DEFAULT_SONNET_MODEL", mappings.sonnet.as_str()),
("ANTHROPIC_DEFAULT_OPUS_MODEL", mappings.opus.as_str()),
("ANTHROPIC_DEFAULT_FABLE_MODEL", mappings.sonnet.as_str()),
("ANTHROPIC_MODEL", model_settings.sonnet.as_str()),
(
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
model_settings.haiku.as_str(),
),
(
"ANTHROPIC_DEFAULT_SONNET_MODEL",
model_settings.sonnet.as_str(),
),
("ANTHROPIC_DEFAULT_OPUS_MODEL", model_settings.opus.as_str()),
(
"ANTHROPIC_DEFAULT_FABLE_MODEL",
model_settings.sonnet.as_str(),
),
("CLAUDE_CODE_SUBAGENT_MODEL", subagent_model.as_str()),
] {
env.insert(
key.to_string(),
serde_json::Value::String(value.to_string()),
);
}
env.insert(
CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV.to_string(),
serde_json::Value::String(max_context_tokens.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()),
);
}
for (key, value) in claude_code_model_presentation_environment(&mappings, models)? {
env.insert(key, serde_json::Value::String(value));
}
if let Some(effort_level) = effort_level {
env.insert(
"CLAUDE_CODE_EFFORT_LEVEL".to_string(),
serde_json::Value::String(effort_level),
);
}
root.insert(
"model".to_string(),
serde_json::Value::String(mappings.sonnet),
serde_json::Value::String(model_settings.sonnet),
);
let mut rendered = serde_json::to_string_pretty(&serde_json::Value::Object(root.clone()))
.map_err(|error| format!("生成 Claude Code 配置失败: {error}"))?;
@@ -5692,7 +5899,10 @@ fn remove_claude_code_conflicting_api_key(path: &Path) -> Result<bool, String> {
})
}
fn update_claude_code_context_window(path: &Path, max_context_tokens: u64) -> Result<bool, String> {
fn update_claude_code_context_window(
path: &Path,
max_context_tokens: Option<u64>,
) -> Result<bool, String> {
update_agent_json_file(path, "Claude Code configuration", |root| {
let managed = root
.get("env")
@@ -5703,8 +5913,11 @@ fn update_claude_code_context_window(path: &Path, max_context_tokens: u64) -> Re
if !managed {
return false;
}
let expected = max_context_tokens.to_string();
let env = ensure_json_object_entry(root, "env");
let Some(max_context_tokens) = max_context_tokens else {
return env.remove(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV).is_some();
};
let expected = max_context_tokens.to_string();
if env
.get(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV)
.and_then(serde_json::Value::as_str)
@@ -5814,10 +6027,7 @@ fn build_claude_desktop_profile(
mappings: Option<&ClaudeDesktopModelMappings>,
) -> Result<String, String> {
let mut root = parse_agent_json_object(existing, "Claude Desktop 网关配置")?;
root.insert(
"coworkEgressAllowedHosts".to_string(),
serde_json::json!(["*"]),
);
root.remove("coworkEgressAllowedHosts");
root.insert(
"disableDeploymentModeChooser".to_string(),
serde_json::json!(true),
@@ -5879,9 +6089,15 @@ fn claude_desktop_inference_model(
route_model
}),
);
if context_window.is_some_and(|window| window >= CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW) {
entry.insert("supports1m".to_string(), serde_json::json!(true));
entry.insert("prefer1m".to_string(), serde_json::json!(true));
if let Some(context_window) = context_window {
entry.insert(
"contextWindow".to_string(),
serde_json::json!(context_window),
);
if context_window >= CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW {
entry.insert("supports1m".to_string(), serde_json::json!(true));
entry.insert("prefer1m".to_string(), serde_json::json!(true));
}
}
serde_json::Value::Object(entry)
}
@@ -19855,7 +20071,9 @@ model:
#[test]
fn claude_agent_config_preserves_existing_fields() {
let rendered = build_claude_agent_config(
Some(r#"{"theme":"dark","env":{"KEEP":"yes","ANTHROPIC_API_KEY":"legacy-key"}}"#),
Some(
r#"{"theme":"dark","env":{"KEEP":"yes","ANTHROPIC_API_KEY":"legacy-key","CLAUDE_CODE_EFFORT_LEVEL":"max"}}"#,
),
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"claude-test",
@@ -19871,9 +20089,46 @@ model:
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!(value["env"].get("CLAUDE_CODE_EFFORT_LEVEL").is_none());
assert_eq!(value["env"]["CLAUDE_CODE_SUBAGENT_MODEL"], "claude-test");
assert_eq!(
value["env"]["ANTHROPIC_CUSTOM_MODEL_OPTION_NAME"],
"claude-test (200K context)"
);
assert_eq!(value["model"], "claude-test");
}
#[test]
fn claude_code_inspection_normalizes_1m_suffix() {
let directory = agent_test_home("claude-code-1m-inspection");
let path = directory.join("settings.json");
fs::write(
&path,
r#"{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317",
"ANTHROPIC_AUTH_TOKEN": "test-key",
"ANTHROPIC_MODEL": "deepseek-v4-pro[1m]",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro[1m]",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-pro[1m]",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash"
},
"model": "deepseek-v4-pro[1m]"
}"#,
)
.unwrap();
let (configured, model) = inspect_claude_agent_config(&path, 8317, "test-key").unwrap();
let mappings = inspect_claude_code_model_mappings(&path).unwrap().unwrap();
assert!(configured);
assert_eq!(model.as_deref(), Some("deepseek-v4-pro"));
assert_eq!(mappings.opus, "deepseek-v4-pro");
assert_eq!(mappings.sonnet, "deepseek-v4-pro");
assert_eq!(mappings.haiku, "deepseek-v4-flash");
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn claude_code_role_mappings_drive_settings_and_launch_environment() {
let mappings = ClaudeDesktopModelMappings {
@@ -19904,7 +20159,7 @@ model:
name: mappings.haiku.clone(),
alias: None,
is_alias: false,
context_window: Some(512_000),
context_window: Some(128_000),
},
];
let rendered = build_claude_agent_config(
@@ -19922,13 +20177,19 @@ model:
assert_eq!(value["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"], "gpt-sonnet");
assert_eq!(value["env"]["ANTHROPIC_DEFAULT_HAIKU_MODEL"], "gpt-haiku");
assert_eq!(value["env"][CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV], "272000");
assert_eq!(
value["env"]["ANTHROPIC_CUSTOM_MODEL_OPTION_NAME"],
"gpt-sonnet (272K context)"
);
let environment = claude_code_launch_environment(
"http://127.0.0.1:8317",
"test-key",
&mappings,
&models,
claude_code_max_context_tokens(&mappings, &models).unwrap(),
);
)
.unwrap();
let read = |key: &str| {
environment
.iter()
@@ -19941,9 +20202,178 @@ model:
assert_eq!(read("ANTHROPIC_DEFAULT_OPUS_MODEL"), Some("gpt-opus"));
assert_eq!(read("ANTHROPIC_DEFAULT_SONNET_MODEL"), Some("gpt-sonnet"));
assert_eq!(read("ANTHROPIC_DEFAULT_HAIKU_MODEL"), Some("gpt-haiku"));
assert_eq!(read("CLAUDE_CODE_SUBAGENT_MODEL"), Some("gpt-haiku"));
assert_eq!(read("CLAUDE_CODE_EFFORT_LEVEL"), None);
assert_eq!(
read("ANTHROPIC_CUSTOM_MODEL_OPTION_NAME"),
Some("gpt-sonnet (272K context)")
);
assert_eq!(read(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV), Some("272000"));
}
#[test]
fn claude_code_deepseek_picker_shows_catalog_name_and_1m_context() {
let mappings = ClaudeDesktopModelMappings {
opus: "deepseek-v4-pro".to_string(),
sonnet: "deepseek-v4-pro".to_string(),
haiku: "deepseek-v4-flash".to_string(),
};
let models = vec![
AgentModelOption {
name: "deepseek-v4-pro".to_string(),
alias: Some("DeepSeek V4 Pro".to_string()),
is_alias: false,
context_window: Some(1_000_000),
},
AgentModelOption {
name: "deepseek-v4-flash".to_string(),
alias: Some("DeepSeek V4 Flash".to_string()),
is_alias: false,
context_window: Some(1_000_000),
},
];
let rendered = build_claude_agent_config(
None,
"http://127.0.0.1:8317",
"test-key",
"deepseek-v4-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"]["ANTHROPIC_DEFAULT_OPUS_MODEL"],
"deepseek-v4-pro[1m]"
);
assert_eq!(
value["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"],
"deepseek-v4-pro[1m]"
);
assert_eq!(
value["env"]["ANTHROPIC_DEFAULT_HAIKU_MODEL"],
"deepseek-v4-flash"
);
assert_eq!(
value["env"]["ANTHROPIC_DEFAULT_FABLE_MODEL"],
"deepseek-v4-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"]["ANTHROPIC_CUSTOM_MODEL_OPTION"],
"deepseek-v4-pro[1m]"
);
assert_eq!(
value["env"]["ANTHROPIC_CUSTOM_MODEL_OPTION_NAME"],
"DeepSeek V4 Pro (1M context)"
);
assert_eq!(
value["env"]["ANTHROPIC_DEFAULT_FABLE_MODEL_NAME"],
"DeepSeek V4 Pro (Fable mapping, 1M context)"
);
assert_eq!(value["model"], "deepseek-v4-pro[1m]");
let environment = claude_code_launch_environment(
"http://127.0.0.1:8317",
"test-key",
&mappings,
&models,
claude_code_max_context_tokens(&mappings, &models).unwrap(),
)
.unwrap();
assert!(!environment
.iter()
.any(|(key, _)| key == CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV));
}
#[test]
fn claude_code_1m_suffix_requires_explicit_catalog_support() {
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(&models, "deepseek-v4-flash", true).unwrap(),
"deepseek-v4-flash[1m]"
);
assert_eq!(
claude_code_model_setting(&models, "deepseek-v4-flash", false).unwrap(),
"deepseek-v4-flash"
);
assert_eq!(
claude_code_model_setting(&models, "gpt-runtime-1m", true).unwrap(),
"gpt-runtime-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(),
};
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");
@@ -19971,8 +20401,8 @@ model:
.unwrap();
assert!(remove_claude_code_conflicting_api_key(&path).unwrap());
assert!(update_claude_code_context_window(&path, 272_000).unwrap());
assert!(!update_claude_code_context_window(&path, 272_000).unwrap());
assert!(update_claude_code_context_window(&path, Some(272_000)).unwrap());
assert!(!update_claude_code_context_window(&path, Some(272_000)).unwrap());
let value: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert!(value["env"].get("ANTHROPIC_API_KEY").is_none());
@@ -19981,6 +20411,26 @@ model:
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn claude_code_1m_variant_removes_global_context_override() {
let directory = agent_test_home("claude-code-remove-context-override");
let path = directory.join("settings.json");
fs::write(
&path,
r#"{"env":{"ANTHROPIC_BASE_URL":"http://127.0.0.1:8317","CLAUDE_CODE_MAX_CONTEXT_TOKENS":"1000000"}}"#,
)
.unwrap();
assert!(update_claude_code_context_window(&path, None).unwrap());
assert!(!update_claude_code_context_window(&path, None).unwrap());
let value: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert!(value["env"]
.get(CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV)
.is_none());
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn claude_code_working_directory_must_be_absolute_and_exist() {
let directory = agent_test_home("claude-code-working-directory");
@@ -20175,7 +20625,7 @@ model:
#[test]
fn claude_desktop_config_builds_gateway_profile_and_index() {
let profile = build_claude_desktop_profile(
Some(r#"{"keep":true}"#),
Some(r#"{"keep":true,"coworkEgressAllowedHosts":["*"]}"#),
"http://127.0.0.1:8317",
DEFAULT_API_KEY,
"claude-sonnet-test",
@@ -20193,6 +20643,7 @@ model:
let meta: serde_json::Value = serde_json::from_str(&meta).unwrap();
assert_eq!(profile["keep"], true);
assert!(profile.get("coworkEgressAllowedHosts").is_none());
assert_eq!(profile["inferenceGatewayApiKey"], DEFAULT_API_KEY);
assert_eq!(profile["inferenceGatewayBaseUrl"], "http://127.0.0.1:8317");
assert_eq!(
@@ -20257,12 +20708,14 @@ model:
serde_json::json!([
{
"name": CLAUDE_DESKTOP_OPUS_MODEL_ID,
"contextWindow": 1000000,
"supports1m": true,
"prefer1m": true
},
{ "name": CLAUDE_DESKTOP_SONNET_MODEL_ID },
{ "name": CLAUDE_DESKTOP_SONNET_MODEL_ID, "contextWindow": 272000 },
{
"name": CLAUDE_DESKTOP_HAIKU_MODEL_ID,
"contextWindow": 1000000,
"supports1m": true,
"prefer1m": true
}
@@ -20722,6 +21175,7 @@ oauth-model-alias:
profile["inferenceModels"],
serde_json::json!([{
"name": "gpt-high",
"contextWindow": 1_000_000,
"supports1m": true,
"prefer1m": true
}])
+1 -1
View File
@@ -805,7 +805,7 @@ export const zhCN = {
'kernel.update.statusChecking': '正在检查',
'kernel.update.notYetChecked': '尚未检查',
'kernel.update.latest': '已是最新版本',
'kernel.update.available': '可安装新版本',
'kernel.update.available': '请在首页中关闭运行后安装新版本',
'appUpdate.eyebrow': '软件升级',
'appUpdate.title': '软件版本管理',
'appUpdate.current': '当前版本',