oc-chatgpt-multi-auth 5.2.0 → 5.2.3

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 (68) hide show
  1. package/README.md +21 -18
  2. package/config/README.md +10 -8
  3. package/config/opencode-legacy.json +47 -73
  4. package/config/opencode-modern.json +32 -38
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +589 -193
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/accounts.d.ts +8 -0
  9. package/dist/lib/accounts.d.ts.map +1 -1
  10. package/dist/lib/accounts.js +145 -28
  11. package/dist/lib/accounts.js.map +1 -1
  12. package/dist/lib/config.d.ts +1 -0
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/config.js +5 -0
  15. package/dist/lib/config.js.map +1 -1
  16. package/dist/lib/logger.d.ts +1 -0
  17. package/dist/lib/logger.d.ts.map +1 -1
  18. package/dist/lib/logger.js +25 -2
  19. package/dist/lib/logger.js.map +1 -1
  20. package/dist/lib/prompts/codex-opencode-bridge.d.ts +4 -3
  21. package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -1
  22. package/dist/lib/prompts/codex-opencode-bridge.js +73 -105
  23. package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -1
  24. package/dist/lib/prompts/codex.d.ts +4 -4
  25. package/dist/lib/prompts/codex.d.ts.map +1 -1
  26. package/dist/lib/prompts/codex.js +35 -37
  27. package/dist/lib/prompts/codex.js.map +1 -1
  28. package/dist/lib/prompts/opencode-codex.d.ts.map +1 -1
  29. package/dist/lib/prompts/opencode-codex.js +100 -25
  30. package/dist/lib/prompts/opencode-codex.js.map +1 -1
  31. package/dist/lib/recovery.d.ts.map +1 -1
  32. package/dist/lib/recovery.js +10 -5
  33. package/dist/lib/recovery.js.map +1 -1
  34. package/dist/lib/request/fetch-helpers.d.ts +2 -1
  35. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  36. package/dist/lib/request/fetch-helpers.js +57 -6
  37. package/dist/lib/request/fetch-helpers.js.map +1 -1
  38. package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
  39. package/dist/lib/request/helpers/model-map.js +35 -25
  40. package/dist/lib/request/helpers/model-map.js.map +1 -1
  41. package/dist/lib/request/request-transformer.d.ts +3 -3
  42. package/dist/lib/request/request-transformer.d.ts.map +1 -1
  43. package/dist/lib/request/request-transformer.js +73 -35
  44. package/dist/lib/request/request-transformer.js.map +1 -1
  45. package/dist/lib/request/response-handler.d.ts.map +1 -1
  46. package/dist/lib/request/response-handler.js +101 -10
  47. package/dist/lib/request/response-handler.js.map +1 -1
  48. package/dist/lib/schemas.d.ts +19 -9
  49. package/dist/lib/schemas.d.ts.map +1 -1
  50. package/dist/lib/schemas.js +5 -0
  51. package/dist/lib/schemas.js.map +1 -1
  52. package/dist/lib/storage/migrations.d.ts +8 -0
  53. package/dist/lib/storage/migrations.d.ts.map +1 -1
  54. package/dist/lib/storage/migrations.js +3 -9
  55. package/dist/lib/storage/migrations.js.map +1 -1
  56. package/dist/lib/storage/paths.d.ts.map +1 -1
  57. package/dist/lib/storage/paths.js +14 -2
  58. package/dist/lib/storage/paths.js.map +1 -1
  59. package/dist/lib/storage.d.ts +1 -0
  60. package/dist/lib/storage.d.ts.map +1 -1
  61. package/dist/lib/storage.js +124 -84
  62. package/dist/lib/storage.js.map +1 -1
  63. package/package.json +26 -12
  64. package/scripts/audit-dev-allowlist.js +114 -0
  65. package/dist/lib/request/local-fast-path.d.ts +0 -15
  66. package/dist/lib/request/local-fast-path.d.ts.map +0 -1
  67. package/dist/lib/request/local-fast-path.js +0 -164
  68. package/dist/lib/request/local-fast-path.js.map +0 -1
package/dist/index.js CHANGED
@@ -28,13 +28,13 @@ import { queuedRefresh } from "./lib/refresh-queue.js";
28
28
  import { openBrowserUrl } from "./lib/auth/browser.js";
29
29
  import { startLocalOAuthServer } from "./lib/auth/server.js";
30
30
  import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
31
- import { getCodexMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getToastDurationMs, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, loadPluginConfig, } from "./lib/config.js";
31
+ import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getToastDurationMs, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, loadPluginConfig, } from "./lib/config.js";
32
32
  import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
33
33
  import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
34
34
  import { checkAndNotify } from "./lib/auto-update-checker.js";
35
35
  import { handleContextOverflow } from "./lib/context-overflow.js";
36
- import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, } from "./lib/accounts.js";
37
- import { getStoragePath, loadAccounts, saveAccounts, clearAccounts, setStoragePath, exportAccounts, importAccounts, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
36
+ import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
37
+ import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
38
38
  import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
39
39
  import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
40
40
  import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
@@ -43,7 +43,7 @@ import { addJitter } from "./lib/rotation.js";
43
43
  import { buildTableHeader, buildTableRow } from "./lib/table-formatter.js";
44
44
  import { setUiRuntimeOptions } from "./lib/ui/runtime.js";
45
45
  import { paintUiText, formatUiBadge, formatUiHeader, formatUiItem, formatUiKeyValue, formatUiSection } from "./lib/ui/format.js";
46
- import { getModelFamily, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
46
+ import { getModelFamily, getCodexInstructions, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
47
47
  import { prewarmOpenCodeCodexPrompt } from "./lib/prompts/opencode-codex.js";
48
48
  import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getRecoveryToastContent, } from "./lib/recovery.js";
49
49
  /**
@@ -124,7 +124,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
124
124
  accountLabel: choice.label,
125
125
  };
126
126
  };
127
- const buildManualOAuthFlow = (pkce, url, onSuccess) => ({
127
+ const buildManualOAuthFlow = (pkce, url, expectedState, onSuccess) => ({
128
128
  url,
129
129
  method: "code",
130
130
  instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
@@ -133,12 +133,29 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
133
133
  if (!parsed.code) {
134
134
  return "No authorization code found. Paste the full callback URL (e.g., http://localhost:1455/auth/callback?code=...)";
135
135
  }
136
+ if (!parsed.state) {
137
+ return "Missing OAuth state. Paste the full callback URL including both code and state parameters.";
138
+ }
139
+ if (parsed.state !== expectedState) {
140
+ return "OAuth state mismatch. Restart login and paste the callback URL generated for this login attempt.";
141
+ }
136
142
  return undefined;
137
143
  },
138
144
  callback: async (input) => {
139
145
  const parsed = parseAuthorizationInput(input);
140
- if (!parsed.code) {
141
- return { type: "failed", reason: "invalid_response", message: "No authorization code provided" };
146
+ if (!parsed.code || !parsed.state) {
147
+ return {
148
+ type: "failed",
149
+ reason: "invalid_response",
150
+ message: "Missing authorization code or OAuth state",
151
+ };
152
+ }
153
+ if (parsed.state !== expectedState) {
154
+ return {
155
+ type: "failed",
156
+ reason: "invalid_response",
157
+ message: "OAuth state mismatch. Restart login and try again.",
158
+ };
142
159
  }
143
160
  const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
144
161
  if (tokens?.type === "success") {
@@ -182,117 +199,123 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
182
199
  const persistAccountPool = async (results, replaceAll = false) => {
183
200
  if (results.length === 0)
184
201
  return;
185
- const now = Date.now();
186
- const stored = replaceAll ? null : await loadAccounts();
187
- const accounts = stored?.accounts ? [...stored.accounts] : [];
188
- const indexByRefreshToken = new Map();
189
- const indexByAccountId = new Map();
190
- const indexByEmail = new Map();
191
- for (let i = 0; i < accounts.length; i += 1) {
192
- const account = accounts[i];
193
- if (!account)
194
- continue;
195
- if (account.refreshToken) {
196
- indexByRefreshToken.set(account.refreshToken, i);
197
- }
198
- if (account.accountId) {
199
- indexByAccountId.set(account.accountId, i);
200
- }
201
- if (account.email) {
202
- indexByEmail.set(account.email, i);
202
+ await withAccountStorageTransaction(async (loadedStorage, persist) => {
203
+ const now = Date.now();
204
+ const stored = replaceAll ? null : loadedStorage;
205
+ const accounts = stored?.accounts ? [...stored.accounts] : [];
206
+ const indexByRefreshToken = new Map();
207
+ const indexByAccountId = new Map();
208
+ const indexByEmail = new Map();
209
+ for (let i = 0; i < accounts.length; i += 1) {
210
+ const account = accounts[i];
211
+ if (!account)
212
+ continue;
213
+ if (account.refreshToken) {
214
+ indexByRefreshToken.set(account.refreshToken, i);
215
+ }
216
+ if (account.accountId) {
217
+ indexByAccountId.set(account.accountId, i);
218
+ }
219
+ if (account.email) {
220
+ indexByEmail.set(account.email, i);
221
+ }
203
222
  }
204
- }
205
- for (const result of results) {
206
- const accountId = result.accountIdOverride ?? extractAccountId(result.access);
207
- const accountIdSource = accountId
208
- ? result.accountIdSource ??
209
- (result.accountIdOverride ? "manual" : "token")
210
- : undefined;
211
- const accountLabel = result.accountLabel;
212
- const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
213
- const existingByEmail = accountEmail && indexByEmail.has(accountEmail)
214
- ? indexByEmail.get(accountEmail)
215
- : undefined;
216
- const existingById = accountId && indexByAccountId.has(accountId)
217
- ? indexByAccountId.get(accountId)
218
- : undefined;
219
- const existingByToken = indexByRefreshToken.get(result.refresh);
220
- const existingIndex = existingById ?? existingByEmail ?? existingByToken;
221
- if (existingIndex === undefined) {
222
- const newIndex = accounts.length;
223
- accounts.push({
224
- accountId,
225
- accountIdSource,
226
- accountLabel,
227
- email: accountEmail,
223
+ for (const result of results) {
224
+ const accountId = result.accountIdOverride ?? extractAccountId(result.access);
225
+ const accountIdSource = accountId
226
+ ? result.accountIdSource ??
227
+ (result.accountIdOverride ? "manual" : "token")
228
+ : undefined;
229
+ const accountLabel = result.accountLabel;
230
+ const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
231
+ const existingByEmail = accountEmail && indexByEmail.has(accountEmail)
232
+ ? indexByEmail.get(accountEmail)
233
+ : undefined;
234
+ const existingById = accountId && indexByAccountId.has(accountId)
235
+ ? indexByAccountId.get(accountId)
236
+ : undefined;
237
+ const existingByToken = indexByRefreshToken.get(result.refresh);
238
+ const existingIndex = existingById ?? existingByEmail ?? existingByToken;
239
+ if (existingIndex === undefined) {
240
+ const newIndex = accounts.length;
241
+ accounts.push({
242
+ accountId,
243
+ accountIdSource,
244
+ accountLabel,
245
+ email: accountEmail,
246
+ refreshToken: result.refresh,
247
+ accessToken: result.access,
248
+ expiresAt: result.expires,
249
+ addedAt: now,
250
+ lastUsed: now,
251
+ });
252
+ indexByRefreshToken.set(result.refresh, newIndex);
253
+ if (accountId) {
254
+ indexByAccountId.set(accountId, newIndex);
255
+ }
256
+ if (accountEmail) {
257
+ indexByEmail.set(accountEmail, newIndex);
258
+ }
259
+ continue;
260
+ }
261
+ const existing = accounts[existingIndex];
262
+ if (!existing)
263
+ continue;
264
+ const oldToken = existing.refreshToken;
265
+ const oldEmail = existing.email;
266
+ const nextEmail = accountEmail ?? existing.email;
267
+ const nextAccountId = accountId ?? existing.accountId;
268
+ const nextAccountIdSource = accountId ? accountIdSource ?? existing.accountIdSource : existing.accountIdSource;
269
+ const nextAccountLabel = accountLabel ?? existing.accountLabel;
270
+ accounts[existingIndex] = {
271
+ ...existing,
272
+ accountId: nextAccountId,
273
+ accountIdSource: nextAccountIdSource,
274
+ accountLabel: nextAccountLabel,
275
+ email: nextEmail,
228
276
  refreshToken: result.refresh,
229
- addedAt: now,
277
+ accessToken: result.access,
278
+ expiresAt: result.expires,
230
279
  lastUsed: now,
231
- });
232
- indexByRefreshToken.set(result.refresh, newIndex);
280
+ };
281
+ if (oldToken !== result.refresh) {
282
+ indexByRefreshToken.delete(oldToken);
283
+ indexByRefreshToken.set(result.refresh, existingIndex);
284
+ }
233
285
  if (accountId) {
234
- indexByAccountId.set(accountId, newIndex);
286
+ indexByAccountId.set(accountId, existingIndex);
235
287
  }
236
- if (accountEmail) {
237
- indexByEmail.set(accountEmail, newIndex);
288
+ if (oldEmail && oldEmail !== nextEmail) {
289
+ indexByEmail.delete(oldEmail);
290
+ }
291
+ if (nextEmail) {
292
+ indexByEmail.set(nextEmail, existingIndex);
238
293
  }
239
- continue;
240
- }
241
- const existing = accounts[existingIndex];
242
- if (!existing)
243
- continue;
244
- const oldToken = existing.refreshToken;
245
- const oldEmail = existing.email;
246
- const nextEmail = accountEmail ?? existing.email;
247
- const nextAccountId = accountId ?? existing.accountId;
248
- const nextAccountIdSource = accountId ? accountIdSource ?? existing.accountIdSource : existing.accountIdSource;
249
- const nextAccountLabel = accountLabel ?? existing.accountLabel;
250
- accounts[existingIndex] = {
251
- ...existing,
252
- accountId: nextAccountId,
253
- accountIdSource: nextAccountIdSource,
254
- accountLabel: nextAccountLabel,
255
- email: nextEmail,
256
- refreshToken: result.refresh,
257
- lastUsed: now,
258
- };
259
- if (oldToken !== result.refresh) {
260
- indexByRefreshToken.delete(oldToken);
261
- indexByRefreshToken.set(result.refresh, existingIndex);
262
- }
263
- if (accountId) {
264
- indexByAccountId.set(accountId, existingIndex);
265
- }
266
- if (oldEmail && oldEmail !== nextEmail) {
267
- indexByEmail.delete(oldEmail);
268
- }
269
- if (nextEmail) {
270
- indexByEmail.set(nextEmail, existingIndex);
271
294
  }
272
- }
273
- if (accounts.length === 0)
274
- return;
275
- const activeIndex = replaceAll
276
- ? 0
277
- : typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex)
278
- ? stored.activeIndex
279
- : 0;
280
- const clampedActiveIndex = Math.max(0, Math.min(activeIndex, accounts.length - 1));
281
- const activeIndexByFamily = {};
282
- for (const family of MODEL_FAMILIES) {
283
- const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
284
- const rawFamilyIndex = replaceAll
295
+ if (accounts.length === 0)
296
+ return;
297
+ const activeIndex = replaceAll
285
298
  ? 0
286
- : typeof storedFamilyIndex === "number" && Number.isFinite(storedFamilyIndex)
287
- ? storedFamilyIndex
288
- : clampedActiveIndex;
289
- activeIndexByFamily[family] = Math.max(0, Math.min(Math.floor(rawFamilyIndex), accounts.length - 1));
290
- }
291
- await saveAccounts({
292
- version: 3,
293
- accounts,
294
- activeIndex: clampedActiveIndex,
295
- activeIndexByFamily,
299
+ : typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex)
300
+ ? stored.activeIndex
301
+ : 0;
302
+ const clampedActiveIndex = Math.max(0, Math.min(activeIndex, accounts.length - 1));
303
+ const activeIndexByFamily = {};
304
+ for (const family of MODEL_FAMILIES) {
305
+ const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
306
+ const rawFamilyIndex = replaceAll
307
+ ? 0
308
+ : typeof storedFamilyIndex === "number" && Number.isFinite(storedFamilyIndex)
309
+ ? storedFamilyIndex
310
+ : clampedActiveIndex;
311
+ activeIndexByFamily[family] = Math.max(0, Math.min(Math.floor(rawFamilyIndex), accounts.length - 1));
312
+ }
313
+ await persist({
314
+ version: 3,
315
+ accounts,
316
+ activeIndex: clampedActiveIndex,
317
+ activeIndexByFamily,
318
+ });
296
319
  });
297
320
  };
298
321
  const showToast = async (message, variant = "success", options) => {
@@ -349,6 +372,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
349
372
  account.email = email;
350
373
  changed = true;
351
374
  }
375
+ if (refreshed.access && refreshed.access !== account.accessToken) {
376
+ account.accessToken = refreshed.access;
377
+ changed = true;
378
+ }
379
+ if (typeof refreshed.expires === "number" && refreshed.expires !== account.expiresAt) {
380
+ account.expiresAt = refreshed.expires;
381
+ changed = true;
382
+ }
352
383
  if (refreshed.refresh && refreshed.refresh !== account.refreshToken) {
353
384
  account.refreshToken = refreshed.refresh;
354
385
  changed = true;
@@ -434,16 +465,31 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
434
465
  return;
435
466
  }
436
467
  const index = props.index ?? props.accountIndex;
437
- if (typeof index === "number" && cachedAccountManager) {
438
- // Convert 1-based index (UI) to 0-based index (internal) if needed,
439
- // or handle 0-based directly. Usually UI lists are 0-based in code but 1-based in display.
440
- // AccountManager.setActiveIndex expects 0-based index.
441
- // Assuming the event passes the raw index from the list.
442
- const account = cachedAccountManager.setActiveIndex(index);
468
+ if (typeof index === "number") {
469
+ const storage = await loadAccounts();
470
+ if (!storage || index < 0 || index >= storage.accounts.length) {
471
+ return;
472
+ }
473
+ const now = Date.now();
474
+ const account = storage.accounts[index];
443
475
  if (account) {
444
- await cachedAccountManager.saveToDisk();
445
- await showToast(`Switched to account ${index + 1}`, "info");
476
+ account.lastUsed = now;
477
+ account.lastSwitchReason = "rotation";
478
+ }
479
+ storage.activeIndex = index;
480
+ storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
481
+ for (const family of MODEL_FAMILIES) {
482
+ storage.activeIndexByFamily[family] = index;
483
+ }
484
+ await saveAccounts(storage);
485
+ // Reload manager from disk so we don't overwrite newer rotated
486
+ // refresh tokens with stale in-memory state.
487
+ if (cachedAccountManager) {
488
+ const reloadedManager = await AccountManager.loadFromDisk();
489
+ cachedAccountManager = reloadedManager;
490
+ accountManagerPromise = Promise.resolve(reloadedManager);
446
491
  }
492
+ await showToast(`Switched to account ${index + 1}`, "info");
447
493
  }
448
494
  }
449
495
  }
@@ -473,6 +519,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
473
519
  */
474
520
  async loader(getAuth, provider) {
475
521
  const auth = await getAuth();
522
+ const pluginConfig = loadPluginConfig();
523
+ applyUiRuntimeFromConfig(pluginConfig);
524
+ const perProjectAccounts = getPerProjectAccounts(pluginConfig);
525
+ setStoragePath(perProjectAccounts ? process.cwd() : null);
476
526
  // Only handle OAuth auth type, skip API key auth
477
527
  if (auth.type !== "oauth") {
478
528
  return {};
@@ -497,7 +547,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
497
547
  if (!accountManagerPromise) {
498
548
  accountManagerPromise = AccountManager.loadFromDisk(auth);
499
549
  }
500
- const accountManager = await accountManagerPromise;
550
+ let accountManager = await accountManagerPromise;
501
551
  cachedAccountManager = accountManager;
502
552
  const refreshToken = auth.type === "oauth" ? auth.refresh : "";
503
553
  const needsPersist = refreshToken &&
@@ -517,9 +567,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
517
567
  };
518
568
  // Load plugin configuration and determine CODEX_MODE
519
569
  // Priority: CODEX_MODE env var > config file > default (true)
520
- const pluginConfig = loadPluginConfig();
521
- applyUiRuntimeFromConfig(pluginConfig);
522
570
  const codexMode = getCodexMode(pluginConfig);
571
+ const requestTransformMode = getRequestTransformMode(pluginConfig);
572
+ const useLegacyRequestTransform = requestTransformMode === "legacy";
523
573
  const fastSessionEnabled = getFastSession(pluginConfig);
524
574
  const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
525
575
  const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
@@ -533,12 +583,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
533
583
  const fallbackToGpt52OnUnsupportedGpt53 = getFallbackToGpt52OnUnsupportedGpt53(pluginConfig);
534
584
  const unsupportedCodexFallbackChain = getUnsupportedCodexFallbackChain(pluginConfig);
535
585
  const toastDurationMs = getToastDurationMs(pluginConfig);
536
- const perProjectAccounts = getPerProjectAccounts(pluginConfig);
537
586
  const fetchTimeoutMs = getFetchTimeoutMs(pluginConfig);
538
587
  const streamStallTimeoutMs = getStreamStallTimeoutMs(pluginConfig);
539
- if (perProjectAccounts) {
540
- setStoragePath(process.cwd());
541
- }
542
588
  const sessionRecoveryEnabled = getSessionRecovery(pluginConfig);
543
589
  const autoResumeEnabled = getAutoResume(pluginConfig);
544
590
  const emptyResponseMaxRetries = getEmptyResponseMaxRetries(pluginConfig);
@@ -559,7 +605,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
559
605
  const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" &&
560
606
  process.env.VITEST !== "true" &&
561
607
  process.env.NODE_ENV !== "test";
562
- if (!startupPrewarmTriggered && prewarmEnabled) {
608
+ if (!startupPrewarmTriggered && prewarmEnabled && useLegacyRequestTransform) {
563
609
  startupPrewarmTriggered = true;
564
610
  const configuredModels = Object.keys(userConfig.models ?? {});
565
611
  prewarmCodexInstructions(configuredModels);
@@ -596,6 +642,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
596
642
  */
597
643
  async fetch(input, init) {
598
644
  try {
645
+ if (cachedAccountManager && cachedAccountManager !== accountManager) {
646
+ accountManager = cachedAccountManager;
647
+ }
599
648
  // Step 1: Extract and rewrite URL for Codex backend
600
649
  const originalUrl = extractRequestUrl(input);
601
650
  const url = rewriteUrlForCodex(originalUrl);
@@ -660,6 +709,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
660
709
  fastSession: fastSessionEnabled,
661
710
  fastSessionStrategy,
662
711
  fastSessionMaxInputItems,
712
+ requestTransformMode,
663
713
  });
664
714
  let requestInit = transformation?.updatedInit ?? baseInit;
665
715
  let transformedBody = transformation?.body;
@@ -720,6 +770,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
720
770
  while (true) {
721
771
  const accountCount = accountManager.getAccountCount();
722
772
  const attempted = new Set();
773
+ let restartAccountTraversalWithFallback = false;
723
774
  while (attempted.size < Math.max(1, accountCount)) {
724
775
  const account = accountManager.getCurrentOrNextForFamilyHybrid(modelFamily, model, { pidOffsetEnabled });
725
776
  if (!account || attempted.has(account.index)) {
@@ -775,12 +826,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
775
826
  await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
776
827
  accountManager.markToastShown(account.index);
777
828
  }
778
- let headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
829
+ const headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
779
830
  model,
780
831
  promptCacheKey,
781
832
  });
782
833
  // Consume a token before making the request for proactive rate limiting
783
- accountManager.consumeToken(account, modelFamily, model);
834
+ const tokenConsumed = accountManager.consumeToken(account, modelFamily, model);
835
+ if (!tokenConsumed) {
836
+ accountManager.recordRateLimit(account, modelFamily, model);
837
+ runtimeMetrics.accountRotations++;
838
+ runtimeMetrics.lastError =
839
+ `Local token bucket depleted for account ${account.index + 1} (${modelFamily}${model ? `:${model}` : ""})`;
840
+ logWarn(`Skipping account ${account.index + 1}: local token bucket depleted for ${modelFamily}${model ? `:${model}` : ""}`);
841
+ break;
842
+ }
784
843
  while (true) {
785
844
  let response;
786
845
  const fetchStart = performance.now();
@@ -868,7 +927,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
868
927
  customChain: unsupportedCodexFallbackChain,
869
928
  });
870
929
  if (fallbackModel) {
871
- const previousModel = model ?? "gpt-5.3-codex";
930
+ const previousModel = model ?? "gpt-5-codex";
872
931
  const previousModelFamily = modelFamily;
873
932
  attemptedUnsupportedFallbackModels.add(previousModel);
874
933
  attemptedUnsupportedFallbackModels.add(fallbackModel);
@@ -896,11 +955,6 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
896
955
  ...(requestInit ?? {}),
897
956
  body: JSON.stringify(transformedBody),
898
957
  };
899
- headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
900
- model,
901
- promptCacheKey,
902
- });
903
- accountManager.consumeToken(account, modelFamily, model);
904
958
  runtimeMetrics.lastError = `Model fallback: ${previousModel} -> ${model}`;
905
959
  logWarn(`Model ${previousModel} is unsupported for this ChatGPT account. Falling back to ${model}.`, {
906
960
  unsupportedCodexPolicy,
@@ -910,7 +964,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
910
964
  fallbackReason: "unsupported-model-entitlement",
911
965
  });
912
966
  await showToast(`Model ${previousModel} is not available for this account. Retrying with ${model}.`, "warning", { duration: toastDurationMs });
913
- continue;
967
+ restartAccountTraversalWithFallback = true;
968
+ break;
914
969
  }
915
970
  if (unsupportedModelInfo.isUnsupported && !fallbackOnUnsupportedCodexModel) {
916
971
  const blockedModel = unsupportedModelInfo.unsupportedModel ?? model ?? "requested model";
@@ -975,6 +1030,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
975
1030
  const successResponse = await handleSuccessResponse(response, isStreaming, {
976
1031
  streamStallTimeoutMs,
977
1032
  });
1033
+ if (!successResponse.ok) {
1034
+ runtimeMetrics.failedRequests++;
1035
+ runtimeMetrics.lastError = `HTTP ${successResponse.status}`;
1036
+ return successResponse;
1037
+ }
978
1038
  if (!isStreaming && emptyResponseMaxRetries > 0) {
979
1039
  const clonedResponse = successResponse.clone();
980
1040
  try {
@@ -1003,6 +1063,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1003
1063
  runtimeMetrics.lastError = null;
1004
1064
  return successResponse;
1005
1065
  }
1066
+ if (restartAccountTraversalWithFallback) {
1067
+ break;
1068
+ }
1069
+ }
1070
+ if (restartAccountTraversalWithFallback) {
1071
+ continue;
1006
1072
  }
1007
1073
  const waitMs = accountManager.getMinWaitTimeForFamily(modelFamily, model);
1008
1074
  const count = accountManager.getAccountCount();
@@ -1052,9 +1118,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1052
1118
  const authPluginConfig = loadPluginConfig();
1053
1119
  applyUiRuntimeFromConfig(authPluginConfig);
1054
1120
  const authPerProjectAccounts = getPerProjectAccounts(authPluginConfig);
1055
- if (authPerProjectAccounts) {
1056
- setStoragePath(process.cwd());
1057
- }
1121
+ setStoragePath(authPerProjectAccounts ? process.cwd() : null);
1058
1122
  const accounts = [];
1059
1123
  const noBrowser = inputs?.noBrowser === "true" ||
1060
1124
  inputs?.["no-browser"] === "true";
@@ -1091,6 +1155,211 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1091
1155
  message.includes("invalid refresh") ||
1092
1156
  message.includes("token has been revoked"));
1093
1157
  };
1158
+ const parseFiniteNumberHeader = (headers, name) => {
1159
+ const raw = headers.get(name);
1160
+ if (!raw)
1161
+ return undefined;
1162
+ const parsed = Number(raw);
1163
+ return Number.isFinite(parsed) ? parsed : undefined;
1164
+ };
1165
+ const parseFiniteIntHeader = (headers, name) => {
1166
+ const raw = headers.get(name);
1167
+ if (!raw)
1168
+ return undefined;
1169
+ const parsed = Number.parseInt(raw, 10);
1170
+ return Number.isFinite(parsed) ? parsed : undefined;
1171
+ };
1172
+ const parseResetAtMs = (headers, prefix) => {
1173
+ const resetAfterSeconds = parseFiniteIntHeader(headers, `${prefix}-reset-after-seconds`);
1174
+ if (typeof resetAfterSeconds === "number" &&
1175
+ Number.isFinite(resetAfterSeconds) &&
1176
+ resetAfterSeconds > 0) {
1177
+ return Date.now() + resetAfterSeconds * 1000;
1178
+ }
1179
+ const resetAtRaw = headers.get(`${prefix}-reset-at`);
1180
+ if (!resetAtRaw)
1181
+ return undefined;
1182
+ const trimmed = resetAtRaw.trim();
1183
+ if (/^\d+$/.test(trimmed)) {
1184
+ const parsedNumber = Number.parseInt(trimmed, 10);
1185
+ if (Number.isFinite(parsedNumber) && parsedNumber > 0) {
1186
+ // Upstream sometimes returns seconds since epoch.
1187
+ return parsedNumber < 10_000_000_000 ? parsedNumber * 1000 : parsedNumber;
1188
+ }
1189
+ }
1190
+ const parsedDate = Date.parse(trimmed);
1191
+ return Number.isFinite(parsedDate) ? parsedDate : undefined;
1192
+ };
1193
+ const hasCodexQuotaHeaders = (headers) => {
1194
+ const keys = [
1195
+ "x-codex-primary-used-percent",
1196
+ "x-codex-primary-window-minutes",
1197
+ "x-codex-primary-reset-at",
1198
+ "x-codex-primary-reset-after-seconds",
1199
+ "x-codex-secondary-used-percent",
1200
+ "x-codex-secondary-window-minutes",
1201
+ "x-codex-secondary-reset-at",
1202
+ "x-codex-secondary-reset-after-seconds",
1203
+ ];
1204
+ return keys.some((key) => headers.get(key) !== null);
1205
+ };
1206
+ const parseCodexQuotaSnapshot = (headers, status) => {
1207
+ if (!hasCodexQuotaHeaders(headers))
1208
+ return null;
1209
+ const primaryPrefix = "x-codex-primary";
1210
+ const secondaryPrefix = "x-codex-secondary";
1211
+ const primary = {
1212
+ usedPercent: parseFiniteNumberHeader(headers, `${primaryPrefix}-used-percent`),
1213
+ windowMinutes: parseFiniteIntHeader(headers, `${primaryPrefix}-window-minutes`),
1214
+ resetAtMs: parseResetAtMs(headers, primaryPrefix),
1215
+ };
1216
+ const secondary = {
1217
+ usedPercent: parseFiniteNumberHeader(headers, `${secondaryPrefix}-used-percent`),
1218
+ windowMinutes: parseFiniteIntHeader(headers, `${secondaryPrefix}-window-minutes`),
1219
+ resetAtMs: parseResetAtMs(headers, secondaryPrefix),
1220
+ };
1221
+ const planTypeRaw = headers.get("x-codex-plan-type");
1222
+ const planType = planTypeRaw && planTypeRaw.trim() ? planTypeRaw.trim() : undefined;
1223
+ const activeLimit = parseFiniteIntHeader(headers, "x-codex-active-limit");
1224
+ return { status, planType, activeLimit, primary, secondary };
1225
+ };
1226
+ const formatQuotaWindowLabel = (windowMinutes) => {
1227
+ if (!windowMinutes || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
1228
+ return "quota";
1229
+ }
1230
+ if (windowMinutes % 1440 === 0)
1231
+ return `${windowMinutes / 1440}d`;
1232
+ if (windowMinutes % 60 === 0)
1233
+ return `${windowMinutes / 60}h`;
1234
+ return `${windowMinutes}m`;
1235
+ };
1236
+ const formatResetAt = (resetAtMs) => {
1237
+ if (!resetAtMs || !Number.isFinite(resetAtMs) || resetAtMs <= 0)
1238
+ return undefined;
1239
+ const date = new Date(resetAtMs);
1240
+ if (!Number.isFinite(date.getTime()))
1241
+ return undefined;
1242
+ const now = new Date();
1243
+ const sameDay = now.getFullYear() === date.getFullYear() &&
1244
+ now.getMonth() === date.getMonth() &&
1245
+ now.getDate() === date.getDate();
1246
+ const time = date.toLocaleTimeString(undefined, {
1247
+ hour: "2-digit",
1248
+ minute: "2-digit",
1249
+ hour12: false,
1250
+ });
1251
+ if (sameDay)
1252
+ return time;
1253
+ const day = date.toLocaleDateString(undefined, { month: "short", day: "2-digit" });
1254
+ return `${time} on ${day}`;
1255
+ };
1256
+ const formatCodexQuotaLine = (snapshot) => {
1257
+ const summarizeWindow = (label, window) => {
1258
+ const used = window.usedPercent;
1259
+ const left = typeof used === "number" && Number.isFinite(used)
1260
+ ? Math.max(0, Math.min(100, Math.round(100 - used)))
1261
+ : undefined;
1262
+ const reset = formatResetAt(window.resetAtMs);
1263
+ let summary = label;
1264
+ if (left !== undefined)
1265
+ summary = `${summary} ${left}% left`;
1266
+ if (reset)
1267
+ summary = `${summary} (resets ${reset})`;
1268
+ return summary;
1269
+ };
1270
+ const primaryLabel = formatQuotaWindowLabel(snapshot.primary.windowMinutes);
1271
+ const secondaryLabel = formatQuotaWindowLabel(snapshot.secondary.windowMinutes);
1272
+ const parts = [
1273
+ summarizeWindow(primaryLabel, snapshot.primary),
1274
+ summarizeWindow(secondaryLabel, snapshot.secondary),
1275
+ ];
1276
+ if (snapshot.planType)
1277
+ parts.push(`plan:${snapshot.planType}`);
1278
+ if (typeof snapshot.activeLimit === "number" && Number.isFinite(snapshot.activeLimit)) {
1279
+ parts.push(`active:${snapshot.activeLimit}`);
1280
+ }
1281
+ if (snapshot.status === 429)
1282
+ parts.push("rate-limited");
1283
+ return parts.join(", ");
1284
+ };
1285
+ const fetchCodexQuotaSnapshot = async (params) => {
1286
+ const QUOTA_PROBE_MODELS = ["gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"];
1287
+ let lastError = null;
1288
+ for (const model of QUOTA_PROBE_MODELS) {
1289
+ try {
1290
+ const instructions = await getCodexInstructions(model);
1291
+ const probeBody = {
1292
+ model,
1293
+ stream: true,
1294
+ store: false,
1295
+ include: ["reasoning.encrypted_content"],
1296
+ instructions,
1297
+ input: [
1298
+ {
1299
+ type: "message",
1300
+ role: "user",
1301
+ content: [{ type: "input_text", text: "quota ping" }],
1302
+ },
1303
+ ],
1304
+ reasoning: { effort: "none", summary: "auto" },
1305
+ text: { verbosity: "low" },
1306
+ };
1307
+ const headers = createCodexHeaders(undefined, params.accountId, params.accessToken, {
1308
+ model,
1309
+ });
1310
+ headers.set("content-type", "application/json; charset=utf-8");
1311
+ const controller = new AbortController();
1312
+ const timeout = setTimeout(() => controller.abort(), 15_000);
1313
+ let response;
1314
+ try {
1315
+ response = await fetch(`${CODEX_BASE_URL}/codex/responses`, {
1316
+ method: "POST",
1317
+ headers,
1318
+ body: JSON.stringify(probeBody),
1319
+ signal: controller.signal,
1320
+ });
1321
+ }
1322
+ finally {
1323
+ clearTimeout(timeout);
1324
+ }
1325
+ const snapshot = parseCodexQuotaSnapshot(response.headers, response.status);
1326
+ if (snapshot) {
1327
+ // We only need headers; cancel the SSE stream immediately.
1328
+ try {
1329
+ await response.body?.cancel();
1330
+ }
1331
+ catch {
1332
+ // Ignore cancellation failures.
1333
+ }
1334
+ return snapshot;
1335
+ }
1336
+ if (!response.ok) {
1337
+ const bodyText = await response.text().catch(() => "");
1338
+ let errorBody = undefined;
1339
+ try {
1340
+ errorBody = bodyText ? JSON.parse(bodyText) : undefined;
1341
+ }
1342
+ catch {
1343
+ errorBody = { error: { message: bodyText } };
1344
+ }
1345
+ const unsupportedInfo = getUnsupportedCodexModelInfo(errorBody);
1346
+ if (unsupportedInfo.isUnsupported) {
1347
+ lastError = new Error(unsupportedInfo.message ?? `Model '${model}' unsupported for this account`);
1348
+ continue;
1349
+ }
1350
+ const message = (typeof errorBody?.error?.message === "string"
1351
+ ? errorBody.error?.message
1352
+ : bodyText) || `HTTP ${response.status}`;
1353
+ throw new Error(message);
1354
+ }
1355
+ lastError = new Error("Codex response did not include quota headers");
1356
+ }
1357
+ catch (error) {
1358
+ lastError = error instanceof Error ? error : new Error(String(error));
1359
+ }
1360
+ }
1361
+ throw lastError ?? new Error("Failed to fetch quotas");
1362
+ };
1094
1363
  const runAccountCheck = async (deepProbe) => {
1095
1364
  const loadedStorage = await hydrateEmails(await loadAccounts());
1096
1365
  const workingStorage = loadedStorage
@@ -1114,7 +1383,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1114
1383
  let ok = 0;
1115
1384
  let disabled = 0;
1116
1385
  let errors = 0;
1117
- console.log(`\nChecking ${deepProbe ? "full account health" : "quotas (fast refresh)"} for all accounts...\n`);
1386
+ console.log(`\nChecking ${deepProbe ? "full account health" : "quotas"} for all accounts...\n`);
1118
1387
  for (let i = 0; i < total; i += 1) {
1119
1388
  const account = workingStorage.accounts[i];
1120
1389
  if (!account)
@@ -1126,52 +1395,149 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1126
1395
  continue;
1127
1396
  }
1128
1397
  try {
1129
- const refreshResult = await queuedRefresh(account.refreshToken);
1130
- if (refreshResult.type !== "success") {
1131
- errors += 1;
1132
- const message = refreshResult.message ?? refreshResult.reason ?? "refresh failed";
1133
- console.log(`[${i + 1}/${total}] ${label}: ERROR (${message})`);
1134
- if (isFlaggableFailure(refreshResult)) {
1135
- const existingIndex = flaggedStorage.accounts.findIndex((flagged) => flagged.refreshToken === account.refreshToken);
1136
- const flaggedRecord = {
1137
- ...account,
1138
- flaggedAt: Date.now(),
1139
- flaggedReason: "token-invalid",
1140
- lastError: message,
1141
- };
1142
- if (existingIndex >= 0) {
1143
- flaggedStorage.accounts[existingIndex] = flaggedRecord;
1398
+ // If we already have a valid cached access token, don't force-refresh.
1399
+ // This avoids flagging accounts where the refresh token has been burned
1400
+ // but the access token is still valid (same behavior as Codex CLI).
1401
+ const nowMs = Date.now();
1402
+ let accessToken = null;
1403
+ let tokenAccountId = undefined;
1404
+ let authDetail = "OK";
1405
+ if (account.accessToken &&
1406
+ (typeof account.expiresAt !== "number" ||
1407
+ !Number.isFinite(account.expiresAt) ||
1408
+ account.expiresAt > nowMs)) {
1409
+ accessToken = account.accessToken;
1410
+ authDetail = "OK (cached access)";
1411
+ tokenAccountId = extractAccountId(account.accessToken);
1412
+ if (tokenAccountId &&
1413
+ shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId) &&
1414
+ tokenAccountId !== account.accountId) {
1415
+ account.accountId = tokenAccountId;
1416
+ account.accountIdSource = "token";
1417
+ storageChanged = true;
1418
+ }
1419
+ }
1420
+ // If Codex CLI has a valid cached access token for this email, use it
1421
+ // instead of forcing a refresh.
1422
+ if (!accessToken) {
1423
+ const cached = await lookupCodexCliTokensByEmail(account.email);
1424
+ if (cached &&
1425
+ (typeof cached.expiresAt !== "number" ||
1426
+ !Number.isFinite(cached.expiresAt) ||
1427
+ cached.expiresAt > nowMs)) {
1428
+ accessToken = cached.accessToken;
1429
+ authDetail = "OK (Codex CLI cache)";
1430
+ if (cached.refreshToken && cached.refreshToken !== account.refreshToken) {
1431
+ account.refreshToken = cached.refreshToken;
1432
+ storageChanged = true;
1144
1433
  }
1145
- else {
1146
- flaggedStorage.accounts.push(flaggedRecord);
1434
+ if (cached.accessToken && cached.accessToken !== account.accessToken) {
1435
+ account.accessToken = cached.accessToken;
1436
+ storageChanged = true;
1437
+ }
1438
+ if (cached.expiresAt !== account.expiresAt) {
1439
+ account.expiresAt = cached.expiresAt;
1440
+ storageChanged = true;
1441
+ }
1442
+ const hydratedEmail = sanitizeEmail(extractAccountEmail(cached.accessToken));
1443
+ if (hydratedEmail && hydratedEmail !== account.email) {
1444
+ account.email = hydratedEmail;
1445
+ storageChanged = true;
1446
+ }
1447
+ tokenAccountId = extractAccountId(cached.accessToken);
1448
+ if (tokenAccountId &&
1449
+ shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId) &&
1450
+ tokenAccountId !== account.accountId) {
1451
+ account.accountId = tokenAccountId;
1452
+ account.accountIdSource = "token";
1453
+ storageChanged = true;
1147
1454
  }
1148
- removeFromActive.add(account.refreshToken);
1149
- flaggedChanged = true;
1150
1455
  }
1151
- continue;
1152
1456
  }
1153
- ok += 1;
1154
- if (refreshResult.refresh !== account.refreshToken) {
1155
- account.refreshToken = refreshResult.refresh;
1156
- storageChanged = true;
1457
+ if (!accessToken) {
1458
+ const refreshResult = await queuedRefresh(account.refreshToken);
1459
+ if (refreshResult.type !== "success") {
1460
+ errors += 1;
1461
+ const message = refreshResult.message ?? refreshResult.reason ?? "refresh failed";
1462
+ console.log(`[${i + 1}/${total}] ${label}: ERROR (${message})`);
1463
+ if (deepProbe && isFlaggableFailure(refreshResult)) {
1464
+ const existingIndex = flaggedStorage.accounts.findIndex((flagged) => flagged.refreshToken === account.refreshToken);
1465
+ const flaggedRecord = {
1466
+ ...account,
1467
+ flaggedAt: Date.now(),
1468
+ flaggedReason: "token-invalid",
1469
+ lastError: message,
1470
+ };
1471
+ if (existingIndex >= 0) {
1472
+ flaggedStorage.accounts[existingIndex] = flaggedRecord;
1473
+ }
1474
+ else {
1475
+ flaggedStorage.accounts.push(flaggedRecord);
1476
+ }
1477
+ removeFromActive.add(account.refreshToken);
1478
+ flaggedChanged = true;
1479
+ }
1480
+ continue;
1481
+ }
1482
+ accessToken = refreshResult.access;
1483
+ authDetail = "OK";
1484
+ if (refreshResult.refresh !== account.refreshToken) {
1485
+ account.refreshToken = refreshResult.refresh;
1486
+ storageChanged = true;
1487
+ }
1488
+ if (refreshResult.access && refreshResult.access !== account.accessToken) {
1489
+ account.accessToken = refreshResult.access;
1490
+ storageChanged = true;
1491
+ }
1492
+ if (typeof refreshResult.expires === "number" &&
1493
+ refreshResult.expires !== account.expiresAt) {
1494
+ account.expiresAt = refreshResult.expires;
1495
+ storageChanged = true;
1496
+ }
1497
+ const hydratedEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
1498
+ if (hydratedEmail && hydratedEmail !== account.email) {
1499
+ account.email = hydratedEmail;
1500
+ storageChanged = true;
1501
+ }
1502
+ tokenAccountId = extractAccountId(refreshResult.access);
1503
+ if (tokenAccountId &&
1504
+ shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId) &&
1505
+ tokenAccountId !== account.accountId) {
1506
+ account.accountId = tokenAccountId;
1507
+ account.accountIdSource = "token";
1508
+ storageChanged = true;
1509
+ }
1157
1510
  }
1158
- const hydratedEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
1159
- if (hydratedEmail && hydratedEmail !== account.email) {
1160
- account.email = hydratedEmail;
1161
- storageChanged = true;
1511
+ if (!accessToken) {
1512
+ throw new Error("Missing access token after refresh");
1162
1513
  }
1163
- const tokenAccountId = extractAccountId(refreshResult.access);
1164
- if (tokenAccountId &&
1165
- shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId) &&
1166
- tokenAccountId !== account.accountId) {
1167
- account.accountId = tokenAccountId;
1168
- account.accountIdSource = "token";
1169
- storageChanged = true;
1514
+ if (deepProbe) {
1515
+ ok += 1;
1516
+ const detail = tokenAccountId
1517
+ ? `${authDetail} (id:${tokenAccountId.slice(-6)})`
1518
+ : authDetail;
1519
+ console.log(`[${i + 1}/${total}] ${label}: ${detail}`);
1520
+ continue;
1521
+ }
1522
+ try {
1523
+ const requestAccountId = resolveRequestAccountId(account.accountId, account.accountIdSource, tokenAccountId) ??
1524
+ tokenAccountId ??
1525
+ account.accountId;
1526
+ if (!requestAccountId) {
1527
+ throw new Error("Missing accountId for quota probe");
1528
+ }
1529
+ const snapshot = await fetchCodexQuotaSnapshot({
1530
+ accountId: requestAccountId,
1531
+ accessToken,
1532
+ });
1533
+ ok += 1;
1534
+ console.log(`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(snapshot)}`);
1535
+ }
1536
+ catch (error) {
1537
+ errors += 1;
1538
+ const message = error instanceof Error ? error.message : String(error);
1539
+ console.log(`[${i + 1}/${total}] ${label}: ERROR (${message.slice(0, 160)})`);
1170
1540
  }
1171
- const deepProbeDetail = deepProbe && tokenAccountId
1172
- ? `OK (id:${tokenAccountId.slice(-6)})`
1173
- : "OK";
1174
- console.log(`[${i + 1}/${total}] ${label}: ${deepProbeDetail}`);
1175
1541
  }
1176
1542
  catch (error) {
1177
1543
  errors += 1;
@@ -1213,6 +1579,33 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1213
1579
  continue;
1214
1580
  const label = flagged.email ?? flagged.accountLabel ?? `Flagged ${i + 1}`;
1215
1581
  try {
1582
+ const cached = await lookupCodexCliTokensByEmail(flagged.email);
1583
+ const now = Date.now();
1584
+ if (cached &&
1585
+ typeof cached.expiresAt === "number" &&
1586
+ Number.isFinite(cached.expiresAt) &&
1587
+ cached.expiresAt > now) {
1588
+ const refreshToken = typeof cached.refreshToken === "string" && cached.refreshToken.trim()
1589
+ ? cached.refreshToken.trim()
1590
+ : flagged.refreshToken;
1591
+ const resolved = resolveAccountSelection({
1592
+ type: "success",
1593
+ access: cached.accessToken,
1594
+ refresh: refreshToken,
1595
+ expires: cached.expiresAt,
1596
+ multiAccount: true,
1597
+ });
1598
+ if (!resolved.accountIdOverride && flagged.accountId) {
1599
+ resolved.accountIdOverride = flagged.accountId;
1600
+ resolved.accountIdSource = flagged.accountIdSource ?? "manual";
1601
+ }
1602
+ if (!resolved.accountLabel && flagged.accountLabel) {
1603
+ resolved.accountLabel = flagged.accountLabel;
1604
+ }
1605
+ restored.push(resolved);
1606
+ console.log(`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: RESTORED (Codex CLI cache)`);
1607
+ continue;
1608
+ }
1216
1609
  const refreshResult = await queuedRefresh(flagged.refreshToken);
1217
1610
  if (refreshResult.type !== "success") {
1218
1611
  console.log(`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: STILL FLAGGED (${refreshResult.message ?? refreshResult.reason ?? "refresh failed"})`);
@@ -1398,8 +1791,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1398
1791
  targetCount = 1;
1399
1792
  }
1400
1793
  if (useManualMode) {
1401
- const { pkce, url } = await createAuthorizationFlow();
1402
- return buildManualOAuthFlow(pkce, url, async (tokens) => {
1794
+ const { pkce, state, url } = await createAuthorizationFlow();
1795
+ return buildManualOAuthFlow(pkce, url, state, async (tokens) => {
1403
1796
  try {
1404
1797
  await persistAccountPool([tokens], startFresh);
1405
1798
  invalidateAccountManagerCache();
@@ -1524,11 +1917,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1524
1917
  const manualPluginConfig = loadPluginConfig();
1525
1918
  applyUiRuntimeFromConfig(manualPluginConfig);
1526
1919
  const manualPerProjectAccounts = getPerProjectAccounts(manualPluginConfig);
1527
- if (manualPerProjectAccounts) {
1528
- setStoragePath(process.cwd());
1529
- }
1530
- const { pkce, url } = await createAuthorizationFlow();
1531
- return buildManualOAuthFlow(pkce, url, async (tokens) => {
1920
+ setStoragePath(manualPerProjectAccounts ? process.cwd() : null);
1921
+ const { pkce, state, url } = await createAuthorizationFlow();
1922
+ return buildManualOAuthFlow(pkce, url, state, async (tokens) => {
1532
1923
  try {
1533
1924
  await persistAccountPool([tokens], false);
1534
1925
  }
@@ -1710,8 +2101,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1710
2101
  return `Switched to ${formatAccountLabel(account, targetIndex)} but failed to persist. Changes may be lost on restart.`;
1711
2102
  }
1712
2103
  if (cachedAccountManager) {
1713
- cachedAccountManager.setActiveIndex(targetIndex);
1714
- await cachedAccountManager.saveToDisk();
2104
+ const reloadedManager = await AccountManager.loadFromDisk();
2105
+ cachedAccountManager = reloadedManager;
2106
+ accountManagerPromise = Promise.resolve(reloadedManager);
1715
2107
  }
1716
2108
  const label = formatAccountLabel(account, targetIndex);
1717
2109
  if (ui.v2Enabled) {
@@ -2032,12 +2424,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2032
2424
  return `Removed ${formatAccountLabel(account, targetIndex)} from memory but failed to persist. Changes may be lost on restart.`;
2033
2425
  }
2034
2426
  if (cachedAccountManager) {
2035
- const managedAccounts = cachedAccountManager.getAccountsSnapshot();
2036
- const managedAccount = managedAccounts.find((acc) => acc.refreshToken === account.refreshToken);
2037
- if (managedAccount) {
2038
- cachedAccountManager.removeAccount(managedAccount);
2039
- await cachedAccountManager.saveToDisk();
2040
- }
2427
+ const reloadedManager = await AccountManager.loadFromDisk();
2428
+ cachedAccountManager = reloadedManager;
2429
+ accountManagerPromise = Promise.resolve(reloadedManager);
2041
2430
  }
2042
2431
  const remaining = storage.accounts.length;
2043
2432
  if (ui.v2Enabled) {
@@ -2090,6 +2479,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2090
2479
  const refreshResult = await queuedRefresh(account.refreshToken);
2091
2480
  if (refreshResult.type === "success") {
2092
2481
  account.refreshToken = refreshResult.refresh;
2482
+ account.accessToken = refreshResult.access;
2483
+ account.expiresAt = refreshResult.expires;
2093
2484
  results.push(` ${getStatusMarker(ui, "ok")} ${label}: Refreshed`);
2094
2485
  refreshedCount++;
2095
2486
  }
@@ -2105,6 +2496,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2105
2496
  }
2106
2497
  }
2107
2498
  await saveAccounts(storage);
2499
+ if (cachedAccountManager) {
2500
+ const reloadedManager = await AccountManager.loadFromDisk();
2501
+ cachedAccountManager = reloadedManager;
2502
+ accountManagerPromise = Promise.resolve(reloadedManager);
2503
+ }
2108
2504
  results.push("");
2109
2505
  results.push(`Summary: ${refreshedCount} refreshed, ${failedCount} failed`);
2110
2506
  if (ui.v2Enabled) {