codex-multi-auth 1.2.4 → 1.2.6

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 (120) hide show
  1. package/README.md +23 -9
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +212 -24
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/accounts.d.ts.map +1 -1
  6. package/dist/lib/accounts.js +8 -0
  7. package/dist/lib/accounts.js.map +1 -1
  8. package/dist/lib/auto-update-checker.d.ts.map +1 -1
  9. package/dist/lib/auto-update-checker.js +72 -36
  10. package/dist/lib/auto-update-checker.js.map +1 -1
  11. package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
  12. package/dist/lib/codex-manager/commands/forecast.js +1 -76
  13. package/dist/lib/codex-manager/commands/forecast.js.map +1 -1
  14. package/dist/lib/codex-manager/commands/report.d.ts +4 -1
  15. package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
  16. package/dist/lib/codex-manager/commands/report.js +128 -75
  17. package/dist/lib/codex-manager/commands/report.js.map +1 -1
  18. package/dist/lib/codex-manager/commands/status.d.ts +4 -1
  19. package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
  20. package/dist/lib/codex-manager/commands/status.js +48 -1
  21. package/dist/lib/codex-manager/commands/status.js.map +1 -1
  22. package/dist/lib/codex-manager/forecast-report-shared.d.ts +35 -0
  23. package/dist/lib/codex-manager/forecast-report-shared.d.ts.map +1 -0
  24. package/dist/lib/codex-manager/forecast-report-shared.js +79 -0
  25. package/dist/lib/codex-manager/forecast-report-shared.js.map +1 -0
  26. package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
  27. package/dist/lib/codex-manager/repair-commands.js +20 -12
  28. package/dist/lib/codex-manager/repair-commands.js.map +1 -1
  29. package/dist/lib/codex-manager.d.ts.map +1 -1
  30. package/dist/lib/codex-manager.js +39 -8
  31. package/dist/lib/codex-manager.js.map +1 -1
  32. package/dist/lib/config.d.ts +4 -0
  33. package/dist/lib/config.d.ts.map +1 -1
  34. package/dist/lib/config.js +42 -5
  35. package/dist/lib/config.js.map +1 -1
  36. package/dist/lib/index.d.ts +1 -1
  37. package/dist/lib/index.d.ts.map +1 -1
  38. package/dist/lib/index.js +1 -1
  39. package/dist/lib/index.js.map +1 -1
  40. package/dist/lib/parallel-probe.d.ts.map +1 -1
  41. package/dist/lib/parallel-probe.js +28 -1
  42. package/dist/lib/parallel-probe.js.map +1 -1
  43. package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
  44. package/dist/lib/preemptive-quota-scheduler.js +16 -6
  45. package/dist/lib/preemptive-quota-scheduler.js.map +1 -1
  46. package/dist/lib/proactive-refresh.d.ts.map +1 -1
  47. package/dist/lib/proactive-refresh.js +16 -5
  48. package/dist/lib/proactive-refresh.js.map +1 -1
  49. package/dist/lib/quota-cache.d.ts.map +1 -1
  50. package/dist/lib/quota-cache.js +36 -30
  51. package/dist/lib/quota-cache.js.map +1 -1
  52. package/dist/lib/quota-probe.d.ts.map +1 -1
  53. package/dist/lib/quota-probe.js +5 -0
  54. package/dist/lib/quota-probe.js.map +1 -1
  55. package/dist/lib/request/failure-policy.js +1 -1
  56. package/dist/lib/request/failure-policy.js.map +1 -1
  57. package/dist/lib/request/fetch-helpers.js +12 -4
  58. package/dist/lib/request/fetch-helpers.js.map +1 -1
  59. package/dist/lib/request/rate-limit-backoff.d.ts +24 -4
  60. package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -1
  61. package/dist/lib/request/rate-limit-backoff.js +84 -19
  62. package/dist/lib/request/rate-limit-backoff.js.map +1 -1
  63. package/dist/lib/request/request-attempt-budget.d.ts +24 -0
  64. package/dist/lib/request/request-attempt-budget.d.ts.map +1 -0
  65. package/dist/lib/request/request-attempt-budget.js +49 -0
  66. package/dist/lib/request/request-attempt-budget.js.map +1 -0
  67. package/dist/lib/request/request-resilience.d.ts +10 -0
  68. package/dist/lib/request/request-resilience.d.ts.map +1 -0
  69. package/dist/lib/request/request-resilience.js +80 -0
  70. package/dist/lib/request/request-resilience.js.map +1 -0
  71. package/dist/lib/request/response-metadata.js +1 -1
  72. package/dist/lib/request/response-metadata.js.map +1 -1
  73. package/dist/lib/request/stream-failover.d.ts +5 -1
  74. package/dist/lib/request/stream-failover.d.ts.map +1 -1
  75. package/dist/lib/request/stream-failover.js +8 -1
  76. package/dist/lib/request/stream-failover.js.map +1 -1
  77. package/dist/lib/runtime/live-sync-entry.d.ts +7 -0
  78. package/dist/lib/runtime/live-sync-entry.d.ts.map +1 -1
  79. package/dist/lib/runtime/live-sync-entry.js +4 -0
  80. package/dist/lib/runtime/live-sync-entry.js.map +1 -1
  81. package/dist/lib/runtime/live-sync.d.ts +3 -0
  82. package/dist/lib/runtime/live-sync.d.ts.map +1 -1
  83. package/dist/lib/runtime/live-sync.js +17 -3
  84. package/dist/lib/runtime/live-sync.js.map +1 -1
  85. package/dist/lib/runtime/runtime-observability.d.ts +46 -0
  86. package/dist/lib/runtime/runtime-observability.d.ts.map +1 -0
  87. package/dist/lib/runtime/runtime-observability.js +167 -0
  88. package/dist/lib/runtime/runtime-observability.js.map +1 -0
  89. package/dist/lib/runtime/runtime-services.d.ts +3 -0
  90. package/dist/lib/runtime/runtime-services.d.ts.map +1 -1
  91. package/dist/lib/runtime/runtime-services.js +19 -7
  92. package/dist/lib/runtime/runtime-services.js.map +1 -1
  93. package/dist/lib/schemas.d.ts +11 -0
  94. package/dist/lib/schemas.d.ts.map +1 -1
  95. package/dist/lib/schemas.js +5 -1
  96. package/dist/lib/schemas.js.map +1 -1
  97. package/dist/lib/session-affinity.d.ts +4 -1
  98. package/dist/lib/session-affinity.d.ts.map +1 -1
  99. package/dist/lib/session-affinity.js +26 -1
  100. package/dist/lib/session-affinity.js.map +1 -1
  101. package/dist/lib/storage/account-clear.d.ts.map +1 -1
  102. package/dist/lib/storage/account-clear.js +43 -13
  103. package/dist/lib/storage/account-clear.js.map +1 -1
  104. package/dist/lib/storage/health.d.ts +22 -0
  105. package/dist/lib/storage/health.d.ts.map +1 -0
  106. package/dist/lib/storage/health.js +15 -0
  107. package/dist/lib/storage/health.js.map +1 -0
  108. package/dist/lib/storage/migrations.d.ts +1 -1
  109. package/dist/lib/storage/migrations.d.ts.map +1 -1
  110. package/dist/lib/storage.d.ts +3 -1
  111. package/dist/lib/storage.d.ts.map +1 -1
  112. package/dist/lib/storage.js +108 -4
  113. package/dist/lib/storage.js.map +1 -1
  114. package/package.json +1 -1
  115. package/scripts/codex-multi-auth.js +1 -1
  116. package/scripts/codex.js +205 -20
  117. package/dist/lib/runtime/metrics.d.ts +0 -29
  118. package/dist/lib/runtime/metrics.d.ts.map +0 -1
  119. package/dist/lib/runtime/metrics.js +0 -97
  120. package/dist/lib/runtime/metrics.js.map +0 -1
package/README.md CHANGED
@@ -19,6 +19,7 @@ Codex CLI-first multi-account OAuth manager for the official `@openai/codex` CLI
19
19
  - Interactive dashboard for account actions and settings
20
20
  - Experimental settings tab for staged sync, backup, and refresh-guard controls
21
21
  - Forecast, report, fix, and doctor commands for operational safety
22
+ - Runtime counters, budget/cooldown state, and multi-auth probe visibility in `codex auth status` / `codex auth report`
22
23
  - Flagged account verification and restore flow
23
24
  - Session affinity and live account sync controls
24
25
  - Proactive refresh and preemptive quota deferral controls
@@ -156,6 +157,14 @@ If browser launch is blocked, use the alternate login paths in [docs/getting-sta
156
157
  | `codex auth report --live --json` | How do I get the full machine-readable health report? |
157
158
  | `codex auth fix --live --model gpt-5-codex` | How do I run live repair probes with a chosen model? |
158
159
 
160
+ ### Reliability behavior
161
+
162
+ - whole-pool replay is disabled by default when every account is rate-limited
163
+ - active requests use a bounded outbound request budget so one prompt cannot walk the full pool indefinitely
164
+ - repeated cross-account 5xx bursts trigger a short cooldown instead of continuing aggressive rotation
165
+ - proactive refresh is staggered to reduce background refresh bursts
166
+ - `codex auth status` surfaces recent runtime request metrics in text output, and `codex auth report --json` exposes the machine-readable cooldown/runtime snapshot
167
+
159
168
  ---
160
169
 
161
170
  ## Dashboard Hotkeys
@@ -187,11 +196,12 @@ If browser launch is blocked, use the alternate login paths in [docs/getting-sta
187
196
  | File | Default path |
188
197
  | --- | --- |
189
198
  | Settings | `~/.codex/multi-auth/settings.json` |
190
- | Accounts | `~/.codex/multi-auth/openai-codex-accounts.json` |
191
- | Flagged accounts | `~/.codex/multi-auth/openai-codex-flagged-accounts.json` |
192
- | Quota cache | `~/.codex/multi-auth/quota-cache.json` |
193
- | Logs | `~/.codex/multi-auth/logs/codex-plugin/` |
194
- | Per-project accounts | `~/.codex/multi-auth/projects/<project-key>/openai-codex-accounts.json` |
199
+ | Accounts | `~/.codex/multi-auth/openai-codex-accounts.json` |
200
+ | Flagged accounts | `~/.codex/multi-auth/openai-codex-flagged-accounts.json` |
201
+ | Quota cache | `~/.codex/multi-auth/quota-cache.json` |
202
+ | Runtime observability | `~/.codex/multi-auth/runtime-observability.json` |
203
+ | Logs | `~/.codex/multi-auth/logs/codex-plugin/` |
204
+ | Per-project accounts | `~/.codex/multi-auth/projects/<project-key>/openai-codex-accounts.json` |
195
205
 
196
206
  Override root with `CODEX_MULTI_AUTH_DIR=<path>`.
197
207
 
@@ -265,7 +275,10 @@ codex auth login
265
275
 
266
276
  - `codex auth` unrecognized: run `where codex`, then follow `docs/troubleshooting.md` for routing fallback commands
267
277
  - Switch succeeds but wrong account appears active: run `codex auth switch <index>`, then restart session
268
- - OAuth callback on port `1455` fails: free the port and re-run `codex auth login`
278
+ - Requests fail fast with a pool cooldown message: wait for the cooldown window or inspect `codex auth status`
279
+ - Requests fail fast after repeated upstream 5xx errors: inspect `codex auth report --json` for runtime traffic and cooldown details
280
+ - Storage cleanup fails with `EBUSY` / `EPERM` (Windows): run `codex auth doctor --fix` to retry, or manually remove `~/.codex/multi-auth/<project-key>/` and re-login
281
+ - OAuth callback on port `1455` fails: free the port and re-run `codex auth login`
269
282
  - Browser launch is blocked or you are in a headless shell: re-run `codex auth login --manual` or set `CODEX_AUTH_NO_BROWSER=1`
270
283
  - `missing field id_token` / `token_expired` / `refresh_token_reused`: re-login affected account
271
284
 
@@ -308,9 +321,10 @@ codex auth doctor --json
308
321
 
309
322
  ## Release Notes
310
323
 
311
- - Current stable: [docs/releases/v1.2.4.md](docs/releases/v1.2.4.md)
312
- - Previous stable: [docs/releases/v1.2.2.md](docs/releases/v1.2.2.md)
313
- - Earlier stable: [docs/releases/v1.2.1.md](docs/releases/v1.2.1.md)
324
+ - Current stable: [docs/releases/v1.2.6.md](docs/releases/v1.2.6.md)
325
+ - Recent patch release: [docs/releases/v1.2.5.md](docs/releases/v1.2.5.md)
326
+ - Previous stable: [docs/releases/v1.2.2.md](docs/releases/v1.2.2.md)
327
+ - Earlier stable: [docs/releases/v1.2.1.md](docs/releases/v1.2.1.md)
314
328
  - Archived prerelease: [docs/releases/v0.1.0-beta.0.md](docs/releases/v0.1.0-beta.0.md)
315
329
 
316
330
  ## License
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;AAqQ5D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAgvH/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;AAEH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;AAyR5D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA2/H/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ import { startLocalOAuthServer } from "./lib/auth/server.js";
30
30
  import { checkAndNotify } from "./lib/auto-update-checker.js";
31
31
  import { CapabilityPolicyStore } from "./lib/capability-policy.js";
32
32
  import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
33
- import { getAutoResume, getCodexMode, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getFallbackToGpt52OnUnsupportedGpt53, getFastSession, getFastSessionMaxInputItems, getFastSessionStrategy, getFetchTimeoutMs, getLiveAccountSync, getLiveAccountSyncDebounceMs, getLiveAccountSyncPollMs, getNetworkErrorCooldownMs, getPerProjectAccounts, getPidOffsetEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaMaxDeferralMs, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getProactiveRefreshBufferMs, getProactiveRefreshGuardian, getProactiveRefreshIntervalMs, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getBackgroundResponses, getResponseContinuation, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getSessionRecovery, getStorageBackupEnabled, getStreamStallTimeoutMs, getToastDurationMs, getTokenRefreshSkewMs, getUnsupportedCodexFallbackChain, getUnsupportedCodexPolicy, loadPluginConfig, } from "./lib/config.js";
33
+ import { getAutoResume, getCodexMode, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getFallbackToGpt52OnUnsupportedGpt53, getFastSession, getFastSessionMaxInputItems, getFastSessionStrategy, getFetchTimeoutMs, getRateLimitDedupWindowMs, getRateLimitMaxBackoffMs, getRateLimitShortRetryThresholdMs, getRateLimitStateResetMs, getLiveAccountSync, getLiveAccountSyncDebounceMs, getLiveAccountSyncPollMs, getNetworkErrorCooldownMs, getPerProjectAccounts, getPidOffsetEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaMaxDeferralMs, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getProactiveRefreshBufferMs, getProactiveRefreshGuardian, getProactiveRefreshIntervalMs, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getBackgroundResponses, getResponseContinuation, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getSessionRecovery, getStorageBackupEnabled, getStreamStallTimeoutMs, getToastDurationMs, getTokenRefreshSkewMs, getUnsupportedCodexFallbackChain, getUnsupportedCodexPolicy, loadPluginConfig, } from "./lib/config.js";
34
34
  import { ACCOUNT_LIMITS, AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, } from "./lib/constants.js";
35
35
  import { handleContextOverflow } from "./lib/context-overflow.js";
36
36
  import { EntitlementCache, resolveEntitlementAccountKey, } from "./lib/entitlement-cache.js";
@@ -44,9 +44,11 @@ import { detectErrorType, getRecoveryToastContent, isRecoverableError, } from ".
44
44
  import { RefreshGuardian } from "./lib/refresh-guardian.js";
45
45
  import { queuedRefresh } from "./lib/refresh-queue.js";
46
46
  import { parseEnvInt, parseFailoverMode, } from "./lib/request/failover-config.js";
47
+ import { capStreamFailoverMax, computeOutboundRequestAttemptBudget, } from "./lib/request/request-attempt-budget.js";
48
+ import { armPoolExhaustionCooldown, buildAdaptiveStreamFailoverCandidateOrder, clearPoolExhaustionCooldown, clearServerBurstCooldown, getPoolExhaustionCooldownRemaining, getServerBurstCooldownRemaining, recordServerBurstFailure, } from "./lib/request/request-resilience.js";
47
49
  import { evaluateFailurePolicy, } from "./lib/request/failure-policy.js";
48
50
  import { applyProxyCompatibleInit, createCodexHeaders, extractRequestUrl, getUnsupportedCodexModelInfo, handleErrorResponse, handleSuccessResponse, isWorkspaceDisabledError, refreshAndUpdateToken, resolveUnsupportedCodexFallbackModel, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
49
- import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
51
+ import { configureRateLimitBackoff, getRateLimitBackoff, MAX_SHORT_RETRY_ATTEMPTS, getRateLimitShortRetryThresholdMs as getConfiguredRateLimitShortRetryThresholdMs, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
50
52
  import { normalizeRequestInit, parseRequestBodyFromInit, } from "./lib/request/request-init.js";
51
53
  import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
52
54
  import { applyResponseCompaction } from "./lib/request/response-compaction.js";
@@ -74,6 +76,7 @@ import { applyLoaderRuntimeSetup } from "./lib/runtime/loader-setup.js";
74
76
  import { buildManualOAuthFlow } from "./lib/runtime/manual-oauth-flow.js";
75
77
  import { applyPreemptiveQuotaSettingsFromConfig, } from "./lib/runtime/quota-settings.js";
76
78
  import { ensureLiveAccountSyncState, ensureRefreshGuardianState, ensureSessionAffinityState, } from "./lib/runtime/runtime-services.js";
79
+ import { mutateRuntimeObservabilitySnapshot } from "./lib/runtime/runtime-observability.js";
77
80
  import { applyAccountStorageScopeFromConfig } from "./lib/runtime/storage-scope.js";
78
81
  import { showRuntimeToast } from "./lib/runtime/toast.js";
79
82
  import { createRuntimeSessionRecoveryHook } from "./lib/runtime/session-recovery.js";
@@ -113,10 +116,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
113
116
  let perProjectStorageWarningShown = false;
114
117
  let liveAccountSync = null;
115
118
  let liveAccountSyncPath = null;
119
+ let liveAccountSyncConfigKey = null;
116
120
  let refreshGuardian = null;
117
121
  let refreshGuardianConfigKey = null;
118
122
  let refreshGuardianCleanupRegistered = false;
119
123
  let sessionAffinityStore = new SessionAffinityStore();
124
+ let sessionAffinityWriteVersion = 0;
120
125
  let sessionAffinityConfigKey = null;
121
126
  const entitlementCache = new EntitlementCache();
122
127
  const preemptiveQuotaScheduler = new PreemptiveQuotaScheduler();
@@ -139,6 +144,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
139
144
  totalRequests: 0,
140
145
  successfulRequests: 0,
141
146
  failedRequests: 0,
147
+ responsesRequests: 0,
148
+ authRefreshRequests: 0,
149
+ diagnosticProbeRequests: 0,
150
+ outboundRequestAttemptBudget: null,
151
+ outboundRequestAttemptsConsumed: 0,
152
+ requestAttemptBudgetExhaustions: 0,
153
+ poolExhaustionFastFails: 0,
154
+ serverBurstFastFails: 0,
142
155
  rateLimitedResponses: 0,
143
156
  serverErrors: 0,
144
157
  networkErrors: 0,
@@ -148,6 +161,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
148
161
  accountRotations: 0,
149
162
  sameAccountRetries: 0,
150
163
  streamFailoverAttempts: 0,
164
+ streamFailoverCandidatesConsidered: 0,
165
+ lastStreamFailoverCandidateCount: 0,
151
166
  streamFailoverRecoveries: 0,
152
167
  streamFailoverCrossAccountRecoveries: 0,
153
168
  cumulativeLatencyMs: 0,
@@ -169,6 +184,26 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
169
184
  findMatchingAccountIndex,
170
185
  modelFamilies: MODEL_FAMILIES,
171
186
  });
187
+ const syncRuntimeObservability = (requestId) => {
188
+ mutateRuntimeObservabilitySnapshot((snapshot) => {
189
+ const now = Date.now();
190
+ const poolRemaining = getPoolExhaustionCooldownRemaining(now);
191
+ const burstRemaining = getServerBurstCooldownRemaining(now);
192
+ snapshot.currentRequestId = requestId;
193
+ snapshot.poolExhaustionCooldownUntil =
194
+ poolRemaining > 0
195
+ ? now + poolRemaining
196
+ : null;
197
+ snapshot.serverBurstCooldownUntil =
198
+ burstRemaining > 0
199
+ ? now + burstRemaining
200
+ : null;
201
+ snapshot.responsesRequests = runtimeMetrics.responsesRequests;
202
+ snapshot.authRefreshRequests = runtimeMetrics.authRefreshRequests;
203
+ snapshot.diagnosticProbeRequests = runtimeMetrics.diagnosticProbeRequests;
204
+ snapshot.runtimeMetrics = { ...runtimeMetrics };
205
+ });
206
+ };
172
207
  const persistAccountPoolAndFlagged = async (results, flaggedStorage, replaceAll = false) => withAccountAndFlaggedStorageTransaction(async (loadedStorage, persist) => {
173
208
  await persistAccountPoolResults({
174
209
  results,
@@ -228,7 +263,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
228
263
  authFallback,
229
264
  currentSync: liveAccountSync,
230
265
  currentPath: liveAccountSyncPath,
266
+ currentConfigKey: liveAccountSyncConfigKey,
231
267
  getLiveAccountSync,
268
+ getLiveAccountSyncDebounceMs,
269
+ getLiveAccountSyncPollMs,
232
270
  getStoragePath,
233
271
  createSync: (oauthFallback) => new LiveAccountSync(async () => {
234
272
  await reloadRuntimeAccountManager(makeReloadAccountManagerDeps(oauthFallback));
@@ -243,6 +281,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
243
281
  });
244
282
  liveAccountSync = next.liveAccountSync;
245
283
  liveAccountSyncPath = next.liveAccountSyncPath;
284
+ liveAccountSyncConfigKey = next.liveAccountSyncConfigKey;
246
285
  };
247
286
  const ensureRefreshGuardian = (pluginConfig) => {
248
287
  const next = ensureRefreshGuardianState({
@@ -387,6 +426,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
387
426
  const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
388
427
  const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
389
428
  const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
429
+ configureRateLimitBackoff({
430
+ dedupWindowMs: getRateLimitDedupWindowMs(pluginConfig),
431
+ stateResetMs: getRateLimitStateResetMs(pluginConfig),
432
+ maxBackoffMs: getRateLimitMaxBackoffMs(pluginConfig),
433
+ shortRetryThresholdMs: getRateLimitShortRetryThresholdMs(pluginConfig),
434
+ });
390
435
  const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
391
436
  const retryAllAccountsRateLimited = getRetryAllAccountsRateLimited(pluginConfig);
392
437
  const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
@@ -401,8 +446,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
401
446
  const networkErrorCooldownMs = getNetworkErrorCooldownMs(pluginConfig);
402
447
  const serverErrorCooldownMs = getServerErrorCooldownMs(pluginConfig);
403
448
  const failoverMode = parseFailoverMode(process.env.CODEX_AUTH_FAILOVER_MODE);
404
- const streamFailoverMax = Math.max(0, parseEnvInt(process.env.CODEX_AUTH_STREAM_FAILOVER_MAX) ??
405
- STREAM_FAILOVER_MAX_BY_MODE[failoverMode]);
449
+ const streamFailoverMax = Math.max(0, capStreamFailoverMax(parseEnvInt(process.env.CODEX_AUTH_STREAM_FAILOVER_MAX) ??
450
+ STREAM_FAILOVER_MAX_BY_MODE[failoverMode]));
406
451
  const streamFailoverSoftTimeoutMs = Math.max(1_000, parseEnvInt(process.env.CODEX_AUTH_STREAM_STALL_SOFT_TIMEOUT_MS) ??
407
452
  STREAM_FAILOVER_SOFT_TIMEOUT_BY_MODE[failoverMode]);
408
453
  const streamFailoverHardTimeoutMs = Math.max(streamFailoverSoftTimeoutMs, parseEnvInt(process.env.CODEX_AUTH_STREAM_STALL_HARD_TIMEOUT_MS) ??
@@ -508,6 +553,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
508
553
  .toString()
509
554
  .trim() || undefined;
510
555
  const sessionAffinityKey = threadIdCandidate ?? promptCacheKey ?? null;
556
+ const sessionAffinityVersion = (sessionAffinityWriteVersion += 1);
511
557
  const effectivePromptCacheKey = (sessionAffinityKey ?? promptCacheKey ?? "")
512
558
  .toString()
513
559
  .trim() || undefined;
@@ -532,9 +578,72 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
532
578
  const requestCorrelationId = setCorrelationId(threadIdCandidate
533
579
  ? `${threadIdCandidate}:${Date.now()}`
534
580
  : undefined);
581
+ const requestTraceId = requestCorrelationId;
535
582
  runtimeMetrics.lastRequestAt = Date.now();
583
+ syncRuntimeObservability(requestTraceId);
536
584
  const abortSignal = requestInit?.signal ?? init?.signal ?? null;
537
585
  const sleep = createAbortableSleep(abortSignal);
586
+ const poolCooldownRemainingMs = getPoolExhaustionCooldownRemaining();
587
+ if (poolCooldownRemainingMs > 0) {
588
+ runtimeMetrics.failedRequests += 1;
589
+ runtimeMetrics.poolExhaustionFastFails += 1;
590
+ runtimeMetrics.lastError = "Pool exhaustion cooldown active";
591
+ syncRuntimeObservability(requestTraceId);
592
+ return new Response(JSON.stringify({
593
+ error: {
594
+ message: `The account pool is cooling down after recent rate-limit exhaustion. Try again in ${formatWaitTime(poolCooldownRemainingMs)} or inspect \`codex auth status\`.`,
595
+ },
596
+ }), { status: 429, headers: { "content-type": "application/json; charset=utf-8" } });
597
+ }
598
+ const serverBurstCooldownRemainingMs = getServerBurstCooldownRemaining();
599
+ if (serverBurstCooldownRemainingMs > 0) {
600
+ runtimeMetrics.failedRequests += 1;
601
+ runtimeMetrics.serverBurstFastFails += 1;
602
+ runtimeMetrics.lastError = "Server burst cooldown active";
603
+ syncRuntimeObservability(requestTraceId);
604
+ return new Response(JSON.stringify({
605
+ error: {
606
+ message: `Multiple accounts recently failed with upstream server errors. Try again in ${formatWaitTime(serverBurstCooldownRemainingMs)} or inspect \`codex auth report --json\`.`,
607
+ },
608
+ }), { status: 503, headers: { "content-type": "application/json; charset=utf-8" } });
609
+ }
610
+ const maxOutboundRequestAttempts = computeOutboundRequestAttemptBudget({
611
+ accountCount: accountManager.getAccountCount(),
612
+ maxSameAccountRetries,
613
+ emptyResponseMaxRetries,
614
+ streamFailoverMax,
615
+ });
616
+ runtimeMetrics.outboundRequestAttemptBudget ??=
617
+ maxOutboundRequestAttempts;
618
+ logDebug("Configured outbound request attempt budget.", {
619
+ requestTraceId,
620
+ budget: maxOutboundRequestAttempts,
621
+ accountCount: accountManager.getAccountCount(),
622
+ maxSameAccountRetries,
623
+ emptyResponseMaxRetries,
624
+ streamFailoverMax,
625
+ });
626
+ syncRuntimeObservability(requestTraceId);
627
+ let outboundRequestAttemptsRemaining = maxOutboundRequestAttempts;
628
+ const tryConsumeOutboundRequestAttempt = (reason, accountIndex) => {
629
+ if (outboundRequestAttemptsRemaining <= 0) {
630
+ runtimeMetrics.requestAttemptBudgetExhaustions += 1;
631
+ runtimeMetrics.lastError =
632
+ `Request attempt budget exhausted after ${maxOutboundRequestAttempts} outbound request(s)`;
633
+ logWarn("Request attempt budget exhausted.", {
634
+ requestTraceId,
635
+ reason,
636
+ accountIndex,
637
+ budget: maxOutboundRequestAttempts,
638
+ consumed: maxOutboundRequestAttempts,
639
+ });
640
+ syncRuntimeObservability(requestTraceId);
641
+ return false;
642
+ }
643
+ runtimeMetrics.outboundRequestAttemptsConsumed += 1;
644
+ outboundRequestAttemptsRemaining -= 1;
645
+ return true;
646
+ };
538
647
  let allRateLimitedRetries = 0;
539
648
  let emptyResponseRetries = 0;
540
649
  const attemptedUnsupportedFallbackModels = new Set();
@@ -599,6 +708,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
599
708
  let accountAuth = accountManager.toAuthDetails(account);
600
709
  try {
601
710
  if (shouldRefreshToken(accountAuth, tokenRefreshSkewMs)) {
711
+ runtimeMetrics.authRefreshRequests += 1;
712
+ syncRuntimeObservability(requestTraceId);
602
713
  accountAuth = (await refreshAndUpdateToken(accountAuth, client));
603
714
  await accountManager.commitRefreshedAuth(account, accountAuth);
604
715
  }
@@ -748,9 +859,23 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
748
859
  continue;
749
860
  }
750
861
  let sameAccountRetryCount = 0;
862
+ let shortRateLimitRetryCount = 0;
751
863
  let successAccountForResponse = account;
752
864
  let successEntitlementAccountKey = entitlementAccountKey;
753
865
  while (true) {
866
+ if (!tryConsumeOutboundRequestAttempt("primary", account.index)) {
867
+ runtimeMetrics.failedRequests++;
868
+ const lastErrorDetail = runtimeMetrics.lastError;
869
+ const message = lastErrorDetail
870
+ ? `${lastErrorDetail}. Try again after the current retries settle.`
871
+ : "Request attempt budget exhausted. Try again shortly.";
872
+ return new Response(JSON.stringify({ error: { message } }), {
873
+ status: 503,
874
+ headers: {
875
+ "content-type": "application/json; charset=utf-8",
876
+ },
877
+ });
878
+ }
754
879
  let response;
755
880
  const fetchStart = performance.now();
756
881
  // Merge user AbortSignal with timeout (Node 18 compatible - no AbortSignal.any)
@@ -777,6 +902,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
777
902
  }
778
903
  try {
779
904
  runtimeMetrics.totalRequests++;
905
+ runtimeMetrics.responsesRequests++;
906
+ syncRuntimeObservability(requestTraceId);
780
907
  response = await fetch(url, applyProxyCompatibleInit(url, {
781
908
  ...requestInit,
782
909
  headers,
@@ -1049,6 +1176,23 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1049
1176
  failoverMode,
1050
1177
  serverRetryAfterMs: serverRetryAfterMs ?? undefined,
1051
1178
  }, { serverCooldownMs: serverErrorCooldownMs });
1179
+ const serverBurstCooldownUntil = recordServerBurstFailure(account.index);
1180
+ if (serverBurstCooldownUntil > 0) {
1181
+ runtimeMetrics.lastError =
1182
+ "Repeated upstream server errors across the account pool";
1183
+ syncRuntimeObservability(requestTraceId);
1184
+ }
1185
+ // Overload-type server errors (502 Bad Gateway, 503 Service
1186
+ // Unavailable, 529 Overloaded) signal upstream capacity
1187
+ // pressure. Notify the quota scheduler so it can proactively
1188
+ // defer subsequent requests for this quota key, mirroring the
1189
+ // 429 handler's scheduler awareness.
1190
+ if ((response.status === 502 ||
1191
+ response.status === 503 ||
1192
+ response.status === 529) &&
1193
+ typeof policy.cooldownMs === "number") {
1194
+ preemptiveQuotaScheduler.markRateLimited(quotaScheduleKey, policy.cooldownMs);
1195
+ }
1052
1196
  if (policy.refundToken) {
1053
1197
  accountManager.refundToken(account, modelFamily, model);
1054
1198
  }
@@ -1072,16 +1216,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1072
1216
  sessionAffinityStore?.forgetSession(sessionAffinityKey);
1073
1217
  break;
1074
1218
  }
1075
- if (response.status === 429) {
1219
+ if (errorResponse.status === 429 && rateLimit) {
1076
1220
  runtimeMetrics.rateLimitedResponses++;
1077
1221
  const retryAfterMs = rateLimit?.retryAfterMs ?? 60_000;
1078
- const { attempt, delayMs } = getRateLimitBackoff(account.index, quotaKey, retryAfterMs);
1222
+ const { delayMs } = getRateLimitBackoff(account.index, quotaKey, retryAfterMs, account.accountId ?? account.email ?? null);
1079
1223
  const cooldownMs = Math.max(delayMs, retryAfterMs);
1080
1224
  preemptiveQuotaScheduler.markRateLimited(quotaScheduleKey, cooldownMs);
1081
1225
  const waitLabel = formatWaitTime(cooldownMs);
1082
- if (cooldownMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS) {
1226
+ const shortRetryThresholdMs = getConfiguredRateLimitShortRetryThresholdMs();
1227
+ if (cooldownMs <=
1228
+ shortRetryThresholdMs &&
1229
+ shortRateLimitRetryCount < MAX_SHORT_RETRY_ATTEMPTS) {
1230
+ shortRateLimitRetryCount += 1;
1083
1231
  if (accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
1084
- await showRuntimeToast(client, `Rate limited. Retrying in ${waitLabel} (attempt ${attempt})...`, "warning", { duration: toastDurationMs });
1232
+ await showRuntimeToast(client, `Rate limited. Retrying in ${waitLabel} (attempt ${shortRateLimitRetryCount})...`, "warning", { duration: toastDurationMs });
1085
1233
  accountManager.markToastShown(account.index);
1086
1234
  }
1087
1235
  await sleep(addJitter(Math.max(MIN_BACKOFF_MS, cooldownMs), 0.2));
@@ -1114,13 +1262,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1114
1262
  runtimeMetrics.cumulativeLatencyMs += fetchLatencyMs;
1115
1263
  let responseForSuccess = response;
1116
1264
  if (isStreaming) {
1117
- const streamFallbackCandidateOrder = [
1118
- account.index,
1119
- ...accountManager
1120
- .getAccountsSnapshot()
1121
- .map((candidate) => candidate.index)
1122
- .filter((index) => index !== account.index),
1123
- ];
1265
+ const streamFallbackCandidateOrder = buildAdaptiveStreamFailoverCandidateOrder(account.index, accountSnapshotList);
1266
+ runtimeMetrics.lastStreamFailoverCandidateCount =
1267
+ streamFallbackCandidateOrder.length;
1268
+ runtimeMetrics.streamFailoverCandidatesConsidered +=
1269
+ streamFallbackCandidateOrder.length;
1270
+ logDebug("Prepared stream failover candidates.", {
1271
+ requestTraceId,
1272
+ primaryAccountIndex: account.index,
1273
+ candidateCount: streamFallbackCandidateOrder.length,
1274
+ candidateIndices: streamFallbackCandidateOrder,
1275
+ });
1276
+ syncRuntimeObservability(requestTraceId);
1124
1277
  responseForSuccess = withStreamingFailover(response, async (failoverAttempt, emittedBytes) => {
1125
1278
  if (abortSignal?.aborted) {
1126
1279
  return null;
@@ -1139,6 +1292,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1139
1292
  let fallbackAuth = accountManager.toAuthDetails(fallbackAccount);
1140
1293
  try {
1141
1294
  if (shouldRefreshToken(fallbackAuth, tokenRefreshSkewMs)) {
1295
+ runtimeMetrics.authRefreshRequests += 1;
1296
+ syncRuntimeObservability(requestTraceId);
1142
1297
  fallbackAuth = (await refreshAndUpdateToken(fallbackAuth, client));
1143
1298
  await accountManager.commitRefreshedAuth(fallbackAccount, fallbackAuth);
1144
1299
  }
@@ -1182,6 +1337,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1182
1337
  logWarn(`Skipping account ${fallbackAccount.index + 1} due to cached entitlement block (${formatWaitTime(fallbackEntitlementBlock.waitMs)} remaining).`);
1183
1338
  continue;
1184
1339
  }
1340
+ if (!tryConsumeOutboundRequestAttempt("stream-failover", fallbackAccount.index)) {
1341
+ return null;
1342
+ }
1185
1343
  if (!accountManager.consumeToken(fallbackAccount, modelFamily, model)) {
1186
1344
  continue;
1187
1345
  }
@@ -1212,6 +1370,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1212
1370
  }
1213
1371
  try {
1214
1372
  runtimeMetrics.totalRequests++;
1373
+ runtimeMetrics.responsesRequests++;
1374
+ syncRuntimeObservability(requestTraceId);
1215
1375
  const fallbackResponse = await fetch(url, applyProxyCompatibleInit(url, {
1216
1376
  ...requestInit,
1217
1377
  headers: fallbackHeaders,
@@ -1253,7 +1413,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1253
1413
  runtimeMetrics.streamFailoverCrossAccountRecoveries += 1;
1254
1414
  runtimeMetrics.accountRotations += 1;
1255
1415
  if (!responseContinuationEnabled) {
1256
- sessionAffinityStore?.remember(sessionAffinityKey, fallbackAccount.index);
1416
+ sessionAffinityStore?.rememberWithVersion(sessionAffinityKey, fallbackAccount.index, Date.now(), sessionAffinityVersion);
1257
1417
  }
1258
1418
  }
1259
1419
  logInfo(`Recovered stream via failover attempt ${failoverAttempt} using account ${fallbackAccount.index + 1}.`, { emittedBytes });
@@ -1290,8 +1450,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1290
1450
  onResponseId: (responseId) => {
1291
1451
  if (!responseContinuationEnabled)
1292
1452
  return;
1293
- sessionAffinityStore?.remember(sessionAffinityKey, successAccountForResponse.index);
1294
- sessionAffinityStore?.updateLastResponseId(sessionAffinityKey, responseId);
1453
+ sessionAffinityStore?.rememberWithVersion(sessionAffinityKey, successAccountForResponse.index, Date.now(), sessionAffinityVersion);
1454
+ sessionAffinityStore?.updateLastResponseId(sessionAffinityKey, responseId, Date.now(), sessionAffinityVersion);
1295
1455
  storedResponseIdForSuccess = true;
1296
1456
  },
1297
1457
  streamStallTimeoutMs,
@@ -1331,6 +1491,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1331
1491
  await sleep(addJitter(emptyResponseRetryDelayMs, 0.2));
1332
1492
  break;
1333
1493
  }
1494
+ if (response.status >= 500) {
1495
+ const serverBurstCooldownUntil = recordServerBurstFailure(account.index);
1496
+ if (serverBurstCooldownUntil > 0) {
1497
+ runtimeMetrics.serverBurstFastFails += 1;
1498
+ runtimeMetrics.lastError =
1499
+ "Repeated upstream server errors across the account pool";
1500
+ syncRuntimeObservability(requestTraceId);
1501
+ return new Response(JSON.stringify({
1502
+ error: {
1503
+ message: `Upstream server failures were observed across multiple accounts. Pausing retries for ${formatWaitTime(serverBurstCooldownUntil - Date.now())}. Check \`codex auth report --json\` for runtime metrics.`,
1504
+ },
1505
+ }), { status: 503, headers: { "content-type": "application/json; charset=utf-8" } });
1506
+ }
1507
+ }
1334
1508
  logWarn(`Empty response after ${emptyResponseMaxRetries} retries. Returning as-is.`);
1335
1509
  }
1336
1510
  }
@@ -1347,7 +1521,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1347
1521
  entitlementCache.clear(successAccountKey, capabilityModelKey);
1348
1522
  if (!responseContinuationEnabled ||
1349
1523
  (!isStreaming && !storedResponseIdForSuccess)) {
1350
- sessionAffinityStore?.remember(sessionAffinityKey, successAccountForResponse.index);
1524
+ sessionAffinityStore?.rememberWithVersion(sessionAffinityKey, successAccountForResponse.index, Date.now(), sessionAffinityVersion);
1351
1525
  }
1352
1526
  runtimeMetrics.successfulRequests++;
1353
1527
  runtimeMetrics.lastError = null;
@@ -1357,6 +1531,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1357
1531
  lastCodexCliActiveSyncIndex =
1358
1532
  successAccountForResponse.index;
1359
1533
  }
1534
+ clearPoolExhaustionCooldown();
1535
+ clearServerBurstCooldown();
1536
+ syncRuntimeObservability(requestTraceId);
1360
1537
  return successResponse;
1361
1538
  }
1362
1539
  if (retryNextAccountBeforeFallback) {
@@ -1391,16 +1568,26 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1391
1568
  allRateLimitedRetries++;
1392
1569
  continue;
1393
1570
  }
1394
- const waitLabel = waitMs > 0 ? formatWaitTime(waitMs) : "a bit";
1571
+ const now = Date.now();
1572
+ const poolCooldownUntil = count > 0 && waitMs > 0
1573
+ ? armPoolExhaustionCooldown(waitMs, now)
1574
+ : 0;
1575
+ const effectiveWaitMs = poolCooldownUntil > 0
1576
+ ? Math.max(0, poolCooldownUntil - now)
1577
+ : waitMs;
1578
+ const waitLabel = effectiveWaitMs > 0
1579
+ ? formatWaitTime(effectiveWaitMs)
1580
+ : "a bit";
1395
1581
  const message = count === 0
1396
1582
  ? "No Codex accounts configured. Run `codex login`."
1397
- : waitMs > 0
1398
- ? `All ${count} account(s) are rate-limited. Try again in ${waitLabel} or add another account with \`codex login\`.`
1399
- : `All ${count} account(s) failed (server errors or auth issues). Check account health with \`codex-health\`.`;
1583
+ : effectiveWaitMs > 0
1584
+ ? `All ${count} account(s) are rate-limited. A short pool cooldown is now active for ${waitLabel}. Try again later or inspect \`codex auth status\`.`
1585
+ : `All ${count} account(s) failed (server errors or auth issues). Check account health with \`codex auth report --json\`.`;
1400
1586
  runtimeMetrics.failedRequests++;
1401
1587
  runtimeMetrics.lastError = message;
1588
+ syncRuntimeObservability(requestTraceId);
1402
1589
  return new Response(JSON.stringify({ error: { message } }), {
1403
- status: waitMs > 0 ? 429 : 503,
1590
+ status: effectiveWaitMs > 0 ? 429 : 503,
1404
1591
  headers: {
1405
1592
  "content-type": "application/json; charset=utf-8",
1406
1593
  },
@@ -1408,6 +1595,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1408
1595
  }
1409
1596
  }
1410
1597
  finally {
1598
+ syncRuntimeObservability(null);
1411
1599
  clearCorrelationId();
1412
1600
  }
1413
1601
  },