codex-multi-auth 2.13.0 → 2.14.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.",
5
5
  "interface": {
6
6
  "composerIcon": "./assets/codex-multi-auth-icon.svg"
package/README.md CHANGED
@@ -298,6 +298,7 @@ Selected runtime/environment overrides:
298
298
  | `CODEX_MULTI_AUTH_BYPASS=1` | Skip multi-auth intercept and forward straight to official Codex |
299
299
  | `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0/1` | Opt out/in of live Responses proxy rotation for forwarded Codex CLI/app sessions |
300
300
  | `CODEX_MULTI_AUTH_RUNTIME_PROXY_UPSTREAM_BASE_URL=http://127.0.0.1:<port>/<path>` | Route the runtime rotation proxy through an explicit loopback HTTP upstream; requires an explicit port and a numeric loopback host (`127.0.0.0/8` or `[::1]`), and rejects credentials, query strings, fragments, HTTPS, and every other host |
301
+ | `CODEX_MULTI_AUTH_MODEL_CAPACITY_RETRY_MS=<ms>` | Wall-clock ceiling on how long one request waits out a "selected model is at capacity" response before giving up (default 600000, max 3600000, `0` disables) |
301
302
  | `CODEX_MULTI_AUTH_APP_ROTATION_IDLE_MS=<ms>` | Override automatic Codex app helper idle shutdown |
302
303
  | `CODEX_MULTI_AUTH_APP_BIND_INSTALL=0/1` | Opt out/in of packaged Codex app bind self-heal on first CLI run or rotation enable |
303
304
  | `CODEX_MULTI_AUTH_APP_LAUNCHER_INSTALL=0/1` | Opt out/in of routing supported app shortcuts on first CLI run or rotation enable |
@@ -322,6 +323,8 @@ Runtime rotation is enabled by default for request-bearing wrapper-launched Code
322
323
 
323
324
  Advanced local proxy chains can set `CODEX_MULTI_AUTH_RUNTIME_PROXY_UPSTREAM_BASE_URL` for one wrapper process. The wrapper passes that URL to both the shadow-runtime and interactive-helper rotation paths and fails closed if the configured upstream is invalid or unavailable; it never falls back silently to the direct backend while an explicit upstream is required. Because the request that reaches the upstream carries the managed OAuth bearer token, the host must be a numeric loopback literal: a name such as `localhost` is rejected, since a hosts-file entry can point it at a routable address. "Fails closed" also covers the case where runtime rotation itself is off (`CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0`, `CODEX_MULTI_AUTH_BYPASS=1`, or the setting disabled): a request-bearing invocation exits non-zero rather than bypassing the configured upstream. Subcommands that make no backend requests still run normally.
324
325
 
326
+ When the backend answers that the selected model is at capacity, the runtime proxy waits and re-sends the request instead of burning the pool. Capacity is a property of the model, not of an account, so rotating spends the transient budget against accounts that all fail identically and the request ends as a 503 within seconds. The wait backs off (2s, 5s, 15s, 30s, then 60s), honors an upstream `retry-after` when one is sent, and stops at a wall-clock deadline set by `CODEX_MULTI_AUTH_MODEL_CAPACITY_RETRY_MS` (10 minutes by default, 1 hour maximum). Set that variable to `0` to restore the previous rotate-and-fail behavior. The responding account is left completely unpenalized: its pool token is refunded and it is never marked rate limited or cooled down, so selection runs normally on the next pass and may pick any healthy account. The wait is abandoned if the client disconnects, so no further upstream request is sent for a response nobody is reading. See [#689](https://github.com/ndycode/codex-multi-auth/issues/689).
327
+
325
328
  Trust boundary: the loopback check bounds exposure to the local machine, and nothing more. It does not authenticate the process listening on that port, and plaintext HTTP is what a local inspection proxy needs, so any process that can bind the port you name receives the managed bearer token in cleartext. Set this variable only on a machine where you trust every local process that can bind a loopback port, and only for as long as you need the chain.
326
329
 
327
330
  Installed wrappers may perform a best-effort daily npm version check during normal forwarded Codex startup. When a newer package is detected, the wrapper only prints a manual notice on an interactive TTY or when `CODEX_MULTI_AUTH_DEBUG=1`: `npm install -g codex-multi-auth@latest`. It never runs npm install or update commands for you.
@@ -412,7 +415,7 @@ codex-multi-auth doctor --json
412
415
 
413
416
  ## Release Notes
414
417
 
415
- - Current stable: [docs/releases/v2.13.0.md](docs/releases/v2.13.0.md) — install via `npm i -g codex-multi-auth`
418
+ - Current stable: [docs/releases/v2.14.0.md](docs/releases/v2.14.0.md) — install via `npm i -g codex-multi-auth`
416
419
  - Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
417
420
  - Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
418
421
  - Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
@@ -35,15 +35,22 @@ export declare function resolveUnsupportedCodexFallbackModel(options: ResolveUns
35
35
  */
36
36
  export declare function shouldFallbackToGpt52OnUnsupportedGpt53(requestedModel: string | undefined, errorBody: unknown): boolean;
37
37
  /**
38
- * Detects whether an error code or response body indicates an entitlement/subscription issue for Codex models.
38
+ * Detects an upstream response saying the selected model is temporarily out of
39
+ * capacity, as opposed to the account being rate limited or the server being
40
+ * broken.
39
41
  *
40
- * Entitlement errors signal that the requested feature is not included in the user's plan and should not be treated as rate limits.
41
- * This function is pure and safe to call concurrently; it performs no filesystem access (including on Windows) and does not read or redact tokens — callers must avoid passing sensitive credentials in `code` or `bodyText`.
42
+ * The distinction matters because the two want opposite handling. A rate limit
43
+ * is per account, so rotating to another account clears it. Capacity is a
44
+ * property of the MODEL, so every account in the pool fails identically and
45
+ * rotating just burns the pool's transient budget in a few seconds, ending the
46
+ * request as a 503. The caller waits and re-sends on the same account instead.
47
+ * See issue #689.
42
48
  *
43
- * @param code - The error code string returned by the service
44
- * @param bodyText - The response body text to inspect for entitlement-related phrases
45
- * @returns `true` if the combined `code` or `bodyText` indicates an entitlement/subscription issue, `false` otherwise
49
+ * @param status - Upstream HTTP status.
50
+ * @param bodyText - Upstream error body, already read.
51
+ * @returns `true` when the request should be waited out rather than rotated.
46
52
  */
53
+ export declare function isModelAtCapacityError(status: number, bodyText: string): boolean;
47
54
  export declare function isEntitlementError(code: string, bodyText: string): boolean;
48
55
  /**
49
56
  * Detects whether an error indicates the workspace/account has been disabled or expired.
@@ -1 +1 @@
1
- {"version":3,"file":"error-classification.d.ts","sourceRoot":"","sources":["../../../lib/request/error-classification.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,WAAW,gBAAgB;IACzB,aAAa,EAAE,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACvB;AAED,sGAAsG;AACtG,eAAO,MAAM,oCAAoC,6CAA6C,CAAC;AAQ/F,eAAO,MAAM,wCAAwC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAoD7E,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACzC,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,sCAAsC;IACtD,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnC,+BAA+B,EAAE,OAAO,CAAC;IACzC,iCAAiC,EAAE,OAAO,CAAC;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACvC;AAiDD,wBAAgB,oCAAoC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAgBzF;AAED,sGAAsG;AACtG,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQ3F;AAED,wBAAgB,4BAA4B,CAC3C,SAAS,EAAE,OAAO,GAChB,yBAAyB,CAqD3B;AAED,wBAAgB,oCAAoC,CACnD,OAAO,EAAE,sCAAsC,GAC7C,MAAM,GAAG,SAAS,CAkCpB;AAED;;GAEG;AACH,wBAAgB,uCAAuC,CACtD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,SAAS,EAAE,OAAO,GAChB,OAAO,CAgBT;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAK1E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAChC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,MAAM,GACrB,OAAO,CAoDT;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,CAmB1E"}
1
+ {"version":3,"file":"error-classification.d.ts","sourceRoot":"","sources":["../../../lib/request/error-classification.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,WAAW,gBAAgB;IACzB,aAAa,EAAE,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACvB;AAED,sGAAsG;AACtG,eAAO,MAAM,oCAAoC,6CAA6C,CAAC;AAQ/F,eAAO,MAAM,wCAAwC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAoD7E,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACzC,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,sCAAsC;IACtD,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnC,+BAA+B,EAAE,OAAO,CAAC;IACzC,iCAAiC,EAAE,OAAO,CAAC;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACvC;AAiDD,wBAAgB,oCAAoC,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAgBzF;AAED,sGAAsG;AACtG,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQ3F;AAED,wBAAgB,4BAA4B,CAC3C,SAAS,EAAE,OAAO,GAChB,yBAAyB,CAqD3B;AAED,wBAAgB,oCAAoC,CACnD,OAAO,EAAE,sCAAsC,GAC7C,MAAM,GAAG,SAAS,CAkCpB;AAED;;GAEG;AACH,wBAAgB,uCAAuC,CACtD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,SAAS,EAAE,OAAO,GAChB,OAAO,CAgBT;AAqCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACrC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACd,OAAO,CAIT;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAK1E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAChC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,MAAM,GACrB,OAAO,CAoDT;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,CAmB1E"}
@@ -242,6 +242,51 @@ export function shouldFallbackToGpt52OnUnsupportedGpt53(requestedModel, errorBod
242
242
  * @param bodyText - The response body text to inspect for entitlement-related phrases
243
243
  * @returns `true` if the combined `code` or `bodyText` indicates an entitlement/subscription issue, `false` otherwise
244
244
  */
245
+ /**
246
+ * Statuses that are never a capacity signal, whatever the body says.
247
+ *
248
+ * Auth, payment, entitlement and not-found responses are terminal for this
249
+ * request and are each handled by their own branch upstream of the capacity
250
+ * check. Retrying them would spin until the deadline for no reason.
251
+ */
252
+ const NON_CAPACITY_STATUSES = new Set([401, 402, 403, 404]);
253
+ /**
254
+ * Phrases and codes that mean the model itself is out of capacity right now.
255
+ *
256
+ * This is matched on the body TEXT rather than on a status code, because the
257
+ * status this arrives with is not documented and has been reported differently
258
+ * by different callers. Matching text the way `isEntitlementError` and
259
+ * `isWorkspaceDisabledError` already do means a wrong guess about the status
260
+ * leaves behavior exactly as it is today, rather than breaking a path.
261
+ *
262
+ * `at capacity` is deliberately broad. In an OpenAI error body that phrase does
263
+ * not occur outside capacity pressure, and a false positive costs a bounded
264
+ * wait and one re-send, not a wrong answer.
265
+ */
266
+ const MODEL_AT_CAPACITY_PATTERN = /\bat capacity\b|\bmodel_capacity_exceeded\b|\bcapacity_exceeded\b|\bcurrently overloaded\b|\boverloaded_error\b|\bslow_down\b/i;
267
+ /**
268
+ * Detects an upstream response saying the selected model is temporarily out of
269
+ * capacity, as opposed to the account being rate limited or the server being
270
+ * broken.
271
+ *
272
+ * The distinction matters because the two want opposite handling. A rate limit
273
+ * is per account, so rotating to another account clears it. Capacity is a
274
+ * property of the MODEL, so every account in the pool fails identically and
275
+ * rotating just burns the pool's transient budget in a few seconds, ending the
276
+ * request as a 503. The caller waits and re-sends on the same account instead.
277
+ * See issue #689.
278
+ *
279
+ * @param status - Upstream HTTP status.
280
+ * @param bodyText - Upstream error body, already read.
281
+ * @returns `true` when the request should be waited out rather than rotated.
282
+ */
283
+ export function isModelAtCapacityError(status, bodyText) {
284
+ if (NON_CAPACITY_STATUSES.has(status))
285
+ return false;
286
+ if (!bodyText)
287
+ return false;
288
+ return MODEL_AT_CAPACITY_PATTERN.test(bodyText);
289
+ }
245
290
  export function isEntitlementError(code, bodyText) {
246
291
  const haystack = `${code} ${bodyText}`.toLowerCase();
247
292
  // "usage_not_included" means the subscription doesn't include this feature
@@ -6,7 +6,7 @@ import { type FastSessionInputTrimPlan } from "./request-transformer.js";
6
6
  import type { UserConfig, RequestBody } from "../types.js";
7
7
  import type { UsageTokenCounts } from "../usage/types.js";
8
8
  export { shouldRefreshToken, refreshAndUpdateToken } from "./token-refresh.js";
9
- export { DEFAULT_UNSUPPORTED_CODEX_FALLBACK_CHAIN, extractUnsupportedCodexModelFromText, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, shouldFallbackToGpt52OnUnsupportedGpt53, isEntitlementError, isWorkspaceDisabledError, createEntitlementErrorResponse, } from "./error-classification.js";
9
+ export { DEFAULT_UNSUPPORTED_CODEX_FALLBACK_CHAIN, extractUnsupportedCodexModelFromText, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, shouldFallbackToGpt52OnUnsupportedGpt53, isEntitlementError, isModelAtCapacityError, isWorkspaceDisabledError, createEntitlementErrorResponse, } from "./error-classification.js";
10
10
  export type { EntitlementError, UnsupportedCodexModelInfo, ResolveUnsupportedCodexFallbackOptions, } from "./error-classification.js";
11
11
  export { extractRequestUrl, rewriteUrlForCodex, resolveProxyUrlForRequest, closeSharedProxyDispatchers, applyProxyCompatibleInit, } from "./url-rewriting.js";
12
12
  export type { ProxyCompatibleRequestInit } from "./url-rewriting.js";
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-helpers.d.ts","sourceRoot":"","sources":["../../../lib/request/fetch-helpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fetch-helpers.d.ts","sourceRoot":"","sources":["../../../lib/request/fetch-helpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAIN,KAAK,wBAAwB,EAC7B,MAAM,0BAA0B,CAAC;AAMlC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAiB1D,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EACN,wCAAwC,EACxC,oCAAoC,EACpC,4BAA4B,EAC5B,oCAAoC,EACpC,uCAAuC,EACvC,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,8BAA8B,GAC9B,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACX,gBAAgB,EAChB,yBAAyB,EACzB,sCAAsC,GACtC,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACN,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,2BAA2B,EAC3B,wBAAwB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,YAAY,EACX,yBAAyB,EACzB,wBAAwB,GACxB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,aAAa;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACrB;AASD,MAAM,WAAW,8BAA8B;IAC9C,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,WAAW,CAAC;IACzB,4BAA4B,CAAC,EAAE,wBAAwB,CAAC,MAAM,CAAC,CAAC;CAChE;AAED,MAAM,WAAW,mBAAmB;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACpC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,wBAAwB,CAC7C,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,UAAU,EACtB,SAAS,UAAO,EAChB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,OAAO,CAAC,EAAE;IACT,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mBAAmB,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1C,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACnC,GACC,OAAO,CAAC,8BAA8B,GAAG,SAAS,CAAC,CA4FrD;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,oBAAoB,GACnC,OAAO,CAAC,mBAAmB,CAAC,CA4C9B;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACvC,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,OAAO,EACpB,OAAO,CAAC,EAAE;IACZ,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC5C,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B,GACC,OAAO,CAAC,QAAQ,CAAC,CAkBnB"}
@@ -11,7 +11,7 @@ import { HTTP_STATUS, ERROR_MESSAGES, LOG_STAGES, MAX_RATE_LIMIT_DELAY_MS, } fro
11
11
  import { CHATGPT_CODEX_UNSUPPORTED_MODEL_CODE, createEntitlementErrorResponse, extractUnsupportedCodexModelFromText, isEntitlementError, isUnsupportedCodexModelForChatGpt, } from "./error-classification.js";
12
12
  import { logDeprecationHeaders } from "./headers.js";
13
13
  export { shouldRefreshToken, refreshAndUpdateToken } from "./token-refresh.js";
14
- export { DEFAULT_UNSUPPORTED_CODEX_FALLBACK_CHAIN, extractUnsupportedCodexModelFromText, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, shouldFallbackToGpt52OnUnsupportedGpt53, isEntitlementError, isWorkspaceDisabledError, createEntitlementErrorResponse, } from "./error-classification.js";
14
+ export { DEFAULT_UNSUPPORTED_CODEX_FALLBACK_CHAIN, extractUnsupportedCodexModelFromText, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, shouldFallbackToGpt52OnUnsupportedGpt53, isEntitlementError, isModelAtCapacityError, isWorkspaceDisabledError, createEntitlementErrorResponse, } from "./error-classification.js";
15
15
  export { extractRequestUrl, rewriteUrlForCodex, resolveProxyUrlForRequest, closeSharedProxyDispatchers, applyProxyCompatibleInit, } from "./url-rewriting.js";
16
16
  export { createCodexHeaders } from "./headers.js";
17
17
  // MAX_RATE_LIMIT_DELAY_MS is centralized in constants.ts (shared with the
@@ -21,6 +21,12 @@ export interface RotationProxyStateInit {
21
21
  tokenRefreshSkewMs: number;
22
22
  networkErrorCooldownMs: number;
23
23
  serverErrorCooldownMs: number;
24
+ /**
25
+ * Wall-clock ceiling on how long ONE request may wait out a model-capacity
26
+ * response before giving up. `0` restores the pre-#689 behaviour of rotating
27
+ * the pool and returning the exhaustion 503.
28
+ */
29
+ modelCapacityRetryMs: number;
24
30
  tokenInvalidationCooldownMs: number;
25
31
  minRotationIntervalMs: number;
26
32
  pidOffsetEnabled: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"rotation-proxy-state.d.ts","sourceRoot":"","sources":["../../../lib/runtime/rotation-proxy-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAKjF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAEnE;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACtC,oBAAoB,EAAE,cAAc,CAAC;IACrC,gBAAgB,EAAE,SAAS,GAAG,QAAQ,CAAC;IACvC,kBAAkB,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC5C,SAAS,EAAE,OAAO,KAAK,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,2BAA2B,EAAE,MAAM,CAAC;IACpC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,yBAAyB,EAAE,MAAM,CAAC;IAClC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,8BAA8B,EAAE,MAAM,CAAC;IACvC,wBAAwB,EAAE,wBAAwB,CAAC;IACnD,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,oBAAoB,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAClD,8BAA8B,EAAE,MAAM,CAAC;IACvC;;;;;OAKG;IACH,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAmB,SAAQ,sBAAsB;IACjE,QAAQ,CAAC,oBAAoB,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAC;IAC5C,QAAQ,CAAC,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACzD,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yBAAyB,EAAE,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IACjE,wBAAwB,EAAE,MAAM,CAAC;CACjC;AAID,gBAAgB;AAChB,wBAAgB,wBAAwB,CACvC,IAAI,EAAE,sBAAsB,GAC1B,kBAAkB,CAuBpB;AAED,gBAAgB;AAChB,wBAAsB,wBAAwB,CAC7C,KAAK,EAAE,kBAAkB,GACvB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAkDhC"}
1
+ {"version":3,"file":"rotation-proxy-state.d.ts","sourceRoot":"","sources":["../../../lib/runtime/rotation-proxy-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAKjF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAEnE;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACtC,oBAAoB,EAAE,cAAc,CAAC;IACrC,gBAAgB,EAAE,SAAS,GAAG,QAAQ,CAAC;IACvC,kBAAkB,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC5C,SAAS,EAAE,OAAO,KAAK,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qBAAqB,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B,2BAA2B,EAAE,MAAM,CAAC;IACpC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,yBAAyB,EAAE,MAAM,CAAC;IAClC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,8BAA8B,EAAE,MAAM,CAAC;IACvC,wBAAwB,EAAE,wBAAwB,CAAC;IACnD,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,oBAAoB,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAClD,8BAA8B,EAAE,MAAM,CAAC;IACvC;;;;;OAKG;IACH,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAmB,SAAQ,sBAAsB;IACjE,QAAQ,CAAC,oBAAoB,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAC;IAC5C,QAAQ,CAAC,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACzD,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yBAAyB,EAAE,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IACjE,wBAAwB,EAAE,MAAM,CAAC;CACjC;AAID,gBAAgB;AAChB,wBAAgB,wBAAwB,CACvC,IAAI,EAAE,sBAAsB,GAC1B,kBAAkB,CAuBpB;AAED,gBAAgB;AAChB,wBAAsB,wBAAwB,CAC7C,KAAK,EAAE,kBAAkB,GACvB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAkDhC"}
@@ -48,6 +48,13 @@ export interface RuntimeRotationProxyOptions {
48
48
  * the value survives the launcher -> detached app-helper process boundary.
49
49
  */
50
50
  forcedAccountIndex?: number | null;
51
+ /**
52
+ * Wall-clock ceiling on how long ONE request may wait out a
53
+ * "selected model is at capacity" response before giving up (issue #689).
54
+ * `0` disables the wait entirely. Falls back to
55
+ * `CODEX_MULTI_AUTH_MODEL_CAPACITY_RETRY_MS`, then to a 10 minute default.
56
+ */
57
+ modelCapacityRetryMs?: number;
51
58
  }
52
59
  export interface RequestContext {
53
60
  body: Buffer;
@@ -1 +1 @@
1
- {"version":3,"file":"rotation-server-types.d.ts","sourceRoot":"","sources":["../../../lib/runtime/rotation-server-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,SAAS,EAAE,MAAM,0BAA0B,CAAC;IAC5C;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAED,MAAM,WAAW,0BAA0B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,2BAA2B;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,8BAA8B,CAAC,EAAE,MAAM,CAAC;IACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;;;;;OASG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,MAAM,gBAAgB,GACzB,YAAY,GACZ,cAAc,GACd,eAAe,GACf,cAAc,GACd,QAAQ,GACR,aAAa,GACb,YAAY,CAAC;AAChB,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,WAAW,8BAA8B;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CAClB"}
1
+ {"version":3,"file":"rotation-server-types.d.ts","sourceRoot":"","sources":["../../../lib/runtime/rotation-server-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,SAAS,EAAE,MAAM,0BAA0B,CAAC;IAC5C;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAED,MAAM,WAAW,0BAA0B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,2BAA2B;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,8BAA8B,CAAC,EAAE,MAAM,CAAC;IACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;;;;;OASG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,MAAM,gBAAgB,GACzB,YAAY,GACZ,cAAc,GACd,eAAe,GACf,cAAc,GACd,QAAQ,GACR,aAAa,GACb,YAAY,CAAC;AAChB,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,WAAW,8BAA8B;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CAClB"}
@@ -26,6 +26,16 @@ export declare function buildQuotaScheduleKey(account: Pick<ManagedAccount, "acc
26
26
  addedAt?: number;
27
27
  recordId?: string;
28
28
  }, family: ModelFamily, model?: string | null): string;
29
+ /**
30
+ * Total time one request may spend waiting out model capacity.
31
+ *
32
+ * `0` disables the behaviour entirely and restores the pre-#689 handling, where
33
+ * a capacity response rotates the pool and 503s. Anything unparseable falls
34
+ * back to the default rather than disabling, so a typo does not silently turn
35
+ * the feature off.
36
+ */
37
+ export declare function normalizeModelCapacityRetryMs(value: unknown): number;
38
+ export declare function resolveModelCapacityRetryMs(env?: NodeJS.ProcessEnv): number;
29
39
  /**
30
40
  * Coerce a forced-account pin from an option (number) or the launcher's env
31
41
  * string into a normalized 0-based index, or null when absent/invalid. Invalid
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-rotation-proxy.d.ts","sourceRoot":"","sources":["../../lib/runtime-rotation-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,eAAe,CAAC;AAkCvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAkDtE,OAAO,KAAK,EAKX,2BAA2B,EAC3B,0BAA0B,EAE1B,MAAM,oCAAoC,CAAC;AAe5C,YAAY,EACX,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,GAC1B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACN,+BAA+B,EAC/B,0BAA0B,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AACxE,OAAO,EACN,+BAA+B,EAC/B,8BAA8B,EAC9B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AA8DtE,kFAAkF;AAClF;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC9C,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GAAG;IACvE,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,GACC,MAAM,CAmBR;AAED,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GAAG;IACvE,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,CAER;AA8gBD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAC1C,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,MAAM,GAAG,IAAI,CAUf;AAED,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,2BAA2B,GAClC,OAAO,CAAC,0BAA0B,CAAC,CAmLrC"}
1
+ {"version":3,"file":"runtime-rotation-proxy.d.ts","sourceRoot":"","sources":["../../lib/runtime-rotation-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,eAAe,CAAC;AAkCvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAqDtE,OAAO,KAAK,EAKX,2BAA2B,EAC3B,0BAA0B,EAE1B,MAAM,oCAAoC,CAAC;AAe5C,YAAY,EACX,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,GAC1B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACN,+BAA+B,EAC/B,0BAA0B,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AACxE,OAAO,EACN,+BAA+B,EAC/B,8BAA8B,EAC9B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AA8DtE,kFAAkF;AAClF;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC9C,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GAAG;IACvE,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,GACC,MAAM,CAmBR;AAED,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GAAG;IACvE,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,CAER;AA0ED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKpE;AAED,wBAAgB,2BAA2B,CAC1C,GAAG,GAAE,MAAM,CAAC,UAAwB,GAClC,MAAM,CAQR;AAueD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAC1C,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,MAAM,GAAG,IAAI,CAUf;AAED,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,2BAA2B,GAClC,OAAO,CAAC,0BAA0B,CAAC,CA6LrC"}
@@ -9,7 +9,7 @@ import { CURRENT_CODEX_MODEL } from "./request/helpers/model-map.js";
9
9
  import { mutateRuntimeObservabilitySnapshot, recordRuntimeAccountRecovery, recordRuntimePoolExhaustion, } from "./runtime/runtime-observability.js";
10
10
  import { createRuntimeUsageRecorder, evaluateRuntimePolicy, loadRuntimePolicyState, } from "./policy/runtime-policy.js";
11
11
  import { createUsageStreamScanner } from "./usage/usage-extraction.js";
12
- import { isWorkspaceDisabledError } from "./request/fetch-helpers.js";
12
+ import { isModelAtCapacityError, isWorkspaceDisabledError, } from "./request/fetch-helpers.js";
13
13
  import { PreemptiveQuotaScheduler, readQuotaSchedulerSnapshot, } from "./preemptive-quota-scheduler.js";
14
14
  import { ContextBudgetGuard } from "./context-budget-guard.js";
15
15
  import { buildContextBudgetHeaders, createContextBudgetPauseResponse, } from "./context-budget-response.js";
@@ -144,6 +144,58 @@ function pinnedRetryBackoffMs(attempt) {
144
144
  const index = Math.min(Math.max(0, attempt - 1), PINNED_RETRY_BACKOFF_STEPS_MS.length - 1);
145
145
  return PINNED_RETRY_BACKOFF_STEPS_MS[index] ?? 0;
146
146
  }
147
+ /**
148
+ * Backoff schedule for a model-capacity wait (issue #689).
149
+ *
150
+ * Deliberately much longer than `PINNED_RETRY_BACKOFF_STEPS_MS`. That schedule
151
+ * spaces out re-sends of a request whose account might already be fine;
152
+ * capacity pressure is upstream and measured in minutes, so re-sending every
153
+ * 250ms would just be a tight poll against a busy backend. The last step
154
+ * repeats for every attempt past the table.
155
+ */
156
+ const CAPACITY_RETRY_BACKOFF_STEPS_MS = [
157
+ 2_000, 5_000, 15_000, 30_000, 60_000,
158
+ ];
159
+ /**
160
+ * Default wall-clock ceiling on capacity waiting for ONE request.
161
+ *
162
+ * The reporter's case is a long-running task started before stepping away, so
163
+ * the default has to be long enough to outlast a real capacity blip. It is
164
+ * still a hard ceiling: the request ends with the normal pool-exhausted 503
165
+ * rather than hanging forever.
166
+ */
167
+ const DEFAULT_MODEL_CAPACITY_RETRY_MS = 10 * 60_000;
168
+ const MAX_MODEL_CAPACITY_RETRY_MS = 60 * 60_000;
169
+ const MODEL_CAPACITY_RETRY_ENV = "CODEX_MULTI_AUTH_MODEL_CAPACITY_RETRY_MS";
170
+ function capacityRetryBackoffMs(attempt) {
171
+ const index = Math.min(Math.max(0, attempt - 1), CAPACITY_RETRY_BACKOFF_STEPS_MS.length - 1);
172
+ return CAPACITY_RETRY_BACKOFF_STEPS_MS[index] ?? 0;
173
+ }
174
+ /**
175
+ * Total time one request may spend waiting out model capacity.
176
+ *
177
+ * `0` disables the behaviour entirely and restores the pre-#689 handling, where
178
+ * a capacity response rotates the pool and 503s. Anything unparseable falls
179
+ * back to the default rather than disabling, so a typo does not silently turn
180
+ * the feature off.
181
+ */
182
+ export function normalizeModelCapacityRetryMs(value) {
183
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
184
+ return DEFAULT_MODEL_CAPACITY_RETRY_MS;
185
+ }
186
+ return Math.min(Math.floor(value), MAX_MODEL_CAPACITY_RETRY_MS);
187
+ }
188
+ export function resolveModelCapacityRetryMs(env = process.env) {
189
+ const raw = (env[MODEL_CAPACITY_RETRY_ENV] ?? "").trim();
190
+ if (!raw)
191
+ return DEFAULT_MODEL_CAPACITY_RETRY_MS;
192
+ // An explicit 0 is the documented kill switch and must survive normalization,
193
+ // which otherwise treats "not a usable number" as "use the default".
194
+ const parsed = Number(raw);
195
+ if (parsed === 0)
196
+ return 0;
197
+ return normalizeModelCapacityRetryMs(parsed);
198
+ }
147
199
  const MAX_REQUEST_BODY_BYTES = 64 * 1024 * 1024;
148
200
  const MAX_THREAD_GOAL_FALLBACKS = 512;
149
201
  const ALLOWED_RESPONSES_PATHS = new Set([
@@ -608,6 +660,14 @@ export async function startRuntimeRotationProxy(options) {
608
660
  const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
609
661
  const networkErrorCooldownMs = getNetworkErrorCooldownMs(pluginConfig);
610
662
  const serverErrorCooldownMs = getServerErrorCooldownMs(pluginConfig);
663
+ // Normalize the explicit option as well as the env var: a caller passing a
664
+ // negative or multi-hour value would otherwise bypass the documented cap.
665
+ // `0` stays 0, since it is the documented kill switch.
666
+ const modelCapacityRetryMs = options.modelCapacityRetryMs === 0
667
+ ? 0
668
+ : options.modelCapacityRetryMs === undefined
669
+ ? resolveModelCapacityRetryMs()
670
+ : normalizeModelCapacityRetryMs(options.modelCapacityRetryMs);
611
671
  const tokenInvalidationCooldownMs = getTokenInvalidationCooldownMs(pluginConfig);
612
672
  const minRotationIntervalMs = getMinRotationIntervalMs(pluginConfig);
613
673
  const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
@@ -654,6 +714,7 @@ export async function startRuntimeRotationProxy(options) {
654
714
  tokenRefreshSkewMs,
655
715
  networkErrorCooldownMs,
656
716
  serverErrorCooldownMs,
717
+ modelCapacityRetryMs,
657
718
  tokenInvalidationCooldownMs,
658
719
  minRotationIntervalMs,
659
720
  pidOffsetEnabled,
@@ -863,6 +924,24 @@ async function handleRequestInner(state, req, res, traceId) {
863
924
  let transientAttemptLimit = Math.max(1, Math.min(accountCount, state.maxRuntimeAccountAttempts));
864
925
  let transientAttempts = 0;
865
926
  let transientExhaustionReason = null;
927
+ // Capacity waits are counted separately from `transientAttempts` and from
928
+ // the pinned selection cap. Both of those bound how many DIFFERENT
929
+ // accounts or re-sends a failing request may burn; a capacity wait is not
930
+ // a failure of any account, it is the same request pausing for an
931
+ // upstream that is busy, and it has its own wall-clock ceiling. See #689.
932
+ let capacityRetries = 0;
933
+ // A wall-clock deadline, not a sum of sleeps. The upstream request and
934
+ // the error-body read also consume the budget, so summing only the
935
+ // planned sleeps let a slow capacity response push one request past the
936
+ // documented ceiling. Request-local, so concurrent requests never share
937
+ // a deadline.
938
+ let capacityDeadlineAt = null;
939
+ // Only the RESPONSE tells us the caller is still there. `req` is an
940
+ // IncomingMessage whose stream Node destroys once the request body has
941
+ // been fully read, which this proxy does up front, so `req.destroyed` is
942
+ // routinely true on a perfectly healthy request and must not be read as a
943
+ // disconnect.
944
+ const clientGone = () => res.destroyed || res.writableEnded;
866
945
  const accountSkipReasons = new Map();
867
946
  let reloadedAfterNoAccount = false;
868
947
  // Read the manual pin and affinity generation from disk (mtime-cached)
@@ -940,11 +1019,79 @@ async function handleRequestInner(state, req, res, traceId) {
940
1019
  if (isPinned) {
941
1020
  transientAttemptLimit = Math.max(1, Math.min(state.maxRuntimeAccountAttempts, MAX_PINNED_TRANSIENT_ATTEMPTS));
942
1021
  }
1022
+ /**
1023
+ * Wait out a "selected model is at capacity" response, then let the loop
1024
+ * re-send the request (issue #689).
1025
+ *
1026
+ * Capacity is a property of the MODEL, not of an account, so the normal
1027
+ * handling is actively wrong for it: rotating spends the pool's transient
1028
+ * budget against accounts that will all fail identically, and the request
1029
+ * ends as a pool-exhausted 503 within seconds. That is what kills a
1030
+ * long-running task started before the user stepped away.
1031
+ *
1032
+ * The responding account is left completely unpenalized: its pool token is
1033
+ * refunded, it is not marked rate limited, not cooled down, and it is
1034
+ * removed from `attemptedIndexes` so it stays selectable. Selection then
1035
+ * runs normally on the next pass, which may pick a different account. That
1036
+ * is deliberate: with capacity being model-wide, no account is a better bet,
1037
+ * and forcing a request-local pin here would duplicate the pinning path for
1038
+ * no gain.
1039
+ *
1040
+ * @param retryAfterMs Upstream hint when one was actually sent, else `null`
1041
+ * so the backoff table is used. Do NOT pass a synthesized default.
1042
+ * @param account Account that just saw the capacity response.
1043
+ * @returns `"retry"` to re-send, `"give-up"` to fall through to normal
1044
+ * handling, `"client-gone"` when the caller must abandon the request.
1045
+ */
1046
+ const waitOutModelCapacity = async (retryAfterMs, account) => {
1047
+ const budgetMs = state.modelCapacityRetryMs;
1048
+ if (!Number.isFinite(budgetMs) || budgetMs <= 0)
1049
+ return "give-up";
1050
+ if (capacityDeadlineAt === null) {
1051
+ capacityDeadlineAt = state.now() + budgetMs;
1052
+ }
1053
+ const remainingMs = capacityDeadlineAt - state.now();
1054
+ if (remainingMs <= 0)
1055
+ return "give-up";
1056
+ if (clientGone())
1057
+ return "client-gone";
1058
+ const hinted = retryAfterMs !== null &&
1059
+ Number.isFinite(retryAfterMs) &&
1060
+ retryAfterMs > 0
1061
+ ? retryAfterMs
1062
+ : capacityRetryBackoffMs(capacityRetries + 1);
1063
+ const waitMs = Math.min(Math.max(0, Math.floor(hinted)), remainingMs);
1064
+ if (waitMs <= 0)
1065
+ return "give-up";
1066
+ capacityRetries += 1;
1067
+ state.status.retries += 1;
1068
+ proxyLog.warn("model at capacity; waiting before re-sending", {
1069
+ traceId,
1070
+ waitMs,
1071
+ attempt: capacityRetries,
1072
+ remainingMs,
1073
+ budgetMs,
1074
+ });
1075
+ // The attempt debited a pool token before the upstream call. The account
1076
+ // did not fail, so refund it: without this a sustained capacity event
1077
+ // drains healthy accounts and later requests are refused admission with
1078
+ // `token-exhausted` well before the retry budget expires.
1079
+ refundConsumedPoolToken(account);
1080
+ attemptedIndexes.delete(account.index);
1081
+ await sleep(waitMs);
1082
+ // A capacity wait runs for tens of seconds. Re-sending an authenticated
1083
+ // upstream request for a response nobody is reading wastes upstream
1084
+ // capacity and extends how long the token is in flight.
1085
+ if (clientGone())
1086
+ return "client-gone";
1087
+ return "retry";
1088
+ };
943
1089
  let runtimeSelectionIterations = 0;
944
1090
  while ((isPinned || attemptedIndexes.size < accountCount) &&
945
1091
  transientAttempts < transientAttemptLimit &&
946
1092
  (!isPinned ||
947
- runtimeSelectionIterations < MAX_PINNED_SELECTION_ITERATIONS)) {
1093
+ runtimeSelectionIterations - capacityRetries <
1094
+ MAX_PINNED_SELECTION_ITERATIONS)) {
948
1095
  // Space out same-account re-sends. The loop has no other delay in it,
949
1096
  // and the pinned retry deliberately waives the account's own cooldown
950
1097
  // (below), so without this two copies of the same non-idempotent
@@ -1197,11 +1344,32 @@ async function handleRequestInner(state, req, res, traceId) {
1197
1344
  }
1198
1345
  if (upstream.status === HTTP_STATUS.TOO_MANY_REQUESTS) {
1199
1346
  const bodyText = await readErrorBody(upstream, state.streamStallTimeoutMs);
1200
- const retryAfterMs = parseRetryAfterHeaderMs(upstream.headers, state.now()) ??
1201
- parseRetryAfterBodyMs(bodyText, state.now()) ??
1202
- 60_000;
1347
+ // Keep the upstream HINT separate from the 60s fallback. Passing the
1348
+ // synthesized default into the capacity wait would make every
1349
+ // hint-less capacity 429 sleep a full minute and leave the 2s/5s/15s
1350
+ // backoff table dead on this path.
1351
+ const retryAfterHintMs = parseRetryAfterHeaderMs(upstream.headers, state.now()) ??
1352
+ parseRetryAfterBodyMs(bodyText, state.now());
1353
+ const retryAfterMs = retryAfterHintMs ?? 60_000;
1203
1354
  // Reading the body awaited I/O; a switch may have landed meanwhile.
1204
1355
  reconcileManualSelection();
1356
+ // A capacity 429 is not this account's quota. Marking it rate
1357
+ // limited would take a healthy account out of the pool for the
1358
+ // retry-after window on an outage that affects every account.
1359
+ if (isModelAtCapacityError(upstream.status, bodyText)) {
1360
+ const outcome = await waitOutModelCapacity(retryAfterHintMs, refreshed.account);
1361
+ if (outcome === "client-gone") {
1362
+ await usageRecorder.record({
1363
+ outcome: "failure",
1364
+ statusCode: upstream.status,
1365
+ errorCode: "client_disconnected_during_capacity_wait",
1366
+ account: refreshed.account,
1367
+ });
1368
+ return;
1369
+ }
1370
+ if (outcome === "retry")
1371
+ continue;
1372
+ }
1205
1373
  state.preemptiveQuotaScheduler.markRateLimited(quotaScheduleKey, retryAfterMs, state.now());
1206
1374
  // A 429 is the upstream quota signal for the attempted account, so
1207
1375
  // keep the consumed runtime token drained.
@@ -1340,7 +1508,23 @@ async function handleRequestInner(state, req, res, traceId) {
1340
1508
  continue;
1341
1509
  }
1342
1510
  if (upstream.status >= 500) {
1343
- await readErrorBody(upstream, state.streamStallTimeoutMs);
1511
+ const bodyText = await readErrorBody(upstream, state.streamStallTimeoutMs);
1512
+ // A capacity 5xx is upstream load, not a broken account, so it
1513
+ // must not cool the account down or count against the pool.
1514
+ if (isModelAtCapacityError(upstream.status, bodyText)) {
1515
+ const outcome = await waitOutModelCapacity(parseRetryAfterHeaderMs(upstream.headers, state.now()), refreshed.account);
1516
+ if (outcome === "client-gone") {
1517
+ await usageRecorder.record({
1518
+ outcome: "failure",
1519
+ statusCode: upstream.status,
1520
+ errorCode: "client_disconnected_during_capacity_wait",
1521
+ account: refreshed.account,
1522
+ });
1523
+ return;
1524
+ }
1525
+ if (outcome === "retry")
1526
+ continue;
1527
+ }
1344
1528
  refundConsumedPoolToken(refreshed.account);
1345
1529
  accountManager.recordFailure(refreshed.account, context.family, context.model);
1346
1530
  accountManager.markAccountCoolingDown(refreshed.account, state.serverErrorCooldownMs, "server-error");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",