Skip to content

publish the Codex quota reading clients warn on - #140

Open
null-topology wants to merge 2 commits into
raine:mainfrom
null-topology:codex-quota-telemetry
Open

publish the Codex quota reading clients warn on#140
null-topology wants to merge 2 commits into
raine:mainfrom
null-topology:codex-quota-telemetry

Conversation

@null-topology

Copy link
Copy Markdown

Builds on #139 — the first of the two commits here is that PR, and this diff becomes a single commit once it merges.

What happens now

During a healthy stream Codex reports how much of each subscription window is spent, as a codex.rate_limits event:

{"type":"codex.rate_limits",
 "rate_limits":{"allowed":true,"limit_reached":false,
   "primary":{"used_percent":4,"window_minutes":300,
              "reset_after_seconds":16394,"reset_at":1788897775},
   "secondary":{"used_percent":16,"window_minutes":10080,
                "reset_after_seconds":584857,"reset_at":1789466238}}}

Nothing forwards it, so the only quota signal a client ever sees is the refusal. Work stops with no warning that the allowance was running low — which is the case where nobody can tell why the assistant stopped answering.

What this changes

The newest reading is kept and put on the response, in the headers clients already parse for rate limit state:

anthropic-ratelimit-unified-status: allowed
anthropic-ratelimit-unified-reset: 1789466238
anthropic-ratelimit-unified-representative-claim: seven_day
anthropic-ratelimit-unified-5h-utilization: 0.0400
anthropic-ratelimit-unified-5h-reset: 1788897775
anthropic-ratelimit-unified-7d-utilization: 0.1600
anthropic-ratelimit-unified-7d-reset: 1789466238

The event arrives ahead of the first content event, so in practice the reading is ready for the response it came from rather than the one after.

Less obvious details:

  • A window is identified by the length it reports, not by the slot it arrived in: which rank carries which length has changed between payload versions. Both spellings of the reset field are read for the same reason — the stream says reset_at, a Codex CLI session log reserialises the same window as resets_at.
  • The spent fraction only reaches a caller once a threshold is declared surpassed, so a window past its line also emits …-<5h|7d>-surpassed-threshold. The defaults, 0.9 for the session window and 0.75 for the weekly, are the points Anthropic's own windows warn at. CCP_CODEX_QUOTA_WARN_AT lowers both for a consumer that wants the figure earlier.
  • A reading whose window has since reopened is dropped rather than published, so the turn after a reset does not announce an allowance as nearly gone at the moment it came back.
  • A refusal keeps its own headers: it carries the exact state of the window that refused it, which beats a reading taken earlier in the turn.

Testing

Verified live against a Codex subscription. The headers above came off a real response, and a client parsed them back into status='allowed' with the window and its reset time. With the threshold lowered so a warning is reachable at a real 4% spend, the same path produced status='allowed_warning', rate_limit_type='five_hour', utilization=0.04 and the reset time.

Nine unit tests cover the recorded payload shape, window attribution, threshold behaviour, the rollover drop, and the refusal precedence.

cargo fmt --check, cargo clippy --all-targets and cargo test are clean (1041 tests).

A spent subscription window arrives as an `error` event carrying
`usage_limit_reached` and the moment the window reopens. Nothing read it:
the failure classified as a retryable rate limit, and the delay lookup
covers `retry_after`, `retry_after_seconds` and `headers.retry-after`, none
of which this event carries. With no delay the live stream spent its whole
budget -- eleven upstream attempts over 2m54s in a capture of the failure,
each answered 429 in under a second -- and then returned 429 with no reset
information, so clients retried immediately against a window that reopens
hours later.

Recognise the event and answer it once, with the headers clients already
read for rate limit state: which window ran out and when it reopens.
Retry-After is deliberately absent, since clients sleep for its full value
and here that is hours; x-should-retry stops the retry loop instead.
Codex reports how much of each subscription window is spent as a
`codex.rate_limits` event inside the stream, and nothing forwarded it. The
only quota signal reaching a client was the refusal itself, so work stopped
with no warning that the allowance was running low.

Keep the newest reading and put it on the response, in the headers clients
already parse for rate limit state: the spent fraction and reset time of each
window, and which one is closest to running out. A window is identified by the
length it reports rather than the slot it arrived in, since which rank carries
which length has changed between payload versions, and both spellings of the
reset field are read for the same reason.

The spent fraction only reaches a caller once a threshold is declared
surpassed, so a window past its line says which line it crossed. The defaults
-- 0.9 for the session window, 0.75 for the weekly -- are the points
Anthropic's own windows warn at; CCP_CODEX_QUOTA_WARN_AT lowers both for a
consumer that wants the figure earlier.

A reading whose window has since reopened is dropped rather than published, so
the turn after a reset does not announce an allowance as nearly gone at the
moment it came back. A refusal keeps its own headers: it carries the exact
state of the window that refused it.
@raine

raine commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Thanks. What do these PRs enable?

@null-topology

Copy link
Copy Markdown
Author

The goal is to use the proxy with the Claude Agent SDK and get the native
rate-limit notification on the Codex route, the same way it arrives on a real
Claude subscription.

The SDK (and Claude Code itself) only learns about limits from the
anthropic-ratelimit-unified-* headers: status, reset time, representative
claim, 5h/7d utilization. With those present the client gets a structured
rate_limit_event it can act on programmatically, e.g. wait until the reset or
switch to another model. Without them a spent Codex window degrades to a bare
"API Error: Request rejected (429)" with no reset time, which is useless to an
automated caller.

#139 covers the spent window. Codex ends the stream with an error event of
type usage_limit_reached carrying the reset time; the proxy treated it as a
retryable 429 without a delay and burned every retry first (measured 11
upstream attempts over ~3 minutes before the client saw anything). Now it
answers once, in ~1.5s, with x-should-retry: false, status rejected and
the reset time. Retry-After is deliberately omitted since clients sleep for
its full value, which here is hours.

#140 covers the healthy case. Codex sends a codex.rate_limits event with
5h/7d usage during normal streams and the proxy dropped it. Now it is forwarded
as the -5h-* / -7d-* headers, so the SDK sees allowed /
allowed_warning with the utilization figure, and Claude Code's own usage
warning works on Codex too. Threshold is configurable via
CCP_CODEX_QUOTA_WARN_AT; a reading whose reset has already passed is not
published.

Verified end to end with the SDK in all three states (allowed,
allowed_warning, rejected). Not covered: the buffered HTTP transport and a 429
on the WebSocket handshake.

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.

2 participants