From 27c41f74162a133abf55773236f7669ccfd2d1e9 Mon Sep 17 00:00:00 2001 From: CSberlin <39453806+CSberlin@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:26:44 +0800 Subject: [PATCH] feat(proxy): add GET /v1/models endpoint for Codex CLI reachability check (#3818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(proxy): add GET /v1/models endpoint for Codex CLI reachability check Codex CLI probes GET /v1/models at startup. Without this endpoint the proxy returns 404, causing Codex to fail before any request reaches the upstream LLM. Return an OpenAI-compatible model list derived from the cc-switch–managed model catalog file. Fixes #3812 * fix(proxy): return Codex catalog schema from /v1/models Codex deserializes the response as a catalog with a top-level `models` field, not the OpenAI `{"object":"list","data":[...]}` envelope. Return the catalog file content directly so the format matches what Codex expects. Co-authored-by: Codex review bot * fix(proxy): guard /v1/models against serving stale catalog Only return the model catalog when config.toml still references it via `model_catalog_json`. After switching to a provider without a custom catalog, the old file lingers on disk — serving it unconditionally would advertise the previous provider's models to Codex. Co-authored-by: Codex review bot * fix(proxy): match relative model_catalog_json in stale-guard cc-switch writes `model_catalog_json = "cc-switch-model-catalog.json"` (relative) via set_codex_model_catalog_json_field. Match on the filename constant rather than the absolute path so the guard works with both relative and absolute paths. Co-authored-by: Codex review bot * fix(proxy): parse model_catalog_json field instead of substring match Replace raw config_text.contains() with proper TOML field parsing so commented-out lines and stray mentions of the filename in other fields don't defeat the stale guard. Also switch from contains() to exact filename match (Path::new(val).file_name() == Some(...)) to stay consistent with resolve_cc_switch_catalog_path in codex_config.rs. Add log::debug! when the guard blocks serving so the operator can distinguish "no models configured" from "guard blocked stale catalog". * refactor(proxy): reuse resolve_cc_switch_catalog_path in handle_models Replace the inline config.toml parsing and filename match in handle_models with the existing resolve_cc_switch_catalog_path helper (now pub(crate)). This removes the duplicated stale-guard logic, keeps a single source of truth for catalog-path ownership, and makes the handler honor absolute model_catalog_json paths the same way Codex live-setting import does. --------- Co-authored-by: Jason --- src-tauri/src/codex_config.rs | 5 ++++- src-tauri/src/proxy/handlers.rs | 35 +++++++++++++++++++++++++++++++++ src-tauri/src/proxy/server.rs | 3 +++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index 5f1a7ddb6..ac9b43bf1 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -766,7 +766,10 @@ pub fn read_codex_model_catalog_simplified_from_live() -> Result, /// Given `config.toml` text, resolve the on-disk path of the cc-switch–owned /// catalog file (returns `None` if `model_catalog_json` is absent or points at /// a file we don't own). Relative paths fall back to `generated_path`. -fn resolve_cc_switch_catalog_path(config_text: &str, generated_path: &Path) -> Option { +pub(crate) fn resolve_cc_switch_catalog_path( + config_text: &str, + generated_path: &Path, +) -> Option { if config_text.trim().is_empty() { return None; } diff --git a/src-tauri/src/proxy/handlers.rs b/src-tauri/src/proxy/handlers.rs index 96076470d..93a51e65c 100644 --- a/src-tauri/src/proxy/handlers.rs +++ b/src-tauri/src/proxy/handlers.rs @@ -62,6 +62,41 @@ pub async fn get_status(State(state): State) -> Result Result, ProxyError> { + let generated_path = crate::codex_config::get_codex_model_catalog_path(); + let active_catalog_path = match crate::codex_config::read_codex_config_text() { + Ok(config_text) => { + crate::codex_config::resolve_cc_switch_catalog_path(&config_text, &generated_path) + } + Err(_) => None, + }; + + let catalog = if let Some(catalog_path) = + active_catalog_path.as_ref().filter(|path| path.exists()) + { + let text = std::fs::read_to_string(catalog_path).unwrap_or_default(); + serde_json::from_str(&text).unwrap_or(json!({"models": []})) + } else { + if active_catalog_path.is_none() { + log::debug!( + "[models] stale guard: catalog not served (model_catalog_json not set to cc-switch catalog)" + ); + } + json!({"models": []}) + }; + Ok(Json(catalog)) +} + // ============================================================================ // Claude API 处理器(包含格式转换逻辑) // ============================================================================ diff --git a/src-tauri/src/proxy/server.rs b/src-tauri/src/proxy/server.rs index b4c551e35..e7b6f601b 100644 --- a/src-tauri/src/proxy/server.rs +++ b/src-tauri/src/proxy/server.rs @@ -315,6 +315,9 @@ impl ProxyServer { "/codex/v1/chat/completions", post(handlers::handle_chat_completions), ) + // OpenAI Models API (Codex CLI reachability check) + .route("/models", get(handlers::handle_models)) + .route("/v1/models", get(handlers::handle_models)) // OpenAI Responses API (Codex CLI,支持带前缀和不带前缀) .route("/responses", post(handlers::handle_responses)) .route("/v1/responses", post(handlers::handle_responses))