oc-codex-multi-auth 6.10.1 → 6.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +30 -13
  2. package/config/README.md +7 -5
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +22 -220
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/accounts/state.d.ts +1 -1
  7. package/dist/lib/accounts/state.js +1 -1
  8. package/dist/lib/accounts/warm-request.d.ts +14 -1
  9. package/dist/lib/accounts/warm-request.d.ts.map +1 -1
  10. package/dist/lib/accounts/warm-request.js +140 -21
  11. package/dist/lib/accounts/warm-request.js.map +1 -1
  12. package/dist/lib/auth/server.d.ts +1 -1
  13. package/dist/lib/auth/server.js +1 -1
  14. package/dist/lib/auto-update-checker.d.ts.map +1 -1
  15. package/dist/lib/auto-update-checker.js +3 -2
  16. package/dist/lib/auto-update-checker.js.map +1 -1
  17. package/dist/lib/codex-usage.d.ts +1 -0
  18. package/dist/lib/codex-usage.d.ts.map +1 -1
  19. package/dist/lib/codex-usage.js +17 -2
  20. package/dist/lib/codex-usage.js.map +1 -1
  21. package/dist/lib/config.d.ts +7 -0
  22. package/dist/lib/config.d.ts.map +1 -1
  23. package/dist/lib/config.js +7 -7
  24. package/dist/lib/config.js.map +1 -1
  25. package/dist/lib/errors.d.ts +1 -1
  26. package/dist/lib/errors.js +1 -1
  27. package/dist/lib/recovery.d.ts +2 -2
  28. package/dist/lib/recovery.js +2 -2
  29. package/dist/lib/request/fetch-helpers.d.ts +8 -2
  30. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  31. package/dist/lib/request/fetch-helpers.js +8 -2
  32. package/dist/lib/request/fetch-helpers.js.map +1 -1
  33. package/dist/lib/request/request-transformer.d.ts +4 -1
  34. package/dist/lib/request/request-transformer.d.ts.map +1 -1
  35. package/dist/lib/request/request-transformer.js +4 -1
  36. package/dist/lib/request/request-transformer.js.map +1 -1
  37. package/dist/lib/request/response-handler.d.ts +1 -0
  38. package/dist/lib/request/response-handler.d.ts.map +1 -1
  39. package/dist/lib/request/response-handler.js +1 -0
  40. package/dist/lib/request/response-handler.js.map +1 -1
  41. package/dist/lib/runtime.d.ts +1 -1
  42. package/dist/lib/runtime.js +1 -1
  43. package/dist/lib/storage/backup.d.ts +1 -1
  44. package/dist/lib/storage/backup.js +1 -1
  45. package/dist/lib/storage/export-import.d.ts +6 -3
  46. package/dist/lib/storage/export-import.d.ts.map +1 -1
  47. package/dist/lib/storage/export-import.js +5 -2
  48. package/dist/lib/storage/export-import.js.map +1 -1
  49. package/dist/lib/storage/keychain.d.ts +2 -2
  50. package/dist/lib/storage/keychain.js +2 -2
  51. package/dist/lib/storage/load-save.js +1 -1
  52. package/dist/lib/storage/load-save.js.map +1 -1
  53. package/dist/lib/storage/worktree-lock.d.ts +2 -2
  54. package/dist/lib/storage/worktree-lock.js +2 -2
  55. package/dist/lib/storage.d.ts +2 -2
  56. package/dist/lib/storage.js +2 -2
  57. package/dist/lib/tools/codex-diag.d.ts +1 -1
  58. package/dist/lib/tools/codex-diag.js +1 -1
  59. package/dist/lib/tools/codex-diff.d.ts.map +1 -1
  60. package/dist/lib/tools/codex-diff.js +1 -2
  61. package/dist/lib/tools/codex-diff.js.map +1 -1
  62. package/dist/lib/tools/index.d.ts +1 -1
  63. package/dist/lib/tools/index.js +1 -1
  64. package/package.json +7 -5
  65. package/scripts/install-oc-codex-multi-auth-core.js +379 -68
package/README.md CHANGED
@@ -46,7 +46,7 @@ Use it when you want OpenCode to run Codex-style coding workflows from your own
46
46
 
47
47
  | Surface | Purpose |
48
48
  | --- | --- |
49
- | `oc-codex-multi-auth` | npm installer bin; updates `~/.config/opencode/opencode.json`, manages `tui.json`, normalizes stale plugin entries, and clears OpenCode plugin cache. Also runs standalone commands: `doctor`, `status`, `list`, `limits`, `dashboard`, `health`, `diag`, `warm` |
49
+ | `oc-codex-multi-auth` | npm CLI; explicit install modes manage OpenCode provider/TUI config, while `update` only clears the managed package cache. Also runs standalone commands: `doctor`, `status`, `list`, `limits`, `dashboard`, `health`, `diag`, `warm` |
50
50
  | OpenCode plugin entry (`index.ts`) | auth loader, OAuth login modes, provider fetch pipeline, account rotation, retry/failover, and `codex-*` tool registry |
51
51
  | OpenCode TUI plugin (`tui.ts`) | prompt quota status, quota details, shared quota cache, and active-account-aware display |
52
52
  | 24 `codex-*` tools | setup, help, status, list, switch, warm, limits, health, metrics, doctor, dashboard, pool, backup, keychain, diagnostics, and recovery actions |
@@ -76,9 +76,9 @@ The plugin does not replace OpenCode. OpenCode remains the host; this package in
76
76
  <details open>
77
77
  <summary><b>For Humans</b></summary>
78
78
 
79
- ### Option A: Standard install (compact modern)
79
+ ### Option A: Standard install (preserve provider config)
80
80
 
81
- Default mode writes 12 base OAuth model families and leaves reasoning depth to OpenCode's variant picker.
81
+ Default mode registers the OpenCode and TUI plugin entries without changing `provider.openai`.
82
82
 
83
83
  ```bash
84
84
  npx -y oc-codex-multi-auth@latest
@@ -88,13 +88,22 @@ Installer flags:
88
88
 
89
89
  | Flag | Effect |
90
90
  | --- | --- |
91
- | (default) / `--modern` | Compact modern catalog: 12 bases, 53 variants |
91
+ | (default) / `--plugin-only` | Register the plugin and TUI integration without changing `provider.openai` |
92
+ | `--modern` | Install compact modern catalog: 12 bases, 53 variants |
92
93
  | `--full` | Compact bases plus 53 explicit selector IDs |
93
94
  | `--legacy` | Explicit-only catalog for older OpenCode |
94
- | `--dry-run` | Show actions without writing |
95
+ | `--dry-run` | Show changed config paths without values or writes |
95
96
  | `--no-cache-clear` | Skip clearing the OpenCode plugin cache |
96
97
 
97
- ### Option B: Full explicit model catalog
98
+ ### Option B: Compact modern model catalog
99
+
100
+ ```bash
101
+ npx -y oc-codex-multi-auth@latest --modern
102
+ ```
103
+
104
+ Use this when OpenCode does not already provide the OAuth model definitions or you want the shipped variant presets.
105
+
106
+ ### Option C: Full explicit model catalog
98
107
 
99
108
  Use this when you want direct selector IDs such as `openai/gpt-5.5-medium` in addition to OpenCode variants.
100
109
 
@@ -102,7 +111,15 @@ Use this when you want direct selector IDs such as `openai/gpt-5.5-medium` in ad
102
111
  npx -y oc-codex-multi-auth@latest --full
103
112
  ```
104
113
 
105
- ### Option C: Verify wiring
114
+ ### Updating without config changes
115
+
116
+ ```bash
117
+ npx -y oc-codex-multi-auth@latest update
118
+ ```
119
+
120
+ `update` clears only the OpenCode-managed package cache. It does not read or write `opencode.json` or `tui.json`; restart OpenCode afterward to install the current package.
121
+
122
+ ### Option D: Verify wiring
106
123
 
107
124
  ```bash
108
125
  opencode --version
@@ -110,7 +127,7 @@ opencode debug config
110
127
  opencode auth login
111
128
  ```
112
129
 
113
- The installer updates `~/.config/opencode/opencode.json`, backs up the previous config, normalizes the plugin entry to `"oc-codex-multi-auth"`, enables the TUI status plugin in `~/.config/opencode/tui.json`, and clears the OpenCode cached plugin copy so OpenCode reinstalls the latest package.
130
+ The default installer only normalizes the plugin entry in `~/.config/opencode/opencode.json`, enables the TUI status plugin in `~/.config/opencode/tui.json`, and clears the cached plugin copy. Catalog modes additionally merge their selected `provider.openai` definitions. Changed config files are backed up before writing.
114
131
 
115
132
  ### Standalone CLI (no agent / no token cost)
116
133
 
@@ -133,13 +150,14 @@ oc-codex-multi-auth diag
133
150
 
134
151
  ### Step-by-step
135
152
 
136
- 1. Install or refresh config:
153
+ 1. Register the plugin without changing `provider.openai`:
137
154
  - `npx -y oc-codex-multi-auth@latest`
155
+ - Use `--modern` only when the shipped compact model catalog is required.
138
156
  2. Run first login flow:
139
157
  - `opencode auth login`
140
158
  3. Validate config:
141
159
  - `opencode debug config`
142
- 4. Run a smoke request (compact modern selectors):
160
+ 4. Run a smoke request (after OpenCode or `--modern` supplies the selector):
143
161
  - `opencode run "Explain this repository" --model=openai/gpt-5.5 --variant=medium`
144
162
  5. Inspect plugin state with the OpenCode tool surface:
145
163
  - `codex-status`
@@ -400,7 +418,7 @@ If the keychain is unavailable, the plugin logs a warning and falls back to JSON
400
418
  <summary><b>60-second recovery</b></summary>
401
419
 
402
420
  ```text
403
- codex-doctor --fix
421
+ codex-doctor fix=true
404
422
  codex-next
405
423
  codex-status format="json"
406
424
  ```
@@ -459,13 +477,12 @@ codex-doctor deep=true format="json"
459
477
  - Maintainer architecture: [docs/development/ARCHITECTURE.md](docs/development/ARCHITECTURE.md)
460
478
  - Testing: [docs/development/TESTING.md](docs/development/TESTING.md)
461
479
  - Discoverability guide: [docs/development/GITHUB_DISCOVERABILITY.md](docs/development/GITHUB_DISCOVERABILITY.md)
462
- - Audit index: [docs/audits/INDEX.md](docs/audits/INDEX.md)
463
480
 
464
481
  ---
465
482
 
466
483
  ## Release Notes
467
484
 
468
- - Current package version: `6.9.1`
485
+ - Current published version: see the npm badge above, or run `npm view oc-codex-multi-auth version`
469
486
  - Changelog: [CHANGELOG.md](CHANGELOG.md)
470
487
  - Releases are automated with [release-please](https://github.com/googleapis/release-please)
471
488
 
package/config/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Configuration
2
2
 
3
- This directory contains the official OpenCode config templates for `oc-codex-multi-auth` v6.9.1.
3
+ This directory contains the official OpenCode config templates for `oc-codex-multi-auth`.
4
4
 
5
5
  ## Required: choose the right config file
6
6
 
@@ -13,17 +13,19 @@ This directory contains the official OpenCode config templates for `oc-codex-mul
13
13
 
14
14
  | Installer flag | What gets written |
15
15
  |----------------|-------------------|
16
- | default / `--modern` | Compact modern: 12 base OAuth families + variant picker |
16
+ | default / `--plugin-only` | Register plugin entries; preserve `provider.openai` |
17
+ | `--modern` | Compact modern: 12 base OAuth families + variant picker |
17
18
  | `--full` | Modern bases **plus** explicit legacy selector IDs |
18
19
  | `--legacy` | Explicit-only catalog (53 preset model entries) |
19
20
 
20
21
  ```bash
21
- npx -y oc-codex-multi-auth@latest # compact modern (default)
22
+ npx -y oc-codex-multi-auth@latest # plugin entries only
23
+ npx -y oc-codex-multi-auth@latest --modern # compact modern catalog
22
24
  npx -y oc-codex-multi-auth@latest --full # modern + explicit IDs
23
25
  npx -y oc-codex-multi-auth@latest --legacy # explicit only
24
26
  ```
25
27
 
26
- Rerun the default installer to remove explicit preset IDs and stale base models left by earlier plugin catalogs.
28
+ Run the installer with `--modern` to remove explicit preset IDs and stale base models left by earlier plugin catalogs.
27
29
 
28
30
  ## Quick pick
29
31
 
@@ -76,7 +78,7 @@ Shared template requirements:
76
78
  - `gpt-5.4-mini` / `gpt-5.4-nano` / Codex models (`gpt-5-codex`, `gpt-5.1-codex*`, …): context **400,000**, output **128,000**
77
79
  - `gpt-5.1`: context **272,000**, output **128,000**
78
80
 
79
- Use `opencode debug config` to verify that these template entries were merged into your effective config. The default compact install shows base OAuth entries such as `gpt-5.5` / `gpt-5.6-sol`; the separate OpenCode variant picker exposes the reasoning presets.
81
+ Use `opencode debug config` to verify that these template entries were merged into your effective config. A `--modern` install shows base OAuth entries such as `gpt-5.5` / `gpt-5.6-sol`; the separate OpenCode variant picker exposes the reasoning presets. The default install writes no catalog, so these entries appear only if OpenCode supplies them itself.
80
82
 
81
83
  If your OpenCode runtime supports global compaction tuning, you can also set values near the largest context windows (for example ~1M context / slightly lower auto-compact limit). Prefer values that match your selected model family.
82
84
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AA8M/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA8wH/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAqN/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA+hH/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ import { CodexAuthError } from "./lib/errors.js";
43
43
  import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, loadFlaggedAccounts, saveFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, getWorkspaceIdentityKey, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
44
44
  import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, isDeactivatedWorkspaceError, isInvalidatedAuthTokenError, createAbortError, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
45
45
  import { shapeBodyForModel } from "./lib/request/helpers/responses-lite.js";
46
- import { createDeactivatedWorkspaceError, DEACTIVATED_WORKSPACE_ERROR_CODE, isDeactivatedWorkspaceErrorMessage, isInvalidatedAuthTokenMessage, } from "./lib/error-sentinels.js";
46
+ import { DEACTIVATED_WORKSPACE_ERROR_CODE, isDeactivatedWorkspaceErrorMessage, isInvalidatedAuthTokenMessage, } from "./lib/error-sentinels.js";
47
47
  import { applyFastSessionDefaults, clampReasoningForModel, upsertBackendModelIdentityMessage, } from "./lib/request/request-transformer.js";
48
48
  import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
49
49
  import { isEmptyResponse } from "./lib/request/response-handler.js";
@@ -58,7 +58,7 @@ import { prewarmOpenCodeCodexPrompt } from "./lib/prompts/opencode-codex.js";
58
58
  import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getRecoveryToastContent, } from "./lib/recovery.js";
59
59
  import { matchesWorkspaceIdentity, upsertFlaggedAccountRecord, createRetryBudgetUsage, serializeSelectionExplainability, formatRoutingValue, formatExplainabilitySummary, } from "./lib/runtime.js";
60
60
  import { createToolRegistry, } from "./lib/tools/index.js";
61
- import { createUsageAccountFingerprint } from "./lib/codex-usage.js";
61
+ import { createUsageAccountFingerprint, fetchCodexUsage, formatUsageLimitSummary, formatUsageLimitTitle, hasUsageWindow, parseCodexUsagePayload, } from "./lib/codex-usage.js";
62
62
  import { clearTuiQuotaSnapshot, parseTuiQuotaSnapshotFromHeaders, writeTuiQuotaSnapshot, } from "./lib/tui-quota-cache.js";
63
63
  /**
64
64
  * OpenAI Codex OAuth authentication plugin for opencode
@@ -2187,226 +2187,26 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2187
2187
  message.includes("invalid refresh") ||
2188
2188
  message.includes("token has been revoked"));
2189
2189
  };
2190
- const parseFiniteNumberHeader = (headers, name) => {
2191
- const raw = headers.get(name);
2192
- if (!raw)
2193
- return undefined;
2194
- const parsed = Number(raw);
2195
- return Number.isFinite(parsed) ? parsed : undefined;
2196
- };
2197
- const parseFiniteIntHeader = (headers, name) => {
2198
- const raw = headers.get(name);
2199
- if (!raw)
2200
- return undefined;
2201
- const parsed = Number.parseInt(raw, 10);
2202
- return Number.isFinite(parsed) ? parsed : undefined;
2203
- };
2204
- const parseResetAtMs = (headers, prefix) => {
2205
- const resetAfterSeconds = parseFiniteIntHeader(headers, `${prefix}-reset-after-seconds`);
2206
- if (typeof resetAfterSeconds === "number" &&
2207
- Number.isFinite(resetAfterSeconds) &&
2208
- resetAfterSeconds > 0) {
2209
- return Date.now() + resetAfterSeconds * 1000;
2190
+ const formatCodexQuotaLine = (usage) => {
2191
+ const parts = [];
2192
+ for (const window of [usage.primary, usage.secondary]) {
2193
+ if (!hasUsageWindow(window))
2194
+ continue;
2195
+ parts.push(`${formatUsageLimitTitle(window.windowMinutes)} ${formatUsageLimitSummary(window)}`);
2210
2196
  }
2211
- const resetAtRaw = headers.get(`${prefix}-reset-at`);
2212
- if (!resetAtRaw)
2213
- return undefined;
2214
- const trimmed = resetAtRaw.trim();
2215
- if (/^\d+$/.test(trimmed)) {
2216
- const parsedNumber = Number.parseInt(trimmed, 10);
2217
- if (Number.isFinite(parsedNumber) && parsedNumber > 0) {
2218
- // Upstream sometimes returns seconds since epoch.
2219
- return parsedNumber < 10_000_000_000 ? parsedNumber * 1000 : parsedNumber;
2220
- }
2197
+ if (hasUsageWindow(usage.codeReview)) {
2198
+ parts.push(`Code review ${formatUsageLimitSummary(usage.codeReview)}`);
2221
2199
  }
2222
- const parsedDate = Date.parse(trimmed);
2223
- return Number.isFinite(parsedDate) ? parsedDate : undefined;
2224
- };
2225
- const hasCodexQuotaHeaders = (headers) => {
2226
- const keys = [
2227
- "x-codex-primary-used-percent",
2228
- "x-codex-primary-window-minutes",
2229
- "x-codex-primary-reset-at",
2230
- "x-codex-primary-reset-after-seconds",
2231
- "x-codex-secondary-used-percent",
2232
- "x-codex-secondary-window-minutes",
2233
- "x-codex-secondary-reset-at",
2234
- "x-codex-secondary-reset-after-seconds",
2235
- ];
2236
- return keys.some((key) => headers.get(key) !== null);
2237
- };
2238
- const parseCodexQuotaSnapshot = (headers, status) => {
2239
- if (!hasCodexQuotaHeaders(headers))
2240
- return null;
2241
- const primaryPrefix = "x-codex-primary";
2242
- const secondaryPrefix = "x-codex-secondary";
2243
- const primary = {
2244
- usedPercent: parseFiniteNumberHeader(headers, `${primaryPrefix}-used-percent`),
2245
- windowMinutes: parseFiniteIntHeader(headers, `${primaryPrefix}-window-minutes`),
2246
- resetAtMs: parseResetAtMs(headers, primaryPrefix),
2247
- };
2248
- const secondary = {
2249
- usedPercent: parseFiniteNumberHeader(headers, `${secondaryPrefix}-used-percent`),
2250
- windowMinutes: parseFiniteIntHeader(headers, `${secondaryPrefix}-window-minutes`),
2251
- resetAtMs: parseResetAtMs(headers, secondaryPrefix),
2252
- };
2253
- const planTypeRaw = headers.get("x-codex-plan-type");
2254
- const planType = planTypeRaw && planTypeRaw.trim() ? planTypeRaw.trim() : undefined;
2255
- const activeLimit = parseFiniteIntHeader(headers, "x-codex-active-limit");
2256
- return { status, planType, activeLimit, primary, secondary };
2257
- };
2258
- const formatQuotaWindowLabel = (windowMinutes) => {
2259
- if (!windowMinutes || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
2260
- return "quota";
2261
- }
2262
- if (windowMinutes % 1440 === 0)
2263
- return `${windowMinutes / 1440}d`;
2264
- if (windowMinutes % 60 === 0)
2265
- return `${windowMinutes / 60}h`;
2266
- return `${windowMinutes}m`;
2267
- };
2268
- const formatResetAt = (resetAtMs) => {
2269
- if (!resetAtMs || !Number.isFinite(resetAtMs) || resetAtMs <= 0)
2270
- return undefined;
2271
- const date = new Date(resetAtMs);
2272
- if (!Number.isFinite(date.getTime()))
2273
- return undefined;
2274
- const now = new Date();
2275
- const sameDay = now.getFullYear() === date.getFullYear() &&
2276
- now.getMonth() === date.getMonth() &&
2277
- now.getDate() === date.getDate();
2278
- const time = date.toLocaleTimeString(undefined, {
2279
- hour: "2-digit",
2280
- minute: "2-digit",
2281
- hour12: false,
2282
- });
2283
- if (sameDay)
2284
- return time;
2285
- const day = date.toLocaleDateString(undefined, { month: "short", day: "2-digit" });
2286
- return `${time} on ${day}`;
2287
- };
2288
- const formatCodexQuotaLine = (snapshot) => {
2289
- const summarizeWindow = (label, window) => {
2290
- const used = window.usedPercent;
2291
- const left = typeof used === "number" && Number.isFinite(used)
2292
- ? Math.max(0, Math.min(100, Math.round(100 - used)))
2293
- : undefined;
2294
- const reset = formatResetAt(window.resetAtMs);
2295
- let summary = label;
2296
- if (left !== undefined)
2297
- summary = `${summary} ${left}% left`;
2298
- if (reset)
2299
- summary = `${summary} (resets ${reset})`;
2300
- return summary;
2301
- };
2302
- const primaryLabel = formatQuotaWindowLabel(snapshot.primary.windowMinutes);
2303
- const secondaryLabel = formatQuotaWindowLabel(snapshot.secondary.windowMinutes);
2304
- const parts = [
2305
- summarizeWindow(primaryLabel, snapshot.primary),
2306
- summarizeWindow(secondaryLabel, snapshot.secondary),
2307
- ];
2308
- if (snapshot.planType)
2309
- parts.push(`plan:${snapshot.planType}`);
2310
- if (typeof snapshot.activeLimit === "number" && Number.isFinite(snapshot.activeLimit)) {
2311
- parts.push(`active:${snapshot.activeLimit}`);
2312
- }
2313
- if (snapshot.status === 429)
2314
- parts.push("rate-limited");
2315
- return parts.join(", ");
2316
- };
2317
- const fetchCodexQuotaSnapshot = async (params) => {
2318
- const QUOTA_PROBE_MODELS = ["gpt-5.4", "gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"];
2319
- let lastError = null;
2320
- for (const model of QUOTA_PROBE_MODELS) {
2321
- try {
2322
- const instructions = await getCodexInstructions(model);
2323
- const probeBody = {
2324
- model,
2325
- stream: true,
2326
- store: false,
2327
- include: ["reasoning.encrypted_content"],
2328
- instructions,
2329
- input: [
2330
- {
2331
- type: "message",
2332
- role: "user",
2333
- content: [{ type: "input_text", text: "quota ping" }],
2334
- },
2335
- ],
2336
- reasoning: { effort: "none", summary: "auto" },
2337
- text: { verbosity: "low" },
2338
- };
2339
- const headers = createCodexHeaders(undefined, params.accountId, params.accessToken, {
2340
- model,
2341
- organizationId: params.organizationId,
2342
- });
2343
- headers.set("content-type", "application/json");
2344
- const controller = new AbortController();
2345
- const timeout = setTimeout(() => controller.abort(), 15_000);
2346
- let response;
2347
- try {
2348
- response = await fetch(`${CODEX_BASE_URL}/codex/responses`, {
2349
- method: "POST",
2350
- headers,
2351
- body: JSON.stringify(probeBody),
2352
- signal: controller.signal,
2353
- });
2354
- }
2355
- finally {
2356
- clearTimeout(timeout);
2357
- }
2358
- const snapshot = parseCodexQuotaSnapshot(response.headers, response.status);
2359
- if (snapshot) {
2360
- // We only need headers; cancel the SSE stream immediately.
2361
- try {
2362
- await response.body?.cancel();
2363
- }
2364
- catch {
2365
- // Ignore cancellation failures.
2366
- }
2367
- return snapshot;
2368
- }
2369
- if (!response.ok) {
2370
- const bodyText = await response.text().catch(() => "");
2371
- let errorBody = undefined;
2372
- try {
2373
- errorBody = bodyText ? JSON.parse(bodyText) : undefined;
2374
- }
2375
- catch {
2376
- errorBody = { error: { message: bodyText } };
2377
- }
2378
- const unsupportedInfo = getUnsupportedCodexModelInfo(errorBody);
2379
- if (unsupportedInfo.isUnsupported) {
2380
- lastError = new Error(unsupportedInfo.message ?? `Model '${model}' unsupported for this account`);
2381
- continue;
2382
- }
2383
- const message = (typeof errorBody?.error?.message === "string"
2384
- ? errorBody.error?.message
2385
- : bodyText) || `HTTP ${response.status}`;
2386
- if (isDeactivatedWorkspaceError(errorBody, response.status)) {
2387
- throw createDeactivatedWorkspaceError();
2388
- }
2389
- // A 401 here proves the token is invalid even when the body
2390
- // carries only a generic "Unauthorized" string. The status is
2391
- // dropped once we throw a plain Error, so normalize to the
2392
- // canonical message the catch below matches — otherwise a
2393
- // non-specific 401 would leave a dead routing slot unflagged
2394
- // for codex-doctor --fix (issue #171).
2395
- if (isInvalidatedAuthTokenError(errorBody, response.status)) {
2396
- throw new Error("Your authentication token has been invalidated. Please try signing in again.");
2397
- }
2398
- throw new Error(message);
2399
- }
2400
- lastError = new Error("Codex response did not include quota headers");
2401
- }
2402
- catch (error) {
2403
- lastError = error instanceof Error ? error : new Error(String(error));
2404
- if (isDeactivatedWorkspaceErrorMessage(lastError.message)) {
2405
- throw lastError;
2406
- }
2200
+ for (const limit of usage.additionalLimits) {
2201
+ if (hasUsageWindow(limit.window)) {
2202
+ parts.push(`${limit.name} ${formatUsageLimitSummary(limit.window)}`);
2407
2203
  }
2408
2204
  }
2409
- throw lastError ?? new Error("Failed to fetch quotas");
2205
+ if (usage.planType)
2206
+ parts.push(`plan:${usage.planType}`);
2207
+ if (usage.credits)
2208
+ parts.push(`credits:${usage.credits}`);
2209
+ return parts.length > 0 ? parts.join(", ") : "quota unavailable";
2410
2210
  };
2411
2211
  const runAccountCheck = async (deepProbe) => {
2412
2212
  const loadedStorage = await hydrateEmails(await loadAccounts());
@@ -2575,13 +2375,15 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2575
2375
  if (!requestAccountId) {
2576
2376
  throw new Error("Missing accountId for quota probe");
2577
2377
  }
2578
- const snapshot = await fetchCodexQuotaSnapshot({
2378
+ const payload = await fetchCodexUsage({
2579
2379
  accountId: requestAccountId,
2580
2380
  accessToken,
2581
2381
  organizationId: account.organizationId,
2382
+ normalizeAccountErrors: true,
2582
2383
  });
2384
+ const usage = parseCodexUsagePayload(payload);
2583
2385
  ok += 1;
2584
- console.log(`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(snapshot)}`);
2386
+ console.log(`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(usage)}`);
2585
2387
  }
2586
2388
  catch (error) {
2587
2389
  errors += 1;