oc-codex-multi-auth 6.14.1 → 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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -41
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts/rate-limits.d.ts +11 -0
- package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
- package/dist/lib/accounts/rate-limits.js +19 -2
- package/dist/lib/accounts/rate-limits.js.map +1 -1
- package/dist/lib/accounts/rotation.d.ts +33 -2
- package/dist/lib/accounts/rotation.d.ts.map +1 -1
- package/dist/lib/accounts/rotation.js +38 -3
- package/dist/lib/accounts/rotation.js.map +1 -1
- package/dist/lib/quota-windows.d.ts +17 -0
- package/dist/lib/quota-windows.d.ts.map +1 -1
- package/dist/lib/quota-windows.js +38 -3
- package/dist/lib/quota-windows.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +5 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +57 -19
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/rotation.d.ts.map +1 -1
- package/dist/lib/rotation.js +6 -1
- package/dist/lib/rotation.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
@@ -103,27 +103,14 @@ function isLoopbackGatewayHost(hostname) {
|
|
|
103
103
|
}
|
|
104
104
|
return LOOPBACK_GATEWAY_HOSTS.has(hostname);
|
|
105
105
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Stable per-account identity for the terminal-error diagnostics counters.
|
|
108
|
-
*
|
|
109
|
-
* `account.index` is not usable here: `AccountManager.removeAccount` reindexes
|
|
110
|
-
* the survivors, so an index recorded before a mid-traversal removal silently
|
|
111
|
-
* refers to a different account afterwards. The model-pool identity survives
|
|
112
|
-
* that reindex; the refresh token is the last-resort fallback for records that
|
|
113
|
-
* have not resolved an account id yet.
|
|
114
|
-
*/
|
|
106
|
+
/** Stable per-seat identity used only for in-memory traversal diagnostics. */
|
|
115
107
|
function getAccountDiagnosticsKey(account) {
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* Business seat in that workspace, and two keys can resolve to the same seat —
|
|
123
|
-
* so pool size must be counted over accounts, never over `poolKeys.length`.
|
|
124
|
-
*/
|
|
125
|
-
function countResolvedPoolAccounts(accounts, poolKeys) {
|
|
126
|
-
return accounts.filter((account) => poolKeys.some((key) => matchesModelPoolAccountKey(account, key))).length;
|
|
108
|
+
return JSON.stringify([
|
|
109
|
+
getModelPoolAccountKey(account) ?? "unresolved",
|
|
110
|
+
account.organizationId ?? null,
|
|
111
|
+
account.email ?? null,
|
|
112
|
+
account.addedAt,
|
|
113
|
+
]);
|
|
127
114
|
}
|
|
128
115
|
/** Configured pool keys matching no live account (typo, or account removed). */
|
|
129
116
|
function countUnresolvedPoolKeys(accounts, poolKeys) {
|
|
@@ -442,26 +429,43 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
442
429
|
* Take an account out of rotation whenever the backend reports one of its
|
|
443
430
|
* quota windows as fully spent (issue #218).
|
|
444
431
|
*
|
|
445
|
-
*
|
|
446
|
-
* and
|
|
447
|
-
* can record it instead of rediscovering it with a failed request
|
|
448
|
-
* subsequent prompt. The block lands on the persisted
|
|
449
|
-
* map, so it is remembered across restarts and shared
|
|
450
|
-
* 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.
|
|
451
453
|
*/
|
|
452
454
|
const applyQuotaExhaustion = (manager, headers, account, family, model) => {
|
|
453
455
|
try {
|
|
454
456
|
const resetAtMs = getQuotaExhaustedResetAtMs(headers);
|
|
455
457
|
if (resetAtMs === undefined)
|
|
456
|
-
return;
|
|
458
|
+
return false;
|
|
457
459
|
if (!manager.markQuotaExhausted(account, resetAtMs, family, model))
|
|
458
|
-
return;
|
|
460
|
+
return true;
|
|
459
461
|
account.lastSwitchReason = "rate-limit";
|
|
460
462
|
manager.saveToDiskDebounced();
|
|
461
463
|
logWarn(`Account ${account.index + 1} (${account.email ?? "unknown"}) has no ${family} quota left; skipping it for ${formatWaitTime(resetAtMs - Date.now())}.`);
|
|
464
|
+
return true;
|
|
462
465
|
}
|
|
463
466
|
catch (error) {
|
|
464
467
|
logDebug(`[${PLUGIN_NAME}] Failed to apply quota exhaustion: ${error instanceof Error ? error.message : String(error)}`);
|
|
468
|
+
return false;
|
|
465
469
|
}
|
|
466
470
|
};
|
|
467
471
|
const resolveActiveIndex = (storage, family = "codex") => {
|
|
@@ -1610,9 +1614,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1610
1614
|
while (true) {
|
|
1611
1615
|
let accountCount = accountManager.getAccountCount();
|
|
1612
1616
|
const attempted = new Set();
|
|
1613
|
-
// Diagnostics for the terminal error message below.
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1617
|
+
// Diagnostics for the terminal error message below. The composite key keeps
|
|
1618
|
+
// legacy Business seats distinct and can also be compared with snapshots
|
|
1619
|
+
// after a mid-traversal account removal.
|
|
1616
1620
|
// Deliberately scoped to this traversal: a fallback restart re-enters
|
|
1617
1621
|
// the loop with a different model, and the message names that model.
|
|
1618
1622
|
const fetchedAccountKeys = new Set();
|
|
@@ -1950,17 +1954,27 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1950
1954
|
latencyMs: fetchLatencyMs,
|
|
1951
1955
|
headers: Object.fromEntries(response.headers.entries()),
|
|
1952
1956
|
});
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
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 {
|
|
1956
1969
|
const contextOverflowResult = await handleContextOverflow(response, model);
|
|
1957
1970
|
if (contextOverflowResult.handled) {
|
|
1958
1971
|
return contextOverflowResult.response;
|
|
1959
1972
|
}
|
|
1960
|
-
const { response: errorResponse, rateLimit, errorBody, retryAsServerError } = await handleErrorResponse(response, {
|
|
1973
|
+
const { response: errorResponse, rateLimit, errorBody, retryAsServerError, quotaHeadersAuthoritative, } = await handleErrorResponse(response, {
|
|
1961
1974
|
requestCorrelationId,
|
|
1962
1975
|
threadId: threadIdCandidate,
|
|
1963
1976
|
});
|
|
1977
|
+
const quotaExhausted = quotaHeadersAuthoritative === true && recordQuotaHeaders();
|
|
1964
1978
|
const workspaceDeactivated = isDeactivatedWorkspaceError(errorBody, response.status);
|
|
1965
1979
|
if (workspaceDeactivated) {
|
|
1966
1980
|
const accountLabel = formatAccountLabel(account, account.index, {
|
|
@@ -2203,7 +2217,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2203
2217
|
runtimeMetrics.rateLimitedResponses++;
|
|
2204
2218
|
const { attempt, delayMs } = getRateLimitBackoff(account.index, quotaKey, rateLimit.retryAfterMs);
|
|
2205
2219
|
const waitLabel = formatWaitTime(delayMs);
|
|
2206
|
-
|
|
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 &&
|
|
2207
2226
|
consumeRetryBudget("rateLimitShort", `Short 429 retry for account ${account.index + 1} after ${delayMs}ms`)) {
|
|
2208
2227
|
if (accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
2209
2228
|
await showToast(`Rate limited. Retrying in ${waitLabel} (attempt ${attempt})...`, "warning", { duration: toastDurationMs });
|
|
@@ -2384,10 +2403,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2384
2403
|
if (strictAccountPool) {
|
|
2385
2404
|
const poolWaitMs = accountManager.getMinWaitTimeForFamily(modelFamily, model, preferredAccountIds);
|
|
2386
2405
|
const effectiveModel = model ?? requestedModel ?? "requested model";
|
|
2387
|
-
const
|
|
2388
|
-
const
|
|
2389
|
-
const
|
|
2390
|
-
const
|
|
2406
|
+
const accountSnapshot = accountManager.getAccountsSnapshot();
|
|
2407
|
+
const poolAccounts = accountSnapshot.filter((account) => preferredAccountIds.some((key) => matchesModelPoolAccountKey(account, key)));
|
|
2408
|
+
const poolAccountCount = poolAccounts.length;
|
|
2409
|
+
const unresolvedKeyCount = countUnresolvedPoolKeys(accountSnapshot, preferredAccountIds);
|
|
2410
|
+
const unavailableCount = poolAccounts.filter((account) => !fetchedAccountKeys.has(getAccountDiagnosticsKey(account))).length;
|
|
2391
2411
|
const waitDetail = poolWaitMs > 0
|
|
2392
2412
|
? ` Try again in ${formatWaitTime(poolWaitMs)}.`
|
|
2393
2413
|
: " Check the pooled accounts with `codex-health`.";
|
|
@@ -2431,7 +2451,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2431
2451
|
}
|
|
2432
2452
|
const waitMs = accountManager.getMinWaitTimeForFamily(modelFamily, model);
|
|
2433
2453
|
const count = accountManager.getAccountCount();
|
|
2434
|
-
|
|
2454
|
+
// Counted over the accounts that are still live rather than as
|
|
2455
|
+
// `count - attemptedCount`: an account removed mid-traversal was
|
|
2456
|
+
// genuinely attempted but is no longer one of the `count`
|
|
2457
|
+
// configured accounts, and that arithmetic would hide a live
|
|
2458
|
+
// account that never got a turn.
|
|
2459
|
+
const unavailableCount = accountManager
|
|
2460
|
+
.getAccountsSnapshot()
|
|
2461
|
+
.filter((account) => !fetchedAccountKeys.has(getAccountDiagnosticsKey(account))).length;
|
|
2435
2462
|
if (retryAllAccountsRateLimited &&
|
|
2436
2463
|
count > 0 &&
|
|
2437
2464
|
waitMs > 0 &&
|