feat: display provider usage limits in settings - #1732
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
For now, I’ve added two images to illustrate the UI: 1. Free tier viewThis screenshot reflects my current setup. I don’t have subscriptions to Codex or Claude Code, I’m using Copilot Pro (available to me as a student). It shows how the weekly usage limit appears in the interface. 2. Pro tier (mocked example)This second screenshot uses dummy data to demonstrate how the UI could look for users on a Pro plan (Codex or Claude Code). It includes both session-based limits and weekly limits for clarity. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 80515efa38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ApprovabilityVerdict: Needs human review 3 blocking correctness issues found. This PR introduces a new feature for displaying provider usage limits with substantial new capabilities including PTY-based probes, new schema types, and UI components. Multiple High severity findings remain unresolved, and a human reviewer has raised architectural concerns about the PTY approach versus using SDKs directly. You can customize Macroscope's approvability policy. Learn more. |
|
Hey @juliusmarminge, could you take a look at this PR when you get a moment? Thanks! |
|
A few concerns after checking the code:
I can test further and commit to this PR if you're okay with it, @Aditya190803. Genuinely want to see this merged - it would be super useful |
|
this is on my list to review still! Just dealing with some larger prep work so haven't had time yet. As for the "it must be more visible and sjhould be 1:1 like codex app": How often do you guys check your limits to warrant it being one click ??? I check it at most a few times a week, so hiding it in settings next to the provider status is fine! |
|
Yes when the single settings page gets too large we'll split it to subpages. We already did the work adding the sidebar when adding the archive |
juliusmarminge
left a comment
There was a problem hiding this comment.
implementation seems way overcomplicated.
why cache the data? the provider check runs once per minute. we can get fresh data on every tick?
how i imaagined this working:
- extend the
checkProviderprobe in ServerProvider to include a newusageLimitsproperty. - on the auth check probes (app server / claude), extract usage data
- stream it down to client as part of the normal provider snapshot
- render the UI on settings page
given i haven't looked into exactly what's possible to probe and not, why is this PR so much more than that?
f9aabcd to
171df70
Compare
036a9b9 to
ed12bb8
Compare
e0ea7e5 to
cfd2e99
Compare
| }; | ||
| } | ||
|
|
||
| return yield* Effect.promise(() => runGrokUsageProbeLoop(child, input, clock)); |
There was a problem hiding this comment.
🟡 Medium provider/grokTuiUsageProbe.ts:157
probeGrokUsageLimits ignores the AbortSignal provided by Effect.promise, so interrupting the effect does not stop collectPtyProbeOutput or kill the spawned Grok PTY process. The child process and its 10-second timer keep running until the timeout fires, and repeated interrupted checks can leave multiple orphaned PTY processes. The runGrokUsageProbeLoop call should pass the signal through and wire interruption to child.kill().
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/grokTuiUsageProbe.ts around line 157:
`probeGrokUsageLimits` ignores the `AbortSignal` provided by `Effect.promise`, so interrupting the effect does not stop `collectPtyProbeOutput` or kill the spawned Grok PTY process. The child process and its 10-second timer keep running until the timeout fires, and repeated interrupted checks can leave multiple orphaned PTY processes. The `runGrokUsageProbeLoop` call should pass the signal through and wire interruption to `child.kill()`.
| return undefined; | ||
| } | ||
|
|
||
| const authenticated = value.authenticated === true; |
There was a problem hiding this comment.
🟡 Medium provider/grokUsageProbe.ts:36
parseGrokAuthCheckSubscription returns { authenticated: false } for any payload that lacks the literal boolean authenticated: true, including malformed responses like {} or { authenticated: "true" }. These flow through grokAuthFromSubscriptionProbe as status: "unauthenticated", incorrectly reporting the user is logged out instead of treating the response as unknown. The check value.authenticated === true silently coerces every non-true value to false rather than rejecting non-boolean values. Consider validating that value.authenticated is a boolean and returning undefined when it is absent or not a boolean.
- const authenticated = value.authenticated === true;
+ if (typeof value.authenticated !== "boolean") {
+ return undefined;
+ }
+ const authenticated = value.authenticated;🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/grokUsageProbe.ts around line 36:
`parseGrokAuthCheckSubscription` returns `{ authenticated: false }` for any payload that lacks the literal boolean `authenticated: true`, including malformed responses like `{}` or `{ authenticated: "true" }`. These flow through `grokAuthFromSubscriptionProbe` as `status: "unauthenticated"`, incorrectly reporting the user is logged out instead of treating the response as unknown. The check `value.authenticated === true` silently coerces every non-`true` value to `false` rather than rejecting non-boolean values. Consider validating that `value.authenticated` is a boolean and returning `undefined` when it is absent or not a boolean.
| } | ||
|
|
||
| function extractResetTimestamp(value: string, checkedAt: string): string | undefined { | ||
| const resetMatch = value.match(/\breset(?:s|ting)?(?:\s+(?:at|on|in))?[:\s-]*([^\n.;]+)/i); |
There was a problem hiding this comment.
🟡 Medium provider/claudeUsageProbe.ts:188
extractResetTimestamp truncates valid ISO timestamps with fractional seconds before they can be parsed. The capture group [^\n.;]+ stops at any period, so resets at 2026-04-17T14:00:00.000Z becomes 2026-04-17T14:00:00. The truncated value has no explicit UTC offset, so the hasExplicitOffset check fails and resetsAt is silently omitted even though the original timestamp was valid. Consider allowing . within the capture so fractional seconds survive.
- const resetMatch = value.match(/\breset(?:s|ting)?(?:\s+(?:at|on|in))?[:\s-]*([^\n.;]+)/i);
+ const resetMatch = value.match(/\breset(?:s|ting)?(?:\s+(?:at|on|in))?[:\s-]*([^\n;]+)/i);🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/claudeUsageProbe.ts around line 188:
`extractResetTimestamp` truncates valid ISO timestamps with fractional seconds before they can be parsed. The capture group `[^\n.;]+` stops at any period, so `resets at 2026-04-17T14:00:00.000Z` becomes `2026-04-17T14:00:00`. The truncated value has no explicit UTC offset, so the `hasExplicitOffset` check fails and `resetsAt` is silently omitted even though the original timestamp was valid. Consider allowing `.` within the capture so fractional seconds survive.
| ], | ||
| unavailableReason: "Could not read usage limits for this Claude account.", | ||
| }); | ||
| } |
There was a problem hiding this comment.
Unused runtime usage-limits parser
Low Severity
The parseClaudeRuntimeUsageLimits function is dead code. It's never called by any server path, including the Claude status probe, and its intended role in processing live rate_limit_event traffic is already handled by existing mechanisms. This means it doesn't contribute to updating Claude usage limits.
Reviewed by Cursor Bugbot for commit d8331d5. Configure here.
| return "finish"; | ||
| } | ||
| return parsed.available ? { settleAfterMs: GROK_USAGE_OUTPUT_SETTLE_MS } : "continue"; | ||
| }, |
There was a problem hiding this comment.
Grok usage write races TUI startup
High Severity
probeGrokUsageLimits writes /usage in onStart immediately after spawn, before the Grok TUI is ready to accept slash commands. If that input is dropped, the probe waits out the timeout and reports usage unavailable even for authenticated accounts.
Reviewed by Cursor Bugbot for commit d8331d5. Configure here.
…imits # Conflicts: # apps/server/src/provider/Drivers/ClaudeDriver.ts # apps/server/src/provider/Layers/ClaudeProvider.ts # apps/server/src/provider/Layers/CodexProvider.ts
Cursor's CLI now supports an interactive `/usage` command; replace the hardcoded "Cursor does not expose subscription usage limits" snapshot with a PTY probe that spawns cursor-agent, reads the `/usage` panel, and parses the Included window's percent and reset date, matching how Claude and Grok already surface usage. Fixes pingdotgg#1732.
| ); | ||
|
|
||
| return parseGrokAuthCheckSubscription(authRaw); | ||
| }); |
There was a problem hiding this comment.
Grok auth probe fails discovery
High Severity
probeGrokAuthViaAcp only swallows AcpRequestError and AcpTransportError. Other ACP failures such as AcpProtocolParseError, AcpProcessExitedError, or AcpInputStreamEndedError still fail the effect. Because auth is probed after models are already built inside discoverGrokProviderViaAcp, those failures discard successful model discovery and mark Grok as errored.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 1993350. Configure here.
| checkedAt, | ||
| reason: "Upstream providers did not report usage information", | ||
| }) | ||
| : undefined); |
There was a problem hiding this comment.
OpenCode usage shown incorrectly
Medium Severity
When any upstream provider is connected, missing managed usage falls back to an unavailable snapshot with reason Upstream providers did not report usage information. That also triggers for inventories that only connect non-managed providers such as OpenAI, even though this feature only supports OpenCode Go/Zen.
Reviewed by Cursor Bugbot for commit 1993350. Configure here.
| ); | ||
| const rateLimitsResponse = yield* client | ||
| .request("account/rateLimits/read", undefined) | ||
| .pipe(Effect.catch(() => Effect.void)); |
There was a problem hiding this comment.
Codex rate-limit catch too narrow
Medium Severity
The optional account/rateLimits/read call uses Effect.catch to degrade to no snapshot. That only handles typed failures, so defects skip the handler and can fail the whole Codex provider probe instead of leaving usage unavailable.
Triggered by learned rule: Effect.exit not Effect.result for recording dispatch outcomes
Reviewed by Cursor Bugbot for commit 1993350. Configure here.
| > | ||
| <div | ||
| className={cn("h-full rounded-full transition-all", color)} | ||
| style={{ width: `${Math.max(0, Math.min(100, window.usedPercent))}%` }} |
There was a problem hiding this comment.
Remaining percent rounds poorly
Low Severity
The label uses 100 - Math.round(usedPercent) while the bar width uses the raw usedPercent. Near full usage, values like 99.6 render as 0% remaining even though the bar is not completely full.
Reviewed by Cursor Bugbot for commit 1993350. Configure here.
| Layer.provideMerge(ProviderInstanceRegistryHydrationLive), | ||
| // PtyAdapter must be in the driver create context (BuiltInDriversEnv) so Grok/Claude | ||
| // usage probes can spawn TUIs. TerminalLayerLive only provide()s it privately. | ||
| Layer.provideMerge(ProviderInstanceRegistryHydrationLive.pipe(Layer.provide(PtyAdapterLive))), |
There was a problem hiding this comment.
PtyAdapter never reaches usage probes
High Severity
PtyAdapterLive is attached with Layer.provide on a Layer.unwrap hydration layer, but Claude/Cursor/Grok drivers never declare PtyAdapter in their env and only read it via serviceOption. Registry construction captures Effect.context<BuiltInDriversEnv> after unwrap, so the adapter is absent at driver.create. PTY usage probes always fall back to “unavailable in this runtime,” which matches the Claude usage failure in review.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 1993350. Configure here.
Addresses the outstanding Cursor Bugbot / Macroscope findings on the provider usage-limits work. - Grok auth probe: catch every typed ACP failure instead of only AcpRequestError/AcpTransportError. The probe runs after model discovery on the same session, so an unhandled parse/exit/stream error discarded successfully discovered models and marked the provider errored. - OpenCode: gate the "did not report usage" fallback on connected *managed* providers (Go / Zen). Inventories connected only to BYO upstreams such as OpenAI no longer claim usage was withheld for a feature that never covered them. - Codex: recover the full cause around the optional account/rateLimits/read call so a defect degrades to "no usage" instead of failing the whole provider probe. - Settings usage bars: derive the bar width and the "% remaining" label from one rounded value. 99.6% used previously rendered "0% remaining" beside a visibly unfilled bar. - Remove parseClaudeRuntimeUsageLimits and its orphaned helpers. Nothing in production called it; ClaudeAdapter already emits account.rate-limits.updated straight from the raw SDK message. - TUI probes: re-issue /usage on a bounded timer. Grok and Cursor both wrote the command at spawn time, and a TUI that had not finished installing its key handlers dropped it, turning a healthy account into "usage unavailable" after the full probe timeout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running the app on Windows surfaced three defects that made the usage feature unusable there. Codex was unaffected throughout because its usage comes from an app-server RPC rather than a PTY probe. - Binary resolution: the probes passed `binaryPath` straight to node-pty, but on Windows these CLIs install as `name.cmd` shims next to an extensionless shell script that PATH lookup finds first. node-pty executes the target directly, so every PTY-backed provider reported "Failed to spawn ... for usage probe". Resolve the executable and route `.cmd`/`.bat` shims through ComSpec. This deliberately avoids `resolveSpawnCommand`, which escapes the executable for `ChildProcess` with `shell: true` where the command is a single string. node-pty takes an argv array and quotes each element itself, so a pre-escaped path arrives double-quoted and cmd.exe answers "is not recognized as an internal or external command". - Claude probe timeout: `claude --print /usage` performs a real API round trip, measured at ~2.7s warm and ~8s cold. The 4s budget expired before the CLI answered on anything but a warm cache, so a working account showed "Could not read usage limits". Raised to 15s. - Server crash: killing a PTY on Windows makes node-pty 1.1.0 resolve its console process list as `undefined` and call `.forEach` on it inside its own promise continuation (windowsPtyAgent.js:141). Nothing handles that rejection, so a probe timeout terminated the whole server, which then crash-looped under `node --watch`. The throw is asynchronous, so no try/catch around `kill()` can intercept it; signal the OS process directly on Windows instead, and never kill a PTY that already exited. Verified end to end: Claude now renders Session and Weekly bars in Settings, and the server survives repeated probe cycles without crashing. Probe tests now pin `HostProcessPlatform` so binary resolution does not depend on the host OS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Following up on my earlier comment: this looks like a great foundation, and I’m not asking to expand this PR’s scope before merge. With T3 Connect now live to the masses, I think usage becomes decision-time information - not just a Settings diagnostic. Some of us are juggling $20 plans and need to know whether the selected server/account/model is already close to its limit. I’d love a follow-up where:
Settings is still the right place for the full breakdown. But the compact, live version belongs where I choose the model and environment imo. The snapshot/probe work in this PR feels like the important foundation for that future UX. |
|
It seems that @NicL9923 addresses allows (gated setting) displaying session/weekly usage limits within both:
All while addressing several comments of this PR thread. |
There was a problem hiding this comment.
Reviewed the new provider usage-limit probes against the Effect service conventions. Three findings, all about how PtyAdapter is acquired and imported; the pure helpers (providerUsageLimits.ts, ptyProbeSupport.ts, codexUsageProbe.ts, openCodeUsageLimits.ts) and the contract/schema additions look fine.
Posted via Macroscope — Effect Service Conventions
| type ProviderSnapshotSettings, | ||
| } from "../providerUpdateSettings.ts"; | ||
| import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts"; | ||
| import { PtyAdapter } from "../../terminal/PtyAdapter.ts"; |
There was a problem hiding this comment.
At a service boundary the service module should be imported as a namespace and used through its module shape, which is also what the rest of the repo does (import * as PtyAdapter from "../../terminal/PtyAdapter.ts" in Layers/ClaudeProvider.ts and terminal/Manager.ts). Consider switching to the namespace import and referencing PtyAdapter.PtyAdapter at the use site (line 130). Same in CursorDriver.ts:27 and GrokDriver.ts:14.
Posted via Macroscope — Effect Service Conventions
| export function probeClaudeUsageLimits( | ||
| input: ClaudeUsageProbeInput, | ||
| ptyAdapter: PtyAdapter.PtyAdapter["Service"], |
There was a problem hiding this comment.
This production probe takes the PtyAdapter implementation as a value parameter, so the dependency never appears in the effect's requirements (Effect.Effect<ClaudeUsageProbeResult>) and every caller has to thread the instance by hand.
Consider acquiring it from the environment instead — declare the requirement (Effect.Effect<ClaudeUsageProbeResult, never, PtyAdapter.PtyAdapter>) and do const ptyAdapter = yield* PtyAdapter.PtyAdapter; inside the Effect.gen — and let the tests supply their stub via Layer.succeed(PtyAdapter.PtyAdapter, ...) rather than as an argument. Same shape in probeCursorUsageLimits (cursorUsageProbe.ts:135) and probeGrokUsageLimits (grokTuiUsageProbe.ts:131).
Posted via Macroscope — Effect Service Conventions
| ) => Effect.Effect<ClaudeCapabilitiesProbe | undefined>, | ||
| environment?: NodeJS.ProcessEnv, | ||
| cwd?: string, | ||
| ptyAdapter?: PtyAdapter.PtyAdapter["Service"], |
There was a problem hiding this comment.
Threading the service instance through an optional parameter hides the dependency: ClaudeDriver resolves the tag with Effect.serviceOption, flattens it to undefined, and passes it two levels down, so PtyAdapter shows up in neither this function's nor the probe's requirements.
Consider dropping the parameter and acquiring it here, keeping the optionality inside Effect:
const ptyAdapter = Option.getOrUndefined(yield* Effect.serviceOption(PtyAdapter.PtyAdapter));That keeps the "usage limits unavailable in this runtime" fallback exactly as-is while making the requirement visible and layer-testable. Same for checkCursorProviderStatus (CursorProvider.ts:996) and checkGrokProviderStatus (GrokProvider.ts:201).
Posted via Macroscope — Effect Service Conventions
| ...(subscriptionTier !== undefined ? { subscriptionTier } : {}), | ||
| ...(authMode !== undefined ? { authMode } : {}), | ||
| }; | ||
| } |
There was a problem hiding this comment.
Grok auth false negatives
High Severity
parseGrokAuthCheckSubscription treats any object response without authenticated === true as explicitly unauthenticated. Missing or differently shaped payloads therefore force Grok into the warning unauthenticated path even after ACP model discovery succeeded, a regression from the prior ready/unknown auth behavior.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 228e10f. Configure here.
…provider-usage-limits # Conflicts: # packages/contracts/src/server.test.ts
…provider-usage-limits
…a190803/t3code into feat/provider-usage-limits
| ], | ||
| }, | ||
| }), | ||
| ).toThrow(); |
There was a problem hiding this comment.
Forward-compat tests deleted
High Severity
Usage-limit tests replaced the previous forward-compatibility coverage instead of adding beside it. Helpers like decodeServerProviders, decodeUpsertKeybindingResult, decodeAvailableEditors, and baseProviderSnapshot remain imported/defined but unused, which strongly suggests those regression tests were removed by mistake.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e08399b. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 18 total unresolved issues (including 17 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4b7792d. Configure here.
| // degrade to "no usage" instead. | ||
| const rateLimitsResponse = yield* client | ||
| .request("account/rateLimits/read", undefined) | ||
| .pipe(Effect.catchCause(() => Effect.void)); |
There was a problem hiding this comment.
Rate-limit catch swallows interrupts
Medium Severity
Switching the Codex rate-limits request to Effect.catchCause(() => Effect.void) recovers interrupts as well as decode defects. An interrupted provider probe can continue and finish successfully instead of aborting, which fights shutdown and parent timeouts.
Reviewed by Cursor Bugbot for commit 4b7792d. Configure here.







Fixes #228.
What Changed
Added provider usage limits to the settings flow end to end for all 4 providers (Codex, Claude, Cursor, OpenCode):
Note: For OpenCode, only the official OpenCode-managed providers (OpenCode Go, OpenCode Zen) are shown.
Why
Users need a visible place to confirm provider usage limits without digging through logs or backend state. This keeps the value available across sessions and makes the current limits easy to inspect from the UI.
UI Changes
The Settings panel now shows provider usage limits in the provider section.
Checklist
Note
Medium Risk
Provider status checks now spawn external CLIs and parse fragile TUI text; failures are degraded but probes add latency and Windows PTY behavior was changed to avoid process crashes.
Overview
Adds subscription/quota usage to provider status snapshots and surfaces it on each provider card in Settings.
The contracts layer gains optional
usageLimits(windows with used %, labels, reset times, and availability reasons). During status checks, Codex reads app-server rate limits; OpenCode Go/Zen aggregates managed-provider inventory usage (BYO upstreams omit usage entirely); Claude, Cursor, and Grok optionally spawn their CLIs via PTY to parse/usageor print-mode output, with shared probe helpers and graceful “unavailable” fallbacks when PTY is missing or accounts are API-key-only. Grok also picks up ACP-based auth metadata from the same discovery pass.Settings UI renders progress bars (% remaining, color thresholds, reset dates). Windows PTY fixes cover
.cmdshim launching and safer process teardown so usage probes do not crash the server.Reviewed by Cursor Bugbot for commit 4b7792d. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Display provider usage limits in the settings UI for Claude, Cursor, Grok, Codex, and OpenCode providers
usageLimitsto theServerProvidercontract schema with validatedusedPercent(0–100), session/weekly windows, availability flags, and reset timestamps..cmd/.batshim routing, settle-after-output semantics, retry logic, and injectable clock for testing.account/rateLimits/readendpoint; OpenCode aggregates usage from connected managed providers (opencode-go/opencode-zen).PtyAdapterLiveis injected into the provider registry hydration layer, adding process-spawn overhead to every provider refresh.Macroscope summarized 4b7792d.