Skip to content

[defer] feat(usage): display and redeem Claude reset credits in dashboard - #14728

Open
fouadSalkini wants to merge 10 commits into
diegosouzapw:release/v3.8.52from
fouadSalkini:feat/claude-reset-credits
Open

fouadSalkini wants to merge 10 commits into
diegosouzapw:release/v3.8.52from
fouadSalkini:feat/claude-reset-credits

Conversation

@fouadSalkini

@fouadSalkini fouadSalkini commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Anthropic offers session-limit and usage resets for Claude accounts, surfaced through the OAuth usage endpoint (/api/oauth/usage?at_wall=1&cedar_ember=1&skip_spend=1):

  1. Banked reset grants (cedar_ember): Usage grants carrying id, resets_left, starts_at, ends_at, and clears scopes (e.g. opus55-launch-promax-20260921 for Pro/Max and opus55-launch-team-20260921 for Team seats).
  2. Weekly session-limit reset (juniper_tide): The weekly 5-hour wall reset (counterpart of Claude Code's hidden /limit-reset command introduced in feat(sse): Claude OAuth lower-priority lane + weekly session-limit reset #13074).

Previously, OmniRoute only supported auto-claiming juniper_tide at the wall during execution. This PR integrates Claude reset credits into the existing reset credit architecture alongside Codex and Grok:

  • Queries /api/oauth/usage?at_wall=1&cedar_ember=1&skip_spend=1 with x-app: cli and User-Agent: claude-cli/... (Anthropic checks the CLI surface before returning grants, otherwise returning ineligible_reason: "surface").
  • Exposes bankedResetCredits on Claude usage responses and displays them in the Provider Limits dashboard with a "Redeem" button.
  • Adds src/lib/usage/claudeResetCredits.ts to list available credits and redeem grants (cedar_ember) or weekly session resets (juniper_tide).
  • Hooks into /api/usage/codex-reset-credit (GET list, POST consume) for provider claude.
  • Updates CodexResetCreditsModal to support Claude reset confirmation and window descriptions.

Tests

  • New unit test suite tests/unit/claude-reset-credits.test.ts (9/9 pass):
    • parseAllClaudeResetCredits extracting cedar_ember grants and juniper_tide resets.
    • claimClaudeResetCredit dispatching cedar_ember payload with grant ID and request ID, vs juniper_tide payload.
    • canProviderRedeemResetCredit, getResetCreditEndpoint, and computeCanRedeemResetCredit recognizing claude.
    • parseQuotaData converting bankedResetCredits into a quota row for Claude cards.
    • CodexResetCreditsModal formatting titles and confirmation text for Claude.
    • getClaudeUsage extracting bankedResetCredits from usage response.
  • All reset credit test suites pass (66/66 total reset tests pass).
  • npm run check:any-budget:t11, npm run check:cycles, npm run check:changelog-integrity, and ESLint clean.

Live Evidence

Deployed to both operator production nodes ahead of merge:

  • Single Linux release build verified byte-identical across nodes (3deb8d4ac077a10429f6eb4d6f96d117a682643aacf19731b614200623bc0245).
  • Live discovery confirmed active grants on production seats:
    • A Claude Max account on node A: opus55-launch-promax-20260921 (resets_left: 1, usable_now: true).
    • Five Claude Team seats on node B: opus55-launch-team-20260921 (resets_left: 1, usable_now: true).
    • Live query to /api/usage/codex-reset-credit?connectionId=<id> returns HTTP 200 with the active grant details.

⚠️ base-red inherited: #14866

@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @fouadSalkini — great idea to surface the banked reset credits in the existing reset modal. Before we can merge: (1) the new query string on oauthUsageUrl breaks 4 existing tests (tests/unit/claude-fable-usage.test.ts mocks endsWith("/api/oauth/usage")) — please update them and re-run the other Claude usage suites; (2) you switched the User-Agent of the regular usage poller to claude-cli/… (external, cli) + x-app: cli and dropped the comment explaining why it was axios-style. That changes the fingerprint of all Claude OAuth traffic, so could you limit the new UA/query to the list/redeem path (or show a live validation)? (3) The new i18n keys aren't in any locale yet, and the isGlmResetProvider change also affects grok-cli — could you call that out or split it? We'll validate the redeem flow live on our side.

@diegosouzapw diegosouzapw added the deferred-v3.8.52 Grande demais / suspeito para o lote atual; precisa de sessão dedicada no ciclo v3.8.52 label Sep 25, 2026
@diegosouzapw diegosouzapw changed the title feat(usage): display and redeem Claude reset credits in dashboard [defer] feat(usage): display and redeem Claude reset credits in dashboard Sep 25, 2026
@fouadSalkini

Copy link
Copy Markdown
Contributor Author

Addressed all points:

  1. Fixed tests/unit/claude-fable-usage.test.ts — all 4 tests pass cleanly.
  2. The regular usage poller's User-Agent, headers, URL, and explanatory comment are restored to the base values (claude-code/<version>), so ordinary usage polling traffic fingerprint is untouched. The new query parameters and CLI headers apply only to the reset-credit list and redeem requests.
  3. Translated the reset credit i18n keys across all 66 locale catalogs.
  4. Kept grok-cli behavior intact with tests passing (39/39 in tests/unit/claude-reset-credits.test.ts and provider-limits-ui.test.ts).

…st request

The regular /api/oauth/usage poll was restored to the base URL and
User-Agent, but upstream only fills `cedar_ember` and `juniper_tide`
when the reset-credit query string is sent. On the base URL both come
back null, so `bankedResetCredits` was never set, the reset-credit
quota row was never built, and the dashboard hid the redeem button
for every Claude connection.

The count now comes from the dedicated reset-credit list request
(query string + CLI headers), memoised per access token: one list
call per 30 minutes, a 15 minute back-off after a failure that keeps
serving the last known count, and one shared in-flight request for
parallel pollers. Opening the credit list re-seeds the count and a
redeem clears it. The regular poller's URL, User-Agent and headers
are unchanged, and a test now pins them together with the list and
redeem request shape.
…o only

The regular usage poller runs from background schedulers, so fetching
the reset-credit list from it sent a CLI-UA `at_wall=1&cedar_ember=1`
request for every Claude account with nobody looking. The poller is now
byte-identical to base and never sends that request.

- The banked count lives in a per-connection memo that only the
  user-opened reset-credit list and the existing opt-in auto-reset seed;
  the provider-limits refresh just reads it. Unknown counts are omitted.
- Redeem and auto-claim forget the count; a per-connection generation
  keeps a list started before a redeem from storing the old count, and
  forget also drops the in-flight request. Concurrent lists share one
  request.
- A count older than one hour, or refused with a non-429 4xx, becomes
  unknown; 429/5xx keep the last count until then.
- The request deadline now covers reading the body, for the list,
  status and claim requests.
- List, claim and organization lookups run in the connection's proxy
  context.
- Dashboard: a Claude OAuth card shows the reset-credits entry point
  while the count is unknown or above zero and hides it only after an
  authoritative empty list; no count is ever invented.
- Classify the Claude reset-credit connection lookup in the hard-lease
  inventory next to its codex/grok/glm siblings.
The opt-in auto-reset (status read and juniper_tide claim) is back to
the base request shape: `?at_wall=1&skip_spend=1` with the axios-style
`claude-code/<version>` User-Agent and no `x-app`. Base's parser
already reads `juniper_tide` from that response. Only the dashboard's
user-initiated reset-credit list and redeem send `cedar_ember=1` and
the CLI headers; the claim picks its headers from a `profile` option
that defaults to the base shape. The auto-reset no longer seeds the
dashboard count, but an auto-claim still forgets it.

- A usage refresh that falls back to the stale cached entry no longer
  serves (or keeps storing) a cached Claude count the memo does not
  confirm, so a redeem followed by a 429 cannot bring back the
  pre-redeem count.
- Only dashboard list calls can join an in-flight list request, and
  they all share one deadline.
- Drop the per-connection generation map: a list result is stored only
  while its request is still the registered in-flight one, so there is
  no bounded state whose eviction could admit a stale result.
@fouadSalkini

Copy link
Copy Markdown
Contributor Author

Follow-up on point (2) and point (3):

Point (2) — request shapes. Restoring the regular poller to the base URL/User-Agent had a side effect: /api/oauth/usage returns cedar_ember / juniper_tide as null unless the reset-credit query string is sent, so the banked-credit count never reached the dashboard and the redeem entry point disappeared (the earlier test hid this by mocking grants on the base URL). Reworked so that no existing Claude OAuth traffic changes shape:

  • The regular usage poller is exactly the base code and never requests reset credits; a test deep-compares its full header object against base.
  • The existing opt-in auto-reset is also back to its base request shape for both the status read and the claim (?at_wall=1&skip_spend=1, claude-code/<version> User-Agent, no x-app); tests pin both against base.
  • Only the dashboard's reset-credits modal — opened by the user — sends the cedar_ember=1 list request with the CLI headers, and only the dashboard redeem uses them for its claim. Nothing sends it in the background.
  • The banked count comes from that user-opened list alone (memoised per connection). Until a list has answered, a Claude OAuth card shows the entry point without a number; an empty list hides it. Redeems and auto-claims clear the count, a throttled refresh after a redeem cannot restore the old value, and the count expires after an hour. List/claim/organization requests run inside the connection's proxy context, and the request deadline covers the body read.

Point (3) — isGlmResetProvider and grok-cli. Calling this out explicitly: on base, isGlmResetProvider was provider !== "codex", so grok-cli was treated as GLM in the reset modal (GLM title, explainer and window copy). This PR makes it an explicit GLM list (glm, glm-cn, glmt, zai), so grok-cli now gets the generic reset-credit copy that Codex uses (including the "Codex reset credits" title). grok-cli's redemption endpoint (/api/usage/codex-reset-credit) and behaviour are unchanged — only the modal wording differs. Happy to split this into its own PR, or add a grok-specific title, whichever you prefer.

Tests: claude-reset-credits 20/20, wiring 2/2, plus the existing Claude usage, limit-reset and provider-limits suites (all green).

fouadSalkini added a commit to fouadSalkini/OmniRoute that referenced this pull request Sep 26, 2026
Shows Claude reset credits in the provider limits dashboard and lets an
admin redeem them through the reset-credit route, as deployed.
fouadSalkini added a commit to fouadSalkini/OmniRoute that referenced this pull request Sep 26, 2026
fouadSalkini added a commit to fouadSalkini/OmniRoute that referenced this pull request Sep 26, 2026
Brings diegosouzapw#14728 from be3d814 to its final head d04c11a:
- the reset-credit count comes from the dashboard's reset-credit list request
  (at_wall=1&cedar_ember=1&skip_spend=1 with the CLI headers), in the new
  claudeResetCreditCount.ts, and is kept in a memo keyed by connection id;
- the regular usage poller (usage/claude.ts) is back to base's request shape;
- the opt-in auto-reset keeps base's at_wall=1&skip_spend=1 request with the
  claude-code/<version> UA and no x-app header.

Also adds the provider-limits UI assertion that Claude reset credits redeem
through the shared picker.
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.51 to release/v3.8.52 September 29, 2026 11:20
@diegosouzapw

Copy link
Copy Markdown
Owner

Re-homed to release/v3.8.52: v3.8.51 entered its release freeze, so the branch now belongs to the release captain and development continues on the next cycle. Nothing is wrong with this PR — it just needed a live base. No action needed from you; CI will re-run against the new base.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deferred-v3.8.52 Grande demais / suspeito para o lote atual; precisa de sessão dedicada no ciclo v3.8.52

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants