Passively capture x-codex-* response headers from proxied Codex API
calls and surface them in /stats and the dashboard.
Unlike Anthropic subscription window tracking (which polls a dedicated
OAuth endpoint), Codex embeds rate-limit data directly in every API
response header — no polling, no new credentials needed.
Changes:
- headroom/subscription/codex_rate_limits.py: new module with
CodexRateLimitWindow, CodexCreditsSnapshot, CodexRateLimitSnapshot
data models and a thread-safe CodexRateLimitState singleton;
parse_codex_rate_limits() parses x-codex-primary/secondary-used-percent,
window-minutes, reset-at, credits, limit-name, and promo-message headers
- headroom/subscription/__init__.py: re-export new public symbols
- headroom/proxy/handlers/openai.py: call
get_codex_rate_limit_state().update_from_headers() after each
proxied /v1/chat/completions and /v1/responses response
- headroom/proxy/server.py: add codex_rate_limits key to /stats
via _get_codex_rate_limit_stats() helper
- headroom/dashboard/templates/dashboard.html: new OpenAI Codex
Rate-Limit Window panel (primary + secondary progress bars, credits
balance, limit name, reset countdown); hidden when no data
- tests/test_codex_rate_limits.py: 25 unit tests covering parsing,
window labels, reset time, credits, state updates
- docs/screenshots/subscription_window_active.png: updated screenshot
showing both Anthropic and Codex panels in the real dashboard
Header schema (from codex-rs/codex-api/src/rate_limits.rs):
x-codex-primary-used-percent / x-codex-primary-window-minutes /
x-codex-primary-reset-at (and secondary- variants)
x-codex-credits-has-credits / -unlimited / -balance
x-codex-limit-name / x-codex-promo-message
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Active state shows the full headroom dashboard with the Anthropic
Subscription Window panel integrated in-line after Savings Breakdown,
including 5h/7d utilisation bars, overage credit bar, Headroom
contribution grid, and anomaly detection alert.
Inactive state shows the standard dashboard without the panel —
it is conditionally rendered only when subscription_window data
is present in /stats.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>