From c4795e98ff6587d1c9be28764167af40af2f976b Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 17 Jul 2026 20:05:23 +0800 Subject: [PATCH] fix(codex): backfill parser-required catalog fields from static template Codex >= 0.144.5 requires supports_reasoning_summaries in every external model catalog entry and rejects the whole file at startup when it is missing, which bricks both the CLI and the desktop app. The ProxyChat catalog template is cloned from dynamic sources first (models_cache.json, then `codex debug models --bundled`). models_cache.json is shared by every Codex install on the machine and each version serializes its own ModelInfo shape, so the cache's field set follows whichever process wrote it last and cannot be assumed to satisfy the current external-catalog schema. Deleting the generated catalog never helped: any provider save or switch regenerated it from the same stale template. Backfill parser-required fields (explicit whitelist, currently only supports_reasoning_summaries) from the bundled static template when the dynamic template lacks them. Optional capability fields are deliberately NOT backfilled so their missing-means-parser-default semantics survive, and existing values always win. --- src-tauri/src/codex_config.rs | 102 +++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index 56d74588b..e1f9cd65d 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -891,13 +891,49 @@ fn load_codex_native_responses_template() -> Value { serde_json::from_str(text).expect("bundled codex native responses template must be valid JSON") } +/// Fields Codex's external-catalog parser REQUIRES (no serde default): when +/// one is missing Codex rejects the whole catalog file at startup ("missing +/// field ..."). `base_instructions` is the other known required field; the +/// templates always carry it and `codex_catalog_model_entry` handles it. +/// When Codex requires a new field, add it here AND to the static templates. +const CODEX_CATALOG_PARSER_REQUIRED_FIELDS: &[&str] = &["supports_reasoning_summaries"]; + +/// `models_cache.json` is shared by every Codex install on the machine (npm +/// CLI, desktop-bundled binary, ...), and each version serializes its own +/// `ModelInfo` shape — the cache's field set follows whichever process wrote +/// it last, so it cannot be assumed to satisfy the current external-catalog +/// schema (observed live: 0.144.5 requires `supports_reasoning_summaries` +/// while a coexisting build kept rewriting the cache without it). Backfill +/// ONLY parser-required fields from the bundled static template: optional +/// capability fields keep their missing-means-default semantics, and existing +/// values always win. +fn fill_template_fields_from_static(template: &mut Value) { + let Some(static_template) = load_codex_model_template_static() else { + return; + }; + let (Some(template_obj), Some(static_obj)) = + (template.as_object_mut(), static_template.as_object()) + else { + return; + }; + for key in CODEX_CATALOG_PARSER_REQUIRED_FIELDS { + if !template_obj.contains_key(*key) { + if let Some(value) = static_obj.get(*key) { + template_obj.insert((*key).to_string(), value.clone()); + } + } + } +} + fn load_codex_model_catalog_template_uncached() -> Result { // ① models_cache.json (created by Codex when it connects to OpenAI) - if let Some(template) = load_codex_model_template_from_cache()? { + if let Some(mut template) = load_codex_model_template_from_cache()? { + fill_template_fields_from_static(&mut template); return Ok(template); } // ② codex CLI (PATH + platform-specific common paths) - if let Some(template) = load_codex_model_template_from_bundled()? { + if let Some(mut template) = load_codex_model_template_from_bundled()? { + fill_template_fields_from_static(&mut template); return Ok(template); } // ③ Static fallback bundled at compile time @@ -2697,6 +2733,68 @@ base_url = "https://production.api/v1" assert_eq!(base_url, Some("https://production.api/v1")); } + #[test] + fn dynamic_template_backfills_parser_required_fields_from_static() { + // Simulate a template cloned from a models_cache.json written by a + // Codex build whose ModelInfo lacks parser-side required fields such + // as `supports_reasoning_summaries` (codex >= 0.144.5 rejects the + // whole catalog file without it). + let mut template = json!({ + "slug": "gpt-5.5", + "context_window": 272_000, + "supports_parallel_tool_calls": false + }); + fill_template_fields_from_static(&mut template); + + assert_eq!( + template + .get("supports_reasoning_summaries") + .and_then(Value::as_bool), + Some(true) + ); + // Keys already present in the dynamic template are never overwritten. + assert_eq!( + template + .get("supports_parallel_tool_calls") + .and_then(Value::as_bool), + Some(false) + ); + assert_eq!( + template.get("context_window").and_then(Value::as_u64), + Some(272_000) + ); + // Optional capability fields must NOT be backfilled: for the catalog + // parser "missing" means the parser default, not the static + // template's value. + assert!(template.get("supports_search_tool").is_none()); + assert!(template.get("supports_image_detail_original").is_none()); + assert!(template.get("web_search_tool_type").is_none()); + } + + #[test] + fn proxy_chat_catalog_entries_carry_reasoning_summaries_flag() { + // End to end: a stale dynamic template, once backfilled, must yield + // catalog entries codex 0.144.5+ can parse. + let mut template = json!({ "slug": "gpt-5.5" }); + fill_template_fields_from_static(&mut template); + let specs = vec![CodexCatalogModelSpec { + model: "k3".to_string(), + display_name: "Kimi K3".to_string(), + context_window: 262_144, + supports_parallel_tool_calls: None, + input_modalities: None, + base_instructions: None, + }]; + let catalog = + codex_model_catalog_from_specs(&specs, &template, CodexCatalogToolProfile::ProxyChat); + assert_eq!( + catalog["models"][0] + .get("supports_reasoning_summaries") + .and_then(Value::as_bool), + Some(true) + ); + } + #[test] fn codex_model_catalog_uses_provider_models_and_context() { let template = json!({