Skip to content

Commit cabc6eb

Browse files
committed
feat: add /openai-reset command to spend rate-limit reset credits
An account with reset credits had no way to spend them: the quota sat exhausted until its window rolled over, while credits that exist to short-circuit exactly that wait went unused. /openai-reset lists each account with its eligibility, and consumes a credit against the one chosen. Redemption carries a per-attempt UUID as the idempotency key, so a retry after a timeout resolves to the original redemption rather than spending a second credit - verified live against the API, where a replayed request returned already_redeemed with the first attempt's timestamp and an unchanged credit count. Attempt state is claimed under the existing account file lock, so two processes cannot redeem concurrently. Credits are spent oldest-first. They expire, so preferring the credit nearest expiry wastes the least. The RPC server's two timeouts are deliberately different values. requestTimeout and headersTimeout bound how long a client may take to DELIVER a request and never bound handler execution - a 500ms receipt bound still returns 200 for a three-second handler. The socket inactivity timer is the one that can destroy a socket out from under a working handler, so it must outlast the slowest legitimate apply: a redemption is bounded by a 60s consume call, and a 2s timer there would kill the connection 58 seconds in regardless of what deadline the client passed. Collapsing the three into one value reintroduces one of two bugs, so the split carries a comment saying so.
1 parent 8ddb28f commit cabc6eb

26 files changed

Lines changed: 5880 additions & 40 deletions

ARCHITECTURE.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@
111111
**Commands (dialogs):**
112112
- Purpose: Per-slash-command payload builders producing `OpenDialogPayload` (text + knobs) and applying user selections to storage. Copies the command context copy per invocation to prevent concurrent sessions from crossing feedback.
113113
- Location: `packages/opencode/src/commands.ts`
114-
- Contains: Command name constants (`OPENAI_*_COMMAND_NAME`), `MODAL_COMMANDS`, `CommandContext` DI shape, `buildDialogPayload`, `applyCommand`, `executeQuotaCommand`/`executeAccountCommand`/`executeRoutingCommand`/`executeKillswitchCommand`/`executeDumpCommand`/`executeLoggingCommand`/`executeCachekeepCommand`.
115-
- Depends on: `core/accounts.ts`, `core/cachekeep.ts`, `core/oauth.ts`, `core/refresh-all-quota.ts`, `quota-manager.ts`, `rpc/protocol.ts`, `logger.ts`, `config.ts`.
114+
- Contains: Command name constants (`OPENAI_*_COMMAND_NAME`), `MODAL_COMMANDS`, `CommandContext` DI shape, `buildDialogPayload`, `applyCommand`, `executeQuotaCommand`/`executeAccountCommand`/`executeRoutingCommand`/`executeKillswitchCommand`/`executeDumpCommand`/`executeLoggingCommand`/`executeCachekeepCommand`/`executeResetCommand`.
115+
- Depends on: `core/accounts.ts`, `core/cachekeep.ts`, `core/oauth.ts`, `core/refresh-all-quota.ts`, `core/reset-credits.ts`, `quota-manager.ts`, `rpc/protocol.ts`, `logger.ts`, `config.ts`.
116116
- Used by: Plugin loader (`auth.loader`), RPC `apply` dispatch.
117117

118118
**CLI (`openai-auth`):**
@@ -165,6 +165,18 @@
165165
4. TUI's `tui.tsx` polls the loader's loopback RPC (`/rpc/pending-notifications`), receives the dialog, and renders it via `command-dialogs.tsx`.
166166
5. User clicks Apply → TUI POSTs `/rpc/apply` → loader's `apply` calls `buildDialogPayload`, mutates storage via `mutateAccounts`, and returns updated knobs for the TUI to re-render.
167167

168+
**`/openai-reset` credit redemption:**
169+
170+
1. The account list reuses each account's valid L1 access token to fetch `wham/usage` and reset-credit inventory in parallel, producing a per-account preview. Only exhausted accounts with an applicable, eligible credit and a stable ChatGPT account identity can continue.
171+
2. Selecting an account opens an explicit L2 confirmation bound to its stable `chatgptAccountId`; the dialog states that one reset credit will be spent and that the action is irreversible.
172+
3. Confirmation resolves the target again and rejects the redemption if its ChatGPT identity no longer matches the bound identity.
173+
4. A new attempt re-fetches quota and credits and re-checks exhaustion and applicable-credit preconditions immediately before claiming a credit. Under the persisted-pair retry rule (3a), an explicit retry instead requires an active in-flight attempt and reuses its `creditId` and `redeemRequestId` pair.
174+
5. `consumeResetCredit` sends the explicit credit ID and redemption UUID to the consume endpoint in a POST bounded by a 60-second timeout. The read-only credit-list GET is bounded by a 15-second timeout; an abort surfaces as an `http_error` list failure.
175+
6. Terminal server outcomes (`reset`, `already_redeemed`, `nothing_to_reset`, `no_credit`) clear the matching in-flight pair and persist `lastOutcome`; only the credit-spending `reset` and `already_redeemed` outcomes start cooldown. HTTP and ambiguous outcomes preserve the pair so a retry can reuse the same identifiers; an expired unreconciled pair requires an explicit replay, while corrupt local state is recorded as locally ambiguous instead of issuing a consume request.
176+
7. A successful or already-redeemed outcome runs the normal targeted quota refresh for the selected account, pushes the result through `QuotaManager`, refreshes the sidebar snapshot, and fetches the remaining applicable-credit count.
177+
178+
Server-side deduplication of a repeated `redeem_request_id` is verified live (2026-07-23): replaying a consumed `(redeem_request_id, credit_id)` returns `already_redeemed` with `windows_reset: 0`, the account's available-credit count does not decrement a second time, and the response carries the original redemption's `redeemed_at`. Replaying a consumed identifier is therefore safe — the server dedupes on it and never spends another credit — which is the invariant the retry path relies on.
179+
168180
**Cache keep-warm (idle session):**
169181

170182
1. Every main-agent (and optionally subagent) request is captured by `buildKeepwarmCapture` from `sendWithAccessToken`. Outside of the configured clock window, capture is skipped.
@@ -195,6 +207,11 @@
195207
- Location: `packages/opencode/src/core/cachekeep.ts`
196208
- Pattern: Target map keyed by session id; interval timer; bounded (`maxTargets`, `maxBytes`) so a long-lived process cannot leak; model-aware TTL adjustment (30-min TTL for GPT-5.6 models) and gpt-5.6 subagent 2-warm limits.
197209

210+
**Reset credit redemption coordinator:**
211+
- Purpose: Preview reset-credit eligibility and redeem exactly one explicit credit for an exhausted account after identity-bound confirmation.
212+
- Location: `packages/opencode/src/core/reset-credits.ts`; command orchestration in `packages/opencode/src/commands.ts` `executeResetCommand`.
213+
- Pattern: Persisted `(creditId, redeemRequestId)` claim before the consume POST; confirm-time identity and new-attempt precondition checks; terminal-only finalization with bounded, identifier-stable retry for ambiguous outcomes.
214+
198215
**`OpenAIWebSocketPool` / `createWebSocketFetch`:**
199216
- Purpose: Session-keyed WebSocket pool with continuation chaining (`previous_response_id`), per-account discriminator so a switch forces a fresh socket, and stream-failure retries.
200217
- Location: `packages/opencode/src/ws-pool.ts`

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Token values and authorization/cookie headers are redacted from the log. Convers
136136

137137
## Slash commands
138138

139-
All commands open an interactive control surface in the TUI (a selectable dialog), and also accept the explicit argument forms below.
139+
All commands open an interactive control surface in the TUI (a selectable dialog). Commands with listed arguments also accept those explicit argument forms.
140140

141141
| Command | Arguments | Purpose |
142142
| --- | --- | --- |
@@ -145,6 +145,7 @@ All commands open an interactive control surface in the TUI (a selectable dialog
145145
| `/openai-routing` | `main-first` · `fallback-first` · `sticky-balanced` · `reset` | Set account preference order, enable sticky balanced routing, or clear the current session pin. |
146146
| `/openai-killswitch` | `on` · `off` · `set <acct>:<5h>,<1w> ...` | Hard-block accounts below per-window quota thresholds. |
147147
| `/openai-cachekeep` | `on` · `off` · `subagents on` · `subagents off` · `sustain on` · `sustain off` | Idle prompt-cache keep-warm; optional subagent mode and main-only idle-pruning bypass. |
148+
| `/openai-reset` | Modal only | Spend one applicable reset credit for an exhausted account after explicit confirmation. |
148149
| `/openai-logging` | `<level>` | Set log level (`error`/`warn`/`info`/`debug`/`trace`) live. |
149150
| `/openai-dump` | `on` · `off` | Toggle transport request dumps for cache debugging. |
150151

STRUCTURE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
- `packages/opencode/src/core/oauth.ts` — PKCE, OAuth flow, JWT parsing.
139139
- `packages/opencode/src/core/quota-manager.ts` — quota cache, backoff, and mid-stream rate limit marking.
140140
- `packages/opencode/src/core/cachekeep.ts` — prompt-cache warmer with model-aware TTL, clock window, subagent warm caps, and main-only sustain that bypasses idle pruning but not memory/LRU caps.
141+
- `packages/opencode/src/core/reset-credits.ts` — reset-credit listing, eligibility checks, persisted redemption claims, bounded consume requests, and terminal-outcome finalization.
141142
- `packages/opencode/src/prompt-context.ts` — assistant model/variant resolver for synthetic command replies.
142143
- `packages/opencode/src/core/provider.ts` — Codex injection seam (`codexRefreshFn`, `whamUsageFn`).
143144
- `packages/opencode/src/core/backoff.ts` — retry/backoff math.
@@ -155,6 +156,7 @@
155156

156157
**Tests:**
157158
- `packages/opencode/src/tests/` — co-located bun tests (`*.test.ts`).
159+
- `packages/opencode/src/tests/reset-credits.test.ts` — reset-credit listing and consumption, redemption preconditions, and atomic persisted redemption state.
158160
- `packages/opencode/bunfig.toml` — bun test config.
159161
- Run: `bun run test` (root) → `cd packages/opencode && bun run test`.
160162

@@ -172,7 +174,7 @@ Example: `packages/opencode/src/tests/accounts-store.test.ts` tests `packages/op
172174
**Types/classes:** PascalCase (`CodexAuthPlugin`, `FallbackAccountManager`, `QuotaManager`, `CacheKeepManager`, `OpenAIWebSocketPool`, `ResponseStreamError`).
173175
Example: `packages/opencode/src/core/cachekeep.ts` exports `CacheKeepManager`.
174176

175-
**Command name constants:** SCREAMING_SNAKE_CASE prefixed with `OPENAI_` (`OPENAI_QUOTA_COMMAND_NAME`, `OPENAI_ACCOUNT_COMMAND_NAME`, `OPENAI_ROUTING_COMMAND_NAME`, `OPENAI_KILLSWITCH_COMMAND_NAME`, `OPENAI_DUMP_COMMAND_NAME`, `OPENAI_LOGGING_COMMAND_NAME`, `OPENAI_CACHEKEEP_COMMAND_NAME`).
177+
**Command name constants:** SCREAMING_SNAKE_CASE prefixed with `OPENAI_` (`OPENAI_QUOTA_COMMAND_NAME`, `OPENAI_ACCOUNT_COMMAND_NAME`, `OPENAI_ROUTING_COMMAND_NAME`, `OPENAI_KILLSWITCH_COMMAND_NAME`, `OPENAI_DUMP_COMMAND_NAME`, `OPENAI_LOGGING_COMMAND_NAME`, `OPENAI_CACHEKEEP_COMMAND_NAME`, `OPENAI_RESET_COMMAND_NAME`).
176178
Example: `packages/opencode/src/commands.ts`.
177179

178180
**Environment variables:** SCREAMING_SNAKE_CASE with the `CORTEXKIT_OPENAI_AUTH_*` and `OPENCODE_OPENAI_AUTH_*` prefixes (negative-prefixed `CORTEXKIT_OPENAI_AUTH_NO_WEB_SEARCH` for the default-on cache fix).

packages/opencode/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Restart OpenCode after changing plugin config, then authenticate:
3535

3636
## Commands
3737

38-
Each opens an interactive dialog in the TUI and also accepts explicit arguments:
38+
Each opens an interactive dialog in the TUI. Commands with listed arguments also accept those explicit arguments:
3939

4040
| Command | Arguments | Purpose |
4141
| --- | --- | --- |
@@ -44,6 +44,7 @@ Each opens an interactive dialog in the TUI and also accepts explicit arguments:
4444
| `/openai-routing` | `main-first` · `fallback-first` · `sticky-balanced` · `reset` | Routing order, sticky balanced session pins, or clear the current pin. |
4545
| `/openai-killswitch` | `on` · `off` · `set <acct>:<5h>,<1w> ...` | Hard-block accounts below quota thresholds. |
4646
| `/openai-cachekeep` | `on` · `off` · `subagents on` · `subagents off` · `sustain on` · `sustain off` | Idle prompt-cache keep-warm; sustain bypasses only main idle pruning. |
47+
| `/openai-reset` | Modal only | Spend one applicable reset credit for an exhausted account after explicit confirmation. |
4748
| `/openai-logging` | `<level>` | Set log level live. |
4849
| `/openai-dump` | `on` · `off` | Toggle transport request dumps. |
4950

packages/opencode/scripts/build-tui.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const shippedSourceFiles = [
1717
'rpc/rpc-dir.ts',
1818
'rpc/port-file.ts',
1919
'rpc/protocol.ts',
20+
'util/error.ts',
2021
'util/open-url.ts',
2122
] as const
2223
const runtimeSpecifiers = new Set([

0 commit comments

Comments
 (0)