Skip to content

feat(codex): show the credit limit on credit-metered ChatGPT workspaces - #833

Open
rbstp wants to merge 1 commit into
getagentseal:mainfrom
rbstp:chatgpt-enterprise-support
Open

feat(codex): show the credit limit on credit-metered ChatGPT workspaces#833
rbstp wants to merge 1 commit into
getagentseal:mainfrom
rbstp:chatgpt-enterprise-support

Conversation

@rbstp

@rbstp rbstp commented Jul 27, 2026

Copy link
Copy Markdown

Summary

  • ChatGPT Business / Edu / Enterprise workspaces on credit-based pricing return rate_limit: null, so CodeBurn rendered an empty quota card with a blank menubar percentage and tab badge. Their only limit is the admin-set monthly credit allowance in spend_control.individual_limit, which was never decoded.
  • Both decoders now read it and render a "Monthly usage limit" bar with used/allowance counts, reset date, and pace projection, promoted to primary when no rate windows exist: app/electron/quota/codex.ts (desktop) and CodexSubscriptionService.swift (menubar).
  • Credit-settled balances drop the $ prefix, composite tiers (enterprise_cbp_usage_based) normalize to their base tier, and reset credits come from the inline rate_limit_reset_credits block instead of a second HTTP request.

Payload quirks are documented in docs/providers/codex.md. Notably plan_type reports "business" even on a true Enterprise workspace, and the accounts endpoints the ChatGPT workspace switcher reads reject the Codex OAuth token with 403 while /wham/usage returns 200 for the same token, so the displayed label is faithfully what OpenAI returns.

Testing

  • I have tested this locally against real data (not just unit tests) - tested on macOS 27
  • npm test passes - one pre-existing failure in tests/cli-durable-totals.test.ts and 26 jsdom resolution errors, both of which reproduce unmodified on main. This PR touches no files in src/ or tests/. The suites covering this change are green: npm --prefix app test 453/453 and swift test --package-path mac 66 + 120.
  • npm run build succeeds
  • Screenshot or terminal output attached below proving it works with real data

before:
file-6f715905fbc7e63e8c89fd966d471ca6

after (did not reconnect claude):
file-d878c4c704345afa56efdea1c7f4dc00

file-177eb85c05773d4794540f73cb237467

matches my usage:
dyn-49fac897ee4a3cf5cfad50be7f8f3a4d

@iamtoruk iamtoruk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this — it's a thorough contribution, and the testing notes (including confirming the pre-existing cli-durable-totals failure on main — that's a known time-of-day flake, fix inbound with the next batch) made review much easier. I ran the suites locally since fork PRs don't get CI here: app 453/453, swift-testing 120/120, builds and typechecks green, and the security-sensitive surfaces (endpoints, token handling, request count) all check out clean.

Three changes needed before merge — the first two are one-liners:

1. Unpinned NumberFormatter locale fails XCTest on non-US-grouping localesmac/Sources/CodeBurnMenubar/Data/CodexUsage.swift:99 (CreditLimit.displayLabel) builds a bare NumberFormatter(). On a machine whose locale groups with periods this renders Monthly usage limit · 12.000 / 10.000 credits and testPercentOnlyOverageKeepsTheImpliedCount fails (CodexPlanParsingTests.swift:182) — XCTest came out 65/66 here. Fix is the repo's existing convention (8 sites, including CodexSubscriptionService.swift:469 in this same PR's file set):

formatter.locale = Locale(identifier: "en_US")

Note en_US_POSIX is the wrong pin for this one — it drops grouping entirely (12000).

2. Same missing pin in the new credits-footer branchmac/Sources/CodeBurnMenubar/AppStore.swift:1397, the inCredits ? .decimal branch, renders 3.410 instead of 3,410 on the same locales, and no test exercises it. Same one-line fix, plus a test would be great. (The TS side hardcodes toLocaleString('en-US'), and your own doc note says the two implementations should stay in sync.)

3. The "next expires" caption silently disappears for existing usersinlineResetCredits in CodexSubscriptionService.swift returns ResetCredits(availableCount:, nextExpiresAt: nil) whenever the usage payload carries rate_limit_reset_credits.available_count, and the companion request is then skipped. HeatmapSection.resetCreditsLabel degrades "3 available · next expires in 2 days" to "3 available" — and that row only renders when availableCount > 0, i.e. exactly the users who lose the information. Suggestion that keeps the saved request on the common path: take the inline shortcut only when available_count == 0, and fall through to the existing fetch otherwise.

Two non-blocking nits, take or leave:

  • The comment on spendControlWindow ("a garbage value is not null, so ?? would stop at it") overstates the code slightly — find stops at the first object-typed candidate, so an object-shaped garbage alias (e.g. individual_limit: { limit: "abc" } in the first position) still returns null. Swift behaves identically so parity holds; only the comment misleads. The test covers a string garbage alias, which is the case find does skip.
  • has_credits semantics are documented from a workspace where it's false. If a seat-based account ever reports has_credits: true next to a dollar balance, the footer drops the $ and rounds to whole units — worth a defensive thought, though blast radius is small.

Happy to re-review as soon as the three fixes land.

Signed-off-by: Richard Boisvert <rboisvert@devolutions.net>
@rbstp
rbstp force-pushed the chatgpt-enterprise-support branch from a81d4a8 to 3e400bf Compare July 27, 2026 12:20
@rbstp

rbstp commented Jul 27, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed review, and for running the suites locally given fork PRs don't get CI here. All three are in, pushed as an amend to the same commit.

1. Locale pinCodexUsage.swift displayLabel now sets locale = Locale(identifier: "en_US"). I reproduced the failure before fixing it: under -AppleLocale de_DE the unpinned formatter renders 12.000 and fr_FR gives 12 000, so testPercentOnlyOverageKeepsTheImpliedCount was genuinely locale-dependent, not just theoretically. Confirmed your en_US_POSIX note too, it drops grouping and yields 12000, so the 8 existing sites are pinned correctly for their contexts but would be wrong here.

2. Credits footer — same pin at AppStore.swift. Worth flagging that it fixes both branches, not only inCredits: the .currency branch was equally exposed, rendering 3.410,40 $ instead of $3,410.40 on those locales.

Added mac/Tests/CodeBurnMenubarTests/CodexQuotaSummaryTests.swift, 5 cases covering the footer in credits and in dollars, the unlimited footer, and the chip label in both normal and reached states. One gotcha worth knowing if you add more AppStore tests: quotaSummary(for:) returns nil unless codexLoadState is pinned, and its default is CodexCredentialStore.isBootstrapCompleted ? .dormant : .notBootstrapped, so it depends on whether the machine running the tests has Codex connected. The helper pins .loaded so the suite is deterministic anywhere.

3. Reset credits — implemented as you suggested. New inlineResetCreditsShortcut returns a value only at available_count == 0; any non-zero count falls through to fetchResetCredits and keeps the caption. The saved request survives on the common path, which is the zero case. Covered by testOnlyAZeroCountSkipsTheCompanionRequest.

Nit 1 — corrected. The comment now says object-shaped garbage still wins the position and that Swift commits to the first candidate that decodes, rather than implying find filters by validity.

Nit 2 — I looked at this and decided against a logic guard, because the payload argues the opposite way. My workspace is credit-metered (it has a spend_control allowance) yet reports has_credits: false with a null balance. So the flag tracks whether the account holds a credit balance, which is orthogonal to whether it is credit-metered. Gating the $ on creditLimit != nil would have been wrong in the other direction, for a credit account holding a balance with no admin cap. I documented the real semantics in docs/providers/codex.md and flagged the has_credits: true path as unobserved against a real account. Happy to add a guard if you have a rule in mind, but I'd rather not encode a guess.

Suites after the changes: XCTest 67/67 (was 66, +1 for the shortcut test), swift-testing 125/125 (was 120, +5 for the new suite), app 453/453, typecheck clean.

Also, good to hear cli-durable-totals is a known time-of-day flake with a fix inbound. That matches what I saw: it passed for me earlier in the session and started failing later the same evening on both my branch and a clean main.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants