Skip to content

feat(usage): show subscription limits - #8327

Closed
mackinleysmith wants to merge 20 commits into
pingdotgg:mainfrom
mackinleysmith:t3code/usage-page-subscription-limits
Closed

feat(usage): show subscription limits#8327
mackinleysmith wants to merge 20 commits into
pingdotgg:mainfrom
mackinleysmith:t3code/usage-page-subscription-limits

Conversation

@mackinleysmith

@mackinleysmith mackinleysmith commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Subscription usage limits detail

What changed

T3 Code's Usage page showed API-equivalent spend and tokens, but subscription users could not see how close they were to provider limits.

This PR reads the structured quota windows exposed by the local Claude Code and Codex runtimes and shows compact meters beside each provider's existing cost and token summary. Web, desktop, and mobile show the same windows and reset countdowns. Providers without supported quota data keep the existing summary without meters.

The server performs this work only when the Usage page requests a summary. Successful values are cached for 180 seconds. Failures back off for 10 minutes instead of repeatedly spawning a CLI. A failed refresh keeps the last successful values visible with their age.

Claude reports five-hour, weekly, and model-scoped weekly windows such as Fable. Codex reuses an account-accurate rate-limit snapshot from a session transcript when one is less than 180 seconds old. It starts the native app-server probe only when no fresh snapshot or cache entry exists. Codex labels five-hour, daily, weekly, monthly, and annual windows with the same 5% duration tolerance used by the Codex TUI. It does not invent an unlimited window that the provider did not report.

Review fixture

Theo noted that CLIProxyAPI authentication makes the native probes hard to exercise on his machine. This development-only fixture keeps the UI reviewable without provider credentials:

T3CODE_DEV_USAGE_LIMITS_FIXTURE=review vp run dev

The fixture is accepted only when the server has a Vite development URL. It renders representative Codex weekly and Claude five-hour, weekly, and Fable windows. Normal builds continue to use fresh local snapshots and native provider probes.

UI changes

Before:

Usage page before subscription meters

After:

Provider summaries with subscription meters

Verification

  • vp test run apps/server/src/usage/usageSubscriptionLimits.test.ts apps/server/src/usage/usageTranscriptReader.test.ts packages/shared/src/usageFormat.test.ts packages/shared/src/usageMerge.test.ts apps/web/src/components/usage/UsagePage.test.tsx (50 tests)
  • Typechecked t3, @t3tools/web, @t3tools/mobile, @t3tools/contracts, and @t3tools/shared
  • Targeted lint and formatting checks passed
  • Verified in the web client with live Claude Code and Codex subscription data
  • Verified the Codex transcript parser against live ~/.codex/sessions rollouts: the account rate_limits line parses and model-scoped limit_id buckets (Spark) are rejected

Checklist

  • This PR is small and focused
  • I explained the problem and why this fixes it
  • I included before/after screenshots
  • Video is not applicable; this change has no animation or interaction changes

Built with GPT-5.6 Sol in the Codex harness through T3 Code.

Note

Add subscription limits to usage page with per-provider quota meters

  • Introduces backend probing for Claude and Codex subscription limits via probeClaudeUsage and probeCodexRateLimits, with in-memory caching, TTL/backoff, and background refresh under a single-permit semaphore in UsageService.make
  • Extends UsageSummary schema with subscriptionLimits field (defaults to empty array on decode) and merges newest per-provider limits across environments in mergeUsage
  • Adds Codex transcript tail-reading (readFreshCodexRateLimitsSnapshot) as a local recovery path when probe fails or cache is stale
  • Renders compact, accessible quota meters on web (UsageLimitMeters) and mobile (UsageRouteScreen) with progress bars, live reset countdowns updated every minute, observation-age text, and tooltips; loading skeletons match the new layout
  • Adds shared formatters formatUsageResetCountdown, formatUsageResetDateTime, and formatUsageObservationAge in usageFormat.ts
  • Behavioral Change: UsageSummary gains a new subscriptionLimits array field; older payloads without it decode to []. UsageService.layerTest stub updated to return subscriptionLimits: [].

Macroscope summarized c902fe1.


Note

Medium Risk
Touches usage summary contract v5, spawns provider CLIs during usage reads, and adds caching/concurrency logic; failures are isolated from transcript scans but probe or normalization bugs could misrepresent quotas.

Overview
Adds subscription quota visibility to the Usage page by extending the usage API with subscriptionLimits (contract v5) and rendering per-provider limit meters on web, desktop, and mobile beside existing cost/token summaries.

The server probes local Claude Code and Codex CLIs (plus a Codex transcript tail fallback when account snapshots are fresh), normalizes windows (5h, weekly, model-scoped, plan-specific Codex durations), and returns limits through a cached refresh path (3m success / 10m failure TTL, last-known-good with stale age, ~5s wait budget) without blocking transcript usage if probes fail. Clients show progress bars, reset countdowns, and stale “last updated” copy; a dev-only T3CODE_DEV_USAGE_LIMITS_FIXTURE=review fixture supports UI review.

Reviewed by Cursor Bugbot for commit c902fe1. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d2e444d4-7043-49bb-9d33-1b7a7290199b

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

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 26, 2026
Comment thread packages/shared/src/usageMerge.ts
Comment thread apps/mobile/src/features/usage/UsageRouteScreen.tsx
Comment thread docs/user/usage.md

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding in apps/web/src/components/usage/UsagePage.tsx: the loading skeleton was not updated alongside the reshaped provider summary row, so the page still shifts when data lands. Details inline.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/usage/UsagePage.tsx
Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread packages/contracts/src/usage.ts Outdated
Comment thread apps/server/src/usage/usageSubscriptionLimits.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This is a substantial new subscription-quota capability spanning provider subprocess/API probes, transcript reads, caching, shared contracts, and web/mobile UI behavior. The broad production runtime and cross-component surface require human review despite backward-compatible schema handling and focused tests.

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

Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread apps/server/src/usage/UsageService.ts
Comment thread apps/server/src/usage/usageSubscriptionLimits.ts
Comment thread apps/server/src/usage/UsageService.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the web usage UI changes (UsagePage.tsx, UsageProviderChart.tsx, UsagePage.test.tsx) for shared-primitive use, Tailwind ownership, and layout/skeleton parity.

The skeleton reshape from the previous round now matches the loaded provider rows (mark size, gap-6/gap-1.5/gap-0.5, second summary line, meter grid, min-h-60 chart), so that earlier finding is resolved. Two smaller items remain, both in apps/web/src/components/usage/UsagePage.tsx; neither blocks the feature.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/usage/UsagePage.tsx Outdated
Comment thread apps/web/src/components/usage/UsagePage.tsx Outdated
@t3dotgg

t3dotgg commented Aug 27, 2026

Copy link
Copy Markdown
Member

Note

🤖 Claude Fable 5 responding on behalf of Theo

Thanks for this. The shape is right: probe only on Usage page loads, cache server side, degrade to no meters on failure. We compared it against CodexBar, ccusage, Claude-Code-Usage-Monitor, CCSeva, and the codex TUI itself. Four things need to change before merge, and a few follow-ups are worth tracking.

Must fix

  1. Rate limit hygiene on the Claude probe. /api/oauth/usage has documented persistent-429 storms (anthropics/claude-code #31021, #30930, #31637), and the community floor for polling it is 180s. Raise SUBSCRIPTION_LIMITS_SUCCESS_TTL_MS to 180s for Claude. More important: the 5s failure TTL treats a 429, a missing binary, and an old CLI the same, so a broken or rate-limited setup respawns a full CLI on nearly every usage read. Classify failures, or at minimum move the failure TTL to minutes. For comparison, CodexBar gates per token on Retry-After, and CCSeva backs off 10 minutes on 429.
  2. Include the seven-day Fable window. The usage endpoint now reports a model-scoped weekly limit for Fable, and that is the number Max users hit first. A user whose Fable weekly is at 95% would see a calm overall weekly here while Claude's own /usage shows the Fable bar nearly full. That is exactly the dashboard mismatch this feature exists to prevent. Two notes from digging: the pinned SDK types (0.3.170) still list seven_day_opus and seven_day_sonnet and have no Fable field, so verify the live key against a real response and decode unknown seven_day_* windows loosely the way CCSeva does (any object with utilization and resets_at is a window) instead of waiting on an SDK bump. That also means UsageLimitWindowKind cannot stay a closed fiveHour | weekly enum; give windows a label now so the contract does not need another change when the next scoped window appears.
  3. Drop or data-drive the synthetic Pro window. normalizeCodexSubscriptionLimits fabricates an unlimited five-hour window for pro/prolite when the API returned none. The codex protocol models unlimited through credits.unlimited, and codex's own /status for a Pro account just shows the weekly window. Showing a meter the API did not report will turn into a confident lie the day OpenAI changes Pro semantics. Either omit the row like /status does, or key it off credits.unlimited.
  4. Docs plan names. docs/user/usage.md says "Codex Pro 5x and Pro 20x plans". Those are Claude Max plan names. Codex plans are free/go/plus/pro/prolite/team/business/enterprise/edu per the app-server schema.

Worth doing, your call whether here or in the follow-up you already offered

  • Skip the Codex probe when fresh local data exists. Codex writes rate_limits snapshots into the session JSONL files UsageService already scans (EventMsg::TokenCount is persisted unconditionally in codex-rs rollout/src/policy.rs), and the app-server pushes account/rateLimits/updated during turns. Those come from backend response headers, so each snapshot was account-accurate at write time; the only risk is staleness from usage on other devices. So gate on age: treat a snapshot or push younger than the success TTL as a cache fill and only spawn the probe when nothing that fresh exists. Same accuracy guarantee as the probe cache, and active users stop paying for spawns and API calls entirely. Never render a stale snapshot as current.
  • Codex window classification. Exact === 300 / === 10080 minute matching with a positional fallback mislabels a weekly-only primary (free and pro accounts) as "5h" when the duration is missing or off by one. The codex TUI uses ±5% tolerance and knows daily/monthly/annual windows.
  • Last-known-good retention. One transient probe failure blanks meters that were fine a minute ago. CodexBar keeps the last values and shows their age.

For what it is worth, we verified the earlier review findings are genuinely fixed at head (contract stays v4 with a decoding default, the mobile date crash, the cache-miss respawn churn, the blocking join), and the page-view-only trigger makes this the most conservative fetch cadence of any tool we compared. No evidence anywhere of accounts banned for polling these endpoints; the failure mode is 429, which is why item 1 is the one that matters most.

@mackinleysmith

mackinleysmith commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

For auth-independent visual review, run:

T3CODE_DEV_USAGE_LIMITS_FIXTURE=review vp run dev

This fixture is deliberate. Theo mentioned that CLIProxyAPI makes the native probes difficult to exercise on his machine, so the PR includes a development-only path for reviewing the UI without provider credentials. It is accepted only when the server has a Vite development URL and renders representative Codex weekly and Claude five-hour, weekly, and Fable windows. Normal builds do not use it.

@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 27, 2026
@mackinleysmith

mackinleysmith commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the stream review in 3cc3032a3, with follow-up review fixes in 6c43b31e0 and 02949bdd4. Here is the item-by-item result.

Must fixes

  1. Claude rate-limit hygiene. Successful reads cache for 180 seconds. Any failed probe now backs off for 10 minutes, so a 429, missing binary, timeout, or old CLI cannot trigger another process on every Usage read. I did not add per-status classification because the Claude SDK control method does not expose the HTTP status or Retry-After value to this call site.

  2. Fable and open-ended windows. Verified against a live Max response. Fable arrives in rate_limits.limits[] as kind: "weekly_scoped" with scope.model.display_name: "Fable". The decoder reads that shape and also accepts loose seven_day_* objects with utilization and resets_at. The live CLI also projects rate_limits.model_scoped[]; limits[] is the superset used here. Window kinds are open strings and each window carries its own label.

  3. Synthetic Codex infinity. Removed. Codex now renders only windows returned by account/rateLimits/read or a fresh transcript snapshot. A live prolite response reported one weekly window and credits.unlimited: false, so the UI now matches Codex /status and shows only that weekly row.

  4. Plan names. Removed the Claude Max-style "Pro 5x/20x" wording from the Codex docs.

Follow-ups included here

  • Fresh Codex snapshots. UsageService checks recent rollout tails for payload.info.rate_limits. A snapshot observed within the 180-second success TTL fills the cache and suppresses the native Codex probe. It never renders an older snapshot as current. While any limits cache entry is active, UsageService performs no transcript tail reads. A cold-cache pass inspects at most eight recent rollout tails, rejects future-dated snapshots beyond a 60-second clock-skew allowance, and falls back to the native probe when no fresh snapshot exists. I verified this after a cold server restart: the authenticated Usage read emitted no probeCodexRateLimits span.
  • Codex window classification. Five-hour, daily, weekly, monthly, and annual windows now use the Codex TUI's 5% duration tolerance. Missing or unknown durations use neutral "Usage" and "Secondary" labels instead of positional guesses.
  • Last-known-good values. A transient failure keeps the previous meters and their original observation time. Web and mobile add a subtle "Limits last updated … ago" note only while showing those stale values.

The development fixture remains intentional for CLIProxyAPI review:

T3CODE_DEV_USAGE_LIMITS_FIXTURE=review vp run dev

It is gated on the Vite development URL and does not ship as a production path. It renders representative Codex weekly and Claude five-hour, weekly, and Fable windows without touching provider auth.

Verification at head: 50 focused tests passed, targeted server/contracts/shared/web/mobile typechecks passed, targeted lint passed, and live Claude/Codex values matched their CLIs. The separate cross-environment multi-account identity problem remains follow-up work; this commit does not claim to solve it.

Comment thread apps/server/src/usage/usageSubscriptionLimits.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI consistency review of the new Usage subscription-limit meters. Two small issues in apps/web/src/components/usage/UsagePage.tsx, both in the new UsageLimitMeters markup; the rest of the reshaped provider rows, the skeleton, and the chart flex geometry look consistent with the loaded layout.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/usage/UsagePage.tsx Outdated
Comment thread apps/web/src/components/usage/UsagePage.tsx Outdated
Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread apps/server/src/usage/usageSubscriptionLimits.ts
Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread apps/server/src/usage/usageSubscriptionLimits.ts Outdated
Comment thread apps/server/src/usage/UsageService.ts
…scription-limits

# Conflicts:
#	apps/server/src/usage/UsageService.ts
#	packages/shared/src/usageMerge.test.ts
#	packages/shared/src/usageMerge.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding: the per-provider chart color swatch was dropped from the summary rows, leaving the provider chart with no color legend when a provider has no quota meters. Previously flagged items (label overflow in the fixed 2.25rem track, window-agnostic aria-valuetext, duplicated meter geometry, skeleton shape) look addressed.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/usage/UsagePage.tsx

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions: one finding — the new subscription-limits code inside UsageService.make reads the process environment from the module global process.env instead of the HostProcessEnvironment reference the same constructor already binds (hostEnvironment, line 154, used at line 387). Everything else in the diff (namespace subpath imports, make/layer shape, Context.Service tag, dependency acquisition via yield*, no new runtimes or runPromise, no new error classes) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread apps/server/src/usage/usageSubscriptionLimits.ts
Comment thread apps/server/src/usage/UsageService.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 67b7533. Configure here.

Comment thread apps/server/src/usage/UsageService.ts
The transcript snapshot parser required `payload.info.rate_limits`, but
codex-rs persists `rate_limits` as a sibling of `info`, so no real rollout
ever matched and every cold read still spawned the app-server probe.

Read the real shape and ignore model-scoped buckets (any `limit_id` other
than `codex`, or absent on older builds) on both the transcript and probe
paths so a Spark turn cannot present its windows as the account limit.

Only short-circuit the page's wait when every enabled provider has settled,
so a fast Codex snapshot no longer ships the page without Claude's meters.

Verified against live rollouts in ~/.codex/sessions: the account line
parses and the codex_bengalfox line is rejected.
@mackinleysmith

Copy link
Copy Markdown
Contributor Author

@t3dotgg I believe all feedback is addressed and this is ready for your eyes again.

One known major gap: when the CLIs route through a proxy or otherwise aren't signed into a single real account, both probes return nothing and the meters are omitted. The empty state is honest, but it means this feature is invisible on setups like yours, as you mentioned on stream last night that your auth doesn't go through the standard login. The pipeline (probe → normalize → cache → contract) can take another source as an adapter; what's missing is an account identity on UsageProviderLimits, which is the same change the cross-environment merge thread needs.

I'd be excited by the opportunity to take that as the follow-up, but I'd rather design it against your real configuration than guess. If you're open to it, could you describe how exactly your auth is set up so I can replicate locally and think it over? Happy to take that in a live discussion instead of this thread if that's easier, or more secure for you to talk about privately.

@t3dotgg

t3dotgg commented Aug 28, 2026

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

We are continuing with #1732 as the provider-limit source and #8445 as its separate composer follow-up. This branch has useful decoder, cache, and backoff work, but keeping its alternate ingestion and UI path would split ownership.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotgg t3dotgg closed this Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants