codex-multi-auth 1.1.11 → 1.2.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 (44) hide show
  1. package/README.md +19 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +185 -29
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/accounts.d.ts +22 -2
  6. package/dist/lib/accounts.d.ts.map +1 -1
  7. package/dist/lib/accounts.js +84 -1
  8. package/dist/lib/accounts.js.map +1 -1
  9. package/dist/lib/auth/browser.d.ts +1 -0
  10. package/dist/lib/auth/browser.d.ts.map +1 -1
  11. package/dist/lib/auth/browser.js +13 -0
  12. package/dist/lib/auth/browser.js.map +1 -1
  13. package/dist/lib/auth/token-utils.d.ts +16 -0
  14. package/dist/lib/auth/token-utils.d.ts.map +1 -1
  15. package/dist/lib/auth/token-utils.js +13 -0
  16. package/dist/lib/auth/token-utils.js.map +1 -1
  17. package/dist/lib/codex-manager/settings-hub.d.ts +21 -0
  18. package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -1
  19. package/dist/lib/codex-manager/settings-hub.js +48 -65
  20. package/dist/lib/codex-manager/settings-hub.js.map +1 -1
  21. package/dist/lib/codex-manager.d.ts +1 -0
  22. package/dist/lib/codex-manager.d.ts.map +1 -1
  23. package/dist/lib/codex-manager.js +365 -33
  24. package/dist/lib/codex-manager.js.map +1 -1
  25. package/dist/lib/request/fetch-helpers.d.ts +18 -0
  26. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  27. package/dist/lib/request/fetch-helpers.js +157 -0
  28. package/dist/lib/request/fetch-helpers.js.map +1 -1
  29. package/dist/lib/schemas.d.ts +7 -0
  30. package/dist/lib/schemas.d.ts.map +1 -1
  31. package/dist/lib/schemas.js +7 -1
  32. package/dist/lib/schemas.js.map +1 -1
  33. package/dist/lib/storage/migrations.d.ts +5 -2
  34. package/dist/lib/storage/migrations.d.ts.map +1 -1
  35. package/dist/lib/storage/migrations.js.map +1 -1
  36. package/dist/lib/storage.d.ts +10 -0
  37. package/dist/lib/storage.d.ts.map +1 -1
  38. package/dist/lib/storage.js +191 -40
  39. package/dist/lib/storage.js.map +1 -1
  40. package/dist/lib/ui/copy.d.ts +24 -4
  41. package/dist/lib/ui/copy.d.ts.map +1 -1
  42. package/dist/lib/ui/copy.js +24 -4
  43. package/dist/lib/ui/copy.js.map +1 -1
  44. package/package.json +4 -3
package/README.md CHANGED
@@ -109,6 +109,21 @@ codex auth fix --dry-run
109
109
  codex auth doctor --fix
110
110
  ```
111
111
 
112
+ If the shell should not launch a browser, use the manual callback flow:
113
+
114
+ ```bash
115
+ codex auth login --manual
116
+ CODEX_AUTH_NO_BROWSER=1 codex auth login
117
+ ```
118
+
119
+ In non-TTY/manual shells, provide the full redirect URL on stdin instead of waiting for a browser callback:
120
+
121
+ ```bash
122
+ echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual
123
+ ```
124
+
125
+ No new npm scripts or storage migration steps are required for this login-flow update.
126
+
112
127
  ---
113
128
 
114
129
  ## Command Toolkit
@@ -234,6 +249,7 @@ codex auth login
234
249
  - `codex auth` unrecognized: run `where codex`, then follow `docs/troubleshooting.md` for routing fallback commands
235
250
  - Switch succeeds but wrong account appears active: run `codex auth switch <index>`, then restart session
236
251
  - OAuth callback on port `1455` fails: free the port and re-run `codex auth login`
252
+ - Browser launch is blocked or you are in a headless shell: re-run `codex auth login --manual` or set `CODEX_AUTH_NO_BROWSER=1`
237
253
  - `missing field id_token` / `token_expired` / `refresh_token_reused`: re-login affected account
238
254
 
239
255
  </details>
@@ -275,10 +291,9 @@ codex auth doctor --json
275
291
 
276
292
  ## Release Notes
277
293
 
278
- - Current stable: [docs/releases/v1.1.11.md](docs/releases/v1.1.11.md)
279
- - Previous stable: [docs/releases/v1.1.10.md](docs/releases/v1.1.10.md)
280
- - Earlier stable: [docs/releases/v0.1.9.md](docs/releases/v0.1.9.md)
281
- - Earlier stable: [docs/releases/v0.1.8.md](docs/releases/v0.1.8.md)
294
+ - Current stable: [docs/releases/v1.2.0.md](docs/releases/v1.2.0.md)
295
+ - Previous stable: [docs/releases/v1.1.10.md](docs/releases/v1.1.10.md)
296
+ - Earlier stable: [docs/releases/v0.1.9.md](docs/releases/v0.1.9.md)
282
297
  - Archived prerelease: [docs/releases/v0.1.0-beta.0.md](docs/releases/v0.1.0-beta.0.md)
283
298
 
284
299
  ## License
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;AAmL5D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA60H/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;AAuL5D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAwhI/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@
25
25
  import { tool } from "@codex-ai/plugin/tool";
26
26
  import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, redactOAuthUrlForLog, REDIRECT_URI, } from "./lib/auth/auth.js";
27
27
  import { queuedRefresh } from "./lib/refresh-queue.js";
28
- import { openBrowserUrl } from "./lib/auth/browser.js";
28
+ import { isBrowserLaunchSuppressed, 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
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, getLiveAccountSync, getLiveAccountSyncDebounceMs, getLiveAccountSyncPollMs, getSessionAffinity, getSessionAffinityTtlMs, getSessionAffinityMaxEntries, getProactiveRefreshGuardian, getProactiveRefreshIntervalMs, getProactiveRefreshBufferMs, getNetworkErrorCooldownMs, getServerErrorCooldownMs, getStorageBackupEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getPreemptiveQuotaMaxDeferralMs, loadPluginConfig, } from "./lib/config.js";
@@ -33,9 +33,9 @@ import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PR
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, lookupCodexCliTokensByEmail, isCodexCliSyncEnabled, } from "./lib/accounts.js";
36
+ import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, resolveRuntimeRequestIdentity, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, isCodexCliSyncEnabled, } from "./lib/accounts.js";
37
37
  import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, findMatchingAccountIndex, StorageError, formatStorageErrorHint, setStorageBackupEnabled, } from "./lib/storage.js";
38
- import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
38
+ import { applyProxyCompatibleInit, createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, isWorkspaceDisabledError, } 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";
41
41
  import { isEmptyResponse } from "./lib/request/response-handler.js";
@@ -213,6 +213,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
213
213
  if (candidates.length === 0) {
214
214
  return tokens;
215
215
  }
216
+ // Convert candidates to workspaces
217
+ const workspaces = candidates.map((c) => ({
218
+ id: c.accountId,
219
+ name: c.label,
220
+ enabled: true,
221
+ isDefault: c.isDefault,
222
+ }));
216
223
  if (candidates.length === 1) {
217
224
  const [candidate] = candidates;
218
225
  if (candidate) {
@@ -221,6 +228,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
221
228
  accountIdOverride: candidate.accountId,
222
229
  accountIdSource: candidate.source,
223
230
  accountLabel: candidate.label,
231
+ workspaces,
224
232
  };
225
233
  }
226
234
  }
@@ -234,6 +242,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
234
242
  accountIdOverride: choice.accountId,
235
243
  accountIdSource: choice.source ?? "token",
236
244
  accountLabel: choice.label,
245
+ workspaces,
237
246
  };
238
247
  };
239
248
  const buildManualOAuthFlow = (pkce, url, expectedState, onSuccess) => ({
@@ -331,6 +340,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
331
340
  allowUniqueAccountIdFallbackWithoutEmail: true,
332
341
  });
333
342
  if (existingIndex === undefined) {
343
+ const initialWorkspaceIndex = result.workspaces && result.workspaces.length > 0
344
+ ? (() => {
345
+ if (accountId) {
346
+ const matchingWorkspaceIndex = result.workspaces.findIndex((workspace) => workspace.id === accountId);
347
+ if (matchingWorkspaceIndex >= 0) {
348
+ return matchingWorkspaceIndex;
349
+ }
350
+ }
351
+ const firstEnabledWorkspaceIndex = result.workspaces.findIndex((workspace) => workspace.enabled !== false);
352
+ return firstEnabledWorkspaceIndex >= 0 ? firstEnabledWorkspaceIndex : 0;
353
+ })()
354
+ : undefined;
334
355
  accounts.push({
335
356
  accountId,
336
357
  accountIdSource,
@@ -341,6 +362,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
341
362
  expiresAt: result.expires,
342
363
  addedAt: now,
343
364
  lastUsed: now,
365
+ workspaces: result.workspaces,
366
+ currentWorkspaceIndex: initialWorkspaceIndex,
344
367
  });
345
368
  continue;
346
369
  }
@@ -351,6 +374,38 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
351
374
  const nextAccountId = accountId ?? existing.accountId;
352
375
  const nextAccountIdSource = accountId ? accountIdSource ?? existing.accountIdSource : existing.accountIdSource;
353
376
  const nextAccountLabel = accountLabel ?? existing.accountLabel;
377
+ // Preserve tracked workspace state when auth refreshes do not return workspace metadata.
378
+ const mergedWorkspaces = result.workspaces
379
+ ? result.workspaces.map((newWs) => {
380
+ const existingWs = existing.workspaces?.find((w) => w.id === newWs.id);
381
+ return existingWs
382
+ ? {
383
+ ...newWs,
384
+ enabled: existingWs.enabled,
385
+ disabledAt: existingWs.disabledAt,
386
+ }
387
+ : newWs;
388
+ })
389
+ : existing.workspaces;
390
+ const currentWorkspaceId = existing.workspaces?.[typeof existing.currentWorkspaceIndex === "number"
391
+ ? existing.currentWorkspaceIndex
392
+ : 0]?.id;
393
+ const nextCurrentWorkspaceIndex = mergedWorkspaces && mergedWorkspaces.length > 0
394
+ ? (() => {
395
+ if (currentWorkspaceId) {
396
+ const matchingWorkspaceIndex = mergedWorkspaces.findIndex((workspace) => workspace.id === currentWorkspaceId);
397
+ if (matchingWorkspaceIndex >= 0) {
398
+ return matchingWorkspaceIndex;
399
+ }
400
+ }
401
+ const defaultWorkspaceIndex = mergedWorkspaces.findIndex((workspace) => workspace.isDefault === true);
402
+ if (defaultWorkspaceIndex >= 0) {
403
+ return defaultWorkspaceIndex;
404
+ }
405
+ const firstEnabledWorkspaceIndex = mergedWorkspaces.findIndex((workspace) => workspace.enabled !== false);
406
+ return firstEnabledWorkspaceIndex >= 0 ? firstEnabledWorkspaceIndex : 0;
407
+ })()
408
+ : existing.currentWorkspaceIndex;
354
409
  accounts[existingIndex] = {
355
410
  ...existing,
356
411
  accountId: nextAccountId,
@@ -361,6 +416,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
361
416
  accessToken: result.access,
362
417
  expiresAt: result.expires,
363
418
  lastUsed: now,
419
+ workspaces: mergedWorkspaces,
420
+ currentWorkspaceIndex: nextCurrentWorkspaceIndex,
364
421
  };
365
422
  }
366
423
  if (accounts.length === 0)
@@ -999,7 +1056,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
999
1056
  const accountKey = resolveEntitlementAccountKey(candidate);
1000
1057
  capabilityBoostByAccount[candidate.index] = capabilityPolicyStore.getBoost(accountKey, model ?? modelFamily);
1001
1058
  }
1002
- while (attempted.size < Math.max(1, accountCount)) {
1059
+ accountAttemptLoop: while (attempted.size < Math.max(1, accountCount)) {
1003
1060
  let account = null;
1004
1061
  if (!usedPreferredSessionAccount &&
1005
1062
  typeof preferredSessionAccountIndex === "number") {
@@ -1065,26 +1122,34 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1065
1122
  accountManager.saveToDiskDebounced();
1066
1123
  continue;
1067
1124
  }
1068
- const hadAccountId = !!account.accountId;
1069
- const tokenAccountId = extractAccountId(accountAuth.access);
1070
- const accountId = resolveRequestAccountId(account.accountId, account.accountIdSource, tokenAccountId);
1125
+ const currentWorkspace = accountManager.getCurrentWorkspace(account);
1126
+ const storedAccountId = currentWorkspace?.id ?? account.accountId;
1127
+ const storedAccountIdSource = currentWorkspace
1128
+ ? "manual"
1129
+ : account.accountIdSource;
1130
+ const storedEmail = account.email;
1131
+ const hadAccountId = !!storedAccountId;
1132
+ const runtimeIdentity = resolveRuntimeRequestIdentity({
1133
+ storedAccountId,
1134
+ source: storedAccountIdSource,
1135
+ storedEmail,
1136
+ accessToken: accountAuth.access,
1137
+ idToken: accountAuth.idToken,
1138
+ });
1139
+ const tokenAccountId = runtimeIdentity.tokenAccountId;
1140
+ const accountId = runtimeIdentity.accountId;
1071
1141
  if (!accountId) {
1072
1142
  accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
1073
1143
  accountManager.saveToDiskDebounced();
1074
1144
  continue;
1075
1145
  }
1076
- const resolvedEmail = extractAccountEmail(accountAuth.access) ?? account.email;
1146
+ const resolvedEmail = runtimeIdentity.email;
1077
1147
  const entitlementAccountKey = resolveEntitlementAccountKey({
1078
- accountId: account.accountId ?? accountId,
1148
+ accountId: storedAccountId ?? accountId,
1079
1149
  email: resolvedEmail,
1080
1150
  refreshToken: account.refreshToken,
1081
1151
  index: account.index,
1082
1152
  });
1083
- account.accountId = accountId;
1084
- if (!hadAccountId && tokenAccountId && accountId === tokenAccountId) {
1085
- account.accountIdSource = account.accountIdSource ?? "token";
1086
- }
1087
- account.email = resolvedEmail;
1088
1153
  const entitlementBlock = entitlementCache.isBlocked(entitlementAccountKey, model ?? modelFamily);
1089
1154
  if (entitlementBlock.blocked) {
1090
1155
  runtimeMetrics.accountRotations++;
@@ -1092,6 +1157,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1092
1157
  logWarn(`Skipping account ${account.index + 1} due to cached entitlement block (${formatWaitTime(entitlementBlock.waitMs)} remaining).`);
1093
1158
  continue;
1094
1159
  }
1160
+ account.accountId = accountId;
1161
+ if (!hadAccountId && tokenAccountId && accountId === tokenAccountId) {
1162
+ account.accountIdSource = storedAccountIdSource ?? "token";
1163
+ }
1164
+ if (resolvedEmail) {
1165
+ account.email = resolvedEmail;
1166
+ }
1095
1167
  if (accountCount > 1 &&
1096
1168
  accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
1097
1169
  const accountLabel = formatAccountLabel(account, account.index);
@@ -1125,6 +1197,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1125
1197
  }
1126
1198
  let sameAccountRetryCount = 0;
1127
1199
  let successAccountForResponse = account;
1200
+ let successEntitlementAccountKey = entitlementAccountKey;
1128
1201
  while (true) {
1129
1202
  let response;
1130
1203
  const fetchStart = performance.now();
@@ -1149,11 +1222,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1149
1222
  }
1150
1223
  try {
1151
1224
  runtimeMetrics.totalRequests++;
1152
- response = await fetch(url, {
1225
+ response = await fetch(url, applyProxyCompatibleInit(url, {
1153
1226
  ...requestInit,
1154
1227
  headers,
1155
1228
  signal: fetchController.signal,
1156
- });
1229
+ }));
1157
1230
  }
1158
1231
  catch (networkError) {
1159
1232
  const fetchAbortReason = fetchController.signal.reason;
@@ -1331,7 +1404,54 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1331
1404
  entitlementCache.markBlocked(entitlementAccountKey, blockedModel, "unsupported-model");
1332
1405
  capabilityPolicyStore.recordUnsupported(entitlementAccountKey, blockedModel);
1333
1406
  }
1334
- if (errorResponse.status === 403 && !unsupportedModelInfo.isUnsupported) {
1407
+ const workspaceErrorCode = errorBody?.error?.code ?? "";
1408
+ const workspaceErrorMessage = errorBody?.error?.message ?? "";
1409
+ const isDisabledWorkspaceError = isWorkspaceDisabledError(errorResponse.status, workspaceErrorCode, workspaceErrorMessage);
1410
+ // Handle workspace disabled/expired errors by rotating to the next workspace
1411
+ // within the same account before falling back to another account.
1412
+ if (isDisabledWorkspaceError) {
1413
+ runtimeMetrics.failedRequests++;
1414
+ runtimeMetrics.lastError = `Workspace disabled for account ${account.index + 1}`;
1415
+ if (!account.workspaces || account.workspaces.length === 0) {
1416
+ logWarn(`Workspace disabled/expired for account ${account.index + 1} without tracked workspaces. Leaving account enabled.`, { errorCode: workspaceErrorCode });
1417
+ if (hasRemainingAccounts) {
1418
+ continue accountAttemptLoop;
1419
+ }
1420
+ return errorResponse;
1421
+ }
1422
+ else {
1423
+ const currentWorkspace = accountManager.getCurrentWorkspace(account);
1424
+ const workspaceName = currentWorkspace?.name ?? currentWorkspace?.id ?? "unknown";
1425
+ logWarn(`Workspace disabled/expired for account ${account.index + 1} - workspace: ${workspaceName}. Rotating to next workspace.`, { errorCode: workspaceErrorCode });
1426
+ const disabledWorkspace = currentWorkspace
1427
+ ? accountManager.disableCurrentWorkspace(account, currentWorkspace.id)
1428
+ : false;
1429
+ let nextWorkspace = disabledWorkspace
1430
+ ? accountManager.rotateToNextWorkspace(account)
1431
+ : accountManager.getCurrentWorkspace(account);
1432
+ if (!disabledWorkspace && (!nextWorkspace || nextWorkspace.enabled === false)) {
1433
+ nextWorkspace = accountManager.rotateToNextWorkspace(account);
1434
+ }
1435
+ if (nextWorkspace) {
1436
+ accountManager.saveToDiskDebounced();
1437
+ const newWorkspaceName = nextWorkspace.name ?? nextWorkspace.id;
1438
+ await showToast(`Workspace ${workspaceName} disabled. Switched to ${newWorkspaceName}.`, "warning", { duration: toastDurationMs });
1439
+ logInfo(`Rotated to workspace ${newWorkspaceName} for account ${account.index + 1}`);
1440
+ // Allow the same account to be selected again with fresh request state.
1441
+ attempted.delete(account.index);
1442
+ continue accountAttemptLoop;
1443
+ }
1444
+ logWarn(`All workspaces disabled for account ${account.index + 1}. Disabling account.`);
1445
+ accountManager.setAccountEnabled(account.index, false);
1446
+ accountManager.saveToDiskDebounced();
1447
+ await showToast(`All workspaces disabled for account ${account.index + 1}. Switching to another account.`, "warning", { duration: toastDurationMs });
1448
+ // Forget session affinity and continue the outer loop so another
1449
+ // enabled account can service the request.
1450
+ sessionAffinityStore?.forgetSession(sessionAffinityKey);
1451
+ continue accountAttemptLoop;
1452
+ }
1453
+ }
1454
+ if (errorResponse.status === 403 && !unsupportedModelInfo.isUnsupported && !isDisabledWorkspaceError) {
1335
1455
  entitlementCache.markBlocked(entitlementAccountKey, model ?? modelFamily, "plan-entitlement");
1336
1456
  capabilityPolicyStore.recordFailure(entitlementAccountKey, capabilityModelKey);
1337
1457
  }
@@ -1452,14 +1572,50 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1452
1572
  });
1453
1573
  continue;
1454
1574
  }
1455
- const fallbackTokenAccountId = extractAccountId(fallbackAuth.access);
1456
- const fallbackAccountId = resolveRequestAccountId(fallbackAccount.accountId, fallbackAccount.accountIdSource, fallbackTokenAccountId);
1575
+ const fallbackStoredAccountId = fallbackAccount.accountId;
1576
+ const fallbackStoredAccountIdSource = fallbackAccount.accountIdSource;
1577
+ const fallbackStoredEmail = fallbackAccount.email;
1578
+ const hadFallbackAccountId = !!fallbackStoredAccountId;
1579
+ const fallbackRuntimeIdentity = resolveRuntimeRequestIdentity({
1580
+ storedAccountId: fallbackStoredAccountId,
1581
+ source: fallbackStoredAccountIdSource,
1582
+ storedEmail: fallbackStoredEmail,
1583
+ accessToken: fallbackAuth.access,
1584
+ idToken: fallbackAuth.idToken,
1585
+ });
1586
+ const fallbackTokenAccountId = fallbackRuntimeIdentity.tokenAccountId;
1587
+ const fallbackAccountId = fallbackRuntimeIdentity.accountId;
1457
1588
  if (!fallbackAccountId) {
1458
1589
  continue;
1459
1590
  }
1591
+ const fallbackResolvedEmail = fallbackRuntimeIdentity.email;
1592
+ const fallbackEntitlementAccountKey = resolveEntitlementAccountKey({
1593
+ accountId: fallbackStoredAccountId ?? fallbackAccountId,
1594
+ email: fallbackResolvedEmail,
1595
+ refreshToken: fallbackAccount.refreshToken,
1596
+ index: fallbackAccount.index,
1597
+ });
1598
+ const fallbackEntitlementBlock = entitlementCache.isBlocked(fallbackEntitlementAccountKey, model ?? modelFamily);
1599
+ if (fallbackEntitlementBlock.blocked) {
1600
+ runtimeMetrics.accountRotations++;
1601
+ runtimeMetrics.lastError =
1602
+ `Entitlement cached block for account ${fallbackAccount.index + 1}`;
1603
+ logWarn(`Skipping account ${fallbackAccount.index + 1} due to cached entitlement block (${formatWaitTime(fallbackEntitlementBlock.waitMs)} remaining).`);
1604
+ continue;
1605
+ }
1460
1606
  if (!accountManager.consumeToken(fallbackAccount, modelFamily, model)) {
1461
1607
  continue;
1462
1608
  }
1609
+ fallbackAccount.accountId = fallbackAccountId;
1610
+ if (!hadFallbackAccountId &&
1611
+ fallbackTokenAccountId &&
1612
+ fallbackAccountId === fallbackTokenAccountId) {
1613
+ fallbackAccount.accountIdSource =
1614
+ fallbackStoredAccountIdSource ?? "token";
1615
+ }
1616
+ if (fallbackResolvedEmail) {
1617
+ fallbackAccount.email = fallbackResolvedEmail;
1618
+ }
1463
1619
  const fallbackHeaders = createCodexHeaders(requestInit, fallbackAccountId, fallbackAuth.access, {
1464
1620
  model,
1465
1621
  promptCacheKey: effectivePromptCacheKey,
@@ -1476,14 +1632,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1476
1632
  }
1477
1633
  try {
1478
1634
  runtimeMetrics.totalRequests++;
1479
- const fallbackResponse = await fetch(url, {
1635
+ const fallbackResponse = await fetch(url, applyProxyCompatibleInit(url, {
1480
1636
  ...requestInit,
1481
1637
  headers: fallbackHeaders,
1482
1638
  signal: fallbackController.signal,
1483
- });
1639
+ }));
1484
1640
  const fallbackSnapshot = readQuotaSchedulerSnapshot(fallbackResponse.headers, fallbackResponse.status);
1485
1641
  if (fallbackSnapshot) {
1486
- preemptiveQuotaScheduler.update(`${resolveEntitlementAccountKey(fallbackAccount)}:${model ?? modelFamily}`, fallbackSnapshot);
1642
+ preemptiveQuotaScheduler.update(`${fallbackEntitlementAccountKey}:${model ?? modelFamily}`, fallbackSnapshot);
1487
1643
  }
1488
1644
  if (!fallbackResponse.ok) {
1489
1645
  try {
@@ -1500,10 +1656,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1500
1656
  else {
1501
1657
  accountManager.recordFailure(fallbackAccount, modelFamily, model);
1502
1658
  }
1503
- capabilityPolicyStore.recordFailure(resolveEntitlementAccountKey(fallbackAccount), capabilityModelKey);
1659
+ capabilityPolicyStore.recordFailure(fallbackEntitlementAccountKey, capabilityModelKey);
1504
1660
  continue;
1505
1661
  }
1506
1662
  successAccountForResponse = fallbackAccount;
1663
+ successEntitlementAccountKey = fallbackEntitlementAccountKey;
1507
1664
  runtimeMetrics.streamFailoverRecoveries += 1;
1508
1665
  if (fallbackAccount.index !== account.index) {
1509
1666
  runtimeMetrics.streamFailoverCrossAccountRecoveries += 1;
@@ -1516,7 +1673,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1516
1673
  catch (streamFailoverError) {
1517
1674
  accountManager.refundToken(fallbackAccount, modelFamily, model);
1518
1675
  accountManager.recordFailure(fallbackAccount, modelFamily, model);
1519
- capabilityPolicyStore.recordFailure(resolveEntitlementAccountKey(fallbackAccount), capabilityModelKey);
1676
+ capabilityPolicyStore.recordFailure(fallbackEntitlementAccountKey, capabilityModelKey);
1520
1677
  logWarn(`Stream failover attempt ${failoverAttempt} failed for account ${fallbackAccount.index + 1}.`, {
1521
1678
  emittedBytes,
1522
1679
  error: streamFailoverError instanceof Error
@@ -1585,9 +1742,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1585
1742
  if (successAccountForResponse.index !== account.index) {
1586
1743
  accountManager.markSwitched(successAccountForResponse, "rotation", modelFamily);
1587
1744
  }
1588
- const successAccountKey = successAccountForResponse.index === account.index
1589
- ? entitlementAccountKey
1590
- : resolveEntitlementAccountKey(successAccountForResponse);
1745
+ const successAccountKey = successEntitlementAccountKey;
1591
1746
  accountManager.recordSuccess(successAccountForResponse, modelFamily, model);
1592
1747
  capabilityPolicyStore.recordSuccess(successAccountKey, capabilityModelKey);
1593
1748
  entitlementCache.clear(successAccountKey, capabilityModelKey);
@@ -1660,9 +1815,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1660
1815
  applyUiRuntimeFromConfig(authPluginConfig);
1661
1816
  applyAccountStorageScope(authPluginConfig);
1662
1817
  const accounts = [];
1663
- const noBrowser = inputs?.noBrowser === "true" ||
1818
+ const noBrowser = inputs?.manual === "true" ||
1819
+ inputs?.noBrowser === "true" ||
1664
1820
  inputs?.["no-browser"] === "true";
1665
- const useManualMode = noBrowser;
1821
+ const useManualMode = noBrowser || isBrowserLaunchSuppressed();
1666
1822
  const explicitLoginMode = inputs?.loginMode === "fresh" || inputs?.loginMode === "add"
1667
1823
  ? inputs.loginMode
1668
1824
  : null;