From 607c930d7b2f08c145f7430be05b8001535a7db0 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Sat, 15 Aug 2026 14:58:56 +0800 Subject: [PATCH] fix(zcode): sync model context windows from CPA core --- src-tauri/src/agents/commands.rs | 4 ++ src-tauri/src/agents/configuration.rs | 17 +++---- src-tauri/src/agents/discovery.rs | 44 +++++++++++------- src-tauri/src/codex_catalog.rs | 54 ++++++++++++++++++++++ src-tauri/src/tests/agent_configuration.rs | 49 +++++++++++++++++++- src/i18n/ja.ts | 2 +- src/i18n/locales/en.ts | 2 +- src/i18n/locales/zh-CN.ts | 2 +- 8 files changed, 146 insertions(+), 28 deletions(-) diff --git a/src-tauri/src/agents/commands.rs b/src-tauri/src/agents/commands.rs index 0f49096..5140e85 100644 --- a/src-tauri/src/agents/commands.rs +++ b/src-tauri/src/agents/commands.rs @@ -640,6 +640,10 @@ pub(crate) async fn fetch_prepared_agent_models( prepare_codex_agent_models(&runtime_models) } else { let mut models = fetch_agent_models(config.port, api_key).await?; + if client == AgentClient::ZCode { + let runtime_models = fetch_codex_runtime_models(config.port, api_key).await?; + codex_catalog::merge_runtime_context_windows(&mut models, &runtime_models); + } if matches!(client, AgentClient::ClaudeCode | AgentClient::ClaudeDesktop) { let content = fetch_management_config_yaml(config).await?; mark_configured_agent_model_aliases(&mut models, &content)?; diff --git a/src-tauri/src/agents/configuration.rs b/src-tauri/src/agents/configuration.rs index d400094..cc0f4e6 100644 --- a/src-tauri/src/agents/configuration.rs +++ b/src-tauri/src/agents/configuration.rs @@ -2713,14 +2713,15 @@ pub(crate) fn build_zcode_agent_config( .into_iter() .map(|model| { let display_name = model.alias.as_deref().unwrap_or(&model.name).to_string(); - ( - model.name, - serde_json::json!({ - "name": display_name, - "kinds": ["anthropic"], - "defaultKind": "anthropic" - }), - ) + let mut entry = serde_json::Map::new(); + entry.insert("name".to_string(), serde_json::json!(display_name)); + if let Some(context_window) = model.context_window { + entry.insert( + "limit".to_string(), + serde_json::json!({ "context": context_window }), + ); + } + (model.name, serde_json::Value::Object(entry)) }) .collect::>(); let managed_provider = ensure_json_object_entry(providers, MANAGED_AGENT_PROVIDER_ID); diff --git a/src-tauri/src/agents/discovery.rs b/src-tauri/src/agents/discovery.rs index 5e9bacd..eb1f542 100644 --- a/src-tauri/src/agents/discovery.rs +++ b/src-tauri/src/agents/discovery.rs @@ -2464,18 +2464,38 @@ pub(crate) fn inspect_zcode_agent_config( .get("provider") .and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID)); let expected_base = format!("http://127.0.0.1:{port}"); - let configured = provider - .and_then(|provider| provider.get("enabled")) - .and_then(serde_json::Value::as_bool) - == Some(true) + let api_format_matches = provider + .and_then(|provider| provider.get("apiFormat")) + .and_then(serde_json::Value::as_str) + .is_none_or(|api_format| api_format == "anthropic-messages"); + let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/"); + let model = root + .get("model") + .and_then(serde_json::Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .and_then(|value| value.strip_prefix(&prefix)) + .map(str::to_string); + let selected_model_exists = model.as_deref().is_some_and(|selected_model| { + provider + .and_then(|provider| provider.get("models")) + .and_then(serde_json::Value::as_object) + .is_some_and(|models| { + models + .keys() + .any(|model| model.eq_ignore_ascii_case(selected_model)) + }) + }); + let configured = selected_model_exists + && api_format_matches + && provider + .and_then(|provider| provider.get("enabled")) + .and_then(serde_json::Value::as_bool) + == Some(true) && provider .and_then(|provider| provider.get("kind")) .and_then(serde_json::Value::as_str) == Some("anthropic") - && provider - .and_then(|provider| provider.get("apiFormat")) - .and_then(serde_json::Value::as_str) - == Some("anthropic-messages") && provider .and_then(|provider| provider.get("options")) .and_then(|options| options.get("baseURL")) @@ -2486,14 +2506,6 @@ pub(crate) fn inspect_zcode_agent_config( .and_then(|options| options.get("apiKey")) .and_then(serde_json::Value::as_str) == Some(api_key); - let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/"); - let model = root - .get("model") - .and_then(serde_json::Value::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(|value| value.strip_prefix(&prefix).unwrap_or(value)) - .map(str::to_string); Ok((configured, model)) } diff --git a/src-tauri/src/codex_catalog.rs b/src-tauri/src/codex_catalog.rs index 1bfa837..fa6109c 100644 --- a/src-tauri/src/codex_catalog.rs +++ b/src-tauri/src/codex_catalog.rs @@ -154,6 +154,26 @@ pub(crate) fn parse_runtime_models(payload: &Value) -> Result Result { @@ -873,6 +893,40 @@ mod tests { assert_eq!(catalog.models[1].alias.as_deref(), Some("gpt-5.5")); } + #[test] + fn runtime_context_windows_override_generic_model_metadata() { + let mut models = vec![ + AgentModelOption { + name: "GPT-Test".to_string(), + alias: None, + is_alias: false, + context_window: Some(200_000), + }, + AgentModelOption { + name: "unmatched".to_string(), + alias: None, + is_alias: false, + context_window: Some(128_000), + }, + AgentModelOption { + name: "max-only".to_string(), + alias: None, + is_alias: false, + context_window: None, + }, + ]; + let runtime_models = runtime(serde_json::json!({"models":[ + {"id":"gpt-test","context_window":372000}, + {"id":"max-only","max_context_window":272000} + ]})); + + merge_runtime_context_windows(&mut models, &runtime_models); + + assert_eq!(models[0].context_window, Some(372_000)); + assert_eq!(models[1].context_window, Some(128_000)); + assert_eq!(models[2].context_window, Some(272_000)); + } + #[test] fn all_runtime_models_advertise_fast_capabilities() { let runtime = runtime(serde_json::json!({"models":[ diff --git a/src-tauri/src/tests/agent_configuration.rs b/src-tauri/src/tests/agent_configuration.rs index f607d89..40c5908 100644 --- a/src-tauri/src/tests/agent_configuration.rs +++ b/src-tauri/src/tests/agent_configuration.rs @@ -617,7 +617,9 @@ fn zcode_agent_config_preserves_other_providers_and_uses_anthropic_messages() { let home = agent_test_home("zcode-agent-config"); let path = home.join(".zcode/v2/config.json"); fs::create_dir_all(path.parent().unwrap()).unwrap(); - let models = test_agent_models(&["gpt-test", "deepseek-test"]); + let mut models = test_agent_models(&["gpt-test", "deepseek-test"]); + models[0].context_window = Some(372_000); + models[1].context_window = Some(272_000); let rendered = build_zcode_agent_config( Some( r#"{"locale":"zh-CN","provider":{"other":{"kind":"openai"},"cpa-gui":{"custom":"keep","options":{"timeout":30}}}}"#, @@ -654,10 +656,55 @@ fn zcode_agent_config_preserves_other_providers_and_uses_anthropic_messages() { "http://127.0.0.1:8317" ); assert_eq!(value["model"], "cpa-gui/gpt-test"); + assert_eq!( + value["provider"][MANAGED_AGENT_PROVIDER_ID]["models"]["gpt-test"]["limit"]["context"], + 372_000 + ); + assert_eq!( + value["provider"][MANAGED_AGENT_PROVIDER_ID]["models"]["deepseek-test"]["limit"]["context"], + 272_000 + ); assert_eq!( inspect_zcode_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(), (true, Some("gpt-test".to_string())) ); + + let mut normalized = value; + let provider = normalized["provider"][MANAGED_AGENT_PROVIDER_ID] + .as_object_mut() + .unwrap(); + provider.remove("apiFormat"); + provider.remove("defaultKind"); + provider.remove("npm"); + fs::write(&path, serde_json::to_string_pretty(&normalized).unwrap()).unwrap(); + assert_eq!( + inspect_zcode_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(), + (true, Some("gpt-test".to_string())) + ); + fs::remove_dir_all(home).unwrap(); +} + +#[test] +fn zcode_inspection_requires_the_default_model_to_belong_to_the_managed_provider() { + let home = agent_test_home("zcode-default-model-inspection"); + let path = home.join(".zcode/v2/config.json"); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let rendered = build_zcode_agent_config( + None, + "http://127.0.0.1:8317", + DEFAULT_API_KEY, + "gpt-test", + &test_agent_models(&["gpt-test"]), + ) + .unwrap(); + let mut value: serde_json::Value = serde_json::from_str(&rendered).unwrap(); + value["model"] = serde_json::json!("other/gpt-test"); + fs::write(&path, serde_json::to_string_pretty(&value).unwrap()).unwrap(); + + assert_eq!( + inspect_zcode_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap(), + (false, None) + ); fs::remove_dir_all(home).unwrap(); } diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index b3207d9..d328521 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -620,7 +620,7 @@ export const jaOverrides = { 'agents.modify.default': 'デフォルト設定', 'agents.modify.clear': '設定を消去', 'agents.modify.invalid': '現在の設定を解析できません。「設定変更を適用」は最小の有効な設定を再構築し、「デフォルト設定」はファイル全体をリセットします。', - 'agents.modify.zcodeRestart': '設定の適用または終了後、モデルプロバイダーを再読み込みするため ZCode を完全終了して再起動してください。', + 'agents.modify.zcodeRestart': '設定の適用または終了後、ZCode を完全終了して再起動してください。デフォルトモデルとコンテキスト長は新しいセッションで有効になります。', 'agents.pi.installTitle': 'Pi CLIProxyAPI プロバイダー', 'agents.pi.installDescription': 'プラグインをインストールまたは更新し、CPA 認証情報、既定のプロバイダー、既定のモデルを修復します', 'agents.pi.install': 'Pi プラグインをインストール', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 49656ec..af1536c 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -620,7 +620,7 @@ export const en: Record = { 'agents.modify.default': 'Default Configuration', 'agents.modify.clear': 'Clear Configuration', 'agents.modify.invalid': 'The current configuration cannot be parsed. Apply Configuration Changes rebuilds a minimal valid configuration; Default Configuration resets the whole file.', - 'agents.modify.zcodeRestart': 'Fully quit and restart ZCode after applying or closing the configuration so it reloads model providers.', + 'agents.modify.zcodeRestart': 'Fully quit and restart ZCode after applying or closing the configuration; the default model and context length take effect in new sessions.', 'agents.pi.installTitle': 'Pi CLIProxyAPI provider', 'agents.pi.installDescription': 'Install or update the plugin and repair CPA credentials, the default provider, and the default model.', 'agents.pi.install': 'Install Pi Plugin', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index d5d1c8b..2e38336 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -618,7 +618,7 @@ export const zhCN = { 'agents.modify.default': '默认配置', 'agents.modify.clear': '清空配置', 'agents.modify.invalid': '当前配置无法解析。点击“应用配置修改”会重建最小合法配置;“默认配置”会重置整个文件。', - 'agents.modify.zcodeRestart': '应用或关闭配置后,请完全退出并重新启动 ZCode 以加载模型供应商。', + 'agents.modify.zcodeRestart': '应用或关闭配置后,请完全退出并重新启动 ZCode;默认模型和上下文长度会在新会话中生效。', 'agents.pi.installTitle': 'Pi CLIProxyAPI provider', 'agents.pi.installDescription': '安装或更新插件,并修复 CPA 凭据、默认 provider 与默认模型', 'agents.pi.install': '安装 Pi 插件',