feat(codex): sync the Codex client surface to openai/codex 0.148.0 - #403
Conversation
Audit of upstream `rust-v0.144.4...rust-v0.148.0` (1286 commits) against the surface shunt mirrors. Three changes: - Bump the pinned Codex CLI identity 0.144.4 -> 0.148.0. Verified upstream: `DEFAULT_ORIGINATOR` is still `codex_cli_rs`, the `version` header is still the built-in openai provider's `CARGO_PKG_VERSION`, and the user-agent format is unchanged, so this is a pure literal bump. The `minimal_client_version` floor (0.144.0) is a separate, server-side number and is left alone. - Send `x-codex-routing-hint`, new in this range (`X_CODEX_ROUTING_HINT_HEADER` / `build_routing_hint_header`, codex-rs/core/src/client.rs): the upstream model slug plus `;tier=<tier>` when a service tier is on the wire. Sent only on the ChatGPT OAuth arm, mirroring upstream's suppression for api-key/bearer/aws providers. The tier predicate mirrors the request body's, so the hint can never advertise a tier the body omitted. The websocket transport carries it as a handshake header, as upstream does. - Map `usage.input_tokens_details.cache_write_tokens` (new upstream field) to Anthropic `cache_creation_input_tokens`, which shunt previously hardcoded to 0. Both detail fields are now peeled off the total with saturating arithmetic, preserving the invariant that the three input fields sum to the upstream `input_tokens`. When the field is absent the emitted usage is byte-identical to before. The upstream WS beta value (`responses_websockets=2026-02-06`), the Responses endpoint, and the request payload shape are all unchanged in this range. Verified against the live ChatGPT backend: the 0.148.0 identity is accepted (no `Model not found`), `x-codex-routing-hint` is accepted, and `cache_write_tokens` is present in the real `response.completed` usage.
There was a problem hiding this comment.
Code Review
This pull request updates the pinned Codex CLI client version from 0.144.4 to 0.148.0 across documentation and code. It introduces support for the x-codex-routing-hint header on both HTTP and WebSocket transports for the ChatGPT OAuth path. Additionally, it refines token usage tracking by extracting cache_write_tokens from OpenAI's input_tokens_details and mapping them to Anthropic's cache_creation_input_tokens to ensure accurate context calculations. Feedback is provided to refactor the routing_hint helper function in src/adapters/responses/request.rs to improve conciseness and readability.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
… hint
Review of the routing hint added in the previous commit found that
`route.upstream_model` is the client's raw `model` string on prefix-route and
default-provider routes (`routing.rs`, only a trailing `[1m]` stripped), so the
hint interpolated untrusted input into a header value. Upstream codex builds
that hint from its own local config and can interpolate it blind; shunt cannot.
Three consequences, all reachable:
- A control character in `model` made `HeaderValue` rejection surface as a
deferred reqwest builder error at `.send()`. `retry.rs` classifies that
non-transient and the Codex OAuth pool charges it to the account as a 30s
`"transport"` cooldown — and because the error is deterministic it repeated
for every account, so one malformed client string could cool the entire pool
before returning 502.
- `model` containing `;` forged a `;tier=` segment the request body never sent,
falsifying the function's own doc comment.
- `model` is length-unbounded, so the header was too.
`routing_hint` now returns `Option<HeaderValue>` and both forward sites omit the
header rather than failing the turn — the shape upstream uses
(`HeaderValue::from_str(..).ok()` behind an `if let Some`) and the one this repo
already uses for the equally client-derived `x-gateway-model`
(`stamp_gateway_headers`).
The slug guard is a positive allowlist, not a denylist of separators:
`HeaderValue` admits every visible ASCII byte plus TAB, so enumerating
metacharacters against a remote grammar shunt cannot observe is a guard that
erodes — a parser splitting on `,` would read `model=gpt-5,tier=priority` as two
fields, the same forge the `;` rule was added to stop. The charset was verified
against every model-id-shaped literal in config, discovery, and the docs: 120
candidates, 0 rejected. The emptiness clause is load-bearing —
`strip_context_window_hint("[1m]")` returns `""`.
Omission is logged at debug only (client-triggerable, so a higher level is a
log-flood vector) and never logs the model string itself, only its length.
Also in this change: cover `cache_write_tokens` through the nested
`/response/usage` shape real streaming SSE uses, and add the hint to
`m7-codex-websocket.md`'s handshake header list.
Greptile SummaryThe PR synchronizes the mirrored Codex client surface with openai/codex 0.148.0.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| src/adapters/responses/request.rs | Updates the Codex identity and safely constructs the optional routing-hint header with positive slug validation. |
| src/adapters/responses/websocket.rs | Propagates the routing hint into fresh Codex WebSocket handshakes while preserving credential gating. |
| src/model/responses.rs | Splits cache-write tokens into Anthropic cache-creation usage while preserving the reported input-token total. |
| tests/responses_translate.rs | Adds translation coverage for nested cache-write token usage. |
Reviews (2): Last reviewed commit: "docs(memory): bring the routing-hint not..." | Re-trigger Greptile
There was a problem hiding this comment.
All reported issues were addressed across 20 files
Architecture diagram
sequenceDiagram
participant Client as Shunt Inbound Client
participant Route as Routing Layer
participant Req as Responses Request Builder
participant WS as WebSocket Turn Context
participant Pool as Codex WS Pool
participant Backend as ChatGPT Backend
participant SSE as SSE Usage Mapper
participant Anthropic as Claude Code Client
Note over Client,Anthropic: Requests with client-controlled model string on Codex OAuth arm
Client->>Route: Send requests
Route->>Route: Resolve route.upstream_model (raw client model, [1m] stripped)
Route-->>Req: Route info
alt ChatGPT OAuth credential
Req->>Req: Build x-codex-routing-hint
alt Model is safe slug (allowlist: 128B, alnum plus hyphen underscore dot colon slash plus)
Req->>Req: Format model=X plus tier=Y when tier is not default
Req->>Backend: NEW: Add x-codex-routing-hint header to HTTP request
else Unusable (control chars, over 128B, empty, semicolon, comma, equals, etc.)
Req->>Req: Omit header, log debug with model length only
end
else api-key/bearer/aws providers
Req->>Backend: No routing hint header
end
Req->>Backend: Send HTTP request with pinned identity 0.148.0
Req-->>Client: Forward streaming response
Note over Client,WS: WebSocket path
Client->>Route: Send request (ws transport)
Route-->>WS: Route info
WS->>WS: Compute routing_hint once per turn
WS->>Pool: begin() with headers
alt Fresh connection
Pool->>Backend: WS handshake with x-codex-routing-hint when safe
else Pooled connection reused
Pool-->>WS: Reuse existing socket (hint is stale from opening turn)
end
Pool-->>Client: Stream events
Note over Backend,Anthropic: Usage accounting at response.completed
Backend-->>SSE: response.completed usage (input_tokens, cached_tokens, cache_write_tokens)
SSE->>SSE: Saturating arithmetic peeling details off total
alt cache_write_tokens present
SSE->>Anthropic: NEW: cache_creation_input_tokens equals cache_write_tokens
else absent
SSE->>Anthropic: cache_creation_input_tokens stays 0
end
SSE->>Anthropic: message_delta usage (three fields sum to prompt total)
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
The index line still read "hint grammar/length not bounded", which contradicts both the linked note and is_hint_safe_slug — the shipped code enforces a 128-byte cap plus a positive character allowlist. Restate the entry as the note's own verdict and keep the real residual (server-side hint grammar unproven).
There was a problem hiding this comment.
All reported issues were addressed across 1 file (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Fixing the index entry in b15a06d left the linked note describing the round-2 state: the frontmatter still called the residual a "denylist-of-one grammar guard" and the body still said a positive slug allowlist "would close it for good". is_hint_safe_slug ships that allowlist (ASCII alphanumerics plus - _ . : / +, 128-byte inclusive bound), so both halves now state it as done and carry the real residual: the server-side parser is unproven.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aad55f04a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".



What
Periodic audit of upstream
openai/codexagainst the client-identity + Responses wire surface shunt mirrors. Range audited:rust-v0.144.4...rust-v0.148.0(1286 commits, upstream stable released 2026-08-18).Audit caveat worth recording: the GitHub compare API caps
.filesat 300 (alphabetical), which over a range this wide silently dropscodex-rs/core/**andcodex-rs/codex-api/**— i.e. exactly the files that matter. Each surface file was fetched at both tags and diffed locally instead.Verified unchanged
WS beta still
responses_websockets=2026-02-06·codex-api/src/endpoint/responses.rsbyte-identical ·DEFAULT_ORIGINATORstillcodex_cli_rs· user-agent format unchanged ·versionheader still the built-in openai provider'sCARGO_PKG_VERSION·ResponseCreateWsRequestwire fields unchanged.Changed here
Pinned identity 0.144.4 → 0.148.0. A pure literal bump. The
minimal_client_versionfloor (0.144.0) is a different, server-side number and is deliberately untouched.user-agent=codex_cli_rs/0.148.0,version= bare0.148.0.New
x-codex-routing-hintheader (X_CODEX_ROUTING_HINT_HEADER/build_routing_hint_header,codex-rs/core/src/client.rs). Valuemodel=<upstream_model>, plus;tier=<tier>when a service tier is on the wire. ChatGPT-OAuth arm only, mirroring upstream's suppression for api-key/bearer/aws providers. HTTP request + WS handshake.cache_write_tokens→cache_creation_input_tokens. Upstream's SSE layer now readsusage.input_tokens_details.cache_write_tokens; shunt previously hardcodedcache_creation_input_tokens: 0. Both detail fields are peeled off the total with saturating arithmetic, preserving the invariant that the three input fields sum to the upstreaminput_tokens.Review round — a defect this PR introduced, and fixed
The first commit shipped (2) by mirroring upstream's construction. That was wrong in a way worth stating plainly: upstream builds that hint from its own local config; shunt builds it from a network-facing client string.
route.upstream_modelis the client's rawmodelon prefix-route and default-provider routes (routing.rs, only a trailing[1m]stripped), so the first commit interpolated untrusted input into a header value.Three reachable consequences, all closed in
3057d5a:modelmadeHeaderValuerejection surface as a deferred reqwest builder error at.send().retry.rsclassifies that non-transient, andpool.rscharges it to the account as a 30s"transport"cooldown. The error is deterministic, so it repeated per account — one malformed client string could cool the entire Codex OAuth pool before returning 502.modelcontaining;forged a;tier=segment the body never sent, falsifying the function's own doc comment.modelhas no length bound, so neither did the header.routing_hintnow returnsOption<HeaderValue>; both sites omit rather than fail — the shape upstream uses and the one this repo already uses for the equally client-derivedx-gateway-model(stamp_gateway_headers). The slug guard is a positive allowlist, not a denylist of separators:HeaderValueadmits every visible ASCII byte plus TAB, so enumerating metacharacters against a grammar shunt cannot observe is a guard that erodes. Charset verified against every model-id-shaped literal in config, discovery, and docs — 120 candidates, 0 rejected. Omission is logged atdebugonly (client-triggerable → higher levels are a log-flood vector) and never logs the model string, only its length.Verification
cargo fmt --all --checkclean ·cargo clippy --all-targets --all-features -- -D warningsclean · 1953 passed, 0 failed across 19 binaries.New tests were mutation-checked, not merely observed green. One mutation exposed a vacuous test of my own: the case
"gpt-5,tier=priority"contains both,and=, and rejects on the=before the,rule is ever reached — so widening the allowlist by one character could never redden it. Every reject case now carries exactly one out-of-allowlist character, so a one-byte widening reddens exactly one case and names it; the realistic whole-string forges are asserted separately.Live-probed against the real ChatGPT backend: the 0.148.0 identity is accepted (no
Model not found),x-codex-routing-hintis accepted, andcache_write_tokensis present in the liveresponse.completedusage.Honest limit on (3): the field read
0on both probe runs, as didcached_tokens, with an identical 2658-token prefix and sharedprompt_cache_keyseconds apart — caching never engaged, so a nonzero value was never observed end to end. The mapping targets a confirmed-real field and its arithmetic is unit-tested, but it is currently inert in practice. Absent or zero, the emitted usage is byte-identical to before, guarded by a regression test.Known issues not caused by this PR
gateway::spend::persist::tests::malformed_typed_limit_is_hidden_without_losing_valid_limitsis flaky — itscapture_logshelper installs a per-test tracing subscriber that races under full-suite parallelism, returning an empty buffer. Last touched by feat(admin): add read/write admin keys and move the spend surface to [server.spend] #389 and feat(gateway): add spend-limit admin API (stage 1) #333, both already on main; this branch touches no gateway or tracing code. It may intermittently redden CI here. Deserves its own issue; deliberately not "fixed" by weakening it.docs/m7-codex-websocket.md§2/§5 still referencesrc/adapters/codex_ws.rs, but that code now lives undersrc/adapters/responses/. Pre-existing, left alone.Not done here (surfaced by the same audit)
codex_rollout_budget_units— new upstream usage field; did not appear in the live probe.misalignment_policy_violation.safety_bufferingnow also arrives via aresponse.metadataevent (shunt implements none of it).x-models-etagrelocated into thecodex.response.metadataevent — shunt never consumed it, so no action.x-codex-bengalfox-*norx-codex-credits-*.Docs
docs/codex-configuration.md§4.4 + §10 ·docs/m7-codex-websocket.md§4 ·docs/running.md·docs/m11-inbound-codex-endpoint.md·site/.../guides/codex.mdxandeffort-and-context.mdxacross en/ko/ja/zh-cn.wiki/untouched (generated).The inbound Codex passthrough is unaffected: it relays a caller's own
x-codex-*headers verbatim through a separate send path, so there is no duplicate-header risk — and the tier-honesty property is scoped in the docs to the headers shunt builds.