v1.2.0
Major reliability rewrite for subscription usage fetching.
Highlights
claude.ai web session bypass for OAuth rate limit
Anthropic aggressively rate-limits the OAuth /api/oauth/usage endpoint for third-party clients (429 with a useless retry-after: 0, only recoverable by re-establishing the OAuth session). The admin UI now supports an optional claude.ai web session path: scrape a sessionKey cookie, org_uuid, anthropic-device-id, and anthropic-anonymous-id from DevTools, paste them into the new form in the Claude Subscription card, and the proxy will fetch usage through claude.ai/api/organizations/{uuid}/usage — the same endpoint the web UI uses, which isn't rate-limited. The session cookie is auto-rotated from Set-Cookie on every successful request, so the session stays alive indefinitely as long as the proxy polls at least once a month.
Unified UsageCache module
Replaced the split cached_usage/window_resets state and three scattered fetch helpers with a single src/usage/ module that owns all subscription usage state. Everything now goes through one well-defined API: snapshot / get_or_refresh / force_refresh / patch_from_headers / invalidate / is_over_subscription_limit.
Two freshness timestamps drive an adaptive refresh:
util_updated_at— 5h/7d utilization, refreshed on every `/v1/messages` response header (near-realtime under active inference). Throttle: 60s.full_fetched_at— full snapshot including extras (`extra_usage`, `seven_day_sonnet`, `seven_day_opus`). Throttle: 5 min.
Under active inference through the proxy, `patch_from_headers` keeps util fresh for free, so only the 5-min threshold fires. With no inference traffic (e.g. when using Claude through another client and only monitoring via the proxy), the 60s threshold fires and the cache falls into a 1/min refresh cadence automatically. No modes, no state machine.
Fixed bugs along the way
- Rate-limit check was silently broken: `routes/auth.rs` was calling `fetch_fresh_subscription_state` on every `/v1/messages` request for keys with `allow_extra_usage=false`, which hit the rate-limited OAuth endpoint and silently returned `SubscriptionState::default()` on 429 — turning the "subscription exhausted" guard into a no-op. Now it reads `usage_cache.is_over_subscription_limit()` with zero I/O.
- `delete_oauth` did not invalidate cached usage, so stale data could be served after logout.
- Error-matching via string comparison (`Err(e) if e == "no web session configured"`) replaced with a proper `FetchError::NotConfigured` variant.
Admin UI improvements
- Auto-polling: the subscription card now refreshes every 60 s while the page is open (backed by the throttled `UsageCache`, so most polls cost nothing upstream).
- Freshness metadata footer: under the limits grid, shows the fetch source (claude.ai web session / OAuth / none), utilization age, and extras age, all ticking live.
- Two refresh buttons: the normal one respects the cache throttle; a new ⚡ force button sends `?force=true` and bypasses the throttle.
- Error visibility: the silent `is_stale` fallback is now surfaced as a bright warning with the upstream status + body, so 429s etc. are no longer invisible.
- Live utilization updates from inference headers: the admin UI now sees 5h/7d percentages change in near-realtime during active Claude Code sessions, not just on the 5-min full-fetch cycle.
Breaking changes
None — the `SubscriptionUsageResponse` JSON gains a few optional fields (`is_stale`, `upstream_error`, `source`, `full_fetched_at`, `util_updated_at`) but is otherwise unchanged.
Full changelog: v1.1.0...v1.2.0