From a690987ea220d081a09656e7c069235106468f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 21 Sep 2026 18:36:46 -0400 Subject: [PATCH] revert: openai-codex goes back to the compiled-in catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts c61977c. The live listing needed no codex CLI on the user's machine — kigi issued the request itself and `client_version` was a compiled-in string — but the project keeps the catalog in the binary, so the fetch path, the `OpenaiCodex` listing dialect, its parser and the pinned client version are gone again, and the codex `User-Agent` returns to its previous value. The models.dev provider rename `kimi-for-coding` → `kimi-code-plan-cn` rode in the same commit and stays: the regenerated snapshot ships under the new id. --- AGENTS.md | 30 +- crates/codegen/kigi-models/src/lib.rs | 266 +++++++++++------- .../codegen/kigi-sampling-types/src/types.rs | 26 +- .../kigi-shell/src/agent/models_fetch.rs | 127 +++++---- 4 files changed, 238 insertions(+), 211 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5643f06..b74f2a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -383,18 +383,13 @@ client-side, no backend surface. API-key `openai` Responses requests carry NONE of this (byte-identical, pinned by a control wire test). `reasoning.effort` carries the thinking level. NO websocket, NO base_instructions. - CATALOG is LIVE (`ListingDialect::OpenaiCodex` → - `parse_openai_codex_listing`): `GET {base}/models?client_version= - {CODEX_CLIENT_VERSION}` with the Bearer alone (no identity headers — - verified), keeping the `visibility=="list" && supported_in_api` models - and carrying their context window + per-model efforts. The query is - REQUIRED (400 without it) and the backend GATES the catalog on it: - `0.104.0` answers with `codex-auto-review` alone, `0.150.0` omits - `gpt-6-astra`, `0.155.1` serves the full set — so - `CODEX_CLIENT_VERSION` (kigi-sampling-types, also the `User-Agent` - version) must track a real upstream codex release, and a model that - never appears is a version-gate symptom first. NO codex-CLI / `~/.codex` - dependency. + CATALOG is HARDCODED (`PlatformId::hardcoded_catalog` → + `openai_codex_wire_models`, mapped through the SAME + `platform_wire_model_to_entry` output): exactly the 4 `visibility=list` && + `supported_in_api=true` models (`gpt-5.6-sol/terra/luna`, `gpt-5.5`, ctx + 272000, per-model efforts) — NO live `/models` fetch, NO codex-CLI / + `~/.codex` dependency; `gpt-5.3-codex-spark` (api=false) and + `gpt-5.4`/`gpt-5.4-mini`/`codex-auto-review` (hidden) are EXCLUDED. - `OAuthFlow::GithubDeviceCopilot` → `auth::github_copilot` (TWO-STAGE). Provider: `github-copilot` (`scope_key oauth/github-copilot`, base `api.individual.githubcopilot.com`, ChatCompletions wire). Stage 1 is an @@ -537,14 +532,9 @@ client-side, no backend surface. `~/.kigi/models_dev_cache.json`). Wire values always win; enrichment never invents model availability. Canonical reasoning efforts: none/minimal/low/medium/high/xhigh/max (`max` split from `xhigh` 2026-07; - Kimi wire spells its top tier `max`, kimi_compat renames). config.toml - accepts any letter case: `ReasoningEffort` deserializes through `FromStr`. - The codex catalog advertises an `ultra` tier above `max` for - `gpt-6-astra`/`gpt-5.6-sol`/`gpt-5.6-terra`; kigi has no such level and - `think_efforts_to_options` drops it with a warning. The models.dev provider - for the Kimi coding plan is `kimi-code-plan-cn` (api.kimi.com — kigi's - endpoint); it was renamed from `kimi-for-coding` in 2026-09 and - `kimi-code-plan-global` is the api.kimi.ai twin. + `ultra` was dropped the same month, no backend accepts it; Kimi wire + spells its top tier `max`, kimi_compat renames). config.toml accepts any + letter case: `ReasoningEffort` deserializes through `FromStr`. ## Milestones (PRD §8.3) diff --git a/crates/codegen/kigi-models/src/lib.rs b/crates/codegen/kigi-models/src/lib.rs index f2abbbb..33a88de 100644 --- a/crates/codegen/kigi-models/src/lib.rs +++ b/crates/codegen/kigi-models/src/lib.rs @@ -61,11 +61,6 @@ pub enum ListingDialect { /// headers, Anthropic's response shape (parsed by /// [`parse_anthropic_listing`]). Anthropic, - /// `GET {base}/models?client_version=`, - /// `Authorization: Bearer`, `{models:[{slug,...}]}` (parsed by - /// [`parse_openai_codex_listing`]). The query is REQUIRED (400 without it) - /// and gates which models the backend returns. - OpenaiCodex, } /// ChatCompletions body-adaptation dialect (leaf-safe mirror of the @@ -1272,12 +1267,17 @@ const OPENAI_CODEX_SPEC: PlatformSpec = PlatformSpec { vendor: "OpenAI", console_host: Some("chatgpt.com"), login_label: Some("ChatGPT Plus/Pro (Codex)"), - // The Codex backend serves its own catalog with context + per-model - // reasoning levels, so models.dev enrichment is neither used nor needed. + // HARDCODED catalog (see `hardcoded_catalog`): NOT enriched from models.dev + // and NOT live-fetched — OpenAI exposes no stable public models endpoint for + // this backend. models_dev_id: None, + // The catalog is compiled-in and already carries context/thinking metadata, + // so enrichment (and its network refresh) is skipped entirely. wire_serves_metadata: true, wire_api: PlatformWireApi::Responses, - listing: ListingDialect::OpenaiCodex, + // Unused: the catalog does NOT come from a live `/models` listing (the + // fetch path short-circuits to `hardcoded_catalog`). + listing: ListingDialect::OpenAi, chat_compat: PlatformChatCompat::Passthrough, key_header: PlatformKeyHeader::Bearer, restrict_to_enriched: false, @@ -1566,6 +1566,79 @@ impl PlatformId { pub fn sends_codex_responses_headers(self) -> bool { matches!(self, Self::OpenaiCodex) } + + /// The compiled-in catalog for a platform that serves NO live `/models` + /// listing (openai-codex), or `None` when the catalog comes from the wire. + /// + /// openai-codex's 4 models are HARDCODED (read from the official Codex CLI's + /// `models_cache.json`, the `visibility=="list"` AND `supported_in_api==true` + /// set) because OpenAI exposes no stable public models endpoint for the + /// ChatGPT Codex backend. Each entry carries context window + per-model + /// selectable reasoning efforts (incl. the codex-only `xhigh`/`max` + /// tiers), so the fetch path maps them through the SAME + /// `platform_wire_model_to_entry` output as a live listing — no new type. + pub fn hardcoded_catalog(self) -> Option> { + match self { + Self::OpenaiCodex => Some(openai_codex_wire_models()), + _ => None, + } + } +} + +/// One hardcoded openai-codex model as a [`WireModel`] (context 272000, thinking +/// + image input, selectable efforts). `efforts` are the exact per-model +/// supported tiers; `default` is the model's default effort. +fn codex_wire_model(slug: &str, display_name: &str, efforts: &[&str], default: &str) -> WireModel { + WireModel { + id: slug.to_string(), + context_length: 272_000, + supports_reasoning: true, + supports_image_in: true, + supports_video_in: false, + display_name: Some(display_name.to_string()), + max_output_tokens: 0, + supports_thinking_type: None, + think_efforts: Some(WireThinkEfforts { + support: true, + valid_efforts: efforts.iter().map(|s| (*s).to_string()).collect(), + default_effort: Some(default.to_string()), + }), + } +} + +/// The HARDCODED openai-codex catalog: exactly the 4 `visibility=="list"` AND +/// `supported_in_api==true` models from the Codex CLI model cache. The +/// `gpt-5.3-codex-spark` (supported_in_api=false → not served by /responses), +/// `gpt-5.4`, `gpt-5.4-mini`, and `codex-auto-review` (visibility="hide") models +/// are deliberately EXCLUDED — they would list-but-not-work or are not +/// user-facing (fail-fast: never advertise a model the backend rejects). +fn openai_codex_wire_models() -> Vec { + vec![ + codex_wire_model( + "gpt-5.6-sol", + "GPT-5.6-Sol", + &["low", "medium", "high", "xhigh", "max"], + "low", + ), + codex_wire_model( + "gpt-5.6-terra", + "GPT-5.6-Terra", + &["low", "medium", "high", "xhigh", "max"], + "medium", + ), + codex_wire_model( + "gpt-5.6-luna", + "GPT-5.6-Luna", + &["low", "medium", "high", "xhigh", "max"], + "medium", + ), + codex_wire_model( + "gpt-5.5", + "GPT-5.5", + &["low", "medium", "high", "xhigh"], + "medium", + ), + ] } /// Split a managed catalog key `{platform_id}/{model_id}` back into its @@ -2011,72 +2084,6 @@ pub fn parse_github_copilot_listing(json: &str) -> Result, serde_ Ok(kept) } -/// One entry of the Codex backend catalog (`{"models":[...]}`). Only the -/// fields Kigi consumes are declared; the response carries many more. -#[derive(serde::Deserialize)] -struct CodexModel { - slug: String, - display_name: Option, - visibility: String, - supported_in_api: bool, - context_window: Option, - default_reasoning_level: Option, - #[serde(default)] - supported_reasoning_levels: Vec, - #[serde(default)] - input_modalities: Vec, -} - -#[derive(serde::Deserialize)] -struct CodexReasoningLevel { - effort: String, -} - -#[derive(serde::Deserialize)] -struct CodexListing { - models: Vec, -} - -/// Parse the ChatGPT Codex backend catalog -/// (`GET {base}/models?client_version=…`). Keeps only the models the backend -/// both shows and serves (`visibility == "list"` AND `supported_in_api`), so a -/// hidden or API-less model (`gpt-reserve`, `codex-auto-review`) never reaches -/// the picker. Context window and the per-model reasoning levels ride the wire; -/// an effort Kigi does not know (`ultra`) is dropped downstream by -/// `think_efforts_to_options`. -pub fn parse_openai_codex_listing(json: &str) -> Result, serde_json::Error> { - let listing: CodexListing = serde_json::from_str(json)?; - let kept = listing - .models - .into_iter() - .filter(|m| m.visibility == "list" && m.supported_in_api) - .map(|m| { - let efforts: Vec = m - .supported_reasoning_levels - .into_iter() - .map(|l| l.effort) - .collect(); - let supports_image_in = m.input_modalities.iter().any(|kind| kind == "image"); - WireModel { - id: m.slug, - context_length: m.context_window.unwrap_or_default(), - supports_reasoning: !efforts.is_empty(), - supports_image_in, - supports_video_in: false, - display_name: m.display_name, - max_output_tokens: 0, - supports_thinking_type: None, - think_efforts: (!efforts.is_empty()).then_some(WireThinkEfforts { - support: true, - valid_efforts: efforts, - default_effort: m.default_reasoning_level, - }), - } - }) - .collect(); - Ok(kept) -} - // Bundled offline fallback catalog /// The raw JSON, embedded at compile time. OFFLINE LAST RESORT: consulted only @@ -2646,47 +2653,90 @@ mod tests { assert_eq!(c.base_url(), "https://mock.codex/codex"); } - /// The Codex catalog is LIVE. The parser keeps only the models the backend - /// shows AND serves, carries the wire context window, and passes every - /// advertised effort through (`ultra` included — the shell drops what it - /// cannot map). Body captured from - /// `GET chatgpt.com/backend-api/codex/models?client_version=0.155.1`, plus - /// `gpt-5.3-codex-spark` — the listed-but-`supported_in_api:false` shape the - /// backend served until recently, which `/responses` rejects. + /// The HARDCODED openai-codex catalog is exactly the 4 supported+listed + /// models, keyed by slug, ctx 272000, each exposing its exact supported + /// efforts (incl. the codex-only `xhigh`/`max` tiers). The + /// list-but-broken / hidden models are absent. Every other platform serves + /// NO hardcoded catalog (its models come from the live wire). #[test] - fn openai_codex_listing_keeps_listed_api_served_models() { - const BODY: &str = r#"{"models":[{"slug":"gpt-6-astra","display_name":"GPT-6-Astra","visibility":"list","supported_in_api":true,"context_window":272000,"default_reasoning_level":"medium","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"},{"effort":"xhigh"},{"effort":"max"},{"effort":"ultra"}],"input_modalities":["text","image"]},{"slug":"gpt-reserve","display_name":"GPT-Reserve","visibility":"hide","supported_in_api":true,"context_window":272000,"default_reasoning_level":"medium","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"},{"effort":"xhigh"},{"effort":"max"}],"input_modalities":["text","image"]},{"slug":"gpt-5.3-codex-spark","display_name":"GPT-5.3-Codex-Spark","visibility":"list","supported_in_api":false,"context_window":272000,"default_reasoning_level":"medium","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"}],"input_modalities":["text","image"]},{"slug":"gpt-5.5","display_name":"GPT-5.5","visibility":"list","supported_in_api":true,"context_window":272000,"default_reasoning_level":"medium","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"},{"effort":"xhigh"}],"input_modalities":["text","image"]}]}"#; - let models = parse_openai_codex_listing(BODY).expect("codex listing parses"); - let ids: Vec<&str> = models.iter().map(|m| m.id.as_str()).collect(); + fn openai_codex_hardcoded_catalog_is_the_four_supported_models() { + let catalog = PlatformId::OpenaiCodex + .hardcoded_catalog() + .expect("openai-codex serves a hardcoded catalog"); + let ids: Vec<&str> = catalog.iter().map(|m| m.id.as_str()).collect(); assert_eq!( ids, - vec!["gpt-6-astra", "gpt-5.5"], - "the hidden model and the listed-but-not-API-served one are dropped" + vec!["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"], + "exactly the 4 visibility=list AND supported_in_api=true models" ); - - let astra = &models[0]; - assert_eq!(astra.display_name.as_deref(), Some("GPT-6-Astra")); - assert_eq!(astra.context_length, 272_000); - assert!(astra.supports_reasoning && astra.supports_image_in); - assert!(!astra.supports_video_in); - let caps = astra.capabilities(); - assert!(caps.contains(&ModelCapability::Thinking)); - assert!(caps.contains(&ModelCapability::ImageIn)); - let efforts = astra.think_efforts.as_ref().expect("astra efforts"); - assert!(efforts.support); - assert_eq!( - efforts.valid_efforts, - ["low", "medium", "high", "xhigh", "max", "ultra"] - ); - assert_eq!(efforts.default_effort.as_deref(), Some("medium")); - assert_eq!( - models[1] + // Excluded: list-but-not-served + hidden models never appear. + for absent in [ + "gpt-5.3-codex-spark", + "gpt-5.4", + "gpt-5.4-mini", + "codex-auto-review", + ] { + assert!( + !ids.contains(&absent), + "{absent} must be excluded from the hardcoded catalog" + ); + } + for m in &catalog { + assert_eq!(m.context_length, 272_000, "{} ctx", m.id); + assert!(m.supports_reasoning && m.supports_image_in, "{} caps", m.id); + let caps = m.capabilities(); + assert!(caps.contains(&ModelCapability::Thinking)); + assert!(caps.contains(&ModelCapability::ImageIn)); + } + // Per-model efforts (the crux of "their thinking method"). + let efforts = |slug: &str| -> Vec { + catalog + .iter() + .find(|m| m.id == slug) + .unwrap() .think_efforts .as_ref() - .expect("gpt-5.5 efforts") - .valid_efforts, - ["low", "medium", "high", "xhigh"] + .unwrap() + .valid_efforts + .clone() + }; + assert_eq!( + efforts("gpt-5.6-sol"), + ["low", "medium", "high", "xhigh", "max"] ); + assert_eq!( + efforts("gpt-5.6-terra"), + ["low", "medium", "high", "xhigh", "max"] + ); + assert_eq!( + efforts("gpt-5.6-luna"), + ["low", "medium", "high", "xhigh", "max"] + ); + assert_eq!(efforts("gpt-5.5"), ["low", "medium", "high", "xhigh"]); + // Default efforts per the model table. + let default = |slug: &str| -> Option { + catalog + .iter() + .find(|m| m.id == slug) + .unwrap() + .think_efforts + .as_ref() + .unwrap() + .default_effort + .clone() + }; + assert_eq!(default("gpt-5.6-sol").as_deref(), Some("low")); + assert_eq!(default("gpt-5.6-terra").as_deref(), Some("medium")); + // Only openai-codex has a hardcoded catalog; every wire platform is None. + for p in PlatformId::ALL { + if p != PlatformId::OpenaiCodex { + assert!( + p.hardcoded_catalog().is_none(), + "{} must not carry a hardcoded catalog", + p.as_str() + ); + } + } } /// The Copilot `/models` filter keeps ONLY openai-completions-served, diff --git a/crates/codegen/kigi-sampling-types/src/types.rs b/crates/codegen/kigi-sampling-types/src/types.rs index 96d13e3..fadf424 100644 --- a/crates/codegen/kigi-sampling-types/src/types.rs +++ b/crates/codegen/kigi-sampling-types/src/types.rs @@ -1198,19 +1198,11 @@ pub const COPILOT_INITIATOR: &str = "user"; pub const CODEX_ORIGINATOR: &str = "codex_cli_rs"; /// `OpenAI-Beta` opt-in the Codex Responses endpoint requires. pub const CODEX_OPENAI_BETA: &str = "responses=experimental"; -/// Codex CLI version kigi presents to the Codex backend. The models catalog -/// (`/codex/models?client_version=`) is GATED on it: an old version answers -/// with a truncated model list (0.104.0 serves no user-facing model at all), -/// so this tracks a real upstream release (openai/codex rust-v0.155.1). -macro_rules! codex_client_version { - () => { - "0.155.1" - }; -} -pub const CODEX_CLIENT_VERSION: &str = codex_client_version!(); /// `User-Agent` presented on the Codex path (overrides the default kigi UA, -/// openai-codex-gated), carrying the same pinned version. -pub const CODEX_USER_AGENT: &str = concat!("codex_cli_rs/", codex_client_version!()); +/// openai-codex-gated). The Codex backend does not strictly validate the UA +/// string (Pi ships its own and it works), so this is a stable best-effort +/// identity, not a pinned build. +pub const CODEX_USER_AGENT: &str = "codex_cli_rs/0.104.0"; /// JWT payload claim namespace carrying the ChatGPT account id. const CODEX_JWT_AUTH_CLAIM: &str = "https://api.openai.com/auth"; @@ -1643,16 +1635,6 @@ mod tests { use super::*; use serde_json::json; - /// The catalog gate and the UA must claim the SAME codex version: the - /// backend serves a truncated model list to an old client_version. - #[test] - fn codex_user_agent_carries_the_pinned_client_version() { - assert_eq!( - CODEX_USER_AGENT, - format!("codex_cli_rs/{CODEX_CLIENT_VERSION}") - ); - } - /// The Codex backend rejects `role: system` input outright /// (400 `{"detail":"System messages are not allowed"}`) — its system /// channel is the top-level `instructions` field, and stateless diff --git a/crates/codegen/kigi-shell/src/agent/models_fetch.rs b/crates/codegen/kigi-shell/src/agent/models_fetch.rs index ee1c7f9..2c6c638 100644 --- a/crates/codegen/kigi-shell/src/agent/models_fetch.rs +++ b/crates/codegen/kigi-shell/src/agent/models_fetch.rs @@ -363,6 +363,19 @@ fn fetch_one_platform_models( bearer: &str, enrichment: &kigi_models::enrichment::EnrichmentCatalog, ) -> Result<(Vec, Option), BackendError> { + // A platform that serves NO live `/models` listing (openai-codex) delivers a + // HARDCODED catalog: short-circuit BEFORE any HTTP, mapping the compiled-in + // `WireModel`s through the SAME `platform_wire_model_to_entry` output a live + // listing produces (context window + per-model reasoning efforts). The + // bearer is unused here (login gates availability; no request is made). + if let Some(wire_models) = platform.hardcoded_catalog() { + let base_url = platform_fetch_base(platform, endpoints); + let models = wire_models + .into_iter() + .map(|wire| platform_wire_model_to_entry(platform, wire, &base_url)) + .collect(); + return Ok((models, None)); + } let client = crate::http::shared_blocking_client(); let url = match platform.listing() { kigi_models::ListingDialect::OpenAi => platform_models_url(platform, endpoints), @@ -371,13 +384,6 @@ fn fetch_one_platform_models( kigi_models::ListingDialect::Anthropic => { format!("{}?limit=1000", platform_models_url(platform, endpoints)) } - // The Codex backend REQUIRES client_version (400 without it) and gates - // the catalog on it: an old version is answered with a truncated list. - kigi_models::ListingDialect::OpenaiCodex => format!( - "{}?client_version={}", - platform_models_url(platform, endpoints), - kigi_sampling_types::CODEX_CLIENT_VERSION - ), }; tracing::info!(platform = platform.as_str(), url = %url, "fetching platform models"); let request = match platform.key_header() { @@ -470,18 +476,6 @@ fn fetch_one_platform_models( } })? } - // The Codex backend serves `{models:[...]}` with its own availability - // fields; the adapter keeps the shown + API-served models and carries - // context window and per-model reasoning levels. - kigi_models::ListingDialect::OpenaiCodex => { - let body = response.text()?; - kigi_models::parse_openai_codex_listing(&body).map_err(|e| { - BackendError::RequestFailed { - status: 200, - body: format!("codex listing parse failed: {e}"), - } - })? - } }; // Canonicalize listing ids before filtering/enrichment/keying. Google's // OpenAI-compat `/models` returns `models/`-prefixed ids while its chat @@ -1477,36 +1471,25 @@ mod tests { ); } - /// openai-codex e2e: the catalog is LIVE. The request carries the pinned - /// `client_version` (the backend 400s without it and truncates the list for - /// an old one) and the session bearer; the hidden model is dropped, and the - /// kept ones are keyed `openai-codex/` on the Responses backend with - /// their wire context window and efforts (`ultra`, which Kigi has no level - /// for, is dropped). + /// openai-codex fetch: the catalog is HARDCODED, so the fetch path + /// short-circuits BEFORE any HTTP — there is NO mock `/models` server, yet + /// the fetch returns exactly the 4 compiled-in models keyed + /// `openai-codex/` on the Responses backend, ctx 272000, each exposing + /// its exact reasoning efforts (incl. the codex-only `xhigh`/`max`). + /// A BOGUS base URL confirms no live `/models` request is attempted (it would + /// otherwise fail against an unroutable host). #[tokio::test(flavor = "multi_thread")] #[serial_test::serial] - async fn openai_codex_catalog_comes_from_the_live_listing() { - const BODY: &str = r#"{"models":[{"slug":"gpt-6-astra","display_name":"GPT-6-Astra","visibility":"list","supported_in_api":true,"context_window":272000,"default_reasoning_level":"medium","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"},{"effort":"xhigh"},{"effort":"max"},{"effort":"ultra"}],"input_modalities":["text","image"]},{"slug":"gpt-reserve","display_name":"GPT-Reserve","visibility":"hide","supported_in_api":true,"context_window":272000,"default_reasoning_level":"medium","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"},{"effort":"xhigh"},{"effort":"max"}],"input_modalities":["text","image"]},{"slug":"gpt-5.5","display_name":"GPT-5.5","visibility":"list","supported_in_api":true,"context_window":272000,"default_reasoning_level":"medium","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"},{"effort":"xhigh"}],"input_modalities":["text","image"]}]}"#; - let codex_server = wiremock::MockServer::start().await; - wiremock::Mock::given(wiremock::matchers::method("GET")) - .and(wiremock::matchers::path("/models")) - .and(wiremock::matchers::query_param( - "client_version", - kigi_sampling_types::CODEX_CLIENT_VERSION, - )) - .and(wiremock::matchers::header( - "Authorization", - "Bearer codex-session-tok", - )) - .respond_with( - wiremock::ResponseTemplate::new(200).set_body_raw(BODY, "application/json"), - ) - .expect(1) - .mount(&codex_server) - .await; - let _base = - kigi_test_support::EnvGuard::set(kigi_models::CODEX_BASE_URL_ENV, codex_server.uri()); + async fn openai_codex_catalog_is_hardcoded_with_no_http_fetch() { + // Unroutable base: if the fetch path tried a live `/models` request it + // would error here; the hardcoded short-circuit ignores it for fetching. + let _base = kigi_test_support::EnvGuard::set( + kigi_models::CODEX_BASE_URL_ENV, + "http://127.0.0.1:1/codex", + ); let endpoints = crate::agent::config::EndpointsConfig::default(); + // The session token merely marks openai-codex "enabled"; it is unused by + // the hardcoded path (no request rides it). let mut oauth_tokens = OAuthSessionTokens::new(); oauth_tokens.insert( kigi_models::PlatformId::OpenaiCodex, @@ -1518,7 +1501,7 @@ mod tests { }) .await .unwrap() - .expect("openai-codex listing fetch must succeed"); + .expect("openai-codex hardcoded catalog fetch must succeed with no HTTP"); assert_eq!( result @@ -1526,35 +1509,57 @@ mod tests { .iter() .map(|m| m.id.as_deref().unwrap_or_default()) .collect::>(), - vec!["openai-codex/gpt-6-astra", "openai-codex/gpt-5.5"], - "hidden models never reach the picker" + vec![ + "openai-codex/gpt-5.6-sol", + "openai-codex/gpt-5.6-terra", + "openai-codex/gpt-5.6-luna", + "openai-codex/gpt-5.5", + ], + "exactly the 4 hardcoded models, keyed openai-codex/" ); - let astra = &result.models[0]; + // Excluded models never appear. + for absent in [ + "openai-codex/gpt-5.3-codex-spark", + "openai-codex/gpt-5.4", + "openai-codex/gpt-5.4-mini", + "openai-codex/codex-auto-review", + ] { + assert!( + !result + .models + .iter() + .any(|m| m.id.as_deref() == Some(absent)), + "{absent} must be absent from the hardcoded catalog" + ); + } + let sol = &result.models[0]; assert_eq!( - astra.api_backend, + sol.api_backend, crate::sampling::ApiBackend::Responses, "openai-codex speaks the Responses wire" ); - assert_eq!(astra.context_window.get(), 272_000); - assert_eq!(astra.name.as_deref(), Some("GPT-6-Astra")); + assert_eq!(sol.context_window.get(), 272_000); + assert_eq!(sol.name.as_deref(), Some("GPT-5.6-Sol")); assert!( - astra.supported_in_api, - "openai-codex carries its OWN pooled credential — it must NOT be gated on the primary (Kimi) session" + sol.supported_in_api, + "openai-codex carries its OWN pooled credential — it must NOT be \ + gated on the primary (Kimi) session" ); assert!( - visible_to_non_primary_session_user(astra), - "a ChatGPT/Codex-only user has no primary OAuth session; their models must still appear in the picker" + visible_to_non_primary_session_user(sol), + "a ChatGPT/Codex-only user has no primary OAuth session; their \ + models must still appear in the picker" ); - assert!(astra.supports_reasoning_effort); + assert!(sol.supports_reasoning_effort); assert_eq!( - astra - .reasoning_efforts + sol.reasoning_efforts .iter() .map(|o| o.id.as_str()) .collect::>(), vec!["low", "medium", "high", "xhigh", "max"], - "`ultra` has no Kigi level and is dropped" + "sol exposes the full codex effort menu" ); + // gpt-5.5 tops out at xhigh (no max). let five_five = result .models .iter()