From 99f1ace7acfbb61e940d9c56e1ee983483d05c08 Mon Sep 17 00:00:00 2001 From: "Matt.Li" <270539505+matt1060338871-pixel@users.noreply.github.com> Date: Fri, 18 Sep 2026 06:27:55 +0100 Subject: [PATCH 1/5] =?UTF-8?q?fix(codex):=20=E7=A7=BB=E9=99=A4=E5=B7=B2?= =?UTF-8?q?=E5=BA=9F=E5=BC=83=E7=9A=84=20provider=20=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/codex_config.rs | 706 +++---------------------- src-tauri/src/codex_desktop.rs | 10 +- src-tauri/src/services/provider/mod.rs | 18 +- 3 files changed, 82 insertions(+), 652 deletions(-) diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index 0300c770..87732edd 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -37,7 +37,7 @@ use std::fs; use std::process::{Command, Stdio}; use std::sync::{Mutex, OnceLock}; use tauri::State; -use toml_edit::{Array, DocumentMut, InlineTable, Item, TableLike}; +use toml_edit::{DocumentMut, Item, TableLike}; pub const CC_SWITCH_CODEX_MODEL_PROVIDER_ID: &str = "custom"; /// Codex MultiRouter 专用的本地 provider id。 @@ -104,16 +104,6 @@ const CC_SWITCH_SUBAGENT_V2_POLICY_BEGIN: &str = "[CCSWITCHMULTI_SUBAGENT_V2_POL const CC_SWITCH_SUBAGENT_V2_POLICY_END: &str = "[CCSWITCHMULTI_SUBAGENT_V2_POLICY_END]"; const CC_SWITCH_CODEX_AGENT_THREADS: i64 = 10; const CC_SWITCH_CODEX_AGENT_DEPTH: i64 = 1; -const CODEX_REASONING_EFFORTS: &[(&str, &str)] = &[ - ("low", "Fast responses with lighter reasoning"), - ( - "medium", - "Balances speed and reasoning depth for everyday tasks", - ), - ("high", "Greater reasoning depth for complex problems"), - ("xhigh", "Extra high reasoning depth for complex problems"), -]; -const CODEX_DEFAULT_REASONING_EFFORT: &str = "medium"; const DEEPSEEK_WINDOWS_EXECUTION_GUIDANCE: &str = "On Windows, use PowerShell syntax and minimal directed commands. For content, use `rg `; for file discovery, use `rg --files `. Use narrow `-g` includes and excludes, including `-g '!node_modules/**'`, `-g '!.git/**'`, `-g '!target/**'`, `-g '!dist/**'`, and `-g '!generated/**'`. First identify a narrow source or test subtree; never recursively scan a user profile/home, drive root, or broad repository root.\nDo not use Unix-only commands such as `wc`, and do not assume `Select-String -Recurse` exists; if `rg` is unavailable, only after identifying a narrow target use `Get-ChildItem -LiteralPath -File -Recurse | Select-String`.\nFor ordinary read-only inspection, call tools without escalation metadata or a justification.\nStop and report as soon as the requested evidence is sufficient; do not keep scanning merely to be exhaustive."; /// Codex model catalog 的工具配置画像。 @@ -2496,95 +2486,6 @@ fn codex_model_catalog_from_specs( json!({ "models": entries }) } -/// 生成 provider inline `models` 使用的 reasoning effort 数组。 -/// -/// Codex Desktop 的不同读取路径对 TOML provider model 的字段兼容度不同; -/// 因此 inline model 同时写 snake_case 和 camelCase 两组字段,后续 app-server -/// 无论是按 config schema 解析还是直接转成前端对象,都能保留 reasoning 菜单。 -fn codex_provider_reasoning_efforts_toml_array( - levels: Option<&Value>, - key: &str, -) -> toml_edit::Value { - let mut array = Array::default(); - let normalized_levels = levels - .and_then(Value::as_array) - .map(|levels| { - levels - .iter() - .filter_map(|level| { - let effort = level - .get("effort") - .or_else(|| level.get("reasoningEffort")) - .and_then(Value::as_str)? - .trim(); - if effort.is_empty() { - return None; - } - let description = level - .get("description") - .and_then(Value::as_str) - .map(str::trim) - .filter(|description| !description.is_empty()) - .unwrap_or(effort); - Some((effort.to_string(), description.to_string())) - }) - .collect::>() - }) - .filter(|levels| !levels.is_empty()) - .unwrap_or_else(|| { - CODEX_REASONING_EFFORTS - .iter() - .map(|(effort, description)| (effort.to_string(), description.to_string())) - .collect() - }); - for (effort, description) in normalized_levels { - let mut level = InlineTable::new(); - level.insert(key, effort.into()); - level.insert("description", description.into()); - array.push(toml_edit::Value::InlineTable(level)); - } - toml_edit::Value::Array(array) -} - -/// 把 catalog 中的字符串数组投影到 provider inline model。 -fn codex_provider_string_toml_array(value: Option<&Value>) -> Option { - let values = value?.as_array()?; - let mut array = Array::default(); - for value in values { - let value = value.as_str()?.trim(); - if !value.is_empty() { - array.push(value); - } - } - Some(toml_edit::Value::Array(array)) -} - -/// 把官方 service tier 对象数组无损投影到 provider inline model。 -/// -/// Codex 当前字段均为标量;遇到未来新增的复合字段时返回 None,让 JSON catalog -/// 继续作为权威来源,避免生成一个结构错误的 TOML 条目。 -fn codex_provider_service_tiers_toml_array(value: Option<&Value>) -> Option { - let tiers = value?.as_array()?; - let mut array = Array::default(); - for tier in tiers { - let tier = tier.as_object()?; - let mut inline = InlineTable::new(); - for (field, value) in tier { - let value = match value { - Value::String(value) => value.as_str().into(), - Value::Bool(value) => (*value).into(), - Value::Number(value) if value.is_i64() => value.as_i64()?.into(), - Value::Number(value) if value.is_f64() => value.as_f64()?.into(), - Value::Null => continue, - _ => return None, - }; - inline.insert(field, value); - } - array.push(toml_edit::Value::InlineTable(inline)); - } - Some(toml_edit::Value::Array(array)) -} - fn codex_model_catalog_from_settings( settings: &Value, config_text: &str, @@ -2629,197 +2530,8 @@ fn codex_model_catalog_from_settings( ))) } -/// 为当前活动 custom provider 生成 Codex Desktop 可枚举的内联模型数组。 -fn codex_provider_models_toml_array( - specs: &[CodexCatalogModelSpec], - catalog: Option<&Value>, -) -> Item { - let mut array = Array::default(); - for spec in specs { - let model_id = spec.model.to_ascii_lowercase(); - let catalog_entry = catalog - .and_then(|catalog| catalog.get("models")) - .and_then(Value::as_array) - .and_then(|models| { - models.iter().find(|model| { - codex_model_stable_id(model).as_deref() == Some(model_id.as_str()) - }) - }); - let display_name = catalog_entry - .and_then(|entry| { - entry - .get("display_name") - .or_else(|| entry.get("displayName")) - }) - .and_then(Value::as_str) - .unwrap_or(&spec.display_name); - let default_reasoning_effort = catalog_entry - .and_then(|entry| { - entry - .get("default_reasoning_level") - .or_else(|| entry.get("defaultReasoningEffort")) - }) - .and_then(Value::as_str) - .unwrap_or(CODEX_DEFAULT_REASONING_EFFORT); - let supported_reasoning_levels = - catalog_entry.and_then(|entry| entry.get("supported_reasoning_levels")); - let mut model = InlineTable::new(); - model.insert("model", spec.model.as_str().into()); - model.insert("slug", spec.model.as_str().into()); - model.insert("id", spec.model.as_str().into()); - if let Some(upstream_model) = &spec.upstream_model { - model.insert("upstreamModel", upstream_model.as_str().into()); - model.insert("upstream_model", upstream_model.as_str().into()); - } - model.insert("display_name", display_name.into()); - model.insert("displayName", display_name.into()); - model.insert("description", display_name.into()); - model.insert( - "context_window", - i64::try_from(spec.context_window) - .unwrap_or(i64::MAX) - .into(), - ); - model.insert( - "contextWindow", - i64::try_from(spec.context_window) - .unwrap_or(i64::MAX) - .into(), - ); - model.insert("default_reasoning_effort", default_reasoning_effort.into()); - model.insert("default_reasoning_level", default_reasoning_effort.into()); - model.insert("defaultReasoningEffort", default_reasoning_effort.into()); - model.insert( - "supported_reasoning_levels", - codex_provider_reasoning_efforts_toml_array(supported_reasoning_levels, "effort"), - ); - model.insert( - "supported_reasoning_efforts", - codex_provider_reasoning_efforts_toml_array( - supported_reasoning_levels, - "reasoning_effort", - ), - ); - model.insert( - "supportedReasoningEfforts", - codex_provider_reasoning_efforts_toml_array( - supported_reasoning_levels, - "reasoningEffort", - ), - ); - if let Some(speed_tiers) = codex_provider_string_toml_array( - catalog_entry.and_then(|entry| entry.get("additional_speed_tiers")), - ) { - model.insert("additional_speed_tiers", speed_tiers.clone()); - model.insert("additionalSpeedTiers", speed_tiers); - } - if let Some(service_tiers) = codex_provider_service_tiers_toml_array( - catalog_entry.and_then(|entry| entry.get("service_tiers")), - ) { - model.insert("service_tiers", service_tiers.clone()); - model.insert("serviceTiers", service_tiers); - } - if let Some(default_service_tier) = catalog_entry - .and_then(|entry| entry.get("default_service_tier")) - .and_then(Value::as_str) - { - model.insert("default_service_tier", default_service_tier.into()); - model.insert("defaultServiceTier", default_service_tier.into()); - } - if let Some(input_modalities) = - codex_provider_string_toml_array(catalog_entry.and_then(|entry| { - entry - .get("input_modalities") - .or_else(|| entry.get("inputModalities")) - })) - { - model.insert("input_modalities", input_modalities.clone()); - model.insert("inputModalities", input_modalities); - } - if let Some(multi_agent_version) = catalog_entry - .and_then(|entry| { - entry - .get("multi_agent_version") - .or_else(|| entry.get("multiAgentVersion")) - }) - .and_then(Value::as_str) - { - model.insert("multi_agent_version", multi_agent_version.into()); - model.insert("multiAgentVersion", multi_agent_version.into()); - } - if let Some(supports_personality) = catalog_entry - .and_then(|entry| { - entry - .get("supports_personality") - .or_else(|| entry.get("supportsPersonality")) - }) - .and_then(Value::as_bool) - { - model.insert("supports_personality", supports_personality.into()); - model.insert("supportsPersonality", supports_personality.into()); - } - if let Some(model_specialty) = catalog_entry - .and_then(|entry| { - entry - .get("model_specialty") - .or_else(|| entry.get("modelSpecialty")) - }) - .and_then(Value::as_str) - { - model.insert("model_specialty", model_specialty.into()); - model.insert("modelSpecialty", model_specialty.into()); - } - model.insert("visibility", "list".into()); - model.insert("show_in_picker", true.into()); - model.insert("supported_in_api", true.into()); - model.insert("hidden", false.into()); - model.insert("isDefault", spec.is_default.into()); - array.push(toml_edit::Value::InlineTable(model)); - } - Item::Value(toml_edit::Value::Array(array)) -} - -/// 将模型目录同步到活动 provider 的 `models` 字段。 -/// -/// Codex Desktop 的 app-server 会把 custom provider 标为“自定义”,但候选菜单仍需要 -/// provider 内部能枚举模型;只写顶层 `model_catalog_json` 对部分 Desktop 版本不够。 -fn set_active_codex_provider_models( - doc: &mut DocumentMut, - specs: &[CodexCatalogModelSpec], - catalog: Option<&Value>, -) { - if specs.is_empty() { - return; - } - let Some(provider_id) = active_codex_model_provider_id(doc) else { - return; - }; - if !is_custom_codex_model_provider_id(&provider_id) { - return; - } - - if doc.get("model_providers").is_none() { - doc["model_providers"] = toml_edit::table(); - } - let Some(model_providers) = doc - .get_mut("model_providers") - .and_then(|item| item.as_table_mut()) - else { - return; - }; - if !model_providers.contains_key(&provider_id) { - model_providers[&provider_id] = toml_edit::table(); - } - if let Some(provider_table) = model_providers - .get_mut(provider_id.as_str()) - .and_then(|item| item.as_table_mut()) - { - provider_table["models"] = codex_provider_models_toml_array(specs, catalog); - } -} - -/// 移除当前活动 custom provider 下由 CCSwitch catalog 投影出的模型数组。 -fn remove_active_codex_provider_models(doc: &mut DocumentMut) { +/// 移除旧版 CCSwitchMulti 投影到活动 custom provider 的非官方 `models` 字段。 +fn remove_legacy_active_codex_provider_models(doc: &mut DocumentMut) { let Some(provider_id) = active_codex_model_provider_id(doc) else { return; }; @@ -2864,21 +2576,22 @@ fn set_codex_model_catalog_json_field( Ok(doc.to_string()) } -/// 同步 Codex Desktop 需要的 catalog 指针和 provider 内联模型。 +/// 同步 Codex Desktop 支持的 catalog 指针,并清理旧版 provider 内联模型。 +/// +/// 当前 Codex 只支持顶层 `model_catalog_json`。旧版 CCSwitchMulti 写入的 +/// `model_providers..models` 会被当作未知配置,因此每次投影时一并迁移掉。 fn set_codex_model_catalog_projection_fields( config_text: &str, catalog_path: Option<&Path>, - specs: Option<&[CodexCatalogModelSpec]>, - catalog: Option<&Value>, ) -> Result { let mut doc = config_text .parse::() .map_err(|e| AppError::Message(format!("Invalid Codex config.toml: {e}")))?; - match (catalog_path, specs) { - (Some(path), Some(specs)) => { + match catalog_path { + Some(path) => { doc["model_catalog_json"] = toml_edit::value(path.to_string_lossy().as_ref()); - set_active_codex_provider_models(&mut doc, specs, catalog); + remove_legacy_active_codex_provider_models(&mut doc); ensure_codex_agents_defaults(&mut doc); ensure_codex_multi_agent_reserved_schema_compatible( &mut doc, @@ -2894,7 +2607,7 @@ fn set_codex_model_catalog_projection_fields( .unwrap_or(false); if should_remove { doc.as_table_mut().remove("model_catalog_json"); - remove_active_codex_provider_models(&mut doc); + remove_legacy_active_codex_provider_models(&mut doc); } } } @@ -6279,12 +5992,8 @@ fn prepare_codex_config_text_with_model_catalog_impl( Value::String(fingerprint.to_string()); } apply_codex_multi_agent_transport_policy(&mut catalog, settings); - let config_text = set_codex_model_catalog_projection_fields( - config_text, - Some(&catalog_path), - Some(&specs), - Some(&catalog), - )?; + let config_text = + set_codex_model_catalog_projection_fields(config_text, Some(&catalog_path))?; let mut doc = config_text .parse::() .map_err(|e| AppError::Message(format!("Invalid Codex config.toml: {e}")))?; @@ -6327,7 +6036,7 @@ fn prepare_codex_config_text_with_model_catalog_impl( } else { restore_codex_models_cache_if_cc_switch_owned()?; prune_stale_codex_managed_agent_files(&get_codex_agents_dir(), &HashSet::new())?; - let config_text = set_codex_model_catalog_projection_fields(config_text, None, None, None)?; + let config_text = set_codex_model_catalog_projection_fields(config_text, None)?; let config_text = set_codex_native_web_search_field( &config_text, profile == CodexCatalogToolProfile::Anthropic, @@ -14504,21 +14213,6 @@ openai_base_url = "http://127.0.0.1:15721/v1" #[test] fn codex_agent_defaults_migrate_legacy_alias_without_overwriting_user_limits() { - let specs = vec![CodexCatalogModelSpec { - model: "qwen3.6".to_string(), - upstream_model: None, - display_name: "Qwen 3.6".to_string(), - context_window: 262_144, - text_only: false, - is_default: false, - supports_parallel_tool_calls: None, - input_modalities: None, - base_instructions: None, - reasoning: None, - reasoning_fingerprint: String::new(), - reasoning_source: "unknown".to_string(), - sort_index: None, - }]; let config = r#"model_provider = "codex_model_router_v2" [agents] @@ -14528,13 +14222,9 @@ max_threads = 8 base_url = "http://127.0.0.1:15721/v1" "#; - let projected = set_codex_model_catalog_projection_fields( - config, - Some(Path::new("catalog")), - Some(&specs), - None, - ) - .expect("project catalog fields"); + let projected = + set_codex_model_catalog_projection_fields(config, Some(Path::new("catalog"))) + .expect("project catalog fields"); let parsed: toml::Value = toml::from_str(&projected).expect("parse projected config"); let agents = parsed.get("agents").expect("agents section should exist"); @@ -14551,194 +14241,8 @@ base_url = "http://127.0.0.1:15721/v1" ); } - #[test] - /// 活动 custom provider 的内联模型也必须使用 enriched catalog 的官方推理档位。 - fn codex_provider_inline_models_use_enriched_reasoning_levels() { - let specs = vec![CodexCatalogModelSpec { - model: "gpt-5.6-sol".to_string(), - upstream_model: None, - display_name: "gpt-5.6-sol".to_string(), - context_window: 272_000, - text_only: false, - is_default: true, - supports_parallel_tool_calls: None, - input_modalities: None, - base_instructions: None, - reasoning: None, - reasoning_fingerprint: String::new(), - reasoning_source: "unknown".to_string(), - sort_index: None, - }]; - let catalog = json!({ - "models": [{ - "slug": "gpt-5.6-sol", - "display_name": "GPT-5.6-Sol", - "default_reasoning_level": "medium", - "supported_reasoning_levels": [ - { "effort": "low", "description": "Low" }, - { "effort": "medium", "description": "Medium" }, - { "effort": "high", "description": "High" }, - { "effort": "xhigh", "description": "Extra High" }, - { "effort": "max", "description": "Max" }, - { "effort": "ultra", "description": "Ultra" } - ] - }] - }); - let config = r#"model_provider = "codex_model_router_v2" - -[model_providers.codex_model_router_v2] -base_url = "http://127.0.0.1:15721/v1" -"#; - - let projected = set_codex_model_catalog_projection_fields( - config, - Some(Path::new("catalog")), - Some(&specs), - Some(&catalog), - ) - .expect("project catalog fields"); - let parsed: toml::Value = toml::from_str(&projected).expect("parse projected config"); - let model = parsed - .get("model_providers") - .and_then(|providers| providers.get("codex_model_router_v2")) - .and_then(|provider| provider.get("models")) - .and_then(|models| models.as_array()) - .and_then(|models| models.first()) - .expect("inline model"); - let efforts = model - .get("supported_reasoning_levels") - .and_then(|levels| levels.as_array()) - .expect("inline reasoning levels") - .iter() - .filter_map(|level| level.get("effort").and_then(|effort| effort.as_str())) - .collect::>(); - - assert_eq!( - model.get("display_name").and_then(|value| value.as_str()), - Some("GPT-5.6-Sol") - ); - assert_eq!( - efforts, - vec!["low", "medium", "high", "xhigh", "max", "ultra"] - ); - } - - #[test] - fn codex_provider_inline_models_keep_deepseek_v4_reasoning_capabilities() { - let specs = vec![CodexCatalogModelSpec { - model: "deepseek-v4-pro".to_string(), - upstream_model: None, - display_name: "DeepSeek V4 Pro".to_string(), - context_window: 1_048_576, - text_only: true, - is_default: true, - supports_parallel_tool_calls: Some(true), - input_modalities: Some(vec!["text".to_string()]), - base_instructions: None, - reasoning: Some( - crate::proxy::providers::codex_reasoning::CodexModelReasoningCapability { - schema_version: None, - support_status: None, - control_kind: None, - supported: Some(true), - supported_efforts: vec!["low".into(), "high".into(), "max".into()], - default_effort: Some("high".into()), - disable_allowed: false, - upstream: - crate::proxy::providers::codex_reasoning::CodexModelReasoningUpstream { - format: "reasoning_object".into(), - parameter: "reasoning.effort".into(), - effort_map: Default::default(), - }, - output_format: None, - source: Some("builtin".into()), - confidence: None, - fetched_at: None, - provider_key: None, - model_revision: None, - codex_ultra_orchestration: None, - }, - ), - reasoning_fingerprint: String::new(), - reasoning_source: "unknown".to_string(), - sort_index: None, - }]; - let catalog = codex_model_catalog_from_specs( - &specs, - &json!({ - "slug": "gpt-5.5", - "display_name": "GPT-5.5", - "default_reasoning_level": "medium", - "supported_reasoning_levels": [ - { "effort": "low" }, - { "effort": "medium" }, - { "effort": "high" }, - { "effort": "xhigh" } - ] - }), - CodexCatalogToolProfile::ProxyChat, - 128_000, - ); - let config = r#"model_provider = "codex_model_router_v2" - -[model_providers.codex_model_router_v2] -base_url = "http://127.0.0.1:15721/v1" -"#; - - let projected = set_codex_model_catalog_projection_fields( - config, - Some(Path::new("catalog")), - Some(&specs), - Some(&catalog), - ) - .expect("project catalog fields"); - let parsed: toml::Value = toml::from_str(&projected).expect("parse projected config"); - let model = parsed["model_providers"]["codex_model_router_v2"]["models"] - .as_array() - .and_then(|models| models.first()) - .expect("inline model"); - - for field in [ - "supported_reasoning_levels", - "supported_reasoning_efforts", - "supportedReasoningEfforts", - ] { - let efforts = model[field] - .as_array() - .expect("inline reasoning levels") - .iter() - .filter_map(|level| { - level - .get("effort") - .or_else(|| level.get("reasoning_effort")) - .or_else(|| level.get("reasoningEffort")) - .and_then(|effort| effort.as_str()) - }) - .collect::>(); - assert_eq!(efforts, vec!["low", "high", "max"], "field {field}"); - } - assert_eq!(model["default_reasoning_level"].as_str(), Some("high")); - assert_eq!(model["default_reasoning_effort"].as_str(), Some("high")); - assert_eq!(model["defaultReasoningEffort"].as_str(), Some("high")); - } - #[test] fn codex_multi_agent_v2_keeps_spawn_agent_reserved_schema_compatible() { - let specs = vec![CodexCatalogModelSpec { - model: "qwen3.6".to_string(), - upstream_model: None, - display_name: "Qwen 3.6".to_string(), - context_window: 262_144, - text_only: false, - is_default: false, - supports_parallel_tool_calls: None, - input_modalities: None, - base_instructions: None, - reasoning: None, - reasoning_fingerprint: String::new(), - reasoning_source: "unknown".to_string(), - sort_index: None, - }]; let config = r#"model_provider = "codex_model_router_v2" [features] @@ -14748,13 +14252,9 @@ multi_agent_v2 = true base_url = "http://127.0.0.1:15721/v1" "#; - let projected = set_codex_model_catalog_projection_fields( - config, - Some(Path::new("catalog")), - Some(&specs), - None, - ) - .expect("project catalog fields"); + let projected = + set_codex_model_catalog_projection_fields(config, Some(Path::new("catalog"))) + .expect("project catalog fields"); let parsed: toml::Value = toml::from_str(&projected).expect("parse projected config"); let multi_agent_v2 = parsed .get("features") @@ -15958,30 +15458,10 @@ max_threads = 10 max_concurrent_threads_per_session = 8 max_depth = 2 "#; - let specs = vec![CodexCatalogModelSpec { - model: "gpt-5.6-sol".to_string(), - upstream_model: None, - display_name: "GPT-5.6-Sol".to_string(), - context_window: 1_000_000, - text_only: false, - is_default: true, - supports_parallel_tool_calls: None, - input_modalities: None, - base_instructions: None, - reasoning: None, - reasoning_fingerprint: String::new(), - reasoning_source: "unknown".to_string(), - sort_index: None, - }]; let catalog_path = get_codex_model_catalog_path(); - let projected = set_codex_model_catalog_projection_fields( - input, - Some(&catalog_path), - Some(&specs), - None, - ) - .expect("project catalog fields"); + let projected = set_codex_model_catalog_projection_fields(input, Some(&catalog_path)) + .expect("project catalog fields"); let parsed: toml::Value = toml::from_str(&projected).expect("parse projected config"); let agents = parsed.get("agents").expect("agents table"); @@ -16002,6 +15482,53 @@ max_depth = 2 ); } + #[test] + fn catalog_projection_removes_legacy_active_provider_models_only() { + let input = r#"model_provider = "codex_model_router_v2" + +[model_providers.codex_model_router_v2] +name = "CCSwitch MultiRouter" +base_url = "http://127.0.0.1:15721/v1" +models = [{ model = "gpt-5.6-sol", slug = "gpt-5.6-sol" }] + +[model_providers.user_owned] +name = "User Provider" +base_url = "https://example.com/v1" +models = [{ model = "user-model" }] + +[mcp_servers.user_owned] +command = "example-mcp" +"#; + let catalog_path = get_codex_model_catalog_path(); + + let projected = set_codex_model_catalog_projection_fields(input, Some(&catalog_path)) + .expect("project catalog fields"); + let parsed: toml::Value = toml::from_str(&projected).expect("parse projected config"); + + assert_eq!( + parsed + .get("model_catalog_json") + .and_then(toml::Value::as_str), + Some(catalog_path.to_string_lossy().as_ref()) + ); + assert!( + parsed["model_providers"]["codex_model_router_v2"] + .get("models") + .is_none(), + "the active provider must not retain the unsupported legacy models field" + ); + assert_eq!( + parsed["model_providers"]["user_owned"]["models"][0]["model"].as_str(), + Some("user-model"), + "inactive user-owned provider settings must be preserved" + ); + assert_eq!( + parsed["mcp_servers"]["user_owned"]["command"].as_str(), + Some("example-mcp"), + "unrelated configuration must be preserved" + ); + } + #[test] fn force_repair_canonicalizes_agent_aliases_and_preserves_user_config() { let input = r#"model_provider = "codex_model_router_v2" @@ -16952,98 +16479,11 @@ base_url = "http://127.0.0.1:15721/v1" .is_none(), "a fixed compact limit would mask the selected model's own budget" ); - let provider_models = prepared_toml - .get("model_providers") - .and_then(|providers| providers.get("custom")) - .and_then(|provider| provider.get("models")) - .and_then(|models| models.as_array()) - .expect("custom provider should expose inline models for Codex Desktop"); - let provider_model_ids: Vec<_> = provider_models - .iter() - .filter_map(|model| model.get("model").and_then(|value| value.as_str())) - .collect(); - assert!( - provider_model_ids.contains(&"qwen3.6"), - "inline provider models must include Qwen so the Desktop menu is not just 自定义" - ); assert!( - provider_model_ids.contains(&"deepseek-v4-flash"), - "inline provider models must include DeepSeek so the Desktop menu can enumerate it" - ); - let inline_official_model = provider_models - .iter() - .find(|model| model.get("model").and_then(|value| value.as_str()) == Some("gpt-5.5")) - .expect("inline provider models should include the routed official model"); - assert_eq!( - inline_official_model - .get("supported_reasoning_levels") - .and_then(|value| value.as_array()) - .map(Vec::len), - Some(4), - "inline official models must retain the same reasoning choices as the merged catalog" - ); - assert_eq!( - inline_official_model - .get("additional_speed_tiers") - .and_then(|value| value.as_array()) - .map(Vec::len), - Some(1), - "inline official models must retain speed tiers when Desktop reloads provider models" - ); - assert_eq!( - inline_official_model - .get("service_tiers") - .and_then(|value| value.as_array()) - .map(Vec::len), - Some(1), - "inline official models must retain service tiers together with reasoning levels" - ); - assert_eq!( - inline_official_model - .get("supportsPersonality") - .and_then(toml::Value::as_bool), - Some(true), - "inline official models must retain personality support" - ); - assert_eq!( - inline_official_model - .get("modelSpecialty") - .and_then(toml::Value::as_str), - Some("coding"), - "inline official models must retain picker specialty" - ); - let inline_qwen_model = provider_models - .iter() - .find(|model| model.get("model").and_then(|value| value.as_str()) == Some("qwen3.6")) - .expect("inline provider models should include Qwen"); - assert_eq!( - inline_qwen_model - .get("service_tiers") - .and_then(|value| value.as_array()) - .map(Vec::len), - Some(0), - "third-party models must not inherit OpenAI service tiers from the template" - ); - - let inline_qwen_modalities = inline_qwen_model - .get("input_modalities") - .expect("inline provider models must retain explicit input modalities"); - assert_eq!( - inline_qwen_model.get("inputModalities"), - Some(inline_qwen_modalities), - "inline snake/camel modality aliases must stay equivalent" - ); - assert_eq!( - inline_qwen_model - .get("multi_agent_version") - .and_then(toml::Value::as_str), - Some("v2"), - "inline models must retain the active Sub-Agent transport version" - ); - assert_eq!( - inline_qwen_model.get("multiAgentVersion"), - inline_qwen_model.get("multi_agent_version"), - "inline snake/camel multi-agent aliases must stay equivalent" + prepared_toml["model_providers"]["custom"] + .get("models") + .is_none(), + "Codex provider config must not contain the unsupported legacy models field" ); let cache: Value = read_json_file(&get_codex_models_cache_path()).expect("read cache"); diff --git a/src-tauri/src/codex_desktop.rs b/src-tauri/src/codex_desktop.rs index 8487235d..de5f7b2a 100644 --- a/src-tauri/src/codex_desktop.rs +++ b/src-tauri/src/codex_desktop.rs @@ -270,7 +270,7 @@ pub(crate) fn load_cc_switch_model_catalog_projection( } Err(format!( - "No routed models were found in {}, the CCSwitchMulti models cache, or the active provider inline catalog", + "No routed models were found in {}, the CCSwitchMulti models cache, or a legacy active provider inline catalog", catalog_path.display() )) } @@ -293,11 +293,11 @@ fn codex_model_catalog_projection_from_value( }) } -/// 从当前活动 provider 的 TOML 内联 `models` 读取最后一道模型菜单回退。 +/// 从旧版配置的活动 provider TOML 内联 `models` 读取最后一道模型菜单回退。 /// -/// 生成 live config 时,CCSM 会把同一份路由目录同时投射到 JSON catalog、models cache -/// 和活动 provider 内联数组。任一 JSON 文件短暂不可读时,内联数组仍能避免整次 Desktop -/// 会话安装空白名单;这里仅读取活动 provider,不能把未启用 provider 的模型重新注入。 +/// 新版 CCSM 不再写入 Codex 已不支持的 provider `models` 字段;这里保留只读兼容, +/// 让尚未完成下一次配置同步的旧安装仍能恢复模型菜单。这里只读取活动 provider, +/// 不能把未启用 provider 的模型重新注入。 fn read_active_codex_provider_inline_model_catalog() -> Option { let text = crate::codex_config::read_codex_config_text().ok()?; let parsed = text.parse::().ok()?; diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index 7a85a6f1..4161ce86 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -3484,16 +3484,10 @@ experimental_bearer_token = "PROXY_MANAGED" Some("PROXY_MANAGED"), "{label}: CCSM-managed routes must use the local placeholder" ); - let provider_models = live_toml - .get("model_providers") - .and_then(|providers| providers.get("codex_model_router_v2")) - .and_then(|provider| provider.get("models")) - .and_then(|models| models.as_array()) - .expect("router provider should expose inline models for Desktop custom picker"); - let provider_model_ids: Vec<&str> = provider_models - .iter() - .filter_map(|model| model.get("model").and_then(|value| value.as_str())) - .collect(); + assert!( + router_facade.get("models").is_none(), + "{label}: router provider must not expose the unsupported legacy models field" + ); let catalog_text = std::fs::read_to_string(crate::codex_config::get_codex_model_catalog_path()) @@ -3559,10 +3553,6 @@ experimental_bearer_token = "PROXY_MANAGED" cache_model_fields.contains(&expected), "{label}: cache should include model field {expected}, got: {cache_model_fields:?}" ); - assert!( - provider_model_ids.contains(&expected), - "{label}: provider inline models should include {expected}, got: {provider_model_ids:?}" - ); } }; From e8f0d5cd16df9a8e0bfe119ed2c2f546f9fa3609 Mon Sep 17 00:00:00 2001 From: "Matt.Li" <270539505+matt1060338871-pixel@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:17:12 +0800 Subject: [PATCH 2/5] =?UTF-8?q?chore(ci):=20=E4=BF=AE=E5=A4=8D=E5=89=8D?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E8=B4=A8=E9=87=8F=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/codex_config.rs | 34 ++++++++----- .../src/codex_paginated_history_repair.rs | 6 --- src-tauri/src/codex_subagent_profiles.rs | 47 +++++++++++++----- src-tauri/src/proxy/handlers.rs | 2 + src-tauri/src/services/session_usage_codex.rs | 11 +++-- .../services/session_usage_codex_discovery.rs | 2 + src-tauri/src/services/usage_stats.rs | 48 +++++++++++++------ .../codex/CodexRouterWorkspacePage.tsx | 5 +- src/utils/deepseekRoleModels.test.ts | 5 +- tests/components/AddProviderDialog.test.tsx | 2 + 10 files changed, 111 insertions(+), 51 deletions(-) diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index 87732edd..f5437c1c 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -4,8 +4,8 @@ use std::path::{Path, PathBuf}; use crate::app_config::AppType; use crate::codex_subagent_profiles::{ compile_subagent_v2_profiles, deepseek_role_identity_for_model, deepseek_role_models_match, - initialize_legacy_subagent_v2, normalize_profile_key, - parse_persisted_subagent_v2, parse_persisted_subagent_v2_tolerant, render_generated_role_toml, + initialize_legacy_subagent_v2, normalize_profile_key, parse_persisted_subagent_v2, + parse_persisted_subagent_v2_tolerant, render_generated_role_toml, CatalogModel as SubagentCatalogModel, CodexSubagentProfileConfig, CompileError as SubagentCompileError, CompileOutput as SubagentCompileOutput, CompileRequest as SubagentCompileRequest, DiagnosticReasonCode as SubagentDiagnosticReasonCode, @@ -3551,12 +3551,12 @@ fn compile_configured_codex_subagent_roles( ); } if let Some(classification) = classification.clone() { - route_classifications.insert(spec.model.to_ascii_lowercase(), classification.clone()); + route_classifications + .insert(spec.model.to_ascii_lowercase(), classification.clone()); // 别名键:profile 用旧 slug(deepseek-v4-flash)时也能查到 // catalog 新 slug(deepseek-flash)的路由分类。 if let Some(role_identity) = deepseek_role_identity_for_model(&spec.model) { - route_classifications - .insert(role_identity.to_string(), classification); + route_classifications.insert(role_identity.to_string(), classification); } } SubagentCatalogModel { @@ -3843,7 +3843,10 @@ fn catalog_profile_draft( Some(canonical) => canonical, None => identity.as_str(), }; - if let Some(mut preset) = defaults.pointer(&format!("/profiles/{preset_key}")).cloned() { + if let Some(mut preset) = defaults + .pointer(&format!("/profiles/{preset_key}")) + .cloned() + { preset["model"] = Value::String(model.to_string()); preset["enabled"] = Value::Bool(enabled_preferred); if !enabled_preferred { @@ -8148,7 +8151,9 @@ wire_api = "responses" assert!(!codex_catalog_model_name_is_text_only( "deepseek-v4-flash-vision-exp" )); - assert!(!codex_catalog_model_name_is_text_only("deepseek-flash-vision")); + assert!(!codex_catalog_model_name_is_text_only( + "deepseek-flash-vision" + )); } #[test] @@ -8157,7 +8162,10 @@ wire_api = "responses" codex_agent_role_name_for_model("deepseek-flash"), "deepseek-flash" ); - assert_eq!(codex_agent_role_name_for_model("deepseek-pro"), "deepseek-pro"); + assert_eq!( + codex_agent_role_name_for_model("deepseek-pro"), + "deepseek-pro" + ); assert!(codex_agent_description_for_model("deepseek-flash") .contains("DeepSeek V4 Flash worker")); assert_eq!( @@ -8176,20 +8184,20 @@ wire_api = "responses" #[test] fn catalog_profile_draft_uses_flash_preset_for_official_alias() { - let draft = catalog_profile_draft("deepseek-flash", true, None) - .expect("flash alias draft"); + let draft = catalog_profile_draft("deepseek-flash", true, None).expect("flash alias draft"); assert_eq!(draft["model"], "deepseek-flash"); assert_eq!(draft["enabled"], true); let strengths = draft["questionnaire"]["taskStrengths"] .as_array() .expect("taskStrengths"); - assert!(strengths.iter().any(|value| value == "long_context_reading")); + assert!(strengths + .iter() + .any(|value| value == "long_context_reading")); } #[test] fn catalog_profile_draft_keeps_generic_stub_for_unknown_models() { - let draft = catalog_profile_draft("some-model", false, None) - .expect("unknown model draft"); + let draft = catalog_profile_draft("some-model", false, None).expect("unknown model draft"); assert_eq!(draft["model"], "some-model"); assert_eq!(draft["enabled"], false); assert_eq!(draft["questionnaire"]["preference"], "eligible"); diff --git a/src-tauri/src/codex_paginated_history_repair.rs b/src-tauri/src/codex_paginated_history_repair.rs index 1ad8519e..e81c8c13 100644 --- a/src-tauri/src/codex_paginated_history_repair.rs +++ b/src-tauri/src/codex_paginated_history_repair.rs @@ -1561,12 +1561,6 @@ fn repaired_projection_status_at( Ok(status) } -pub(crate) fn repaired_projections_caught_up( - outcome: &PaginatedHistoryRepairOutcome, -) -> Result { - Ok(repaired_projection_status(outcome)?.is_caught_up()) -} - /// 游标是否停在一个合法的记录边界上(0 与文件末尾都算合法)。 /// /// 这正是我们修复的损坏判据的反面:损坏的游标指向记录内部,Codex 从这里读不出完整 diff --git a/src-tauri/src/codex_subagent_profiles.rs b/src-tauri/src/codex_subagent_profiles.rs index 358173e8..856d9c02 100644 --- a/src-tauri/src/codex_subagent_profiles.rs +++ b/src-tauri/src/codex_subagent_profiles.rs @@ -3726,8 +3726,14 @@ mod tests { Some("deepseek-v4-pro") ); // *-vision* 是独立视觉模型,不属于文本角色族。 - assert_eq!(deepseek_role_identity_for_model("deepseek-v4-flash-vision-exp"), None); - assert_eq!(deepseek_role_identity_for_model("deepseek-flash-vision"), None); + assert_eq!( + deepseek_role_identity_for_model("deepseek-v4-flash-vision-exp"), + None + ); + assert_eq!( + deepseek_role_identity_for_model("deepseek-flash-vision"), + None + ); // 其它 DeepSeek 模型不属于 flash/pro 角色族。 assert_eq!(deepseek_role_identity_for_model("deepseek-chat"), None); assert_eq!(deepseek_role_identity_for_model("gpt-5.6-sol"), None); @@ -3735,14 +3741,32 @@ mod tests { #[test] fn deepseek_role_models_match_treats_aliases_as_same_model() { - assert!(deepseek_role_models_match("deepseek-flash", "deepseek-v4-flash")); - assert!(deepseek_role_models_match("deepseek-v4-flash", "deepseek-flash")); - assert!(deepseek_role_models_match("DEEPSEEK-FLASH", "deepseek-v4-flash")); - assert!(deepseek_role_models_match("deepseek-pro", "deepseek-v4-pro")); + assert!(deepseek_role_models_match( + "deepseek-flash", + "deepseek-v4-flash" + )); + assert!(deepseek_role_models_match( + "deepseek-v4-flash", + "deepseek-flash" + )); + assert!(deepseek_role_models_match( + "DEEPSEEK-FLASH", + "deepseek-v4-flash" + )); + assert!(deepseek_role_models_match( + "deepseek-pro", + "deepseek-v4-pro" + )); assert!(deepseek_role_models_match("qwen3.8", "QWEN3.8")); // 同族不跨角色,不同模型不匹配。 - assert!(!deepseek_role_models_match("deepseek-flash", "deepseek-v4-pro")); - assert!(!deepseek_role_models_match("deepseek-v4-flash-vision-exp", "deepseek-flash")); + assert!(!deepseek_role_models_match( + "deepseek-flash", + "deepseek-v4-pro" + )); + assert!(!deepseek_role_models_match( + "deepseek-v4-flash-vision-exp", + "deepseek-flash" + )); assert!(!deepseek_role_models_match("qwen3.8", "qwen3.6")); } @@ -3814,10 +3838,9 @@ mod tests { vec![valid(profile("deepseek-v4-flash", "deepseek-v4-flash"))], ))); // 只保留视觉模型:文本 flash 角色不得被视觉型号顶替。 - compile_request.catalog_models = - vec![catalog("deepseek-v4-flash-vision-exp", true)]; - let output = compile_subagent_v2_profiles(&compile_request) - .expect("compile vision-only catalog"); + compile_request.catalog_models = vec![catalog("deepseek-v4-flash-vision-exp", true)]; + let output = + compile_subagent_v2_profiles(&compile_request).expect("compile vision-only catalog"); let flash_status = output .profile_statuses .iter() diff --git a/src-tauri/src/proxy/handlers.rs b/src-tauri/src/proxy/handlers.rs index 6890b86a..e8b04b9c 100644 --- a/src-tauri/src/proxy/handlers.rs +++ b/src-tauri/src/proxy/handlers.rs @@ -3605,6 +3605,7 @@ fn create_codex_chat_sse_stream_from_verified_profile( @@ -3664,6 +3665,7 @@ fn chat_completion_to_response_from_verified_profile( ) } +#[allow(clippy::too_many_arguments)] fn chat_completion_to_response_from_verified_profile_for_client( body: Value, tool_context: &transform_codex_chat::CodexToolContext, diff --git a/src-tauri/src/services/session_usage_codex.rs b/src-tauri/src/services/session_usage_codex.rs index eb310386..482e420e 100644 --- a/src-tauri/src/services/session_usage_codex.rs +++ b/src-tauri/src/services/session_usage_codex.rs @@ -61,7 +61,6 @@ fn take_codex_append_bytes_read() -> u64 { struct CodexCheckpoint { last_byte_offset: i64, file_size: i64, - file_modified: i64, head_window_len: Option, head_fingerprint: Option, tail_fingerprint: Option, @@ -362,6 +361,7 @@ struct CodexParseSnapshot { /// 只读的、已完成 parent replay 剥离的 rollout token 事件。 /// /// 状态页必须复用此解析器,而不能把 `total_token_usage` 当成可直接相加的请求用量。 +#[cfg(test)] #[derive(Debug, Clone)] pub(crate) struct VerifiedCodexRolloutUsageEvent { pub model: String, @@ -373,6 +373,7 @@ pub(crate) struct VerifiedCodexRolloutUsageEvent { /// 解析失败、父 replay 无法验证、或某个可计费用量缺少时间戳时返回 `None`。 /// 这让读侧把它表示成未知,而不是把累计快照猜成真实用量。 +#[cfg(test)] pub(crate) fn read_verified_codex_rollout_usage( file_path: &Path, rollout_index: &RolloutIndex, @@ -759,7 +760,7 @@ fn load_codex_checkpoint( Ok(( last_byte_offset, file_size, - file_modified, + _file_modified, head_window_len, head_fingerprint, tail_fingerprint, @@ -771,7 +772,6 @@ fn load_codex_checkpoint( .map(|state| CodexCheckpoint { last_byte_offset, file_size, - file_modified, head_window_len, head_fingerprint, tail_fingerprint, @@ -785,6 +785,7 @@ fn load_codex_checkpoint( } } +#[allow(clippy::too_many_arguments)] fn save_codex_checkpoint_on_conn( conn: &rusqlite::Connection, file_path: &Path, @@ -1046,6 +1047,7 @@ pub fn sync_codex_usage(db: &Database) -> Result { } /// 收集所有 Codex 会话 JSONL 文件 +#[cfg(test)] fn collect_codex_session_files(codex_dir: &Path) -> Vec { let mut files = Vec::new(); @@ -1086,6 +1088,7 @@ pub(crate) fn build_rollout_index(files: &[PathBuf]) -> RolloutIndex { } /// 递归扫描目录下的 .jsonl 文件(限制最大深度) +#[cfg(test)] fn collect_jsonl_recursive(dir: &Path, files: &mut Vec, depth: u32, max_depth: u32) { let entries = match fs::read_dir(dir) { Ok(e) => e, @@ -1102,6 +1105,7 @@ fn collect_jsonl_recursive(dir: &Path, files: &mut Vec, depth: u32, max } } +#[cfg(test)] fn parse_codex_file( file_path: &Path, root_thread_id: Option, @@ -1783,6 +1787,7 @@ fn save_codex_session_metadata_on_conn( Ok(()) } +#[allow(clippy::too_many_arguments)] fn sync_codex_append( db: &Database, file_path: &Path, diff --git a/src-tauri/src/services/session_usage_codex_discovery.rs b/src-tauri/src/services/session_usage_codex_discovery.rs index 892a6a88..d34e15b5 100644 --- a/src-tauri/src/services/session_usage_codex_discovery.rs +++ b/src-tauri/src/services/session_usage_codex_discovery.rs @@ -29,7 +29,9 @@ pub struct CodexDiscoveryRoot { #[derive(Debug, Clone, Default)] pub struct CodexDiscoveryBatch { pub paths: Vec, + #[allow(dead_code)] pub full_scan: bool, + #[allow(dead_code)] pub next_full_scan_at: Option, } diff --git a/src-tauri/src/services/usage_stats.rs b/src-tauri/src/services/usage_stats.rs index 6faff714..eb5c705a 100644 --- a/src-tauri/src/services/usage_stats.rs +++ b/src-tauri/src/services/usage_stats.rs @@ -1107,6 +1107,14 @@ struct CodexSubagentUsageBucket { last_used_at: Option, } +type CodexSubagentSessionRollup = ( + Option, + Option, + Option, + Option, + HashMap, +); + /// 子 Agent 用量查询的 session_id 分块大小。 /// /// 有些 SQLite 构建仍然使用 999 左右的绑定变量上限;这里保持 500 的保守分块, @@ -1115,6 +1123,7 @@ const CODEX_SUBAGENT_USAGE_SESSION_CHUNK: usize = 500; /// 把 SQL 聚合行累计到会话和模型桶中。 #[allow(clippy::too_many_arguments)] +#[cfg(test)] fn add_codex_subagent_usage_sample( session_buckets: &mut HashMap>, session_id: String, @@ -1176,6 +1185,7 @@ fn add_codex_subagent_model_bucket( } /// 判断一个用量桶是否已经包含真实 token。 +#[cfg(test)] fn codex_subagent_bucket_has_tokens(bucket: &CodexSubagentUsageBucket) -> bool { bucket.total_tokens > 0 || bucket.input_tokens > 0 @@ -1762,16 +1772,7 @@ fn build_codex_subagent_usage_stats_from_db( AppError::Database(format!("查询 Codex 子 Agent 本地统计失败: {error}")) })?; - let mut sessions: HashMap< - String, - ( - Option, - Option, - Option, - Option, - HashMap, - ), - > = HashMap::new(); + let mut sessions: HashMap = HashMap::new(); for row in rows { let ( session_id, @@ -6150,7 +6151,8 @@ mod tests { /// 发到一个并不服务它的 provider,不产生 token/成本——不应出现在模型统计里, /// 避免把同一模型拆散到多个 provider 名下分开统计。真实跨 provider 用量不受影响。 #[test] - fn test_get_model_stats_drops_zero_usage_cross_provider_routing_ghosts() -> Result<(), AppError> { + fn test_get_model_stats_drops_zero_usage_cross_provider_routing_ghosts() -> Result<(), AppError> + { let db = Database::memory()?; { let conn = lock_conn!(db.conn); @@ -6172,8 +6174,16 @@ mod tests { latency_ms, status_code, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params![ - "real-deepseek", "provider-real", "codex", "deepseek-flash", - 100, 50, "0.01", 100, 200, 1000 + "real-deepseek", + "provider-real", + "codex", + "deepseek-flash", + 100, + 50, + "0.01", + 100, + 200, + 1000 ], )?; // 路由残留:同一个 deepseek-flash 被错配到 Qwen,0 token、0 成本。 @@ -6184,8 +6194,16 @@ mod tests { latency_ms, status_code, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params![ - "ghost-deepseek-qwen", "provider-ghost", "codex", "deepseek-flash", - 0, 0, "0", 100, 200, 1001 + "ghost-deepseek-qwen", + "provider-ghost", + "codex", + "deepseek-flash", + 0, + 0, + "0", + 100, + 200, + 1001 ], )?; } diff --git a/src/components/codex/CodexRouterWorkspacePage.tsx b/src/components/codex/CodexRouterWorkspacePage.tsx index c2dc4d63..0565c6d8 100644 --- a/src/components/codex/CodexRouterWorkspacePage.tsx +++ b/src/components/codex/CodexRouterWorkspacePage.tsx @@ -103,7 +103,10 @@ import { } from "@/lib/query/usage"; import { cn } from "@/lib/utils"; import { resolveFetchedCodexModelContextWindow } from "@/utils/codexModelContext"; -import { catalogHasRoleModel, deepSeekRoleForModel } from "@/utils/deepseekRoleModels"; +import { + catalogHasRoleModel, + deepSeekRoleForModel, +} from "@/utils/deepseekRoleModels"; import { catalogModelLabel, CODEX_SPAWN_AGENT_PRIORITY_MODELS, diff --git a/src/utils/deepseekRoleModels.test.ts b/src/utils/deepseekRoleModels.test.ts index e7984339..499554ca 100644 --- a/src/utils/deepseekRoleModels.test.ts +++ b/src/utils/deepseekRoleModels.test.ts @@ -57,7 +57,10 @@ describe("catalogHasRoleModel", () => { catalogHasRoleModel([{ model: "deepseek-flash" }], "deepseek-v4-flash"), ).toBe(true); expect( - catalogHasRoleModel([{ model: "deepseek-v4-flash" }], "deepseek-v4-flash"), + catalogHasRoleModel( + [{ model: "deepseek-v4-flash" }], + "deepseek-v4-flash", + ), ).toBe(true); expect(catalogHasRoleModel([], "deepseek-v4-flash")).toBe(false); }); diff --git a/tests/components/AddProviderDialog.test.tsx b/tests/components/AddProviderDialog.test.tsx index c4d92c60..d05107d5 100644 --- a/tests/components/AddProviderDialog.test.tsx +++ b/tests/components/AddProviderDialog.test.tsx @@ -13,6 +13,7 @@ const queryClientMocks = vi.hoisted(() => ({ })); const universalProtocolMocks = vi.hoisted(() => ({ preflightCodex: vi.fn(), + restoreCodex: vi.fn().mockResolvedValue(null), prepareCodex: vi.fn(), commitCodex: vi.fn(), preflight: vi.fn(), @@ -53,6 +54,7 @@ vi.mock("@/lib/api/protocol-compatibility", async (importOriginal) => { ...actual, preflightCodexProviderProtocolCompatibility: universalProtocolMocks.preflightCodex, + restoreCodexProviderProtocolEvidence: universalProtocolMocks.restoreCodex, prepareCodexProviderSet: universalProtocolMocks.prepareCodex, commitCodexProviderSet: universalProtocolMocks.commitCodex, preflightUniversalCodexProtocolCompatibility: From e35aacbd431db865e11a04045f0d31df1df4f588 Mon Sep 17 00:00:00 2001 From: "Matt.Li" <270539505+matt1060338871-pixel@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:26:03 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(ci):=20=E5=85=BC=E5=AE=B9=E9=9D=9E=20Wi?= =?UTF-8?q?ndows=20=E5=B9=B3=E5=8F=B0=E4=B8=A5=E6=A0=BC=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/codex_paginated_history_repair.rs | 7 +++++++ src-tauri/src/process_identity.rs | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src-tauri/src/codex_paginated_history_repair.rs b/src-tauri/src/codex_paginated_history_repair.rs index e81c8c13..5d592fc1 100644 --- a/src-tauri/src/codex_paginated_history_repair.rs +++ b/src-tauri/src/codex_paginated_history_repair.rs @@ -1561,6 +1561,13 @@ fn repaired_projection_status_at( Ok(status) } +#[allow(dead_code)] +pub(crate) fn repaired_projections_caught_up( + outcome: &PaginatedHistoryRepairOutcome, +) -> Result { + Ok(repaired_projection_status(outcome)?.is_caught_up()) +} + /// 游标是否停在一个合法的记录边界上(0 与文件末尾都算合法)。 /// /// 这正是我们修复的损坏判据的反面:损坏的游标指向记录内部,Codex 从这里读不出完整 diff --git a/src-tauri/src/process_identity.rs b/src-tauri/src/process_identity.rs index 38700e1a..cc3331ca 100644 --- a/src-tauri/src/process_identity.rs +++ b/src-tauri/src/process_identity.rs @@ -31,11 +31,13 @@ impl ProcessIdentity { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ProcessIdentityError { NotFound, + #[cfg_attr(target_os = "macos", allow(dead_code))] AccessDenied, Unavailable(u32), } impl ProcessIdentityError { + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] pub(crate) fn from_win32(code: u32) -> Self { const ERROR_ACCESS_DENIED: u32 = 5; const ERROR_INVALID_PARAMETER: u32 = 87; @@ -96,6 +98,7 @@ pub(crate) struct TcpPortRow { pub pid: u32, } +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] fn tcp_state_label(state: u32) -> &'static str { match state { 1 => "CLOSED", @@ -439,6 +442,7 @@ pub(crate) fn harden_socket_handle_not_inheritable(raw_socket: usize) { } #[cfg(not(target_os = "windows"))] +#[allow(dead_code)] pub(crate) fn harden_socket_handle_not_inheritable(_raw_socket: usize) {} pub(crate) fn current_executable_path() -> Option { From 00263919a0d50a90845ba6d3f2ed300ce38cd41d Mon Sep 17 00:00:00 2001 From: "Matt.Li" <270539505+matt1060338871-pixel@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:33:34 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(ci):=20=E5=AF=B9=E9=BD=90=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E9=A2=84=E6=A3=80=E7=9A=84=E5=B9=B3=E5=8F=B0=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/codex_paginated_history_repair.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src-tauri/src/codex_paginated_history_repair.rs b/src-tauri/src/codex_paginated_history_repair.rs index 5d592fc1..93c2c6ef 100644 --- a/src-tauri/src/codex_paginated_history_repair.rs +++ b/src-tauri/src/codex_paginated_history_repair.rs @@ -61,8 +61,11 @@ pub struct BlockedRolloutReasonGroup { pub samples: Vec, } +#[cfg(any(target_os = "windows", test))] const BLOCKED_REASON_SAMPLE_LIMIT: usize = 5; +#[cfg(any(target_os = "windows", test))] const IMMUTABLE_BLOCKED_PREFIX: &str = "codex_paginated_history_immutable: "; +#[cfg(any(target_os = "windows", test))] const IMMUTABLE_BLOCKED_TRAILER: &str = "; provider migration cannot safely rewrite byte-addressed history"; @@ -70,6 +73,7 @@ const IMMUTABLE_BLOCKED_TRAILER: &str = /// /// 保护性跳过来自两组代码:迁移守卫的 `codex_paginated_history_immutable` /// 报文,以及分页谱系/投影游标检查抛出的 `code: key=value` 报文。 +#[cfg(any(target_os = "windows", test))] fn classify_blocked_reason(message: &str) -> (String, String, Option) { let trimmed = message.trim(); if let Some(rest) = trimmed.strip_prefix(IMMUTABLE_BLOCKED_PREFIX) { @@ -118,6 +122,7 @@ fn classify_blocked_reason(message: &str) -> (String, String, Option) { (code, String::new(), sample) } +#[cfg(any(target_os = "windows", test))] fn group_blocked_reasons(blocked: &[String]) -> Vec { let mut groups: Vec = Vec::new(); for message in blocked { From 8d12481872d9cd9e7a8fe00195c9f2031e263e3b Mon Sep 17 00:00:00 2001 From: "Matt.Li" <270539505+matt1060338871-pixel@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:41:05 +0100 Subject: [PATCH 5/5] =?UTF-8?q?fix(ci):=20=E9=99=90=E5=88=B6=20Windows=20?= =?UTF-8?q?=E4=B8=93=E7=94=A8=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/process_identity.rs | 1 + src-tauri/src/services/proxy.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src-tauri/src/process_identity.rs b/src-tauri/src/process_identity.rs index cc3331ca..76e4b554 100644 --- a/src-tauri/src/process_identity.rs +++ b/src-tauri/src/process_identity.rs @@ -942,6 +942,7 @@ mod tests { } #[test] + #[cfg(target_os = "windows")] fn port_rows_are_attributed_to_the_listening_process() { let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind listener"); let port = listener.local_addr().expect("addr").port(); diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 2ece416c..cde9da1e 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -5626,6 +5626,7 @@ mod tests { assert_eq!(ProxyService::stale_listener_owner_pid(port), None); } + #[cfg(target_os = "windows")] #[tokio::test] #[serial] async fn release_stale_listener_holders_never_touches_foreign_children() {