oc-chatgpt-multi-auth 5.2.1 → 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 (63) hide show
  1. package/README.md +21 -16
  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 +163 -125
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/accounts.d.ts.map +1 -1
  9. package/dist/lib/accounts.js +5 -18
  10. package/dist/lib/accounts.js.map +1 -1
  11. package/dist/lib/config.d.ts +1 -0
  12. package/dist/lib/config.d.ts.map +1 -1
  13. package/dist/lib/config.js +5 -0
  14. package/dist/lib/config.js.map +1 -1
  15. package/dist/lib/logger.d.ts +1 -0
  16. package/dist/lib/logger.d.ts.map +1 -1
  17. package/dist/lib/logger.js +25 -2
  18. package/dist/lib/logger.js.map +1 -1
  19. package/dist/lib/prompts/codex-opencode-bridge.d.ts +4 -3
  20. package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -1
  21. package/dist/lib/prompts/codex-opencode-bridge.js +73 -106
  22. package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -1
  23. package/dist/lib/prompts/codex.d.ts +4 -4
  24. package/dist/lib/prompts/codex.d.ts.map +1 -1
  25. package/dist/lib/prompts/codex.js +27 -30
  26. package/dist/lib/prompts/codex.js.map +1 -1
  27. package/dist/lib/recovery.d.ts.map +1 -1
  28. package/dist/lib/recovery.js +10 -5
  29. package/dist/lib/recovery.js.map +1 -1
  30. package/dist/lib/request/fetch-helpers.d.ts +2 -1
  31. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  32. package/dist/lib/request/fetch-helpers.js +57 -6
  33. package/dist/lib/request/fetch-helpers.js.map +1 -1
  34. package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
  35. package/dist/lib/request/helpers/model-map.js +35 -25
  36. package/dist/lib/request/helpers/model-map.js.map +1 -1
  37. package/dist/lib/request/request-transformer.d.ts +3 -3
  38. package/dist/lib/request/request-transformer.d.ts.map +1 -1
  39. package/dist/lib/request/request-transformer.js +73 -35
  40. package/dist/lib/request/request-transformer.js.map +1 -1
  41. package/dist/lib/request/response-handler.d.ts.map +1 -1
  42. package/dist/lib/request/response-handler.js +101 -10
  43. package/dist/lib/request/response-handler.js.map +1 -1
  44. package/dist/lib/schemas.d.ts +7 -9
  45. package/dist/lib/schemas.d.ts.map +1 -1
  46. package/dist/lib/schemas.js +1 -0
  47. package/dist/lib/schemas.js.map +1 -1
  48. package/dist/lib/storage/migrations.d.ts.map +1 -1
  49. package/dist/lib/storage/migrations.js +1 -9
  50. package/dist/lib/storage/migrations.js.map +1 -1
  51. package/dist/lib/storage/paths.d.ts.map +1 -1
  52. package/dist/lib/storage/paths.js +14 -2
  53. package/dist/lib/storage/paths.js.map +1 -1
  54. package/dist/lib/storage.d.ts +1 -0
  55. package/dist/lib/storage.d.ts.map +1 -1
  56. package/dist/lib/storage.js +124 -84
  57. package/dist/lib/storage.js.map +1 -1
  58. package/package.json +26 -12
  59. package/scripts/audit-dev-allowlist.js +114 -0
  60. package/dist/lib/request/local-fast-path.d.ts +0 -15
  61. package/dist/lib/request/local-fast-path.d.ts.map +0 -1
  62. package/dist/lib/request/local-fast-path.js +0 -164
  63. 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
36
  import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
37
- import { getStoragePath, loadAccounts, saveAccounts, clearAccounts, setStoragePath, exportAccounts, importAccounts, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.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";
@@ -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,121 +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
277
  accessToken: result.access,
230
278
  expiresAt: result.expires,
231
- addedAt: now,
232
279
  lastUsed: now,
233
- });
234
- indexByRefreshToken.set(result.refresh, newIndex);
280
+ };
281
+ if (oldToken !== result.refresh) {
282
+ indexByRefreshToken.delete(oldToken);
283
+ indexByRefreshToken.set(result.refresh, existingIndex);
284
+ }
235
285
  if (accountId) {
236
- indexByAccountId.set(accountId, newIndex);
286
+ indexByAccountId.set(accountId, existingIndex);
237
287
  }
238
- if (accountEmail) {
239
- indexByEmail.set(accountEmail, newIndex);
288
+ if (oldEmail && oldEmail !== nextEmail) {
289
+ indexByEmail.delete(oldEmail);
290
+ }
291
+ if (nextEmail) {
292
+ indexByEmail.set(nextEmail, existingIndex);
240
293
  }
241
- continue;
242
- }
243
- const existing = accounts[existingIndex];
244
- if (!existing)
245
- continue;
246
- const oldToken = existing.refreshToken;
247
- const oldEmail = existing.email;
248
- const nextEmail = accountEmail ?? existing.email;
249
- const nextAccountId = accountId ?? existing.accountId;
250
- const nextAccountIdSource = accountId ? accountIdSource ?? existing.accountIdSource : existing.accountIdSource;
251
- const nextAccountLabel = accountLabel ?? existing.accountLabel;
252
- accounts[existingIndex] = {
253
- ...existing,
254
- accountId: nextAccountId,
255
- accountIdSource: nextAccountIdSource,
256
- accountLabel: nextAccountLabel,
257
- email: nextEmail,
258
- refreshToken: result.refresh,
259
- accessToken: result.access,
260
- expiresAt: result.expires,
261
- lastUsed: now,
262
- };
263
- if (oldToken !== result.refresh) {
264
- indexByRefreshToken.delete(oldToken);
265
- indexByRefreshToken.set(result.refresh, existingIndex);
266
- }
267
- if (accountId) {
268
- indexByAccountId.set(accountId, existingIndex);
269
- }
270
- if (oldEmail && oldEmail !== nextEmail) {
271
- indexByEmail.delete(oldEmail);
272
- }
273
- if (nextEmail) {
274
- indexByEmail.set(nextEmail, existingIndex);
275
294
  }
276
- }
277
- if (accounts.length === 0)
278
- return;
279
- const activeIndex = replaceAll
280
- ? 0
281
- : typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex)
282
- ? stored.activeIndex
283
- : 0;
284
- const clampedActiveIndex = Math.max(0, Math.min(activeIndex, accounts.length - 1));
285
- const activeIndexByFamily = {};
286
- for (const family of MODEL_FAMILIES) {
287
- const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
288
- const rawFamilyIndex = replaceAll
295
+ if (accounts.length === 0)
296
+ return;
297
+ const activeIndex = replaceAll
289
298
  ? 0
290
- : typeof storedFamilyIndex === "number" && Number.isFinite(storedFamilyIndex)
291
- ? storedFamilyIndex
292
- : clampedActiveIndex;
293
- activeIndexByFamily[family] = Math.max(0, Math.min(Math.floor(rawFamilyIndex), accounts.length - 1));
294
- }
295
- await saveAccounts({
296
- version: 3,
297
- accounts,
298
- activeIndex: clampedActiveIndex,
299
- 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
+ });
300
319
  });
301
320
  };
302
321
  const showToast = async (message, variant = "success", options) => {
@@ -549,6 +568,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
549
568
  // Load plugin configuration and determine CODEX_MODE
550
569
  // Priority: CODEX_MODE env var > config file > default (true)
551
570
  const codexMode = getCodexMode(pluginConfig);
571
+ const requestTransformMode = getRequestTransformMode(pluginConfig);
572
+ const useLegacyRequestTransform = requestTransformMode === "legacy";
552
573
  const fastSessionEnabled = getFastSession(pluginConfig);
553
574
  const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
554
575
  const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
@@ -584,7 +605,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
584
605
  const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" &&
585
606
  process.env.VITEST !== "true" &&
586
607
  process.env.NODE_ENV !== "test";
587
- if (!startupPrewarmTriggered && prewarmEnabled) {
608
+ if (!startupPrewarmTriggered && prewarmEnabled && useLegacyRequestTransform) {
588
609
  startupPrewarmTriggered = true;
589
610
  const configuredModels = Object.keys(userConfig.models ?? {});
590
611
  prewarmCodexInstructions(configuredModels);
@@ -688,6 +709,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
688
709
  fastSession: fastSessionEnabled,
689
710
  fastSessionStrategy,
690
711
  fastSessionMaxInputItems,
712
+ requestTransformMode,
691
713
  });
692
714
  let requestInit = transformation?.updatedInit ?? baseInit;
693
715
  let transformedBody = transformation?.body;
@@ -748,6 +770,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
748
770
  while (true) {
749
771
  const accountCount = accountManager.getAccountCount();
750
772
  const attempted = new Set();
773
+ let restartAccountTraversalWithFallback = false;
751
774
  while (attempted.size < Math.max(1, accountCount)) {
752
775
  const account = accountManager.getCurrentOrNextForFamilyHybrid(modelFamily, model, { pidOffsetEnabled });
753
776
  if (!account || attempted.has(account.index)) {
@@ -803,12 +826,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
803
826
  await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
804
827
  accountManager.markToastShown(account.index);
805
828
  }
806
- let headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
829
+ const headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
807
830
  model,
808
831
  promptCacheKey,
809
832
  });
810
833
  // Consume a token before making the request for proactive rate limiting
811
- 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
+ }
812
843
  while (true) {
813
844
  let response;
814
845
  const fetchStart = performance.now();
@@ -896,7 +927,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
896
927
  customChain: unsupportedCodexFallbackChain,
897
928
  });
898
929
  if (fallbackModel) {
899
- const previousModel = model ?? "gpt-5.3-codex";
930
+ const previousModel = model ?? "gpt-5-codex";
900
931
  const previousModelFamily = modelFamily;
901
932
  attemptedUnsupportedFallbackModels.add(previousModel);
902
933
  attemptedUnsupportedFallbackModels.add(fallbackModel);
@@ -924,11 +955,6 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
924
955
  ...(requestInit ?? {}),
925
956
  body: JSON.stringify(transformedBody),
926
957
  };
927
- headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
928
- model,
929
- promptCacheKey,
930
- });
931
- accountManager.consumeToken(account, modelFamily, model);
932
958
  runtimeMetrics.lastError = `Model fallback: ${previousModel} -> ${model}`;
933
959
  logWarn(`Model ${previousModel} is unsupported for this ChatGPT account. Falling back to ${model}.`, {
934
960
  unsupportedCodexPolicy,
@@ -938,7 +964,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
938
964
  fallbackReason: "unsupported-model-entitlement",
939
965
  });
940
966
  await showToast(`Model ${previousModel} is not available for this account. Retrying with ${model}.`, "warning", { duration: toastDurationMs });
941
- continue;
967
+ restartAccountTraversalWithFallback = true;
968
+ break;
942
969
  }
943
970
  if (unsupportedModelInfo.isUnsupported && !fallbackOnUnsupportedCodexModel) {
944
971
  const blockedModel = unsupportedModelInfo.unsupportedModel ?? model ?? "requested model";
@@ -1003,6 +1030,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1003
1030
  const successResponse = await handleSuccessResponse(response, isStreaming, {
1004
1031
  streamStallTimeoutMs,
1005
1032
  });
1033
+ if (!successResponse.ok) {
1034
+ runtimeMetrics.failedRequests++;
1035
+ runtimeMetrics.lastError = `HTTP ${successResponse.status}`;
1036
+ return successResponse;
1037
+ }
1006
1038
  if (!isStreaming && emptyResponseMaxRetries > 0) {
1007
1039
  const clonedResponse = successResponse.clone();
1008
1040
  try {
@@ -1031,6 +1063,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1031
1063
  runtimeMetrics.lastError = null;
1032
1064
  return successResponse;
1033
1065
  }
1066
+ if (restartAccountTraversalWithFallback) {
1067
+ break;
1068
+ }
1069
+ }
1070
+ if (restartAccountTraversalWithFallback) {
1071
+ continue;
1034
1072
  }
1035
1073
  const waitMs = accountManager.getMinWaitTimeForFamily(modelFamily, model);
1036
1074
  const count = accountManager.getAccountCount();
@@ -1245,7 +1283,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1245
1283
  return parts.join(", ");
1246
1284
  };
1247
1285
  const fetchCodexQuotaSnapshot = async (params) => {
1248
- const QUOTA_PROBE_MODELS = ["gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.1-codex"];
1286
+ const QUOTA_PROBE_MODELS = ["gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"];
1249
1287
  let lastError = null;
1250
1288
  for (const model of QUOTA_PROBE_MODELS) {
1251
1289
  try {
@@ -1753,8 +1791,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1753
1791
  targetCount = 1;
1754
1792
  }
1755
1793
  if (useManualMode) {
1756
- const { pkce, url } = await createAuthorizationFlow();
1757
- return buildManualOAuthFlow(pkce, url, async (tokens) => {
1794
+ const { pkce, state, url } = await createAuthorizationFlow();
1795
+ return buildManualOAuthFlow(pkce, url, state, async (tokens) => {
1758
1796
  try {
1759
1797
  await persistAccountPool([tokens], startFresh);
1760
1798
  invalidateAccountManagerCache();
@@ -1880,8 +1918,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1880
1918
  applyUiRuntimeFromConfig(manualPluginConfig);
1881
1919
  const manualPerProjectAccounts = getPerProjectAccounts(manualPluginConfig);
1882
1920
  setStoragePath(manualPerProjectAccounts ? process.cwd() : null);
1883
- const { pkce, url } = await createAuthorizationFlow();
1884
- return buildManualOAuthFlow(pkce, url, async (tokens) => {
1921
+ const { pkce, state, url } = await createAuthorizationFlow();
1922
+ return buildManualOAuthFlow(pkce, url, state, async (tokens) => {
1885
1923
  try {
1886
1924
  await persistAccountPool([tokens], false);
1887
1925
  }