Skip to content

feat(codex): forward quota telemetry to the client - #127

Open
rikbrown wants to merge 2 commits into
raine:mainfrom
rikbrown:rik/forward-codex-rate-limit-headers
Open

rikbrown wants to merge 2 commits into
raine:mainfrom
rikbrown:rik/forward-codex-rate-limit-headers

Conversation

@rikbrown

Copy link
Copy Markdown
Contributor

Problem

Codex reports how much of a ChatGPT subscription is spent in two places: x-codex-* headers on an HTTP response, and a codex.rate_limits event on the WebSocket transport. Neither reaches the client. The proxy answers in Anthropic's format, which has no equivalent field, so the event is classified as control and becomes a ping, and the response headers are dropped.

That leaves anything sitting in front of the proxy — a rotating multi-account gateway, a quota readout — unable to see the limit it is about to hit. The first sign of a spent window is a 429.

Change

A new providers/codex/rate_limits.rs keeps the newest snapshot and stamps it onto every Codex /v1/messages response, under the header names Codex itself uses, so a client that already parses them needs no second format. Both transports feed it: the HTTP response head, and the codex.rate_limits event.

Only quota headers are forwarded. x-codex-turn-state is continuation state, and a large opaque blob to put on every response.

Codex sends the event ahead of the first generated output and the response head is built from that output, so a response normally carries the numbers of the request it answers. One that arrives before any telemetry does carries the previous snapshot, or none.

Which window is which is left to the client, because it varies: a ChatGPT Pro subscription reports its weekly limit as primary and meters no secondary window at all, so the -window-minutes header is what identifies a bucket, not the position.

Testing

  • Six unit tests in the new module, covering both sources, the header filter, and the stamped response.
  • cargo test — 1033 pass. cargo clippy --all-targets and cargo fmt --check clean.
  • Checked against a live ChatGPT Pro subscription on both transports, streaming and buffered.

The HTTP API reference gains a paragraph describing the headers.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S3HCmE8ZPT1o59UCGaoEeV

@rikbrown rikbrown changed the title Forward Codex quota telemetry to the client feat: forward Codex quota telemetry to the client Aug 31, 2026
@rikbrown rikbrown changed the title feat: forward Codex quota telemetry to the client feat(codex): forwar quota telemetry to the client Aug 31, 2026
@rikbrown rikbrown changed the title feat(codex): forwar quota telemetry to the client feat(codex): forward quota telemetry to the client Aug 31, 2026
@rikbrown
rikbrown force-pushed the rik/forward-codex-rate-limit-headers branch 2 times, most recently from 42aed13 to dd66fed Compare September 13, 2026 14:10
Rik Brown added 2 commits September 19, 2026 12:45
Codex reports how much of a ChatGPT subscription is spent twice: as
`x-codex-*` headers on an HTTP response, and as a `codex.rate_limits`
event on the WebSocket transport. The proxy answers in Anthropic's
format, which has no equivalent field, so neither reaches the client and
a tool in front of the proxy — a rotating multi-account gateway, a quota
readout — cannot see the limit it is about to hit.

Keep the newest snapshot and stamp it onto every Codex response under
the header names Codex itself uses, so a client that already reads them
needs no second format. The event precedes the first generated output on
both transports and the response head is built from that output, so a
response normally carries the numbers of the request it answers.

Only quota headers are forwarded: `x-codex-turn-state` is continuation
state, and a large opaque blob to put on every response.
@rikbrown
rikbrown force-pushed the rik/forward-codex-rate-limit-headers branch from dd66fed to d340f74 Compare September 19, 2026 11:45
@rikbrown

Copy link
Copy Markdown
Contributor Author

Rebased onto current main — still the same two commits, content unchanged.

Flagging an overlap so it isn't discovered at merge time: #149 also surfaces the Codex rate-limit snapshot to the client, as a typed codex_rate_limits field on the terminal message_delta usage. This PR does it as x-codex-* response headers. Same data, different channel, and the two don't conflict textually — #149 is in reducer.rs/live_stream.rs, this is a separate module plus its call sites.

The reasoning/thinking tokens half of #149 is orthogonal to this and useful on its own; nothing here competes with it.

On the rate-limit half, the one property headers have that the usage field doesn't: they arrive before the body, so a caller can read the meter without parsing the stream to completion. That matters if you're pooling subscriptions and want to route on quota before committing a turn, or if you're forwarding responses without buffering them. The usage field is the better fit if the consumer is already reading usage anyway and wants it in one place. They can also coexist — the header path doesn't preclude the field.

Happy to defer if you'd rather have one mechanism: I can close this, or rework it to read from whatever #149 lands as. Just say which you prefer rather than carrying both by accident.

rikbrown added a commit to rikbrown/claude-code-proxy that referenced this pull request Sep 19, 2026
parse_codex_usage already reads output_tokens_details.reasoning_tokens
into CodexUsage, but map_codex_usage_to_anthropic dropped it, so Claude
Code recorded thinking_tokens as 0 for every Codex turn. Emit it as
output_tokens_details.{reasoning_tokens, thinking_tokens}; the second
name is what Claude Code records for Anthropic models, so per-turn
accounting in transcripts stays comparable across providers.

Ported from the reasoning-tokens half of upstream PR raine#149
(raine/claude-code-proxy, author sommaht, branch usage-visibility).
The other half of that PR, the codex_rate_limits usage field, is left
out on purpose: this fork already surfaces the same data as x-codex-*
response headers via rate_limits.rs (upstream PR raine#127), and two sources
of truth for the same meter would fight at the next rebase. Drop this
commit at whichever rebase sees raine#149 land upstream.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant