fix(server): Claude usage meter follows live rate-limit updates below the warning threshold#28869

open5
main
fix/issue-28852
opened10d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 1d ago

Problem

Claude's usage meter falls behind Claude Code's own /usage during active work. The Claude CLI sends a rate_limit_event every time a usage window's whole-percent value changes. Below the warning threshold, though, the per-window readings are only in an untyped unifiedWindows field. The top-level utilization is set only after a warning threshold has been passed. claudeRateLimitEventToUpdate read only the top-level fields, so under normal use it dropped every live frame. The meter then only moved when the 5-minute background provider check ran.

To reproduce: run Claude turns and watch the Session meter while the account is below its warning threshold. The CLI streams new readings, but T3's meter stays at the last probe value.

Change

claudeRateLimitEventToUpdate now decodes unifiedWindows with a Schema and maps each entry onto the rows the probe already creates:

  • five_hour → Session
  • seven_day → Weekly
  • seven_day_overage_included → the row for the model the probe named (still dropped while unnamed)

The top-level bucket is still read for older CLIs that don't send unifiedWindows. When both sources name the same window, the unifiedWindows value wins and no row is duplicated. Contracts, settings, polling and the adapter are unchanged.

Scope and approval

Fixes pingdotgg/t3code#28852

This is a focused fix for a triaged bug in the existing Claude usage-limit stream mapping. It adds no option, contract field or persisted state.

Verification

  • vp test run apps/server/src/provider/claudeUsageLimits.test.ts: 10/10 pass.
    • maps every window in the unifiedWindows the CLI streams below the warning threshold fails on main (expected undefined to deeply equal { windows: [ …(2) ] }) and passes here.
    • keeps one row per window when a warning frame also carries unifiedWindows covers the case where both sources name the same window (no duplicate row) and the overage-included bucket arriving through unifiedWindows.
  • vp run check:changed: format, lint, knip, typecheck (3 packages) and tests pass.

Runtime transcript (server-only fix, no UI change). I ran a real T3 server (t3 serve, fresh database each side) whose Claude provider runs the real Claude CLI 2.1.304, signed in as a synthetic subscriber. The CLI talks to a local stub Anthropic API. The stub returns anthropic-ratelimit-unified-* headers with 5-hour utilization rising 3 points per request and 7-day fixed at 31%, and it answers /api/oauth/usage, so the initial usage probe succeeds. Three turns were driven with t3 thread start --wait. After each step I read Claude's published usageLimits from the provider status cache.

Session meter, stepBefore (main claudeUsageLimits.ts)After (this branch)
initial probe12%12%
after turn 112%15%
after turn 212%21%
after turn 312%27%
unifiedWindows.five_hour frames T3 received0.12 … 0.270.12 … 0.27
Before transcript (main's claudeUsageLimits.ts, blob d0acf43bb4)
1initial probe: checkedAt 2026-11-01T20:09:37.831Z unavailable None windows [('five_hour', 12), ('seven_day', 31)]
2after turn 1: checkedAt 2026-11-01T20:09:37.831Z unavailable None windows [('five_hour', 12), ('seven_day', 31)]
3after turn 2: checkedAt 2026-11-01T20:09:37.831Z unavailable None windows [('five_hour', 12), ('seven_day', 31)]
4after turn 3: checkedAt 2026-11-01T20:09:37.831Z unavailable None windows [('five_hour', 12), ('seven_day', 31)]
5stub 5h utilization sent on each /v1/messages response: 0.12 0.15 0.18 0.21 0.24 0.27
6rate_limit_event unifiedWindows.five_hour received by T3: 0.12 0.15 0.18 0.21 0.24 0.27
After transcript (this branch, blob 68135fc2e9)
1initial probe: checkedAt 2026-11-01T20:08:09.058Z unavailable None windows [('five_hour', 12), ('seven_day', 31)]
2after turn 1: checkedAt 2026-11-01T20:08:17.017Z unavailable None windows [('five_hour', 15), ('seven_day', 31)]
3after turn 2: checkedAt 2026-11-01T20:08:24.352Z unavailable None windows [('five_hour', 21), ('seven_day', 31)]
4after turn 3: checkedAt 2026-11-01T20:08:31.651Z unavailable None windows [('five_hour', 27), ('seven_day', 31)]
5stub 5h utilization sent on each /v1/messages response: 0.12 0.15 0.18 0.21 0.24 0.27
6rate_limit_event unifiedWindows.five_hour received by T3: 0.12 0.15 0.18 0.21 0.24 0.27
A frame the CLI sent below the warning threshold, from T3's provider event log
json
1{"status":"allowed","resetsAt":1793576909,"rateLimitType":"five_hour","isUsingOverage":false,"unifiedWindows":{"five_hour":{"utilization":0.18,"resetsAt":1793576909},"seven_day":{"utilization":0.31,"resetsAt":1793994509}}}

The stub, CLI wrapper, TLS proxy and driver scripts are scratch and are not committed. They could not be uploaded because ghdrop is not available in this environment. All data in them is synthetic.

Not verified: a real Claude subscription account (none is available here; the stub reproduces the CLI's documented frames and headers), and a visual check in a client. The fix changes only server-side mapping into the existing provider snapshot, so web, desktop and mobile render it through their existing meter paths.

Model and harness: Claude Opus 5.5 (lead, red test, implementation, review B) and GPT-6.1 Sol (review A) in T3 Code.

🤖 Generated with Claude Code

Checks

19 successful · 15 skipped
All checks passed
3610683

Added 3 commits

  • test(server): reproduce #28852 Claude unifiedWindows rate-limit events dropped457e96f
  • fix(server): Claude usage meter follows every streamed rate-limit windowa2149de
  • Merge remote-tracking branch 'origin/main' into fix/issue-2885200327aa
Synced from GitHub

Approvability

Verdict: Approved at a2149de

Macroscope's review found this PR approvable — Claude’s usage meter now consumes live per-window updates below the warning threshold while preserving older CLI compatibility. The change affects usage reporting, not rate-limit enforcement or billing.

No code changes detected at 3610683. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

External image Run configuration

External image Commits

Reviewing files that changed from the base of the PR and between 35bb7bbabdefb23ef4235a6f42b5bf8de62c6f55 and 36106834ddfb58f2cbe132512f4bf366413c81df.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

No actionable comments were generated in the recent review.

Walkthrough

Claude rate-limit event parsing now converts recognized unifiedWindows entries into usage updates. It also accepts legacy top-level events when their window IDs are not already present. Tests cover standard and warning events.

Changes: Claude usage windows

Layer / File(s)Summary
Decode and collect usage windows
apps/server/src/provider/claudeUsageLimits.ts, apps/server/src/provider/claudeUsageLimits.test.ts
The parser validates streamed window fields, maps recognized windows to updates, and avoids duplicate IDs when it processes legacy top-level events. Tests cover percentage conversion, reset timestamps, and named overage-included rows.

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related issues

  • Issue 27750 concerns streamed five_hour events. This change maps five_hour to a Session window.
  • Issue 24114 concerns unifiedWindows data during setup-token sessions. This change adds decoding for unifiedWindows; unsupported-probe handling is not included.
  • Issue 27772 identifies rate-limit events as a source of active-session readings. This change maps unifiedWindows from those events; probe back-off is not included.

Suggested reviewers: maria-rcks

Merge Risk: External image Minimal · previous review 00327

The usage-window update has no identified merge-blocking risk.

Pre-merge checks: Passed 4

Check nameStatusExplanation
Title checkPassedThe title is concise, conventional, and clearly identifies the main change: fixing live Claude usage-meter updates below the warning threshold.
Description checkPassedThe description includes the required Problem, Change, Scope and approval, and Verification sections. It explains the failure mode, implementation, linked issue, focused tests, runtime validation, lim…
Linked Issues checkPassedIssue [#28852] requires the T3 usage meter to stay current during active Claude Code work. claudeRateLimitEventToUpdate now decodes streamed unifiedWindows values and maps five_hour and `seven_d…
Out of Scope Changes checkPassedThe diff changes only the Claude usage-limit event mapper and its tests. The changes directly support issue [#28852] by processing live rate-limit frames. No unrelated contracts, settings, polling, ad…
Full details: Description check

Explanation

The description includes the required Problem, Change, Scope and approval, and Verification sections. It explains the failure mode, implementation, linked issue, focused tests, runtime validation, limitations, and agent details.

Full details: Linked Issues check

Explanation

Issue [#28852] requires the T3 usage meter to stay current during active Claude Code work. claudeRateLimitEventToUpdate now decodes streamed unifiedWindows values and maps five_hour and seven_day to the existing Session and Weekly rows. It also maps the named overage row and preserves the older top-level event path. The added tests cover below-warning updates and duplicate prevention when both sources provide a window.

Full details: Out of Scope Changes check

Explanation

The diff changes only the Claude usage-limit event mapper and its tests. The changes directly support issue [#28852] by processing live rate-limit frames. No unrelated contracts, settings, polling, adapter, UI, or persisted-state changes are present.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
External image Recent review info

External image Run configuration

External image Commits

Reviewing files that changed from the base of the PR and between d897d3f3f00f2cb534c813e637154d39c8d1cff6 and 00327aaada0003870e0cefc6e7dcb67c6dc5f79e.

External image Files selected for processing: 2

  • apps/server/src/provider/claudeUsageLimits.test.ts
  • apps/server/src/provider/claudeUsageLimits.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiB✅
CodexThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
CodexLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiB✅
CodexLive turn WebSocket decoded21.5 KiB21.5 KiB0 B (0.0%)24.4 KiB✅
CodexLive turn messages110 (0.0%)8✅
ClaudeTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiB✅
ClaudeThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
ClaudeLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiB✅
ClaudeLive turn WebSocket decoded21.9 KiB21.9 KiB0 B (0.0%)24.4 KiB✅
ClaudeLive turn messages110 (0.0%)8✅

Baseline: c90b678 · PR result: 3610683 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Added 1 commit

  • Merge origin/main into fix/issue-2885235bb7bb

The automated pipeline stopped on this pull request and will not merge it:

someone else pushed to fix/issue-28852 (head is now 00327aaada); the pipeline does not build on commits it did not review

It stays open for a person to finish or close.

Added 1 commit

  • Merge origin/main into fix/issue-288523610683

The automated pipeline stopped on this pull request and will not merge it:

the proof has no capture anyone can open:

  • Permanent blocker: first-party Claude Pro OAuth is unavailable, preventing the required live usage comparison on both commits.
  • Session and Weekly meter updates below the warning threshold remain unverified in the real app.
  • No public capture URLs exist because no valid runtime capture could be produced; no upload was attempted.
  • Both verdict flags are false because neither outcome was observed, not because either commit was shown to behave incorrectly.

It stays open for a person to finish or close.