Repository navigation
feat(codex): spend free resets when the Codex pool runs dry - #436
Merged
MagicalTux merged 3 commits intoSep 25, 2026
Merged
Conversation
Builds on the reading of `quota.resetCredits` (KarpelesLab#430): this is the one action that count makes possible, and it is off unless an operator arms it. A Codex account whose weekly window has run out is often holding the credit that would clear it. Codex's own client spends one by hand, from its usage screen, so a pooled account stayed walled for the rest of its week while the credit sat unspent and expiring. The trigger is the refusal itself. Once every Codex account reads over threshold, selection turns the request away BEFORE choosing one: the proxy answers "(none available)" and no upstream request is made — so there is no 429 to hang this off, and on a spent pool that is what happens to every request. The hook therefore sits in forwardRequest's `!account` branch, ahead of the retry-after measurement and the hold/retry paths, and is offered only the accounts whose `unavailableReason` a reset would actually clear: `quota` and `throttled`, never `disabled`, `capped`, `entitlement`, `error` or `route`, all of which survive a cleared window and would take a credit for nothing. A decline or a throw leaves the refusal byte-for-byte what it was; a success re-selects, costing no retry from the budget because nothing was ever sent. The two routes are not part of the public API and are documented nowhere; these were recovered from the Codex binary and then verified live: GET /wham/rate-limit-reset-credits detail rows: id, status, expires_at, is_supported_by_plan. ISO-8601 STRINGS here, while the Rust app-server layer states the same fields as epoch seconds — the two must not be parsed the same way POST /wham/rate-limit-reset-credits/consume {redeem_request_id, credit_id?} Two traps in that last one, both covered by tests: the verdict field is `code`, not the `outcome` the app-server layer uses, and it answers 200 when it refuses as readily as when it works. Classify on the body, never the status — reading "you hold no credits" as a redemption that worked is not recoverable. Spending a credit is irreversible and they are scarce, so the policy is deliberately mean. A redemption needs the WEEKLY window spent — never the 5-hour one, which comes back on its own within hours while the weekly one walls an account off for days — and then either the whole Codex pool is dry, so the credit actually unblocks work rather than topping up an account rotation would have stepped past, or the credit expires within three days and holding it costs more than spending it. A credit the plan cannot spend is filtered before any expiry reasoning, so it can never be what makes an expiring-credit decision look justified. At most one credit per dry pool, which takes three guards rather than one. Refusals arriving together join a single attempt. An attempt that spent a credit — or that failed in a way that cannot rule out having spent one — holds the WHOLE fleet off, not only the account it touched: an attempt walks the pool, so a sibling that never touched the endpoint is just as able to spend the second credit, and the alternative (the redeemed account reading available again) leans on a quota re-read that is allowed to fail. And the pool is re-resolved per decision, so once that re-read does land, the account it returned to service is what makes every sibling answer "another Codex account can still serve". `ctx.resetRedeemTried` bounds it to one attempt per request, so a redeem that reports success but leaves the account unselectable — upstream not yet caught up with its own reset — costs that request one re-selection rather than a loop. `redeem_request_id` is an idempotency key. A consume whose verdict never arrived may still have been acted on, so it replays its key instead of minting a fresh one, and `already_redeemed` is upstream answering for the POST that did land — which is why it counts as a success. Off unless armed: `autoRedeemResets` is a fleet switch, default false, in `createDefaultConfig` and `config.example.json`, toggleable from the TUI settings screen and applied live on reload so it can be killed without a restart. The per-account `accounts[].autoRedeemReset` is a veto only — it can say never for this account, never yes. The attempt is awaited inline, before the re-selection, because a dry pool has nowhere to rotate to: the redemption IS the recovery for that request. It is bounded by a single 10s deadline across token refresh, detail fetch, consume and the re-read together, leaving the rest of the client's response-head budget for the retry it exists to enable. The refresh is raced against that deadline rather than measured after it returns: `ensureTokenFresh` takes no timeout of its own and every concurrent refusal joins the same one, so a check that runs afterwards can only report a promise already broken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rikbrown
force-pushed
the
feat/codex-reset-credits
branch
from
September 20, 2026 09:33
cd0c89d to
953b5f9
Compare
rikbrown
marked this pull request as ready for review
September 20, 2026 09:33
# Conflicts: # src/tui.js
This was referenced Sep 25, 2026
Merged
MagicalTux
added a commit
that referenced
this pull request
Sep 29, 2026
Thirty-two commits since 1.1.21. Two change routing on an existing config without an opt-in (#480, #481); the rest is opt-in, additive, or display. Behaviour changes #481 an API-key account's 401 is a cooldown, not a permanent `error`: 1 min, then 5, 15 and 60 for every further rejection with no success in between; any 2xx/3xx resets it. The request still fails over and the client never sees the 401. OAuth accounts are unchanged #480 with session distribution on, requests carrying no session id stay within the top priority tier, so a fallback gateway no longer answers Claude Code's bootstrap and connector calls #470 a 200 whose SSE stream reports a provider failure before any output (`server_is_overloaded`, `response.failed`) fails over once, like a status-shaped failure would #465 a reload removes running accounts whose config entry is gone from disk, so `teamclaude remove` from another shell takes effect at once #460 `import` refuses an account whose token upstream has definitively rejected (401/403), even with `--name`; a 5xx or timeout still imports Rename #483 the project is being renamed to TeamRouter (#72). This release accepts the new name everywhere the old one is read and changes nothing an install has on disk: `teamrouter` runs the same CLI, every `TEAMCLAUDE_*` variable is also read as `TEAMROUTER_*` (which wins when both are set), every `/teamclaude/…` control route also answers at `/teamrouter/…`, and `~/.config/teamrouter.json` is used when it exists Features #441 per-account egress proxy (`accounts[].routing`: http, socks4/4a, socks5/5h) for refresh, probes and requests; `login --routing`, `teamclaude routing set/show/clear`, a connection check before it is relied on, and a short hold when the proxy is unreachable #427 `accounts[].allowExtraUsage: true` lets a paid extra-usage account serve once every account is past its threshold, instead of a 429 #466 `accounts[].maxSpend`, a money cap judged against the month-to-date extra-usage spend upstream reports; the TUI shows what an account has billed #436 `autoRedeemResets` spends a free Codex rate-limit reset credit when the Codex pool runs dry (off by default) #482 `advisorEligibility: "strict" | "prefer"`; when the advisor model narrows selection to a subset of the fleet the log says so, and status carries the reading (`advisorNarrowing`) #478 `stripOverageHeaders` drops another org's per-organization billing headers from responses, for a pool spanning several orgs (#476) #471 `quota.unified5hSeenAt` / `unified7dSeenAt` in status: when upstream last stated each shared window #446 client and dimension usage for the last 5h and 24h in status and the dashboard, resumed across restarts #458 #459 #461 the dashboard sets the switch threshold, enables/disables and reprioritizes an account, and has a light theme remembered per browser #464 `l` in the TUI signs an account in `error` in again from the dashboard #457 status records which Codex limit meters each model (`quota.codexModelLimits`) #442 `quotaBarPercent` drops the percentage beside a TUI bar's countdown #451 `stripRequestFields` takes `content.<block type>` to drop content blocks a strict Anthropic-compatible upstream rejects #469 `proxy.mcp` schemas declare their item types, the write audit line records what happened, and the write queue has a depth (#447–#450) Fixes #477 a refused WebSocket handshake whose headers all drop is relayed as a well-formed head instead of a blank line and body bytes #474 two members of one ChatGPT workspace are told apart by user id, so a second `login --codex` no longer replaces the first #469 a Codex Responses stream with no Content-Type is relayed as a stream and booked; thread repair on the global upstream; TUI settings and status gaps; a hint when a local login would have served #463 a Codex row with no session window draws one wide weekly bar Tests #484 #485 #486 the suite asserts behaviour, not the scheduler: wall-clock upper bounds are gone, and subprocess tests spawn the server through `test-helpers/spawn-server.js`, which verifies the server it reached by `server.pid` (new in status) instead of trusting a port Tooling #452 #453 #454 #455 docker workflow actions bumped
rikbrown
added a commit
to rikbrown/teamclaude
that referenced
this pull request
Oct 7, 2026
…redits Upstream KarpelesLab#430 and KarpelesLab#436 now report and redeem free rate-limit reset credits, so what is left of this change is the fork's side of it. Persists quota.planType and quota.codexModelBuckets, which were learned and then dropped on every restart, and clamps a restored bucket table to the 32 newest — the writers cap, but the eviction only runs when a new slug arrives, so an over-long table would otherwise stand. docs/openai.md gains the reset-credit section, for a pool reached through the Codex sidecar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rikbrown
added a commit
to rikbrown/teamclaude
that referenced
this pull request
Oct 7, 2026
Upstream KarpelesLab#436 redeems a credit on the pool-dry refusal alone, and docs/accounts.md documents the count, the switch and the policy. The copy here had drifted from it (it still described a second trigger, on an upstream 429), so it now says only what the sidecar changes: nothing, since the credits belong to the pooled accounts and never to the conduit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3 of 4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
OpenAI grants ChatGPT/Codex subscriptions occasional free rate-limit reset credits. Redeeming one clears that account's spent rate-limit windows before their reset. The Codex client exposes it as a manual action only.
A pooled Codex account can therefore sit out a weekly window while an expiring credit remains unused. With two spent accounts, every request gets
429 (none available)while the reset sits in the account's billing state.What changed
New
src/codex-reset-credits.jsredeems credits.forwardRequestinvokes it when selection cannot choose a Codex account.Trigger. When every Codex account is over threshold, selection returns
(none available)before an upstream request. The hook is inforwardRequest's!accountbranch, after candidate discovery and before hold/retry-after paths. It receives only accounts whoseunavailableReasona reset can clear:quotaandthrottled, notdisabled,capped,entitlement,error, orroute. It runs only for Codex requests. A decline or throw preserves the refusal byte-for-byte; success reselects without consuming a retry budget because there was no upstream attempt.Policy.
shouldRedeemResetis pure and fully table-tested. A redemption requires a spent weekly window, never only a 5-hour window. It also requires either a dry Codex pool or a credit expiring within three days.is_supported_by_plan: falsecredits are removed before expiry reasoning.One credit per dry pool. Three guards enforce it:
ctx.resetRedeemTriedlimits each request to one attempt when upstream reports success but the account remains unselectable.Off unless armed.
autoRedeemResetsis a fleet switch, false by default, increateDefaultConfigandconfig.example.json. It is available from the TUI settings screen and applies on reload.accounts[].autoRedeemResetis veto-only: it can say never, not yes.Time budget. The attempt waits inline because a dry pool cannot rotate. One 10s deadline covers token refresh, detail fetch, consume, and re-read, leaving the rest of the Codex client's 60s response-head budget.
ensureTokenFreshraces the deadline because it has no timeout and concurrent refusals join it. A late refresh continues in the background; the attempt declines. Steps with nothing left are skipped and decline with cooldown.Wire format
The endpoints are not in the public OpenAI API and are documented nowhere. They were recovered from the Codex binary and verified against a live account:
GET /wham/rate-limit-reset-creditsid,status,expires_at,is_supported_by_planPOST /wham/rate-limit-reset-credits/consume{redeem_request_id, credit_id?}Two details are covered by tests:
code, notoutcome; values arereset,nothing_to_reset,no_credit, andalready_redeemed.A transport error, timeout, or unreadable body does not mean upstream declined: it may have acted on the POST. The fleet-wide hold follows, and
redeem_request_idis an idempotency key reused by a retry.already_redeemedis treated as success.Detail-row
granted_at/expires_atare ISO-8601 strings; the app-server layer uses epoch seconds for the same fields.Verification
npm test— 1936 tests pass, including 62 new tests intest/codex-reset-credits.test.js; the base PR provides the file's other 6.npm run lint,npm run typecheck— clean.node scripts/typecheck-strict.mjs— the new file has 0 strict diagnostics and the tree is 8 below this PR's base. A requiredRecord<string, any>annotation onhooksinindex.jsalso fixes pre-existing diagnostics.No test calls the network. Tests inject
fetch; two proxy-level tests use a localhttpserver. Tests specifically verify that a failed quota re-read holds all other accounts, a consume without a received verdict stops the walk, and a token refresh exceeding the budget is left running.The behaviour was observed with two accounts at 100% weekly with a credit: requests were refused before sending, the credit was redeemed on refusal, and the request was served.
Deliberate limitations
x-codex-*window as a durable quota rejection is separate work. Once the 429 path identifies it, the second trigger is a few lines using the sameResetCreditRedeemer.🤖 Generated with Claude Code