oc-codex-multi-auth 6.14.2 → 6.14.4

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