From 9024c63ff1e6f1369a47be1c84613031b3677bb5 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sun, 12 Jul 2026 07:44:46 +0000 Subject: [PATCH 1/5] feat(codex): send version header so backend serves newer models The ChatGPT Codex backend gates newer models (e.g. gpt-5.6-luna) behind a recent client version advertised via the `version` HTTP header. Without it POST /responses returns 404 `Model not found`; with `version: 0.144.1` it returns 200. Mirror the Codex CLI so newer models are usable through the router; overridable via CODEX_CLIENT_VERSION. Adds a unit test and changelog fragment. --- .../20260712_090000_codex_version_header.md | 10 +++++++ src/subscription_proxy.rs | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 changelog.d/20260712_090000_codex_version_header.md diff --git a/changelog.d/20260712_090000_codex_version_header.md b/changelog.d/20260712_090000_codex_version_header.md new file mode 100644 index 0000000..6f7e30d --- /dev/null +++ b/changelog.d/20260712_090000_codex_version_header.md @@ -0,0 +1,10 @@ +--- +bump: minor +--- + +### Added +- Codex subscriptions now send a `version` header (default `0.144.1`, overridable via + `CODEX_CLIENT_VERSION`) when proxying to the ChatGPT backend. The backend gates newer + models (e.g. `gpt-5.6-luna`) behind a recent client version; without the header + `POST /responses` returns `Model not found`. This mirrors the Codex CLI so newer models + are usable through the router. diff --git a/src/subscription_proxy.rs b/src/subscription_proxy.rs index da6c8ed..0ff0353 100644 --- a/src/subscription_proxy.rs +++ b/src/subscription_proxy.rs @@ -273,6 +273,13 @@ fn subscription_headers( // identifies the originating client. out.push(("openai-beta", "responses=experimental".to_string())); out.push(("originator", "codex_cli_rs".to_string())); + // Codex gates newer models (e.g. gpt-5.6-luna) behind a recent client version + // advertised via the `version` header; without it the backend replies "Model not + // found". Mirror the Codex CLI. Overridable via CODEX_CLIENT_VERSION. + out.push(( + "version", + std::env::var("CODEX_CLIENT_VERSION").unwrap_or_else(|_| "0.144.1".to_string()), + )); } out } @@ -537,6 +544,25 @@ mod tests { ); } + #[test] + fn codex_headers_include_version() { + let token = SubscriptionToken { + access_token: "a".into(), + refresh_token: None, + expires_at_ms: None, + account_id: Some("acct_9".into()), + resource_url: None, + }; + let headers = subscription_headers(SubscriptionProvider::Codex, &token); + // The Codex backend gates newer models behind a recent client version + // advertised via the `version` header. + assert!( + headers + .iter() + .any(|(k, v)| *k == "version" && !v.is_empty()) + ); + } + #[test] fn rate_limit_headers_are_selected() { let mut headers = HeaderMap::new(); From 22f1e314b236f77392d73807f206a60bfecd28d2 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sun, 12 Jul 2026 08:15:09 +0000 Subject: [PATCH 2/5] style: rustfmt normalize_codex_responses_body (cargo fmt --check) --- src/subscription_proxy.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/subscription_proxy.rs b/src/subscription_proxy.rs index 0ff0353..2c39977 100644 --- a/src/subscription_proxy.rs +++ b/src/subscription_proxy.rs @@ -389,8 +389,8 @@ fn normalize_codex_responses_body(body: &mut serde_json::Value) { // Hoist system/developer turns out of `input` (Codex forbids them there). let mut hoisted: Vec = Vec::new(); if let Some(serde_json::Value::Array(items)) = obj.get_mut("input") { - items.retain(|item| { - match item.get("role").and_then(serde_json::Value::as_str) { + items.retain( + |item| match item.get("role").and_then(serde_json::Value::as_str) { Some("system" | "developer") => { if let Some(text) = input_item_text(item) { hoisted.push(text); @@ -398,8 +398,8 @@ fn normalize_codex_responses_body(body: &mut serde_json::Value) { false } _ => true, - } - }); + }, + ); } // Merge existing instructions + hoisted system turns; fall back to a default. From 58cb34dfb8e2a913506e8daf0c1d53b8fb75a1fd Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sun, 12 Jul 2026 08:27:48 +0000 Subject: [PATCH 3/5] style: cargo fmt --all (remaining hunk) --- src/subscription_proxy.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/subscription_proxy.rs b/src/subscription_proxy.rs index 2c39977..d939b1b 100644 --- a/src/subscription_proxy.rs +++ b/src/subscription_proxy.rs @@ -415,7 +415,10 @@ fn normalize_codex_responses_body(body: &mut serde_json::Value) { } else { parts.join("\n\n") }; - obj.insert("instructions".to_string(), serde_json::Value::String(instructions)); + obj.insert( + "instructions".to_string(), + serde_json::Value::String(instructions), + ); } #[cfg(test)] From d1b5b4ec3534135dde6c973739d62489bd56cd48 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Tue, 14 Jul 2026 04:55:24 +0000 Subject: [PATCH 4/5] style(codex): backtick ChatGPT/OpenClaw in docs to satisfy clippy::doc_markdown The Lint and Format Check job runs `cargo clippy --all-targets --all-features` with `-D warnings`, which fails on two doc comments in subscription_proxy.rs that referenced `ChatGPT` and `OpenClaw` without backticks (clippy::doc_markdown). Wrap them in backticks to match the rest of the file. No functional change. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/subscription_proxy.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/subscription_proxy.rs b/src/subscription_proxy.rs index d939b1b..0379570 100644 --- a/src/subscription_proxy.rs +++ b/src/subscription_proxy.rs @@ -211,7 +211,7 @@ pub async fn forward_subscription_openai( /// Collapse a Codex Responses SSE body into the final Responses JSON object. /// -/// The ChatGPT Codex backend only streams (`text/event-stream`-style `event:` / +/// The `ChatGPT` Codex backend only streams (`text/event-stream`-style `event:` / /// `data:` lines). For non-streaming clients we extract the `response` object /// carried by the terminal `response.completed` event and return it verbatim, so /// the client receives the single JSON object it expects. Returns `None` if no @@ -373,7 +373,7 @@ fn input_item_text(item: &serde_json::Value) -> Option { /// - **requires** a non-empty `instructions` field (HTTP 400 "Instructions are /// required"). /// -/// Standard Responses clients (e.g. OpenClaw's gateway) send `max_output_tokens` +/// Standard Responses clients (e.g. `OpenClaw`'s gateway) send `max_output_tokens` /// and put the system prompt as a `system` message in `input`, so without this /// shaping the backend rejects every request. System turns are hoisted into /// `instructions`; a default is used only if nothing remains. From fe5f29f931ca41e0d314c586b2fdf012e8d5490e Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:05:57 +0000 Subject: [PATCH 5/5] build: sync Cargo.lock self-version to 0.20.0 The Build Package job runs `cargo build --release` then `cargo package --list`. Cargo.toml was bumped to 0.20.0 but Cargo.lock's own package entry still recorded 0.19.0, so every build rewrote Cargo.lock, leaving the working tree dirty and `cargo package --list` aborting ("files ... not yet committed into git: Cargo.lock"). Sync the lockfile's self-version so the tree stays clean. No dependency changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 42f4be3..67c596e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,7 +1341,7 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "link-assistant-router" -version = "0.19.0" +version = "0.20.0" dependencies = [ "aes-gcm", "async-trait",