Skip to content

feat(codex): let an account stop at its usage limit instead of spending ChatGPT credits - #6422

Closed
sungyongcho wants to merge 1 commit into
lidge-jun:devfrom
sungyongcho:feat/codex-credits-after-limit
Closed

sungyongcho wants to merge 1 commit into
lidge-jun:devfrom
sungyongcho:feat/codex-credits-after-limit

Conversation

@sungyongcho

@sungyongcho sungyongcho commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #6334.

An account that holds ChatGPT credits is not refused by upstream when one of its usage windows reaches 100%: upstream serves the request and draws the credit balance. Account selection only takes an account off on quota after a refusal, so such an account was never moved off, and with autoSwitchThreshold: 0 it kept spending credits while other accounts in the pool still had included usage. This adds one per-account switch, Use credits after limit, on every Codex account card, the main account included.

  • On (the default): nothing changes. The setting is stored as absence, so a pool that never touches the switch routes exactly as before.
  • Off: the account is skipped while one of its usage windows is full, and the pool moves on in its normal order. Selection order, thresholds and priorities are otherwise untouched; the switch only removes an account at 100%, it never reorders the pool.
  • When the hold ends. A held account gets no traffic and therefore no new observation, so the reading has to end on its own. The long window (weekly, or monthly on 30-day plans) counts only while its reset is still ahead, and a full reading without a reset time does not hold the account. The 5-hour window uses the existing isTerminalShortWindow rule. So the account comes back after its reset without anyone refreshing it.
  • Pool accounts. src/codex/account-credit-use.ts owns the list (noCreditCodexAccountIds) and the full-window rule. In src/codex/routing/selection.ts the hold is checked wherever plan exclusion is checked: the eligible list, isCodexAccountSelectable (an active or bound account is served without passing through the eligible list), and codexAccountBlockReason, which reports a new credits_off reason. isCodexAccountRotationExcluded carries plan exclusion and the hold into the two legacy keep-the-active-account fallbacks and the transient-only affinity check in src/codex/routing.ts, so a pool whose last available account is held selects none instead of spending. routing.ts stays at its size cap: those three call sites are replaced, not added to.
  • Main account. The eligible list's main branch checks the hold too. The main login also has paths that never reach selection (an explicit main request, a caller carrying the main credential, Reserve), so src/codex/auth-context.ts checks it where the main-account hard lock is checked: assertMainAccountPolicy throws a new CodexMainAccountCreditsOffError, a cooldown error mapped like the hard-lock refusal, and requestOwnedMainPinState stops preserving a caller's own main credential. Both read the main policy quota the hard lock reads and never the plan, because several of those callers may not open the physical auth file. With the hard lock on (the default) the main account is usually stopped at 98% first; the switch matters when the lock is off or set to 100%.
  • Display. The switch is shown while the page's existing Codex credits display (showCodexCredits) is on, so the credits row and the switch appear and hide together. With the display off, an account whose switch is off shows a Credits off badge, so a held account never looks ordinary. The display setting still never changes routing.
  • Reset credits untouched. Nothing here redeems or reads reset credits.
  • API. PUT /api/codex-auth/accounts/credits with { id, creditsAfterLimit } for pool accounts and __main__ (400 for a bad id or a non-boolean, 404 for an unknown account). The account list DTO gains creditsAfterLimit on every row. gui/src/components/AccountCreditsToggle.tsx reuses the custom-threshold row styles, so no CSS was added; strings are in all ten locales. A deleted account is forgotten from the list.
  • No CLI verb yet. The route is declared in the route registry as deferred-verb (owner "[Feature]: per-account switch to stop using an account once it reaches its usage limit instead of spending its ChatGPT credits #6334 follow-up", named in structure/codex-account-controls.md); ocx config set noCreditCodexAccountIds covers scripted use. Happy to add an ocx account verb if you want it in this PR.
  • Docs: the noCreditCodexAccountIds row in the configuration reference, the route in the management API reference, a paragraph next to the Codex credits switch in the Codex integration guide, and a section in structure/codex-account-controls.md.

Observed on my pool before this change: one account went from 90% to 100% weekly while its balance went from 62,500 to 0, while the other pool accounts read 0% weekly the evening before; another account at 100% weekly kept being selected and its balance kept falling. Details are in #6334.

Verification

  • New tests/codex-integration/codex-credits-after-limit.test.ts (15 tests): the default keeps a 100% account selected; with the switch off it is skipped by resolve and preview and reports credits_off; a thread already bound to it moves off; below 100% nothing changes; an elapsed reset and a reading without a reset time do not hold; the last account held selects none; only listed ids are stored, __main__ included; weekly reset in milliseconds and seconds, monthly on a 30-day plan, the 5-hour window; and the route (on, off, __main__, bad id, unknown id, non-boolean).
  • New tests/codex-integration/codex-credits-after-limit-main.test.ts (6 tests, hard lock off so only the switch can refuse): by default a full main login keeps serving; with the switch off it is refused with CodexMainAccountCreditsOffError carrying the reset, also for an explicit main request; the pool moves to another account; a caller carrying the main credential moves too, with physical auth reads forbidden; an elapsed reset or 99% releases it; and selection alone leaves the held main out.
  • Each hold point was broken on purpose and the expected test failed, then reverted: the legacy-fallback condition, the check in isCodexAccountSelectable, the reset-time rule, assertMainAccountPolicy, requestOwnedMainPinState, and the main branch of the eligible list.
  • New gui/tests/codex-credits-after-limit-toggle.test.tsx (6 tests: main and pool cards show the switch with the credits display, on by default for rows without the field; with the display off the switch is hidden and a held account is badged; unwired pages show nothing; click and pending-write blocking) and a controller test in gui/tests/codex-account-pool-behaviour.test.tsx. Treating a missing field as off made the default tests fail; reverted.
  • Registered both test files in scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json, and the new writer in tests/config/config-rebase-provenance-writers.test.ts.
  • bun run typecheck, bun run structure:check and bun run privacy:scan: pass.
  • GUI: bun run build, bun run lint, bun run lint:i18n pass, and the whole GUI suite the way CI runs it, cd gui && bun test --isolate tests: 2725 pass, 0 fail.
  • npx react-doctor@0.9.11 --verbose --scope changed against dev: 20 files, no issues.
  • Root files around the change (main hard-lock auth, Reserve auth context, main policy binding wait, Codex auth context, Codex auth API, route registry, test layout, and the new files): 554 pass, 0 fail.
  • Full root suite, bun run test: 35002 pass, 77 skip, 96 fail. 95 of them are in 18 files this PR does not touch, and the same files fail with the same counts at the dev tip without this PR (tests/update/update-restart-lease.test.ts checked twice there). The 96th was a 30 s timeout in tests/server/server-auth.test.ts while I was capturing screenshots in parallel; that file passes alone, 117 of 117, twice. My machine has no standalone Bun, so the suite runs through the packaged ocx binary in Bun mode; that explains the subprocess failures such as Unknown command: -e.
  • Not verified: behaviour against the live upstream with a real account. I did not want the check to spend credits or move traffic onto another account. The screenshots use the built dashboard served with fictional account data (the harness is a small Python server that answers the management API with canned rows), not a running proxy.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. The route follows the existing pause route (management auth, id validation, __main__ sentinel), the default leaves routing unchanged, and the main-account check reads only the in-memory policy quota, never the physical auth file.

Screenshots

Dashboard built from this branch with fictional data (a main account at 0% and two Pro pool accounts at 100% weekly):

  1. Codex credits display on: the switch is on by default on every card, the main card included.
  2. The switch turned off on sub2.
  3. The same in the light theme.
  4. Codex credits display off: credits rows and switches are hidden, and sub2 carries the Credits off badge.
  5. Narrow layout; the row wraps like the existing controls.
1-display-on-dark 2-sub2-off-dark 3-sub2-off-light 4-display-off-badge-dark 5-narrow-dark

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • Required local validation passed; commands, results, and any full-suite exception are documented.

  • I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

@coderabbitai

coderabbitai Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

❤️ Share

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

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

⚠️ Deterministic hygiene checks failed.

  • unsponsored_surface — This changes an authentication, workflow, release-automation, or dependency surface. MAINTAINERS.md requires security review for these; ask a maintainer to apply maintainer-sponsored once they have reviewed it. Paths: src/codex/auth-context.ts.

@github-actions github-actions Bot added the enhancement New feature or request label Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • hygiene: unsponsored_surface.

What to do

  • Fix unsponsored_surface — This changes an authentication, workflow, release-automation, or dependency surface. MAINTAINERS.md requires security review for these; ask a maintainer to apply maintainer-sponsored once they have reviewed it. Paths: src/codex/auth-context.ts.
  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ Required local validation passed; commands, results, and any full-suite exception are documented.
  • ⬜ I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This pull request was already a draft. Its draft status will be preserved after every issue above is resolved.
@sungyongcho Tick the boxes once required local validation has passed with commands, results, and any full-suite exception documented, your branch is on the latest dev commit, and every correct Codex and CodeRabbit finding is resolved.

@sungyongcho

Copy link
Copy Markdown
Contributor Author

@lidge-jun The hygiene gate flags src/codex/auth-context.ts as an authentication surface, so this PR needs a security review and the maintainer-sponsored label before it can go further. When you have time, could you take a look? Here is the whole change to that file, so the review stays small (+33/−3):

  • CodexAuthPolicyConfig (line 597) adds noCreditCodexAccountIds to the picked keys. At runtime the policy is always the full config object, so this only widens the type.
  • assertMainAccountPolicy (line 731) gets one more check right after the hard-lock check. If the main login's switch is off and one of its usage windows reads 100% with its reset still ahead, it throws CodexMainAccountCreditsOffError (line 526). That error extends CodexAccountCooldownError like CodexMainAccountHardLockError, so it reaches the client through the existing cooldown mapping.
  • requestOwnedMainPinState (line 146): a caller carrying its own main credential is no longer kept on main while the main login is held. This is the same place where the hard lock already stops preserving it.
  • The new check goes through mainCreditsHoldResetAt (line 725). It reads only getMainPolicyQuota(), the evidence the hard lock reads, and never the plan or the physical auth file. tests/codex-integration/codex-credits-after-limit-main.test.ts covers the caller-credential case with physical auth reads stubbed to throw.

Nothing in the file changes how a credential is read, stored, refreshed or forwarded, and with the switch at its default (on) none of the new branches run. I am happy to move the main-account part into a separate PR if you would rather review the pool part first.

lidge-jun added a commit that referenced this pull request Oct 2, 2026
Refines #6422 for #6334. An account at 100% is now switched out by default
and returns after its reset; spending credits is opt-in.

- Store the allow-list creditCodexAccountIds instead of the opt-out list
  noCreditCodexAccountIds; absence means no account spends credits, and a
  new account starts off.
- PUT /api/codex-auth/accounts/credits also takes { all } for the global
  switch: true lists __main__ and every pool account, false clears it.
- Dashboard: one global "Use credits" switch beside the Codex credits
  display switch, derived from the rows (off / mixed / on, like the quota
  auto-refresh control), and a count button that opens a per-account panel
  below the header. Cards drop their own switch and show a "Uses credits"
  badge on accounts allowed to spend.

Co-authored-by: Sungyong Cho <dev@sungyongcho.com>
lidge-jun added a commit that referenced this pull request Oct 2, 2026
Refines #6422 for #6334. An account at 100% is now switched out by default
and returns after its reset; spending credits is opt-in.

- Store the allow-list creditCodexAccountIds instead of the opt-out list
  noCreditCodexAccountIds; absence means no account spends credits, and a
  new account starts off.
- PUT /api/codex-auth/accounts/credits also takes { all } for the global
  switch: true lists __main__ and every pool account, false clears it.
- Dashboard: one global "Use credits" switch beside the Codex credits
  display switch, derived from the rows (off / mixed / on, like the quota
  auto-refresh control), and a count button that opens a per-account panel
  below the header. Cards drop their own switch and show a "Uses credits"
  badge on accounts allowed to spend.

Co-authored-by: Sungyong Cho <dev@sungyongcho.com>
lidge-jun added a commit that referenced this pull request Oct 2, 2026
…tGPT credits opt-in (#6444)

An account at 100% of a usage window is now switched out by default and returns after its reset; spending ChatGPT credits is opt-in through creditCodexAccountIds. The Codex Auth header has one global "Use credits" switch beside the credits display, and each account's switch lives in its card's more menu. Refines #6422 and closes #6334.

Co-authored-by: Sungyong Cho <dev@sungyongcho.com>
@lidge-jun

Copy link
Copy Markdown
Owner

Thank you, @sungyongcho. This landed through #6444 (squash 10428d0120), which carries your commit with a Co-authored-by trailer.

The maintainers made one product change on top: spending credits after the limit is now opt-in. By default, an account at 100% is switched out and returns after its reset. The setting is stored as an allow-list, creditCodexAccountIds, which replaces noCreditCodexAccountIds. The dashboard has one global Use credits switch next to the Codex credits display, and each account's switch sits in its card's ⋯ menu. Your selection, main-account, and full-window logic landed unchanged. Closing this in favor of #6444.

@lidge-jun lidge-jun closed this Oct 2, 2026
@sungyongcho
sungyongcho deleted the feat/codex-credits-after-limit branch October 3, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request intake: hygiene-blocked Deterministic PR hygiene checks failed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants