oc-codex-multi-auth 6.14.2 → 6.14.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAsU/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAg6H/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAsU/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAq8H/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -429,26 +429,43 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
429
429
  * Take an account out of rotation whenever the backend reports one of its
430
430
  * quota windows as fully spent (issue #218).
431
431
  *
432
- * Every Codex response — success or 429 — carries `x-codex-*-used-percent`
433
- * and the matching reset time, so the moment an account hits 0% left we
434
- * can record it instead of rediscovering it with a failed request on every
435
- * subsequent prompt. The block lands on the persisted `rateLimitResetTimes`
436
- * map, so it is remembered across restarts and shared with other processes,
437
- * and it clears itself once the window rolls over.
432
+ * Codex carries `x-codex-*-used-percent` and the matching reset time on a
433
+ * served request and on a refused one alike, so the moment an account hits
434
+ * 0% left we can record it instead of rediscovering it with a failed request
435
+ * on every subsequent prompt. The block lands on the persisted
436
+ * `rateLimitResetTimes` map, so it is remembered across restarts and shared
437
+ * with other processes, and it clears itself once the window rolls over.
438
+ *
439
+ * Call this only for responses whose headers are authoritative: one the
440
+ * backend served, or one it refused for a confirmed usage limit. Every other
441
+ * error class — entitlement (issues #16/#17), auth, 5xx, or an upstream
442
+ * overload dressed up as a 429 — can echo a stale quota snapshot next to an
443
+ * unrelated failure, and writing that echo locked healthy accounts out for
444
+ * hours. `handleErrorResponse` makes that call once and reports it back as
445
+ * `quotaHeadersAuthoritative`.
446
+ *
447
+ * The resulting gap is deliberate: an account that really is spent but gets
448
+ * a 5xx stays selectable until its next genuine 429, which costs one wasted
449
+ * request — cheaper than a false multi-hour block on a healthy account.
450
+ *
451
+ * @returns true when the headers report a spent window, whether or not the
452
+ * stored block moved. Callers use it to stop retrying this account.
438
453
  */
439
454
  const applyQuotaExhaustion = (manager, headers, account, family, model) => {
440
455
  try {
441
456
  const resetAtMs = getQuotaExhaustedResetAtMs(headers);
442
457
  if (resetAtMs === undefined)
443
- return;
458
+ return false;
444
459
  if (!manager.markQuotaExhausted(account, resetAtMs, family, model))
445
- return;
460
+ return true;
446
461
  account.lastSwitchReason = "rate-limit";
447
462
  manager.saveToDiskDebounced();
448
463
  logWarn(`Account ${account.index + 1} (${account.email ?? "unknown"}) has no ${family} quota left; skipping it for ${formatWaitTime(resetAtMs - Date.now())}.`);
464
+ return true;
449
465
  }
450
466
  catch (error) {
451
467
  logDebug(`[${PLUGIN_NAME}] Failed to apply quota exhaustion: ${error instanceof Error ? error.message : String(error)}`);
468
+ return false;
452
469
  }
453
470
  };
454
471
  const resolveActiveIndex = (storage, family = "codex") => {
@@ -1937,17 +1954,27 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1937
1954
  latencyMs: fetchLatencyMs,
1938
1955
  headers: Object.fromEntries(response.headers.entries()),
1939
1956
  });
1940
- void recordPromptQuotaHeaders(response, account, accountCount);
1941
- applyQuotaExhaustion(accountManager, response.headers, account, modelFamily, model);
1942
- if (!response.ok) {
1957
+ // The TUI snapshot and the durable rotation block read the same
1958
+ // `x-codex-*` headers, so they share one authority decision — gating
1959
+ // only the block would leave the status line reporting "0% left" for
1960
+ // an account the router considers healthy.
1961
+ const recordQuotaHeaders = () => {
1962
+ void recordPromptQuotaHeaders(response, account, accountCount);
1963
+ return applyQuotaExhaustion(accountManager, response.headers, account, modelFamily, model);
1964
+ };
1965
+ if (response.ok) {
1966
+ recordQuotaHeaders();
1967
+ }
1968
+ else {
1943
1969
  const contextOverflowResult = await handleContextOverflow(response, model);
1944
1970
  if (contextOverflowResult.handled) {
1945
1971
  return contextOverflowResult.response;
1946
1972
  }
1947
- const { response: errorResponse, rateLimit, errorBody, retryAsServerError } = await handleErrorResponse(response, {
1973
+ const { response: errorResponse, rateLimit, errorBody, retryAsServerError, quotaHeadersAuthoritative, } = await handleErrorResponse(response, {
1948
1974
  requestCorrelationId,
1949
1975
  threadId: threadIdCandidate,
1950
1976
  });
1977
+ const quotaExhausted = quotaHeadersAuthoritative === true && recordQuotaHeaders();
1951
1978
  const workspaceDeactivated = isDeactivatedWorkspaceError(errorBody, response.status);
1952
1979
  if (workspaceDeactivated) {
1953
1980
  const accountLabel = formatAccountLabel(account, account.index, {
@@ -2190,7 +2217,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2190
2217
  runtimeMetrics.rateLimitedResponses++;
2191
2218
  const { attempt, delayMs } = getRateLimitBackoff(account.index, quotaKey, rateLimit.retryAfterMs);
2192
2219
  const waitLabel = formatWaitTime(delayMs);
2193
- if (delayMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS &&
2220
+ // A spent window will still be spent in a second, and the block
2221
+ // just written for it is monotonic — a short retry that happened
2222
+ // to succeed could not walk it back, so the account would serve
2223
+ // traffic while rotation still considers it blocked. Rotate.
2224
+ if (!quotaExhausted &&
2225
+ delayMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS &&
2194
2226
  consumeRetryBudget("rateLimitShort", `Short 429 retry for account ${account.index + 1} after ${delayMs}ms`)) {
2195
2227
  if (accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
2196
2228
  await showToast(`Rate limited. Retrying in ${waitLabel} (attempt ${attempt})...`, "warning", { duration: toastDurationMs });