From ca76f344fbfd04a3a5120f9576038453ee1beb04 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Thu, 10 Sep 2026 14:22:02 +0800 Subject: [PATCH] fix(codex): preserve context defaults from the client model API --- src-tauri/src/agents/commands.rs | 98 ++----------------- src-tauri/src/codex_catalog.rs | 4 +- .../src/codex_catalog/runtime_context.rs | 94 +++++++----------- src/i18n/ja.ts | 29 +++++- src/i18n/locales/en.ts | 29 +++++- src/i18n/locales/zh-CN.ts | 29 +++++- src/services/codexModelCatalog.ts | 2 +- tests/codexModelCatalog.test.ts | 12 +-- 8 files changed, 126 insertions(+), 171 deletions(-) diff --git a/src-tauri/src/agents/commands.rs b/src-tauri/src/agents/commands.rs index 2602145..2e24f27 100644 --- a/src-tauri/src/agents/commands.rs +++ b/src-tauri/src/agents/commands.rs @@ -135,7 +135,7 @@ pub(crate) async fn refresh_agent_config_statuses( #[tauri::command] pub(crate) async fn get_agent_models( - app: tauri::AppHandle, + _app: tauri::AppHandle, gui_config_state: tauri::State<'_, GuiConfigState>, client: String, ) -> Result, String> { @@ -151,11 +151,6 @@ pub(crate) async fn get_agent_models( }; let config = gui_config_state.snapshot()?; let prepared = fetch_prepared_agent_models(client, &config).await?; - if client == AgentClient::Codex { - if let Err(error) = sync_prepared_codex_model_catalog(&app, &config, &prepared) { - eprintln!("自动刷新已应用的 Codex 模型目录失败: {error}"); - } - } Ok(prepared.models) } @@ -794,53 +789,20 @@ pub(crate) async fn fetch_codex_runtime_models( Err("本地内核不支持 Codex 模型列表接口".to_string()) } -// The client_version response contains synthesized Codex templates; its context -// fields are not the raw core model definitions. Resolve defaults through the -// management APIs before generating or editing the Codex catalog. +// Codex client metadata distinguishes the default context from the maximum. +// Do not overwrite either with the generic model-definitions context_length. pub(crate) async fn fetch_codex_catalog_runtime_models( config: &GuiConfigFile, ) -> Result, String> { - let (runtime, definitions, content) = tokio::join!( + let (runtime, content) = tokio::join!( fetch_codex_runtime_models(config.port, effective_agent_api_key(config)), - fetch_codex_context_definitions(config), fetch_management_config_yaml(config), ); let mut runtime = runtime?; - let definitions = definitions?; - let content = content?; - let mut aliases = runtime.iter().map(|model| AgentModelOption { - name: model.slug.clone(), alias: None, is_alias: false, context_window: None, - }).collect::>(); - mark_configured_agent_model_aliases(&mut aliases, &content)?; - codex_catalog::merge_context_definitions(&mut runtime, &definitions, &aliases); - codex_catalog::apply_configured_context_limits(&mut runtime, &content)?; + codex_catalog::apply_configured_context_limits(&mut runtime, &content?)?; Ok(runtime) } -async fn fetch_codex_context_definitions( - config: &GuiConfigFile, -) -> Result, String> { - // API-key sources may expose models from other channels, so do not filter by - // active OAuth credentials. Model IDs still come only from the available list. - let channels = ["gemini", "vertex", "aistudio", "antigravity", "claude", "codex", "kimi", "xai"]; - let results = futures_util::future::join_all(channels.iter().map(|channel| - fetch_oauth_channel_model_definitions(config, channel) - )).await; - let mut definitions = Vec::new(); - let mut successes = 0; - let mut first_error = None; - for result in results { - match result { - Ok(models) => { successes += 1; definitions.extend(models); } - Err(error) => { first_error.get_or_insert(error); } - } - } - if successes == 0 { - return Err(format!("读取 CPA 模型上下文定义失败: {}", first_error.unwrap_or_default())); - } - Ok(definitions) -} - pub(crate) async fn fetch_prepared_agent_models( client: AgentClient, config: &GuiConfigFile, @@ -1267,56 +1229,12 @@ pub(crate) async fn update_agent_config( gui_config_state: tauri::State<'_, GuiConfigState>, client: String, model: String, + oauth_configuration: bool, claude_code_model_mappings: Option, claude_desktop_model_mappings: Option, ) -> Result { - let client = AgentClient::parse(&client)?; - let home = app - .path() - .home_dir() - .map_err(|error| format!("无法获取用户目录: {error}"))?; - let config = gui_config_state.snapshot()?; - let port = config.port; - let api_key = effective_agent_api_key(&config); - let prepared = fetch_prepared_agent_models(client, &config).await?; - let model = resolve_available_agent_model(&prepared.models, &validate_agent_model(&model)?)?; - let claude_code_model_mappings = resolve_claude_code_model_mappings( - client, - &prepared.models, - &model, - claude_code_model_mappings, - )?; - let claude_desktop_model_mappings = resolve_claude_desktop_model_mappings( - client, - &prepared.models, - &model, - claude_desktop_model_mappings, - )?; - if let Some(mappings) = claude_desktop_model_mappings.as_ref() { - ensure_claude_desktop_model_aliases(&config, mappings, &prepared.models).await?; - } - let _guard = AGENT_CONFIG_FILE_LOCK - .lock() - .map_err(|_| "智能体配置文件锁已损坏".to_string())?; - let oauth_configuration = if client == AgentClient::Codex { - current_codex_oauth_configuration(&home)? - } else { - false - }; - apply_agent_configuration_with_oauth( - client, - &home, - port, - api_key, - &model, - AgentConfigurationOptions { - models: &prepared.models, - codex_catalog: prepared.codex_catalog.as_deref(), - oauth_configuration, - claude_code_model_mappings: claude_code_model_mappings.as_ref(), - claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(), - }, - ) + apply_agent_config(app, gui_config_state, client, model, oauth_configuration, + claude_code_model_mappings, claude_desktop_model_mappings).await } pub(crate) fn validate_agent_can_enable( diff --git a/src-tauri/src/codex_catalog.rs b/src-tauri/src/codex_catalog.rs index ab64260..43dc7ed 100644 --- a/src-tauri/src/codex_catalog.rs +++ b/src-tauri/src/codex_catalog.rs @@ -9,7 +9,7 @@ pub(crate) use customizations::{ editor_snapshot, load_customizations, save_customizations, CatalogEditorRequest, CatalogEditorSnapshot, }; -pub(crate) use runtime_context::{apply_configured_context_limits, merge_context_definitions}; +pub(crate) use runtime_context::apply_configured_context_limits; const MODEL_CATALOG_JSON: &str = include_str!("../resources/codex_models/model-catalog.json"); const FALLBACK_MODEL_JSON: &str = include_str!("../resources/codex_models/fallback-model.json"); @@ -165,7 +165,7 @@ pub(crate) fn parse_runtime_models(payload: &Value) -> Result 0) - .min(); - if let Some(context) = context { - runtime.context_window = Some(context); - runtime.max_context_window = - Some(runtime.max_context_window.unwrap_or(context).max(context)); - runtime.context_source = "definition"; - } - } -} - pub(crate) fn apply_configured_context_limits( runtime_models: &mut [CodexRuntimeModel], content: &str, @@ -85,32 +58,13 @@ mod tests { use super::*; #[test] - fn raw_core_definitions_replace_synthesized_272k_for_models_and_aliases() { - let mut runtime = parse_runtime_models(&serde_json::json!({"models":[ + fn codex_client_context_defaults_are_preserved_in_editor_and_generated_catalog() { + let runtime = parse_runtime_models(&serde_json::json!({"models":[ {"slug":"gpt-5.6-sol","context_window":272000,"max_context_window":872000}, - {"slug":"gemini-3.8-flash-high","context_window":272000,"max_context_window":272000}, - {"slug":"fast-alias","context_window":272000,"max_context_window":272000}, - {"slug":"no-definition","context_window":272000,"max_context_window":272000} + {"slug":"fast-alias","context_window":128000,"max_context_window":256000}, + {"slug":"missing-context"} ]})) .unwrap(); - let definitions = crate::parse_codex_model_definitions(&serde_json::json!({"models":[ - {"id":"gpt-5.6-sol","context_length":921000}, - {"id":"gemini-3.8-flash-high","context_length":1048576} - ]})) - .unwrap(); - let aliases = vec![AgentModelOption { - name: "fast-alias".to_string(), - alias: Some("GPT-5.6-SOL".to_string()), - is_alias: true, - context_window: None, - }]; - merge_context_definitions(&mut runtime, &definitions, &aliases); - assert_eq!(runtime[0].context_window, Some(921_000)); - assert_eq!(runtime[0].max_context_window, Some(921_000)); - assert_eq!(runtime[1].context_window, Some(1_048_576)); - assert_eq!(runtime[2].context_window, Some(921_000)); - assert_eq!(runtime[2].context_source, "definition"); - assert_eq!(runtime[3].context_source, "compatibility"); let sources = parse_sources(MODEL_CATALOG_JSON).unwrap(); let state = CatalogState { sources, @@ -119,19 +73,39 @@ mod tests { }; let snapshot = customizations::snapshot_for_state(&runtime, &state).unwrap(); let snapshot = serde_json::to_value(snapshot).unwrap(); - let model = snapshot["models"] - .as_array() - .unwrap() - .iter() - .find(|model| model["slug"] == "gpt-5.6-sol") - .unwrap(); - assert_eq!(model["configuration"]["context_window"], 921_000); - assert_eq!(model["defaults"]["context_window"], 921_000); - assert_eq!(model["contextSource"], "definition"); + let prepared = + prepare_catalog_with_customizations(&runtime, &state.sources, &state.customizations) + .unwrap(); + let catalog: Value = serde_json::from_str(&prepared.json).unwrap(); + for (slug, context, maximum, source) in [ + ("gpt-5.6-sol", 272_000, 872_000, "client"), + ("fast-alias", 128_000, 256_000, "client"), + ("missing-context", 272_000, 272_000, "template"), + ] { + let model = snapshot["models"] + .as_array() + .unwrap() + .iter() + .find(|model| model["slug"] == slug) + .unwrap(); + for field in ["configuration", "defaults"] { + assert_eq!(model[field]["context_window"], context); + assert_eq!(model[field]["max_context_window"], maximum); + } + assert_eq!(model["contextSource"], source); + let generated = catalog["models"] + .as_array() + .unwrap() + .iter() + .find(|model| model["slug"] == slug) + .unwrap(); + assert_eq!(generated["context_window"], context); + assert_eq!(generated["max_context_window"], maximum); + } } #[test] - fn core_configured_context_limit_takes_precedence_over_static_definitions() { + fn core_configured_context_limit_takes_precedence_over_client_defaults() { let mut runtime = parse_runtime_models(&serde_json::json!({"models":[ {"slug":"custom-alias","context_window":921000,"max_context_window":921000}, {"slug":"unmodified","context_window":128000,"max_context_window":128000} diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index cd5c315..d0d1ad9 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -76,9 +76,8 @@ export const jaOverrides = { 'agents.catalog.displayName': '表示名', 'agents.catalog.description': 'モデルの説明', 'agents.catalog.context': 'コンテキストウィンドウ(トークン)', - 'agents.catalog.contextSource.definition': 'コンテキストの取得元:CPA コア API のモデル定義。', + 'agents.catalog.contextSource.client': 'コンテキストの取得元:CPA コアの Codex クライアントモデル一覧 API。', 'agents.catalog.contextSource.configuration': 'コンテキストの取得元:CPA コアでこのモデルに設定された max-context-length。', - 'agents.catalog.contextSource.compatibility': 'CPA にこのモデル固有のコンテキスト定義がありません。現在の値は互換カタログの値であり、汎用テンプレートの既定値である可能性があります。', 'agents.catalog.contextSource.template': 'コアに有効なコンテキスト情報がありません。ソフトウェアのテンプレートの代替値を使用しています。', 'agents.catalog.contextSource.customized': 'コンテキストの取得元:このモデルのカスタム設定。', 'agents.catalog.maximum': '最大コンテキストウィンドウ(トークン)', @@ -810,6 +809,28 @@ export const jaOverrides = { 'agents.status.invalid': '設定が無効', 'agents.status.applied': '適用済み', 'agents.status.unconfigured': '未設定', + "agents.history.button": "設定履歴を復元", + "agents.history.description": "ソフトウェア管理の項目のみ復元し、他の設定を保持します。復元前に現在の設定を保存し、フォームを更新します。", + "agents.history.versions": "履歴", + "agents.history.empty": "履歴はありません。設定更新時に自動保存されます。", + "agents.history.files": "{count} ファイル", + "agents.history.preview": "復元プレビュー", + "agents.history.loading": "読み込み中…", + "agents.history.select": "履歴を選択して復元する項目を確認してください。", + "agents.history.unchanged": "設定は最新です。", + "agents.history.updated": "設定を更新し、履歴を保存しました。", + "agents.history.restored": "設定を復元しました。他の設定は保持されています。", + "agents.history.field": "項目", + "agents.history.current": "現在", + "agents.history.target": "履歴値", + "agents.history.restore": "この履歴を復元", + "agents.history.before": "{source}前のバックアップ", + "agents.history.source.update": "設定更新", + "agents.history.source.default": "既定設定", + "agents.history.source.clear": "設定消去", + "agents.history.source.restore": "復元", + "agents.history.source.sync": "自動同期", + "agents.history.source.legacy": "旧バージョンのバックアップ", 'agents.title': 'エージェント設定', 'agents.embedded.title': 'クライアントとモデルを選んで起動', 'agents.embedded.subtitle': 'ない場合は先にインストールしてください', @@ -937,10 +958,10 @@ export const jaOverrides = { 'agents.pi.uninstall': 'Pi プラグインをアンインストール', 'agents.pi.uninstalling': 'プラグインをアンインストール中', 'agents.default.title': 'デフォルト設定にリセット', - 'agents.default.description': '選択中のモデルを使って {name} の関連設定ファイルをすべて書き直し、その他のユーザー内容を削除します。バックアップは作成されず、元に戻せません。', + 'agents.default.description': '選択中のモデルを使って {name} の関連設定ファイルをすべて書き直し、その他のユーザー内容を削除します。操作前に履歴を保存します。履歴から復元できるのは管理対象フィールドのみで、削除された独自設定は復元されません。', 'agents.default.confirm': 'デフォルト設定を書き込む', 'agents.clear.title': 'Codex 設定を消去', - 'agents.clear.description': 'Codex 設定ディレクトリの auth.json と config.toml を完全に削除し、このアプリが記録した適用状態をリセットします。バックアップは作成されず、元に戻せません。', + 'agents.clear.description': 'Codex 設定ディレクトリの auth.json と config.toml を完全に削除します。操作前に履歴を保存します。履歴から復元できるのは管理対象フィールドのみで、削除された独自設定は復元されません。', 'agents.clear.confirm': '設定を消去', 'agents.clear.success': 'Codex 設定を消去しました。', 'apiAccess.compatibleName': 'OpenAI 互換 {number}', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index a22291e..ab35f6d 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -26,9 +26,8 @@ export const en: Record = { 'agents.catalog.displayName': 'Display name', 'agents.catalog.description': 'Description', 'agents.catalog.context': 'Context window (tokens)', - 'agents.catalog.contextSource.definition': 'Context source: model definitions from the CPA core API.', + 'agents.catalog.contextSource.client': 'Context source: the CPA core Codex client model catalog API.', 'agents.catalog.contextSource.configuration': 'Context source: max-context-length configured for this model in the CPA core.', - 'agents.catalog.contextSource.compatibility': 'CPA provides no independent context definition for this model. The current value comes from its compatibility catalog and may be a generic template default.', 'agents.catalog.contextSource.template': 'The core provides no valid context data. The current value is a fallback from the application template.', 'agents.catalog.contextSource.customized': 'Context source: your custom settings for this model.', 'agents.catalog.maximum': 'Maximum context window (tokens)', @@ -810,6 +809,28 @@ export const en: Record = { 'agents.status.invalid': 'Invalid Configuration', 'agents.status.applied': 'Applied', 'agents.status.unconfigured': 'Not Configured', + "agents.history.button": "Restore configuration history", + "agents.history.description": "Restore only software-managed fields and preserve other settings. Current configuration is backed up first; form drafts refresh after restoring.", + "agents.history.versions": "Versions", + "agents.history.empty": "No history yet. Updating configuration saves versions automatically.", + "agents.history.files": "{count} files", + "agents.history.preview": "Restore preview", + "agents.history.loading": "Loading…", + "agents.history.select": "Select a version to preview the fields to restore.", + "agents.history.unchanged": "Configuration is already up to date.", + "agents.history.updated": "Configuration updated and history saved.", + "agents.history.restored": "Configuration restored. Other custom settings were preserved.", + "agents.history.field": "Field", + "agents.history.current": "Current", + "agents.history.target": "Historical", + "agents.history.restore": "Restore this version", + "agents.history.before": "Before {source}", + "agents.history.source.update": "Update", + "agents.history.source.default": "Default configuration", + "agents.history.source.clear": "Clear configuration", + "agents.history.source.restore": "Restore", + "agents.history.source.sync": "Automatic sync", + "agents.history.source.legacy": "Legacy backup", 'agents.title': 'Agent Configuration', 'agents.embedded.title': 'Select a client and model, then start', 'agents.embedded.subtitle': 'Install it first if it is not available', @@ -937,10 +958,10 @@ export const en: Record = { 'agents.pi.uninstall': 'Uninstall Pi Plugin', 'agents.pi.uninstalling': 'Uninstalling plugin', 'agents.default.title': 'Reset to Default Configuration', - 'agents.default.description': 'This rewrites every related {name} configuration file using the selected model and deletes all other user content in those files. No backup is created and the operation cannot be undone.', + 'agents.default.description': 'This rewrites every related {name} configuration file using the selected model and deletes all other user content in those files. A history snapshot is saved first. History recovery restores only managed fields, not deleted custom settings.', 'agents.default.confirm': 'Write Default Configuration', 'agents.clear.title': 'Clear Codex Configuration', - 'agents.clear.description': 'This permanently deletes auth.json and config.toml from the Codex configuration directory and resets the application state tracked by this app. No backup is created and the operation cannot be undone.', + 'agents.clear.description': 'This permanently deletes auth.json and config.toml from the Codex configuration directory. A history snapshot is saved first. History recovery restores only managed fields, not deleted custom settings.', 'agents.clear.confirm': 'Clear Configuration', 'agents.clear.success': 'The Codex configuration has been cleared.', 'apiAccess.compatibleName': 'OpenAI Compatible {number}', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 7a32628..f7266ed 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -25,9 +25,8 @@ export const zhCN = { 'agents.catalog.displayName': '显示名称', 'agents.catalog.description': '模型说明', 'agents.catalog.context': '上下文窗口(tokens)', - 'agents.catalog.contextSource.definition': '上下文来源:CPA 内核的模型定义 API。', + 'agents.catalog.contextSource.client': '上下文来源:CPA 内核的 Codex 客户端模型列表 API。', 'agents.catalog.contextSource.configuration': '上下文来源:CPA 内核中为此模型配置的 max-context-length。', - 'agents.catalog.contextSource.compatibility': 'CPA 未提供此模型的独立上下文定义,当前值来自兼容目录,可能是通用模板默认值。', 'agents.catalog.contextSource.template': '内核未提供有效的上下文数据,当前使用软件模板的后备值。', 'agents.catalog.contextSource.customized': '上下文来源:你为此模型保存或编辑的自定义配置。', 'agents.catalog.maximum': '最大上下文窗口(tokens)', @@ -809,6 +808,28 @@ export const zhCN = { 'agents.status.invalid': '配置无效', 'agents.status.applied': '已应用', 'agents.status.unconfigured': '尚未配置', + "agents.history.button": "恢复历史配置", + "agents.history.description": "仅恢复软件管理的配置字段,保留其他自定义设置。恢复前会自动备份当前配置;当前表单草稿将按恢复结果刷新。", + "agents.history.versions": "历史版本", + "agents.history.empty": "暂无配置历史。更新配置后会自动保留版本。", + "agents.history.files": "{count} 个文件", + "agents.history.preview": "恢复变更预览", + "agents.history.loading": "正在读取…", + "agents.history.select": "选择一个历史版本,查看将恢复的字段。", + "agents.history.unchanged": "配置已是最新,无需更新。", + "agents.history.updated": "配置已更新,历史备份已保存。", + "agents.history.restored": "已恢复历史配置,其他自定义设置已保留。", + "agents.history.field": "配置字段", + "agents.history.current": "当前值", + "agents.history.target": "历史值", + "agents.history.restore": "恢复此版本", + "agents.history.before": "{source}前备份", + "agents.history.source.update": "更新配置", + "agents.history.source.default": "默认配置", + "agents.history.source.clear": "清空配置", + "agents.history.source.restore": "恢复配置", + "agents.history.source.sync": "自动同步", + "agents.history.source.legacy": "旧版备份", 'agents.title': '智能体配置', 'agents.embedded.title': '选择客户端和模型后启动', 'agents.embedded.subtitle': '如果没有客户端请自行安装', @@ -936,10 +957,10 @@ export const zhCN = { 'agents.pi.uninstall': '卸载 Pi 插件', 'agents.pi.uninstalling': '正在卸载插件', 'agents.default.title': '重置为默认配置', - 'agents.default.description': '这会使用当前选中模型重写 {name} 的全部相关配置文件,并删除其中其他用户内容。该操作没有备份且无法恢复。', + 'agents.default.description': '这会使用当前选中模型重写 {name} 的全部相关配置文件,并删除其中其他用户内容。操作前会保存历史备份;历史恢复仅恢复软件管理的字段,不能恢复被清除的其他自定义内容。', 'agents.default.confirm': '确认写入默认配置', 'agents.clear.title': '清空 Codex 配置', - 'agents.clear.description': '这会永久删除 Codex 配置目录中的 auth.json 和 config.toml,并重置软件记录的应用状态。该操作没有备份且无法恢复。', + 'agents.clear.description': '这会永久删除 Codex 配置目录中的 auth.json 和 config.toml。操作前会保存历史备份;历史恢复仅恢复软件管理的字段,不能恢复被清除的其他自定义内容。', 'agents.clear.confirm': '确认清空配置', 'agents.clear.success': 'Codex 配置已清空。', 'apiAccess.compatibleName': 'OpenAI 兼容 {number}', diff --git a/src/services/codexModelCatalog.ts b/src/services/codexModelCatalog.ts index f188105..e14d640 100644 --- a/src/services/codexModelCatalog.ts +++ b/src/services/codexModelCatalog.ts @@ -25,7 +25,7 @@ export type CodexModelConfiguration = { export type CodexCatalogEditorModel = { slug: string; hasOfficialTemplate: boolean; - contextSource: "definition" | "configuration" | "compatibility" | "template"; + contextSource: "client" | "configuration" | "template"; customized: boolean; configuration: CodexModelConfiguration; defaults: CodexModelConfiguration; diff --git a/tests/codexModelCatalog.test.ts b/tests/codexModelCatalog.test.ts index d67a56d..426e7d2 100644 --- a/tests/codexModelCatalog.test.ts +++ b/tests/codexModelCatalog.test.ts @@ -28,18 +28,18 @@ const configuration = (): CodexModelConfiguration => ({ }); describe('Codex 模型列表编辑', () => { - test("区分内核模型定义与兼容目录的后备上下文", () => { + test("区分 Codex 客户端 API、模板后备与自定义上下文", () => { const model: CodexCatalogEditorModel = { slug: "model-a", hasOfficialTemplate: true, customized: false, - contextSource: "definition", configuration: configuration(), defaults: configuration(), + contextSource: "client", configuration: configuration(), defaults: configuration(), }; - expect(codexContextSourceHint(model)).toBe("agents.catalog.contextSource.definition"); - expect(codexContextSourceHint({ ...model, contextSource: "compatibility" })) - .toBe("agents.catalog.contextSource.compatibility"); + expect(codexContextSourceHint(model)).toBe("agents.catalog.contextSource.client"); + expect(codexContextSourceHint({ ...model, contextSource: "template" })) + .toBe("agents.catalog.contextSource.template"); model.configuration.context_window = 64_000; expect(codexContextSourceHint(model)).toBe("agents.catalog.contextSource.customized"); model.configuration = cloneCodexModelConfiguration(model.defaults); - expect(codexContextSourceHint(model)).toBe("agents.catalog.contextSource.definition"); + expect(codexContextSourceHint(model)).toBe("agents.catalog.contextSource.client"); }); test('克隆配置时隔离数组字段', () => {