feat(proxy): add GET /v1/models endpoint for Codex CLI reachability check (#3818)

* 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 <farion1231@gmail.com>
This commit is contained in:
CSberlin
2026-06-07 20:26:44 +08:00
committed by GitHub
parent 6716a4c408
commit 27c41f7416
3 changed files with 42 additions and 1 deletions
+4 -1
View File
@@ -766,7 +766,10 @@ pub fn read_codex_model_catalog_simplified_from_live() -> Result<Option<Value>,
/// Given `config.toml` text, resolve the on-disk path of the cc-switchowned
/// 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<PathBuf> {
pub(crate) fn resolve_cc_switch_catalog_path(
config_text: &str,
generated_path: &Path,
) -> Option<PathBuf> {
if config_text.trim().is_empty() {
return None;
}
+35
View File
@@ -62,6 +62,41 @@ pub async fn get_status(State(state): State<ProxyState>) -> Result<Json<ProxySta
Ok(Json(status))
}
/// GET /v1/models — Codex model list (reachability check)
///
/// Codex CLI probes this endpoint at startup and deserializes the response as a
/// catalog with a top-level `models` field. Return the cc-switchmanaged model
/// catalog file directly so the format always matches what the current version
/// of Codex expects.
///
/// Only serves the catalog when the live config.toml still references the
/// cc-switchowned `model_catalog_json`, using the same path ownership rules as
/// Codex live-setting import.
pub async fn handle_models() -> Result<Json<Value>, 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 处理器(包含格式转换逻辑)
// ============================================================================
+3
View File
@@ -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))