oc-codex-multi-auth 6.19.0 → 6.20.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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +180 -79
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts/persistence.d.ts +22 -0
- package/dist/lib/accounts/persistence.d.ts.map +1 -1
- package/dist/lib/accounts/persistence.js +79 -0
- package/dist/lib/accounts/persistence.js.map +1 -1
- package/dist/lib/accounts/rate-limits.d.ts +22 -0
- package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
- package/dist/lib/accounts/rate-limits.js +25 -0
- package/dist/lib/accounts/rate-limits.js.map +1 -1
- package/dist/lib/accounts/rotation.d.ts +22 -23
- package/dist/lib/accounts/rotation.d.ts.map +1 -1
- package/dist/lib/accounts/rotation.js +62 -37
- package/dist/lib/accounts/rotation.js.map +1 -1
- package/dist/lib/accounts/stale-state.d.ts +31 -0
- package/dist/lib/accounts/stale-state.d.ts.map +1 -1
- package/dist/lib/accounts/stale-state.js +56 -1
- package/dist/lib/accounts/stale-state.js.map +1 -1
- package/dist/lib/accounts/state.d.ts +6 -0
- package/dist/lib/accounts/state.d.ts.map +1 -1
- package/dist/lib/accounts/state.js +15 -1
- package/dist/lib/accounts/state.js.map +1 -1
- package/dist/lib/auth/login-runner.d.ts +3 -0
- package/dist/lib/auth/login-runner.d.ts.map +1 -1
- package/dist/lib/auth/login-runner.js +35 -0
- package/dist/lib/auth/login-runner.js.map +1 -1
- package/dist/lib/codex-usage.d.ts +13 -5
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +33 -21
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/parallel-probe.d.ts.map +1 -1
- package/dist/lib/parallel-probe.js +4 -2
- package/dist/lib/parallel-probe.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +24 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +66 -20
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/schemas.d.ts +9 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +3 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/flagged.d.ts.map +1 -1
- package/dist/lib/storage/flagged.js +5 -0
- package/dist/lib/storage/flagged.js.map +1 -1
- package/dist/lib/storage/migrations.d.ts +21 -0
- package/dist/lib/storage/migrations.d.ts.map +1 -1
- package/dist/lib/storage/migrations.js.map +1 -1
- package/dist/lib/storage/normalize.d.ts.map +1 -1
- package/dist/lib/storage/normalize.js +57 -1
- package/dist/lib/storage/normalize.js.map +1 -1
- package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
- package/dist/lib/tools/codex-doctor.js +8 -66
- package/dist/lib/tools/codex-doctor.js.map +1 -1
- package/dist/lib/tools/codex-health.d.ts.map +1 -1
- package/dist/lib/tools/codex-health.js +7 -1
- package/dist/lib/tools/codex-health.js.map +1 -1
- package/dist/lib/tools/codex-list.d.ts.map +1 -1
- package/dist/lib/tools/codex-list.js +14 -1
- package/dist/lib/tools/codex-list.js.map +1 -1
- package/dist/lib/tools/codex-status.d.ts.map +1 -1
- package/dist/lib/tools/codex-status.js +6 -1
- package/dist/lib/tools/codex-status.js.map +1 -1
- package/dist/lib/tools/doctor-repair.d.ts +10 -0
- package/dist/lib/tools/doctor-repair.d.ts.map +1 -0
- package/dist/lib/tools/doctor-repair.js +86 -0
- package/dist/lib/tools/doctor-repair.js.map +1 -0
- package/dist/lib/tools/index.d.ts +3 -0
- package/dist/lib/tools/index.d.ts.map +1 -1
- package/dist/lib/tools/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/install-oc-codex-multi-auth-core.js +133 -6
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;AA6U/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA6rI/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -45,7 +45,7 @@ import { resolveDisplayEmail } from "./lib/account-display.js";
|
|
|
45
45
|
import { CodexAuthError } from "./lib/errors.js";
|
|
46
46
|
import { getStoragePath, loadAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, loadFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
|
|
47
47
|
import { getWorkspaceIdentityKey } from "./lib/storage/identity.js";
|
|
48
|
-
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, isDeactivatedWorkspaceError, isInvalidatedAuthTokenError, createAbortError, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
48
|
+
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, isDeactivatedWorkspaceError, isInvalidatedAuthTokenError, createAbortError, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, isDefaultAutoFallbackModel, pickFallbackChainTarget, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
49
49
|
import { shapeBodyForModel } from "./lib/request/helpers/responses-lite.js";
|
|
50
50
|
import { DEACTIVATED_WORKSPACE_ERROR_CODE, isDeactivatedWorkspaceErrorMessage, isInvalidatedAuthTokenMessage, } from "./lib/error-sentinels.js";
|
|
51
51
|
import { applyFastSessionDefaults, clampReasoningForModel, upsertBackendModelIdentityMessage, } from "./lib/request/request-transformer.js";
|
|
@@ -468,8 +468,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
468
468
|
* served request and on a refused one alike, so the moment an account hits
|
|
469
469
|
* 0% left we can record it instead of rediscovering it with a failed request
|
|
470
470
|
* on every subsequent prompt. The block lands on the persisted
|
|
471
|
-
* `
|
|
472
|
-
*
|
|
471
|
+
* account-wide `quotaExhaustedUntil` field, so it is remembered across
|
|
472
|
+
* restarts and clears itself once the window rolls over.
|
|
473
473
|
*
|
|
474
474
|
* Call this only for responses whose headers are authoritative: one the
|
|
475
475
|
* backend served, or one it refused for a confirmed usage limit. Every other
|
|
@@ -495,7 +495,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
495
495
|
return true;
|
|
496
496
|
account.lastSwitchReason = "rate-limit";
|
|
497
497
|
manager.saveToDiskDebounced();
|
|
498
|
-
logWarn(`Account ${account.index + 1}
|
|
498
|
+
logWarn(`Account ${account.index + 1} has no shared subscription quota left; skipping it for ${formatWaitTime(resetAtMs - Date.now())}.`);
|
|
499
499
|
return true;
|
|
500
500
|
}
|
|
501
501
|
catch (error) {
|
|
@@ -750,6 +750,23 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
750
750
|
return null;
|
|
751
751
|
return `resets in ${formatWaitTime(remaining)}`;
|
|
752
752
|
};
|
|
753
|
+
// Account-wide subscription-quota exhaustion is a DIFFERENT block from the
|
|
754
|
+
// per-family rate limits above: it lives on its own field and is reported
|
|
755
|
+
// with its own label so a spent weekly quota is never shown as a transient
|
|
756
|
+
// 429 ("rate limit").
|
|
757
|
+
const getQuotaExhaustedUntil = (account, now) => {
|
|
758
|
+
const until = account.quotaExhaustedUntil;
|
|
759
|
+
if (typeof until !== "number" || !Number.isFinite(until) || until <= now) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
return until;
|
|
763
|
+
};
|
|
764
|
+
const formatQuotaExhaustionEntry = (account, now) => {
|
|
765
|
+
const until = getQuotaExhaustedUntil(account, now);
|
|
766
|
+
if (until === null)
|
|
767
|
+
return null;
|
|
768
|
+
return `quota exhausted, resets in ${formatWaitTime(until - now)}`;
|
|
769
|
+
};
|
|
753
770
|
const applyUiRuntimeFromConfig = (pluginConfig) => {
|
|
754
771
|
return setUiRuntimeOptions({
|
|
755
772
|
v2Enabled: getCodexTuiV2(pluginConfig),
|
|
@@ -1265,6 +1282,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1265
1282
|
resolveActiveIndex,
|
|
1266
1283
|
getRateLimitResetTimeForFamily,
|
|
1267
1284
|
formatRateLimitEntry,
|
|
1285
|
+
formatQuotaExhaustionEntry,
|
|
1268
1286
|
buildJsonAccountIdentity,
|
|
1269
1287
|
buildRoutingVisibilitySnapshot,
|
|
1270
1288
|
appendRoutingVisibilityText,
|
|
@@ -1659,6 +1677,86 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1659
1677
|
if (model) {
|
|
1660
1678
|
attemptedUnsupportedFallbackModels.add(model);
|
|
1661
1679
|
}
|
|
1680
|
+
// Degrading the model mid-request touches several coupled pieces:
|
|
1681
|
+
// the attempted-model set, the routing snapshot, the model's own
|
|
1682
|
+
// instructions, the reasoning clamp (only the 5.6 tiers accept
|
|
1683
|
+
// `max`, so an un-clamped sol -> gpt-5.5 hop turns a graceful
|
|
1684
|
+
// fallback into a hard 400) and the per-model body shape. Both the
|
|
1685
|
+
// entitlement fallback and the quota fallback go through here so
|
|
1686
|
+
// the two can never drift apart on any of them.
|
|
1687
|
+
const applyModelFallback = async (previousModel, target, reason) => {
|
|
1688
|
+
attemptedUnsupportedFallbackModels.add(previousModel);
|
|
1689
|
+
attemptedUnsupportedFallbackModels.add(target);
|
|
1690
|
+
model = target;
|
|
1691
|
+
modelFamily = getModelFamily(model);
|
|
1692
|
+
quotaKey = `${modelFamily}:${model}`;
|
|
1693
|
+
fallbackApplied = true;
|
|
1694
|
+
fallbackFrom = previousModel;
|
|
1695
|
+
fallbackTo = model;
|
|
1696
|
+
fallbackReason = reason;
|
|
1697
|
+
const fallbackInstructions = await getCodexInstructions(model);
|
|
1698
|
+
if (transformedBody && typeof transformedBody === "object") {
|
|
1699
|
+
transformedBody = {
|
|
1700
|
+
...transformedBody,
|
|
1701
|
+
model,
|
|
1702
|
+
instructions: fallbackInstructions,
|
|
1703
|
+
input: upsertBackendModelIdentityMessage(transformedBody.input, model),
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
else {
|
|
1707
|
+
let fallbackBody = {
|
|
1708
|
+
model,
|
|
1709
|
+
instructions: fallbackInstructions,
|
|
1710
|
+
};
|
|
1711
|
+
if (requestInit?.body && typeof requestInit.body === "string") {
|
|
1712
|
+
try {
|
|
1713
|
+
const parsed = JSON.parse(requestInit.body);
|
|
1714
|
+
fallbackBody = {
|
|
1715
|
+
...parsed,
|
|
1716
|
+
model,
|
|
1717
|
+
instructions: fallbackInstructions,
|
|
1718
|
+
};
|
|
1719
|
+
if (Array.isArray(fallbackBody.input)) {
|
|
1720
|
+
fallbackBody.input = upsertBackendModelIdentityMessage(fallbackBody.input, model);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
catch {
|
|
1724
|
+
// Keep minimal fallback body if parsing fails.
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
transformedBody = fallbackBody;
|
|
1728
|
+
}
|
|
1729
|
+
const clampedReasoning = clampReasoningForModel(transformedBody.reasoning, model);
|
|
1730
|
+
if (clampedReasoning !== transformedBody.reasoning) {
|
|
1731
|
+
transformedBody = {
|
|
1732
|
+
...transformedBody,
|
|
1733
|
+
reasoning: clampedReasoning,
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
requestInit = {
|
|
1737
|
+
...(requestInit ?? {}),
|
|
1738
|
+
body: JSON.stringify(shapeBodyForModel(transformedBody)),
|
|
1739
|
+
};
|
|
1740
|
+
if (runtimeMetrics.lastSelectionSnapshot) {
|
|
1741
|
+
runtimeMetrics.lastSelectionSnapshot = {
|
|
1742
|
+
...runtimeMetrics.lastSelectionSnapshot,
|
|
1743
|
+
family: modelFamily,
|
|
1744
|
+
model: model ?? null,
|
|
1745
|
+
requestedModel,
|
|
1746
|
+
effectiveModel: model ?? null,
|
|
1747
|
+
quotaKey,
|
|
1748
|
+
fallbackApplied,
|
|
1749
|
+
fallbackFrom,
|
|
1750
|
+
fallbackTo,
|
|
1751
|
+
fallbackReason,
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
// A degraded model must not degrade again without bound, even if a
|
|
1756
|
+
// custom chain is cyclic. The attempted set already prevents
|
|
1757
|
+
// revisiting a model; this caps the total hops per request.
|
|
1758
|
+
const MAX_QUOTA_FALLBACK_SWITCHES = 3;
|
|
1759
|
+
let quotaFallbackSwitches = 0;
|
|
1662
1760
|
while (true) {
|
|
1663
1761
|
let accountCount = accountManager.getAccountCount();
|
|
1664
1762
|
const attempted = new Set();
|
|
@@ -1697,6 +1795,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1697
1795
|
break;
|
|
1698
1796
|
}
|
|
1699
1797
|
attempted.add(account.index);
|
|
1798
|
+
// Hybrid's last-resort result is not necessarily eligible. Requests
|
|
1799
|
+
// must honor active blocks rather than sending it upstream anyway.
|
|
1800
|
+
if (selectionExplainability.some((entry) => entry.index === account.index && !entry.eligible)) {
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1700
1803
|
runtimeMetrics.lastSelectedAccountIndex = account.index;
|
|
1701
1804
|
runtimeMetrics.lastQuotaKey = quotaKey;
|
|
1702
1805
|
if (runtimeMetrics.lastSelectionSnapshot) {
|
|
@@ -2109,81 +2212,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2109
2212
|
if (fallbackModel) {
|
|
2110
2213
|
const previousModel = model ?? "gpt-5-codex";
|
|
2111
2214
|
const previousModelFamily = modelFamily;
|
|
2112
|
-
attemptedUnsupportedFallbackModels.add(previousModel);
|
|
2113
|
-
attemptedUnsupportedFallbackModels.add(fallbackModel);
|
|
2114
2215
|
accountManager.refundToken(account, previousModelFamily, previousModel);
|
|
2115
|
-
|
|
2116
|
-
modelFamily = getModelFamily(model);
|
|
2117
|
-
quotaKey = `${modelFamily}:${model}`;
|
|
2118
|
-
fallbackApplied = true;
|
|
2119
|
-
fallbackFrom = previousModel;
|
|
2120
|
-
fallbackTo = model;
|
|
2121
|
-
fallbackReason = "fallback-unsupported-model-entitlement";
|
|
2122
|
-
const fallbackInstructions = await getCodexInstructions(model);
|
|
2123
|
-
if (transformedBody && typeof transformedBody === "object") {
|
|
2124
|
-
transformedBody = {
|
|
2125
|
-
...transformedBody,
|
|
2126
|
-
model,
|
|
2127
|
-
instructions: fallbackInstructions,
|
|
2128
|
-
input: upsertBackendModelIdentityMessage(transformedBody.input, model),
|
|
2129
|
-
};
|
|
2130
|
-
}
|
|
2131
|
-
else {
|
|
2132
|
-
let fallbackBody = {
|
|
2133
|
-
model,
|
|
2134
|
-
instructions: fallbackInstructions,
|
|
2135
|
-
};
|
|
2136
|
-
if (requestInit?.body && typeof requestInit.body === "string") {
|
|
2137
|
-
try {
|
|
2138
|
-
const parsed = JSON.parse(requestInit.body);
|
|
2139
|
-
fallbackBody = {
|
|
2140
|
-
...parsed,
|
|
2141
|
-
model,
|
|
2142
|
-
instructions: fallbackInstructions,
|
|
2143
|
-
};
|
|
2144
|
-
if (Array.isArray(fallbackBody.input)) {
|
|
2145
|
-
fallbackBody.input = upsertBackendModelIdentityMessage(fallbackBody.input, model);
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
catch {
|
|
2149
|
-
// Keep minimal fallback body if parsing fails.
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
transformedBody = fallbackBody;
|
|
2153
|
-
}
|
|
2154
|
-
// The carried-over reasoning effort was clamped for the ORIGINAL
|
|
2155
|
-
// model; the fallback target may not accept it (`max` exists only
|
|
2156
|
-
// on the 5.6 tiers, so a sol -> gpt-5.5 hop must degrade it or the
|
|
2157
|
-
// graceful fallback turns into a hard 400).
|
|
2158
|
-
const clampedReasoning = clampReasoningForModel(transformedBody.reasoning, model);
|
|
2159
|
-
if (clampedReasoning !== transformedBody.reasoning) {
|
|
2160
|
-
transformedBody = {
|
|
2161
|
-
...transformedBody,
|
|
2162
|
-
reasoning: clampedReasoning,
|
|
2163
|
-
};
|
|
2164
|
-
}
|
|
2165
|
-
// Shape for whichever model this attempt targets. A 5.6 -> 5.5 fallback
|
|
2166
|
-
// must go out in the classic shape, and a 5.6 -> 5.6 hop must re-fold
|
|
2167
|
-
// the new model's instructions into `input` rather than leaving them
|
|
2168
|
-
// at the top level.
|
|
2169
|
-
requestInit = {
|
|
2170
|
-
...(requestInit ?? {}),
|
|
2171
|
-
body: JSON.stringify(shapeBodyForModel(transformedBody)),
|
|
2172
|
-
};
|
|
2173
|
-
if (runtimeMetrics.lastSelectionSnapshot) {
|
|
2174
|
-
runtimeMetrics.lastSelectionSnapshot = {
|
|
2175
|
-
...runtimeMetrics.lastSelectionSnapshot,
|
|
2176
|
-
family: modelFamily,
|
|
2177
|
-
model: model ?? null,
|
|
2178
|
-
requestedModel,
|
|
2179
|
-
effectiveModel: model ?? null,
|
|
2180
|
-
quotaKey,
|
|
2181
|
-
fallbackApplied,
|
|
2182
|
-
fallbackFrom,
|
|
2183
|
-
fallbackTo,
|
|
2184
|
-
fallbackReason,
|
|
2185
|
-
};
|
|
2186
|
-
}
|
|
2216
|
+
await applyModelFallback(previousModel, fallbackModel, "fallback-unsupported-model-entitlement");
|
|
2187
2217
|
runtimeMetrics.lastError = `Model fallback: ${previousModel} -> ${model}`;
|
|
2188
2218
|
runtimeMetrics.lastErrorCategory = "model-fallback";
|
|
2189
2219
|
logWarn(`Model ${previousModel} is unsupported for this ChatGPT account. Falling back to ${model}.`, {
|
|
@@ -2279,7 +2309,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2279
2309
|
await sleep(addJitter(Math.max(MIN_BACKOFF_MS, delayMs), 0.2));
|
|
2280
2310
|
continue;
|
|
2281
2311
|
}
|
|
2282
|
-
|
|
2312
|
+
// Authoritative subscription exhaustion already has its own block.
|
|
2313
|
+
// Do not duplicate its reset as a model/family transient 429; retain
|
|
2314
|
+
// any genuine transient state written by other in-flight requests.
|
|
2315
|
+
if (!quotaExhausted) {
|
|
2316
|
+
accountManager.markRateLimitedWithReason(account, delayMs, modelFamily, parseRateLimitReason(rateLimit.code), model);
|
|
2317
|
+
}
|
|
2283
2318
|
accountManager.recordRateLimit(account, modelFamily, model);
|
|
2284
2319
|
account.lastSwitchReason = "rate-limit";
|
|
2285
2320
|
runtimeMetrics.accountRotations++;
|
|
@@ -2507,6 +2542,72 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2507
2542
|
const unavailableCount = accountManager
|
|
2508
2543
|
.getAccountsSnapshot()
|
|
2509
2544
|
.filter((account) => !fetchedAccountKeys.has(getAccountDiagnosticsKey(account))).length;
|
|
2545
|
+
const enabledSelection = count > 0 ? accountManager
|
|
2546
|
+
.getSelectionExplainability(modelFamily, model)
|
|
2547
|
+
.filter((entry) => entry.enabled) : [];
|
|
2548
|
+
const upstreamBlocked = enabledSelection.length > 0 && enabledSelection.every((entry) => entry.rateLimitedUntil !== undefined || entry.quotaExhaustedUntil !== undefined);
|
|
2549
|
+
// Every enabled account has an active upstream block. Before waiting out a
|
|
2550
|
+
// block that can run for days (`retryAllAccountsMaxRetries`
|
|
2551
|
+
// defaults to Infinity), degrade to the next chain model that is
|
|
2552
|
+
// actually usable right now. Gated exactly like the entitlement
|
|
2553
|
+
// auto-fallback -- same default-selector entry models, same
|
|
2554
|
+
// opt-out env vars -- even when an entry ID was selected directly.
|
|
2555
|
+
// Local token depletion and auth cooldown alone never trigger it.
|
|
2556
|
+
// An account-wide quota block fails the eligibility test
|
|
2557
|
+
// on every candidate, so it correctly falls through to the wait.
|
|
2558
|
+
if (upstreamBlocked &&
|
|
2559
|
+
waitMs > 0 &&
|
|
2560
|
+
count > 0 &&
|
|
2561
|
+
model &&
|
|
2562
|
+
quotaFallbackSwitches < MAX_QUOTA_FALLBACK_SWITCHES &&
|
|
2563
|
+
isDefaultAutoFallbackModel(model, attemptedUnsupportedFallbackModels)) {
|
|
2564
|
+
const rejected = new Set();
|
|
2565
|
+
let usableFallback;
|
|
2566
|
+
while (true) {
|
|
2567
|
+
const candidate = pickFallbackChainTarget({
|
|
2568
|
+
currentModel: model,
|
|
2569
|
+
attemptedModels: new Set([
|
|
2570
|
+
...attemptedUnsupportedFallbackModels,
|
|
2571
|
+
...rejected,
|
|
2572
|
+
]),
|
|
2573
|
+
customChain: unsupportedCodexFallbackChain,
|
|
2574
|
+
fallbackToGpt52OnUnsupportedGpt53,
|
|
2575
|
+
});
|
|
2576
|
+
if (!candidate || rejected.has(candidate))
|
|
2577
|
+
break;
|
|
2578
|
+
// Only degrade to a model some account can serve NOW,
|
|
2579
|
+
// otherwise the hop just moves the same block sideways.
|
|
2580
|
+
const candidatePool = getModelAccountPool(pluginConfig, candidate);
|
|
2581
|
+
const strictCandidatePool = candidatePool.length > 0 &&
|
|
2582
|
+
getModelAccountPoolMode(pluginConfig, candidate) === "strict";
|
|
2583
|
+
const candidateAccounts = accountManager.getAccountsSnapshot();
|
|
2584
|
+
const candidateEligible = accountManager.getSelectionExplainability(getModelFamily(candidate), candidate).some((entry) => entry.eligible && (!strictCandidatePool ||
|
|
2585
|
+
candidateAccounts.some((account) => account.index === entry.index &&
|
|
2586
|
+
candidatePool.some((key) => matchesModelPoolAccountKey(account, key)))));
|
|
2587
|
+
// A preferred pool may spill into general accounts; a strict
|
|
2588
|
+
// pool must contain an eligible member. This does not select
|
|
2589
|
+
// an account or advance any rotation cursor.
|
|
2590
|
+
if (candidateEligible) {
|
|
2591
|
+
usableFallback = candidate;
|
|
2592
|
+
break;
|
|
2593
|
+
}
|
|
2594
|
+
rejected.add(candidate);
|
|
2595
|
+
}
|
|
2596
|
+
if (usableFallback) {
|
|
2597
|
+
const previousModel = model;
|
|
2598
|
+
quotaFallbackSwitches++;
|
|
2599
|
+
await applyModelFallback(previousModel, usableFallback, "fallback-quota-exhausted");
|
|
2600
|
+
runtimeMetrics.lastError = `Model fallback: ${previousModel} -> ${model}`;
|
|
2601
|
+
runtimeMetrics.lastErrorCategory = "model-fallback";
|
|
2602
|
+
logWarn(`All ${count} account(s) are rate-limited or out of quota for ${previousModel}. Falling back to ${model}.`, {
|
|
2603
|
+
requestedModel: previousModel,
|
|
2604
|
+
effectiveModel: model,
|
|
2605
|
+
fallbackApplied: true,
|
|
2606
|
+
fallbackReason: "fallback-quota-exhausted",
|
|
2607
|
+
});
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2510
2611
|
if (retryAllAccountsRateLimited &&
|
|
2511
2612
|
count > 0 &&
|
|
2512
2613
|
waitMs > 0 &&
|