oc-codex-multi-auth 6.10.0 → 6.11.0

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 (42) hide show
  1. package/README.md +28 -10
  2. package/config/README.md +6 -4
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +53 -225
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/auth/server.d.ts.map +1 -1
  7. package/dist/lib/auth/server.js +7 -3
  8. package/dist/lib/auth/server.js.map +1 -1
  9. package/dist/lib/auto-update-checker.d.ts.map +1 -1
  10. package/dist/lib/auto-update-checker.js +3 -2
  11. package/dist/lib/auto-update-checker.js.map +1 -1
  12. package/dist/lib/codex-usage.d.ts +1 -0
  13. package/dist/lib/codex-usage.d.ts.map +1 -1
  14. package/dist/lib/codex-usage.js +17 -2
  15. package/dist/lib/codex-usage.js.map +1 -1
  16. package/dist/lib/oauth-success.d.ts +1 -0
  17. package/dist/lib/oauth-success.d.ts.map +1 -1
  18. package/dist/lib/oauth-success.html +198 -707
  19. package/dist/lib/oauth-success.js +202 -708
  20. package/dist/lib/oauth-success.js.map +1 -1
  21. package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
  22. package/dist/lib/tools/codex-doctor.js +168 -142
  23. package/dist/lib/tools/codex-doctor.js.map +1 -1
  24. package/dist/lib/tools/codex-health.d.ts.map +1 -1
  25. package/dist/lib/tools/codex-health.js +41 -38
  26. package/dist/lib/tools/codex-health.js.map +1 -1
  27. package/dist/lib/tools/codex-refresh.d.ts.map +1 -1
  28. package/dist/lib/tools/codex-refresh.js +24 -103
  29. package/dist/lib/tools/codex-refresh.js.map +1 -1
  30. package/dist/lib/tools/index.d.ts +1 -0
  31. package/dist/lib/tools/index.d.ts.map +1 -1
  32. package/dist/lib/tools/index.js.map +1 -1
  33. package/dist/lib/tools/refresh-account.d.ts +63 -0
  34. package/dist/lib/tools/refresh-account.d.ts.map +1 -0
  35. package/dist/lib/tools/refresh-account.js +159 -0
  36. package/dist/lib/tools/refresh-account.js.map +1 -0
  37. package/dist/lib/ui/beginner.d.ts +1 -0
  38. package/dist/lib/ui/beginner.d.ts.map +1 -1
  39. package/dist/lib/ui/beginner.js +12 -1
  40. package/dist/lib/ui/beginner.js.map +1 -1
  41. package/package.json +157 -155
  42. package/scripts/install-oc-codex-multi-auth-core.js +195 -46
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`
package/config/README.md CHANGED
@@ -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,MAkvH/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
@@ -896,6 +896,35 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
896
896
  });
897
897
  }
898
898
  };
899
+ const reloadCachedAccountManager = async () => {
900
+ if (!cachedAccountManager)
901
+ return;
902
+ const previous = cachedAccountManager;
903
+ // Flush the outgoing manager's pending debounced save BEFORE reading
904
+ // fresh disk state. Otherwise a queued save from `previous` can fire
905
+ // after this reload and silently overwrite the single-use refresh
906
+ // tokens codex-health/codex-refresh just persisted via
907
+ // withAccountStorageTransaction — a lost-update on rotated credentials.
908
+ // Mirrors invalidateAccountManagerCache above.
909
+ try {
910
+ await previous.flushPendingSave();
911
+ }
912
+ catch (error) {
913
+ logWarn(`Failed to flush pending save while reloading account manager: ${error instanceof Error ? error.message : String(error)}`);
914
+ }
915
+ try {
916
+ const reloadedManager = await AccountManager.loadFromDisk();
917
+ cachedAccountManager = reloadedManager;
918
+ accountManagerPromise = Promise.resolve(reloadedManager);
919
+ // Dispose only after the replacement is installed so we never leak
920
+ // the outgoing manager's shutdown handler on every reload, and so a
921
+ // load failure leaves the working manager intact.
922
+ previous.disposeShutdownHandler();
923
+ }
924
+ catch (error) {
925
+ logWarn(`Failed to reload account manager: ${error instanceof Error ? error.message : String(error)}`);
926
+ }
927
+ };
899
928
  const persistAuthenticatedSelections = async (results, replaceAll) => {
900
929
  try {
901
930
  await persistAccountPool(results, replaceAll);
@@ -948,11 +977,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
948
977
  await clearPromptQuotaCache();
949
978
  // Reload manager from disk so we don't overwrite newer rotated
950
979
  // refresh tokens with stale in-memory state.
951
- if (cachedAccountManager) {
952
- const reloadedManager = await AccountManager.loadFromDisk();
953
- cachedAccountManager = reloadedManager;
954
- accountManagerPromise = Promise.resolve(reloadedManager);
955
- }
980
+ await reloadCachedAccountManager();
956
981
  await showToast(`Switched to account ${index + 1}`, "info");
957
982
  }
958
983
  }
@@ -984,6 +1009,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
984
1009
  accountManagerPromise = value;
985
1010
  },
986
1011
  },
1012
+ reloadCachedAccountManager,
987
1013
  runtimeMetrics,
988
1014
  beginnerSafeModeRef: {
989
1015
  get current() {
@@ -2161,226 +2187,26 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2161
2187
  message.includes("invalid refresh") ||
2162
2188
  message.includes("token has been revoked"));
2163
2189
  };
2164
- const parseFiniteNumberHeader = (headers, name) => {
2165
- const raw = headers.get(name);
2166
- if (!raw)
2167
- return undefined;
2168
- const parsed = Number(raw);
2169
- return Number.isFinite(parsed) ? parsed : undefined;
2170
- };
2171
- const parseFiniteIntHeader = (headers, name) => {
2172
- const raw = headers.get(name);
2173
- if (!raw)
2174
- return undefined;
2175
- const parsed = Number.parseInt(raw, 10);
2176
- return Number.isFinite(parsed) ? parsed : undefined;
2177
- };
2178
- const parseResetAtMs = (headers, prefix) => {
2179
- const resetAfterSeconds = parseFiniteIntHeader(headers, `${prefix}-reset-after-seconds`);
2180
- if (typeof resetAfterSeconds === "number" &&
2181
- Number.isFinite(resetAfterSeconds) &&
2182
- resetAfterSeconds > 0) {
2183
- return Date.now() + resetAfterSeconds * 1000;
2184
- }
2185
- const resetAtRaw = headers.get(`${prefix}-reset-at`);
2186
- if (!resetAtRaw)
2187
- return undefined;
2188
- const trimmed = resetAtRaw.trim();
2189
- if (/^\d+$/.test(trimmed)) {
2190
- const parsedNumber = Number.parseInt(trimmed, 10);
2191
- if (Number.isFinite(parsedNumber) && parsedNumber > 0) {
2192
- // Upstream sometimes returns seconds since epoch.
2193
- return parsedNumber < 10_000_000_000 ? parsedNumber * 1000 : parsedNumber;
2194
- }
2195
- }
2196
- const parsedDate = Date.parse(trimmed);
2197
- return Number.isFinite(parsedDate) ? parsedDate : undefined;
2198
- };
2199
- const hasCodexQuotaHeaders = (headers) => {
2200
- const keys = [
2201
- "x-codex-primary-used-percent",
2202
- "x-codex-primary-window-minutes",
2203
- "x-codex-primary-reset-at",
2204
- "x-codex-primary-reset-after-seconds",
2205
- "x-codex-secondary-used-percent",
2206
- "x-codex-secondary-window-minutes",
2207
- "x-codex-secondary-reset-at",
2208
- "x-codex-secondary-reset-after-seconds",
2209
- ];
2210
- return keys.some((key) => headers.get(key) !== null);
2211
- };
2212
- const parseCodexQuotaSnapshot = (headers, status) => {
2213
- if (!hasCodexQuotaHeaders(headers))
2214
- return null;
2215
- const primaryPrefix = "x-codex-primary";
2216
- const secondaryPrefix = "x-codex-secondary";
2217
- const primary = {
2218
- usedPercent: parseFiniteNumberHeader(headers, `${primaryPrefix}-used-percent`),
2219
- windowMinutes: parseFiniteIntHeader(headers, `${primaryPrefix}-window-minutes`),
2220
- resetAtMs: parseResetAtMs(headers, primaryPrefix),
2221
- };
2222
- const secondary = {
2223
- usedPercent: parseFiniteNumberHeader(headers, `${secondaryPrefix}-used-percent`),
2224
- windowMinutes: parseFiniteIntHeader(headers, `${secondaryPrefix}-window-minutes`),
2225
- resetAtMs: parseResetAtMs(headers, secondaryPrefix),
2226
- };
2227
- const planTypeRaw = headers.get("x-codex-plan-type");
2228
- const planType = planTypeRaw && planTypeRaw.trim() ? planTypeRaw.trim() : undefined;
2229
- const activeLimit = parseFiniteIntHeader(headers, "x-codex-active-limit");
2230
- return { status, planType, activeLimit, primary, secondary };
2231
- };
2232
- const formatQuotaWindowLabel = (windowMinutes) => {
2233
- if (!windowMinutes || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
2234
- return "quota";
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)}`);
2235
2196
  }
2236
- if (windowMinutes % 1440 === 0)
2237
- return `${windowMinutes / 1440}d`;
2238
- if (windowMinutes % 60 === 0)
2239
- return `${windowMinutes / 60}h`;
2240
- return `${windowMinutes}m`;
2241
- };
2242
- const formatResetAt = (resetAtMs) => {
2243
- if (!resetAtMs || !Number.isFinite(resetAtMs) || resetAtMs <= 0)
2244
- return undefined;
2245
- const date = new Date(resetAtMs);
2246
- if (!Number.isFinite(date.getTime()))
2247
- return undefined;
2248
- const now = new Date();
2249
- const sameDay = now.getFullYear() === date.getFullYear() &&
2250
- now.getMonth() === date.getMonth() &&
2251
- now.getDate() === date.getDate();
2252
- const time = date.toLocaleTimeString(undefined, {
2253
- hour: "2-digit",
2254
- minute: "2-digit",
2255
- hour12: false,
2256
- });
2257
- if (sameDay)
2258
- return time;
2259
- const day = date.toLocaleDateString(undefined, { month: "short", day: "2-digit" });
2260
- return `${time} on ${day}`;
2261
- };
2262
- const formatCodexQuotaLine = (snapshot) => {
2263
- const summarizeWindow = (label, window) => {
2264
- const used = window.usedPercent;
2265
- const left = typeof used === "number" && Number.isFinite(used)
2266
- ? Math.max(0, Math.min(100, Math.round(100 - used)))
2267
- : undefined;
2268
- const reset = formatResetAt(window.resetAtMs);
2269
- let summary = label;
2270
- if (left !== undefined)
2271
- summary = `${summary} ${left}% left`;
2272
- if (reset)
2273
- summary = `${summary} (resets ${reset})`;
2274
- return summary;
2275
- };
2276
- const primaryLabel = formatQuotaWindowLabel(snapshot.primary.windowMinutes);
2277
- const secondaryLabel = formatQuotaWindowLabel(snapshot.secondary.windowMinutes);
2278
- const parts = [
2279
- summarizeWindow(primaryLabel, snapshot.primary),
2280
- summarizeWindow(secondaryLabel, snapshot.secondary),
2281
- ];
2282
- if (snapshot.planType)
2283
- parts.push(`plan:${snapshot.planType}`);
2284
- if (typeof snapshot.activeLimit === "number" && Number.isFinite(snapshot.activeLimit)) {
2285
- parts.push(`active:${snapshot.activeLimit}`);
2197
+ if (hasUsageWindow(usage.codeReview)) {
2198
+ parts.push(`Code review ${formatUsageLimitSummary(usage.codeReview)}`);
2286
2199
  }
2287
- if (snapshot.status === 429)
2288
- parts.push("rate-limited");
2289
- return parts.join(", ");
2290
- };
2291
- const fetchCodexQuotaSnapshot = async (params) => {
2292
- const QUOTA_PROBE_MODELS = ["gpt-5.4", "gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"];
2293
- let lastError = null;
2294
- for (const model of QUOTA_PROBE_MODELS) {
2295
- try {
2296
- const instructions = await getCodexInstructions(model);
2297
- const probeBody = {
2298
- model,
2299
- stream: true,
2300
- store: false,
2301
- include: ["reasoning.encrypted_content"],
2302
- instructions,
2303
- input: [
2304
- {
2305
- type: "message",
2306
- role: "user",
2307
- content: [{ type: "input_text", text: "quota ping" }],
2308
- },
2309
- ],
2310
- reasoning: { effort: "none", summary: "auto" },
2311
- text: { verbosity: "low" },
2312
- };
2313
- const headers = createCodexHeaders(undefined, params.accountId, params.accessToken, {
2314
- model,
2315
- organizationId: params.organizationId,
2316
- });
2317
- headers.set("content-type", "application/json");
2318
- const controller = new AbortController();
2319
- const timeout = setTimeout(() => controller.abort(), 15_000);
2320
- let response;
2321
- try {
2322
- response = await fetch(`${CODEX_BASE_URL}/codex/responses`, {
2323
- method: "POST",
2324
- headers,
2325
- body: JSON.stringify(probeBody),
2326
- signal: controller.signal,
2327
- });
2328
- }
2329
- finally {
2330
- clearTimeout(timeout);
2331
- }
2332
- const snapshot = parseCodexQuotaSnapshot(response.headers, response.status);
2333
- if (snapshot) {
2334
- // We only need headers; cancel the SSE stream immediately.
2335
- try {
2336
- await response.body?.cancel();
2337
- }
2338
- catch {
2339
- // Ignore cancellation failures.
2340
- }
2341
- return snapshot;
2342
- }
2343
- if (!response.ok) {
2344
- const bodyText = await response.text().catch(() => "");
2345
- let errorBody = undefined;
2346
- try {
2347
- errorBody = bodyText ? JSON.parse(bodyText) : undefined;
2348
- }
2349
- catch {
2350
- errorBody = { error: { message: bodyText } };
2351
- }
2352
- const unsupportedInfo = getUnsupportedCodexModelInfo(errorBody);
2353
- if (unsupportedInfo.isUnsupported) {
2354
- lastError = new Error(unsupportedInfo.message ?? `Model '${model}' unsupported for this account`);
2355
- continue;
2356
- }
2357
- const message = (typeof errorBody?.error?.message === "string"
2358
- ? errorBody.error?.message
2359
- : bodyText) || `HTTP ${response.status}`;
2360
- if (isDeactivatedWorkspaceError(errorBody, response.status)) {
2361
- throw createDeactivatedWorkspaceError();
2362
- }
2363
- // A 401 here proves the token is invalid even when the body
2364
- // carries only a generic "Unauthorized" string. The status is
2365
- // dropped once we throw a plain Error, so normalize to the
2366
- // canonical message the catch below matches — otherwise a
2367
- // non-specific 401 would leave a dead routing slot unflagged
2368
- // for codex-doctor --fix (issue #171).
2369
- if (isInvalidatedAuthTokenError(errorBody, response.status)) {
2370
- throw new Error("Your authentication token has been invalidated. Please try signing in again.");
2371
- }
2372
- throw new Error(message);
2373
- }
2374
- lastError = new Error("Codex response did not include quota headers");
2375
- }
2376
- catch (error) {
2377
- lastError = error instanceof Error ? error : new Error(String(error));
2378
- if (isDeactivatedWorkspaceErrorMessage(lastError.message)) {
2379
- throw lastError;
2380
- }
2200
+ for (const limit of usage.additionalLimits) {
2201
+ if (hasUsageWindow(limit.window)) {
2202
+ parts.push(`${limit.name} ${formatUsageLimitSummary(limit.window)}`);
2381
2203
  }
2382
2204
  }
2383
- 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";
2384
2210
  };
2385
2211
  const runAccountCheck = async (deepProbe) => {
2386
2212
  const loadedStorage = await hydrateEmails(await loadAccounts());
@@ -2549,13 +2375,15 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2549
2375
  if (!requestAccountId) {
2550
2376
  throw new Error("Missing accountId for quota probe");
2551
2377
  }
2552
- const snapshot = await fetchCodexQuotaSnapshot({
2378
+ const payload = await fetchCodexUsage({
2553
2379
  accountId: requestAccountId,
2554
2380
  accessToken,
2555
2381
  organizationId: account.organizationId,
2382
+ normalizeAccountErrors: true,
2556
2383
  });
2384
+ const usage = parseCodexUsagePayload(payload);
2557
2385
  ok += 1;
2558
- console.log(`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(snapshot)}`);
2386
+ console.log(`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(usage)}`);
2559
2387
  }
2560
2388
  catch (error) {
2561
2389
  errors += 1;