Skip to content

feat: weekly usage limit including Weekly (All) and model-specific quota (e.g., Fable) - #86

Merged
Carl723000 merged 7 commits into
ClaudeCodeUsage:mainfrom
zeyutang:feat/generic-quota-limits
Aug 7, 2026
Merged

Carl723000 merged 7 commits into
ClaudeCodeUsage:mainfrom
zeyutang:feat/generic-quota-limits

Conversation

@zeyutang

@zeyutang zeyutang commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

showOpusWeekly had stopped working entirely, not merely gone stale. Anthropic's OAuth usage endpoint no longer populates the per-model quota fields the extension read, so the figure that setting exists to show was silently blank.

The real data moved into a generic limits array, where a scoped cap names its own model:

"seven_day_opus": null,          // ← what showOpusWeekly read
"limits": [
  { "kind": "session",       "percent": 0,  "resets_at": null,  "is_active": false },
  { "kind": "weekly_all",    "percent": 9,  "resets_at": "...", "is_active": false },
  { "kind": "weekly_scoped", "percent": 17, "resets_at": "...", "is_active": true,
    "scope": { "model": { "id": null, "display_name": "Fable" } } }
]

This reads that array instead, so no model name is hardcoded anywhere and the next rename costs no code change. It also closes a risk gap rather than only a display one: the status bar's warning colour considered just the all-models figure, so an account could sit green at wk 9% while its binding per-model cap ran near full.

Before
before

After
Xnip_2026Aug04_0950

What changed

Reading the API.

  • A new pure module, src/quotaWindows.ts, normalizes either payload generation into one ordered window list. The legacy flat fields stay as a fallback for accounts still served the old shape.
  • hasExpiredWindow and the dashboard's quota banner read those fields directly too, so they route through the normalizer as well.

Status bar.

  • Caps that reset together collapse into one segment, wk 9% (fable 17%), so the shared countdown prints once instead of per cap. A cap on its own schedule keeps its own segment and countdown.
  • The scope name is lowercased to sit with the 5h and wk segments.

Tooltip.

  • Each cap gets its own row and bar, whether or not the status bar is showing it, so a per-model cap is always one hover away. Rows are labelled Weekly (All) and Weekly (Fable), localized across all eight locales.
  • Reset cells read time left (wall clock), and the countdown follows resetCountdownFormat, which it previously ignored.
  • A per-model cap appears only once it has usage against it, so it is absent at the start of a week rather than sitting at 0%. The 5-hour and all-models figures always show, since 5h 0% is the signal that tracking works at all.
  • Usage credits show the amount spent this month and its reset date, once something has been spent.
  • Pad the Resets column and more breath room between the Share and Resets column

Setting.

  • showOpusWeekly becomes showScopedWeekly, with a one-shot migration carrying the stored value over. It stays opt-in and off by default.

Fixes found along the way

Not in the original scope, but in the code this touches:

  • The tooltip countdown ignored resetCountdownFormat. On the default decimal the same window read 4.5h in the status bar and 4h 29m in the tooltip.
  • The 5-hour row showed no reset time at all.
  • Reset times were truncated to the second, so a cap resetting at 16:59:59 displayed as 16:59 while the cap it resets alongside displayed 17:00.
  • Shares always carried a decimal, so a whole 8% rendered as 8.0%. Precision now follows the field the API used.
  • Credits parsing returned nothing when the toggle was off, making spend that had already happened disappear, and nothing when the cap was absent or unlimited, hiding real spend outright.
  • Five settings entries (four pt-BR, one id) were silently falling back to English.

Notes for review

Row merging is tolerance-based, not equality-based.
The two weekly caps are stamped microseconds apart and straddle a minute boundary (23:59:59.227Z against 00:00:00.227Z), so an equality test would refuse to merge them. Caps merge when their resets agree within five minutes; anything genuinely separate differs by hours or days.

Share precision follows the API's field, not the value.
limits[].percent is serialized as an integer (3) while the legacy utilization is a float (3.0), and JSON.parse collapses both to 3. The normalizer records which shape a window came from, so the modern path prints 3% and the legacy path 3.0%. Keying on the field rather than sniffing the raw response text avoids threading raw JSON through the HTTP layer, at the cost of missing a future 9.0 inside limits[].

The credits reset is derived, not reported.
The payload carries no reset for credits (daily and weekly are both null). Sep 1 comes from the calendar, which is why it renders as a bare date rather than a countdown: the day is trustworthy, the exact instant and its billing timezone are not.

Credits show an amount, not a share.
The cap is user-adjustable and may be unlimited, so a percentage of it says little. The percentage survives only as the bar's fill, and is null (empty bar) when there is no finite cap.

extra_usage.used_credits scale is assumed.
The spend block is preferred because its minor units and exponent are unambiguous. The older extra_usage fallback assumes used_credits follows decimal_places like monthly_limit does, which could not be confirmed against live data since it has only ever been observed at 0.

Credit currency follows the account, not the viewer.
It comes from the API (spend.used.currency), so a non-USD account renders as 42.00 EUR rather than a localized symbol. Consistent with how the rest of the extension formats monetary cost.

Testing

npm run compile clean; npm test at 244 tests, 243 passing with 1 pre-existing skip.

  • src/test/quotaWindows.test.ts, 34 new tests: both payload generations, null per-model fields, unknown scoped kinds, rollover per window period, row merging, zero-usage hiding, per-source precision, credits parsing across toggle-off and uncapped cases.
  • src/test/quotaFormat.test.ts, 11 to 35 tests.
  • src/test/quotaI18n.test.ts, 4 new tests: quota strings across all eight locales, plus a repo-wide invariant that every settings key is translated. That invariant is what surfaced the five untranslated entries.

Normalizer and formatter output verified end-to-end against a live /api/oauth/usage response in all three reset formats and all eight locales. UI verified in the Extension Development Host.

Reset-formatting tests are anchored to local wall time rather than a fixed epoch, after a first CI run failed on it: 1_700_000_000_000 is 22:13 UTC, so the +4.8h fixture crossed into the next UTC day and CI saw a weekday the assertions did not expect. Verified in 14 timezones including +05:45 and +12:45 offsets.

* normalize the usage API's limits[] array instead of retired per-model fields
* rename showOpusWeekly to showScopedWeekly, migrating the stored value
* show usage credits and their monthly reset in the quota tooltip
* merge weekly caps that reset together into one tooltip row
* make the tooltip follow resetCountdownFormat and show a wall clock per row
* round reset times to the minute and drop implied .1f percent precision
* fill five missing pt-BR / id settings translations
* print the shared weekly countdown once instead of per cap
* give each tooltip cap its own row and bar
* label the all-models row, localised across eight locales
* put the credit spend after its reset date
* read "Weekly (All)" to keep the window column narrow
* abbreviate the label in all eight locales
@github-actions github-actions Bot added the feature Minor bump — new feature label Aug 3, 2026
* a fixed epoch put the 4.8h fixture on the next UTC day, so CI saw a
  weekday the assertions did not expect
* local-noon anchor keeps same-day and next-day true in every timezone
* replace the credit percentage with the spend, keeping the bar on the cap
* keep reporting spend after credits are toggled off
* handle an absent or unlimited cap instead of hiding the row
* pad the monthly reset date away from the amount
* print no decimal for the integer limits[] field, one for the legacy float
* correct the showScopedWeekly help text in all eight locales
* pin that an idle per-model cap leaves no trace in the status bar
* gap the right-aligned share and reset cells
* drop the credits row's one-off pad
@Carl723000
Carl723000 merged commit 60b8cd1 into ClaudeCodeUsage:main Aug 7, 2026
2 checks passed
@zeyutang
zeyutang deleted the feat/generic-quota-limits branch August 7, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Minor bump — new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants