Skip to content

codex: capture rate-limit snapshots, serve GET /usage#62

Open
kevinsslin wants to merge 2 commits into
raine:mainfrom
kevinsslin:feat/codex-usage-endpoint
Open

codex: capture rate-limit snapshots, serve GET /usage#62
kevinsslin wants to merge 2 commits into
raine:mainfrom
kevinsslin:feat/codex-usage-endpoint

Conversation

@kevinsslin

Copy link
Copy Markdown
Contributor

Problem

The Codex backend pushes a codex.rate_limits snapshot on responses whether or not a limit is reached, but the proxy only acts on limit_reached: true (correctly turning it into a stream error) and discards every other snapshot. So there's no way to ask the proxy "how much quota do I have left" before starting a long run; the community workaround is the unofficial chatgpt.com/backend-api/wham/usage side channel with hand-extracted OAuth tokens.

Change

Single concern: capture the latest snapshot and expose it.

  • New providers/codex/rate_limits.rs: a process-wide latest-snapshot store. The rate_limits object is stored and served verbatim (plus a captured_at stamp) because the upstream payload shape is undocumented; re-modeling it field by field would break on upstream changes.
  • Recorded at both stream choke points: live WS payload translation (translate_live_stream_payload) and the buffered reducer. Existing limit_reached: true error behavior is untouched.
  • New GET /usage: {"codex": {"rate_limits": {...}, "captured_at": "..."}}, {"codex": null} until a request has produced a snapshot.
$ curl -s localhost:18765/usage | jq '.codex.rate_limits.primary'

Testing

  • 2 unit tests (ignore non-events, record/replace) + 1 integration test (empty -> populated round trip through the router)
  • cargo test all green; cargo fmt --all --check clean; cargo clippy --all-targets -- -D warnings pre-existing error count unchanged (34), none in touched files

Possible follow-ups (kept out of this PR deliberately)

  • Show the snapshot in the monitor TUI footer
  • Parse HTTP-transport rate-limit response headers as a second snapshot source

The upstream pushes a codex.rate_limits snapshot on responses whether or
not a limit is reached, but limit_reached:false snapshots were discarded.
The only way to check remaining quota was the unofficial
chatgpt.com/backend-api/wham/usage side channel with hand-extracted
tokens.

Record the latest snapshot at both stream choke points (live WebSocket
payload translation and the buffered reducer) and serve it verbatim,
with a captured_at stamp, from GET /usage. Verbatim because the payload
shape is undocumented and re-modeling it field by field would break on
upstream changes.

Operators can now preflight long runs with:
  curl -s localhost:18765/usage | jq .codex.rate_limits
@kevinsslin

Copy link
Copy Markdown
Contributor Author

Prior art note: this is the second attempt at this feature. #16 proposed the same approach in May and was closed by its author because, in his testing then, subscription-auth sessions never emitted codex.rate_limits events, so GET /usage would have returned {codex: null} in the common case. (#8 also carries a usage endpoint, built on session telemetry instead, inside a larger multi-provider draft.)

That premise no longer holds. Captured today (2026-07-19) on a plain ChatGPT Pro subscription login (browser PKCE, Keychain storage, default WebSocket transport, gpt-5.4-mini and gpt-5.6-sol), first request after startup:

{
  "codex": {
    "captured_at": "2026-07-19T06:32:42.650243Z",
    "rate_limits": {
      "allowed": true,
      "limit_reached": false,
      "primary": {
        "reset_after_seconds": 518773,
        "reset_at": 1784961534,
        "used_percent": 55,
        "window_minutes": 10080
      },
      "secondary": null
    }
  }
}

I can't say whether the difference from May is the WebSocket-first transport, the July quota changes on the backend, or both; but on current subscription traffic the snapshots flow on every response, which is exactly the case #16 was closed over. Happy to adjust shape/naming if you'd prefer something closer to what you had in mind with tmux-agent-usage.

Completes the /usage feature so quota survives a proxy restart and is
visible in the monitor:

- rate_limits.rs mirrors each snapshot to state_dir/codex-usage.json and
  loads it on a cold read, so GET /usage answers immediately after a
  restart instead of returning null until the first upstream request.
  Writes are best-effort and never block request handling.
- The monitor TUI footer shows the weekly window as remaining percent
  (Codex-app convention), color-coded, with a LIMIT flag when reached.

Tests isolate XDG_STATE_HOME so the persisted mirror never touches the
real state dir.
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