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:
1 parent
cc2636d815
commit
03cf1167f7
5 files changed
+813
-55
No files matched your search
+1
-1
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}])
|
||||
|
||||
@@ -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': '当前版本',
|
||||
|
||||
Reference in new issue
Block a user