oc-codex-multi-auth 6.19.0 → 6.21.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/README.md +15 -12
- package/config/README.md +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +473 -145
- package/dist/index.js.map +1 -1
- package/dist/lib/account-display.d.ts.map +1 -1
- package/dist/lib/account-display.js +3 -1
- package/dist/lib/account-display.js.map +1 -1
- package/dist/lib/accounts/persistence.d.ts +25 -1
- package/dist/lib/accounts/persistence.d.ts.map +1 -1
- package/dist/lib/accounts/persistence.js +105 -2
- 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 +32 -0
- package/dist/lib/accounts/stale-state.d.ts.map +1 -1
- package/dist/lib/accounts/stale-state.js +86 -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/accounts/warm-recovery.d.ts +8 -0
- package/dist/lib/accounts/warm-recovery.d.ts.map +1 -0
- package/dist/lib/accounts/warm-recovery.js +70 -0
- package/dist/lib/accounts/warm-recovery.js.map +1 -0
- package/dist/lib/accounts/warm-request.d.ts +2 -0
- package/dist/lib/accounts/warm-request.d.ts.map +1 -1
- package/dist/lib/accounts/warm-request.js +4 -2
- package/dist/lib/accounts/warm-request.js.map +1 -1
- package/dist/lib/accounts.d.ts +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +2 -2
- package/dist/lib/accounts.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/circuit-breaker.d.ts +5 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -1
- package/dist/lib/circuit-breaker.js +23 -6
- package/dist/lib/circuit-breaker.js.map +1 -1
- package/dist/lib/codex-reset.d.ts +7 -1
- package/dist/lib/codex-reset.d.ts.map +1 -1
- package/dist/lib/codex-reset.js +9 -2
- package/dist/lib/codex-reset.js.map +1 -1
- package/dist/lib/codex-usage.d.ts +16 -6
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +68 -23
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/config.d.ts +2 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +38 -14
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/parallel-probe.d.ts.map +1 -1
- package/dist/lib/parallel-probe.js +5 -2
- package/dist/lib/parallel-probe.js.map +1 -1
- package/dist/lib/quota-notifications.d.ts.map +1 -1
- package/dist/lib/quota-notifications.js +10 -1
- package/dist/lib/quota-notifications.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 +88 -38
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/response-handler.d.ts.map +1 -1
- package/dist/lib/request/response-handler.js +85 -43
- package/dist/lib/request/response-handler.js.map +1 -1
- package/dist/lib/rotation.d.ts +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/load-save.d.ts +1 -0
- package/dist/lib/storage/load-save.d.ts.map +1 -1
- package/dist/lib/storage/load-save.js +11 -0
- package/dist/lib/storage/load-save.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 +93 -1
- package/dist/lib/storage/normalize.js.map +1 -1
- package/dist/lib/storage/state.d.ts +1 -0
- package/dist/lib/storage/state.d.ts.map +1 -1
- package/dist/lib/storage/state.js +12 -0
- package/dist/lib/storage/state.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-limits.d.ts.map +1 -1
- package/dist/lib/tools/codex-limits.js +13 -1
- package/dist/lib/tools/codex-limits.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-reset.d.ts.map +1 -1
- package/dist/lib/tools/codex-reset.js +40 -1
- package/dist/lib/tools/codex-reset.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/codex-warm.d.ts +2 -1
- package/dist/lib/tools/codex-warm.d.ts.map +1 -1
- package/dist/lib/tools/codex-warm.js +48 -5
- package/dist/lib/tools/codex-warm.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 +4 -1
- package/dist/lib/tools/index.d.ts.map +1 -1
- package/dist/lib/tools/index.js.map +1 -1
- package/dist/lib/tui-quota-cache.d.ts.map +1 -1
- package/dist/lib/tui-quota-cache.js +3 -1
- package/dist/lib/tui-quota-cache.js.map +1 -1
- package/dist/tui.d.ts +4 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +24 -14
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/scripts/install-oc-codex-multi-auth-core.js +168 -11
package/dist/index.js
CHANGED
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
|
|
24
24
|
*/
|
|
25
25
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
26
|
+
import { watchFile, unwatchFile } from "node:fs";
|
|
27
|
+
import { consumeLastWrittenAccountsDigest } from "./lib/storage/load-save.js";
|
|
28
|
+
import { subscribeToStoragePathChanges } from "./lib/storage/state.js";
|
|
29
|
+
import { isKeychainOptInEnabled } from "./lib/storage/keychain.js";
|
|
30
|
+
import { AnyAccountStorageSchema } from "./lib/schemas.js";
|
|
31
|
+
import { registerCleanup, unregisterCleanup } from "./lib/shutdown.js";
|
|
26
32
|
import { createHash } from "node:crypto";
|
|
27
33
|
import { homedir } from "node:os";
|
|
28
34
|
import { join } from "node:path";
|
|
@@ -45,7 +51,7 @@ import { resolveDisplayEmail } from "./lib/account-display.js";
|
|
|
45
51
|
import { CodexAuthError } from "./lib/errors.js";
|
|
46
52
|
import { getStoragePath, loadAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, loadFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
|
|
47
53
|
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";
|
|
54
|
+
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, isDeactivatedWorkspaceError, isInvalidatedAuthTokenError, createAbortError, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, isDefaultAutoFallbackModel, pickFallbackChainTarget, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
49
55
|
import { shapeBodyForModel } from "./lib/request/helpers/responses-lite.js";
|
|
50
56
|
import { DEACTIVATED_WORKSPACE_ERROR_CODE, isDeactivatedWorkspaceErrorMessage, isInvalidatedAuthTokenMessage, } from "./lib/error-sentinels.js";
|
|
51
57
|
import { applyFastSessionDefaults, clampReasoningForModel, upsertBackendModelIdentityMessage, } from "./lib/request/request-transformer.js";
|
|
@@ -468,8 +474,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
468
474
|
* served request and on a refused one alike, so the moment an account hits
|
|
469
475
|
* 0% left we can record it instead of rediscovering it with a failed request
|
|
470
476
|
* on every subsequent prompt. The block lands on the persisted
|
|
471
|
-
* `
|
|
472
|
-
*
|
|
477
|
+
* account-wide `quotaExhaustedUntil` field, so it is remembered across
|
|
478
|
+
* restarts and clears itself once the window rolls over.
|
|
473
479
|
*
|
|
474
480
|
* Call this only for responses whose headers are authoritative: one the
|
|
475
481
|
* backend served, or one it refused for a confirmed usage limit. Every other
|
|
@@ -495,7 +501,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
495
501
|
return true;
|
|
496
502
|
account.lastSwitchReason = "rate-limit";
|
|
497
503
|
manager.saveToDiskDebounced();
|
|
498
|
-
logWarn(`Account ${account.index + 1}
|
|
504
|
+
logWarn(`Account ${account.index + 1} has no shared subscription quota left; skipping it for ${formatWaitTime(resetAtMs - Date.now())}.`);
|
|
499
505
|
return true;
|
|
500
506
|
}
|
|
501
507
|
catch (error) {
|
|
@@ -750,6 +756,23 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
750
756
|
return null;
|
|
751
757
|
return `resets in ${formatWaitTime(remaining)}`;
|
|
752
758
|
};
|
|
759
|
+
// Account-wide subscription-quota exhaustion is a DIFFERENT block from the
|
|
760
|
+
// per-family rate limits above: it lives on its own field and is reported
|
|
761
|
+
// with its own label so a spent weekly quota is never shown as a transient
|
|
762
|
+
// 429 ("rate limit").
|
|
763
|
+
const getQuotaExhaustedUntil = (account, now) => {
|
|
764
|
+
const until = account.quotaExhaustedUntil;
|
|
765
|
+
if (typeof until !== "number" || !Number.isFinite(until) || until <= now) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
return until;
|
|
769
|
+
};
|
|
770
|
+
const formatQuotaExhaustionEntry = (account, now) => {
|
|
771
|
+
const until = getQuotaExhaustedUntil(account, now);
|
|
772
|
+
if (until === null)
|
|
773
|
+
return null;
|
|
774
|
+
return `quota exhausted, resets in ${formatWaitTime(until - now)}`;
|
|
775
|
+
};
|
|
753
776
|
const applyUiRuntimeFromConfig = (pluginConfig) => {
|
|
754
777
|
return setUiRuntimeOptions({
|
|
755
778
|
v2Enabled: getCodexTuiV2(pluginConfig),
|
|
@@ -816,6 +839,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
816
839
|
.filter((entry) => entry.length > 0)));
|
|
817
840
|
};
|
|
818
841
|
const supportsInteractiveMenus = () => {
|
|
842
|
+
if (process.env.FORCE_INTERACTIVE_MODE === "1")
|
|
843
|
+
return true;
|
|
819
844
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
820
845
|
return false;
|
|
821
846
|
if (process.env.OPENCODE_TUI === "1")
|
|
@@ -824,6 +849,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
824
849
|
return false;
|
|
825
850
|
if (process.env.TERM_PROGRAM === "opencode")
|
|
826
851
|
return false;
|
|
852
|
+
if (process.env.ELECTRON_RUN_AS_NODE === "1")
|
|
853
|
+
return false;
|
|
827
854
|
return true;
|
|
828
855
|
};
|
|
829
856
|
const promptAccountIndexSelection = async (ui, storage, title) => {
|
|
@@ -1095,22 +1122,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1095
1122
|
logDebug(`[${PLUGIN_NAME}] Startup preflight skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
1096
1123
|
}
|
|
1097
1124
|
};
|
|
1098
|
-
const invalidateAccountManagerCache = () => {
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
// pointing at a stale instance. Flush first (best-effort, in the
|
|
1102
|
-
// background) so any queued debounced save is not silently dropped.
|
|
1125
|
+
const invalidateAccountManagerCache = (clearedSnapshots) => {
|
|
1126
|
+
// Retire before flushing: keep disk membership authoritative while
|
|
1127
|
+
// publishing queued rate-limit evidence through the volatile merge.
|
|
1103
1128
|
const previous = cachedAccountManager;
|
|
1104
1129
|
cachedAccountManager = null;
|
|
1105
1130
|
accountManagerPromise = null;
|
|
1106
1131
|
if (previous) {
|
|
1132
|
+
previous.disposeShutdownHandler(false, clearedSnapshots);
|
|
1107
1133
|
void previous
|
|
1108
1134
|
.flushPendingSave()
|
|
1109
1135
|
.catch((error) => {
|
|
1110
1136
|
logWarn(`Failed to flush pending save while invalidating account manager: ${error instanceof Error ? error.message : String(error)}`);
|
|
1111
|
-
})
|
|
1112
|
-
.finally(() => {
|
|
1113
|
-
previous.disposeShutdownHandler();
|
|
1114
1137
|
});
|
|
1115
1138
|
}
|
|
1116
1139
|
};
|
|
@@ -1143,6 +1166,142 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1143
1166
|
logWarn(`Failed to reload account manager: ${error instanceof Error ? error.message : String(error)}`);
|
|
1144
1167
|
}
|
|
1145
1168
|
};
|
|
1169
|
+
let watchedAccountsPath;
|
|
1170
|
+
let observedAccountsDigest;
|
|
1171
|
+
let accountsReloadTimer;
|
|
1172
|
+
let accountsWatcherDisposed = false;
|
|
1173
|
+
let accountsWatchGeneration = 0;
|
|
1174
|
+
let unsubscribeAccountsPath;
|
|
1175
|
+
const stopAccountsWatcher = () => {
|
|
1176
|
+
accountsWatchGeneration += 1;
|
|
1177
|
+
if (watchedAccountsPath)
|
|
1178
|
+
unwatchFile(watchedAccountsPath, onAccountsStatChanged);
|
|
1179
|
+
watchedAccountsPath = undefined;
|
|
1180
|
+
observedAccountsDigest = undefined;
|
|
1181
|
+
clearTimeout(accountsReloadTimer);
|
|
1182
|
+
accountsReloadTimer = undefined;
|
|
1183
|
+
};
|
|
1184
|
+
const disposeAccountsWatcher = () => {
|
|
1185
|
+
accountsWatcherDisposed = true;
|
|
1186
|
+
unsubscribeAccountsPath?.();
|
|
1187
|
+
stopAccountsWatcher();
|
|
1188
|
+
unregisterCleanup(disposeAccountsWatcher);
|
|
1189
|
+
};
|
|
1190
|
+
const readAccountsDigest = async (path) => {
|
|
1191
|
+
try {
|
|
1192
|
+
const content = await readFile(path, "utf8");
|
|
1193
|
+
if (!AnyAccountStorageSchema.safeParse(JSON.parse(content)).success)
|
|
1194
|
+
return;
|
|
1195
|
+
return createHash("sha256").update(content).digest("hex");
|
|
1196
|
+
}
|
|
1197
|
+
catch {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
const reloadForExternalAccountsChange = async (path, generation, attempt = 0, retired) => {
|
|
1202
|
+
const digest = await readAccountsDigest(path);
|
|
1203
|
+
if (generation !== accountsWatchGeneration || !digest || path !== getStoragePath())
|
|
1204
|
+
return;
|
|
1205
|
+
if (digest === consumeLastWrittenAccountsDigest(path))
|
|
1206
|
+
return;
|
|
1207
|
+
const previous = cachedAccountManager;
|
|
1208
|
+
try {
|
|
1209
|
+
// A null cache means an invalidation retired the incumbent; the
|
|
1210
|
+
// reload still must adopt the external change. Bailing here would
|
|
1211
|
+
// strand it: onAccountsFileChanged already marked this digest
|
|
1212
|
+
// observed, and an in-flight load started before the write can
|
|
1213
|
+
// still resolve and install a pre-change snapshot whose
|
|
1214
|
+
// full-membership save then deletes the imported accounts.
|
|
1215
|
+
if (previous) {
|
|
1216
|
+
if (previous !== retired) {
|
|
1217
|
+
previous.disposeShutdownHandler(true);
|
|
1218
|
+
retired = previous;
|
|
1219
|
+
}
|
|
1220
|
+
await previous.flushPendingSave();
|
|
1221
|
+
}
|
|
1222
|
+
const reloaded = await AccountManager.loadFromDisk();
|
|
1223
|
+
if (generation !== accountsWatchGeneration || accountsWatcherDisposed || path !== getStoragePath()) {
|
|
1224
|
+
reloaded.disposeShutdownHandler();
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const outgoing = cachedAccountManager;
|
|
1228
|
+
if (outgoing && outgoing !== retired) {
|
|
1229
|
+
// Another actor replaced the cached manager while this reload
|
|
1230
|
+
// was in flight (concurrent fetch reload or tool mutation).
|
|
1231
|
+
// Retire the incumbent with the same external-reload
|
|
1232
|
+
// semantics, or its queued membership save can clobber the
|
|
1233
|
+
// external change this reload is about to adopt.
|
|
1234
|
+
outgoing.disposeShutdownHandler(true);
|
|
1235
|
+
retired = outgoing;
|
|
1236
|
+
}
|
|
1237
|
+
cachedAccountManager = reloaded;
|
|
1238
|
+
accountManagerPromise = Promise.resolve(reloaded);
|
|
1239
|
+
observedAccountsDigest = digest;
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
logWarn("Could not reload externally updated account storage");
|
|
1243
|
+
if (attempt < 2 && generation === accountsWatchGeneration && !accountsWatcherDisposed) {
|
|
1244
|
+
accountsReloadTimer = setTimeout(() => {
|
|
1245
|
+
accountsReloadTimer = undefined;
|
|
1246
|
+
void reloadForExternalAccountsChange(path, generation, attempt + 1, retired);
|
|
1247
|
+
}, 1500);
|
|
1248
|
+
accountsReloadTimer.unref();
|
|
1249
|
+
}
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
logDebug("Reloaded cached account manager after external accounts file change");
|
|
1253
|
+
};
|
|
1254
|
+
const onAccountsFileChanged = async () => {
|
|
1255
|
+
if (accountsWatcherDisposed)
|
|
1256
|
+
return;
|
|
1257
|
+
if (watchedAccountsPath !== getStoragePath()) {
|
|
1258
|
+
await ensureAccountsWatcher();
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const path = watchedAccountsPath;
|
|
1262
|
+
if (!path)
|
|
1263
|
+
return;
|
|
1264
|
+
const generation = accountsWatchGeneration;
|
|
1265
|
+
const digest = await readAccountsDigest(path);
|
|
1266
|
+
if (generation !== accountsWatchGeneration || !digest || digest === observedAccountsDigest)
|
|
1267
|
+
return;
|
|
1268
|
+
observedAccountsDigest = digest;
|
|
1269
|
+
clearTimeout(accountsReloadTimer);
|
|
1270
|
+
accountsReloadTimer = undefined;
|
|
1271
|
+
if (digest === consumeLastWrittenAccountsDigest(path))
|
|
1272
|
+
return;
|
|
1273
|
+
accountsReloadTimer = setTimeout(() => {
|
|
1274
|
+
accountsReloadTimer = undefined;
|
|
1275
|
+
void reloadForExternalAccountsChange(path, generation);
|
|
1276
|
+
}, 500);
|
|
1277
|
+
accountsReloadTimer.unref();
|
|
1278
|
+
};
|
|
1279
|
+
const onAccountsStatChanged = () => {
|
|
1280
|
+
void onAccountsFileChanged();
|
|
1281
|
+
};
|
|
1282
|
+
const ensureAccountsWatcher = async () => {
|
|
1283
|
+
if (accountsWatcherDisposed)
|
|
1284
|
+
return;
|
|
1285
|
+
const path = getStoragePath();
|
|
1286
|
+
if (path === watchedAccountsPath)
|
|
1287
|
+
return;
|
|
1288
|
+
stopAccountsWatcher();
|
|
1289
|
+
if (isKeychainOptInEnabled())
|
|
1290
|
+
return;
|
|
1291
|
+
unsubscribeAccountsPath ??= subscribeToStoragePathChanges(() => {
|
|
1292
|
+
void ensureAccountsWatcher();
|
|
1293
|
+
});
|
|
1294
|
+
watchedAccountsPath = path;
|
|
1295
|
+
const generation = accountsWatchGeneration;
|
|
1296
|
+
const initialDigest = await readAccountsDigest(path);
|
|
1297
|
+
if (generation !== accountsWatchGeneration)
|
|
1298
|
+
return;
|
|
1299
|
+
observedAccountsDigest = initialDigest;
|
|
1300
|
+
// Stat polling follows the path across the storage writer's temp-file rename.
|
|
1301
|
+
watchFile(path, { interval: 1500, persistent: false }, onAccountsStatChanged);
|
|
1302
|
+
unregisterCleanup(disposeAccountsWatcher);
|
|
1303
|
+
registerCleanup(disposeAccountsWatcher);
|
|
1304
|
+
};
|
|
1146
1305
|
const persistAuthenticatedSelections = async (results, replaceAll) => {
|
|
1147
1306
|
try {
|
|
1148
1307
|
await persistAccountPool(results, replaceAll);
|
|
@@ -1175,6 +1334,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1175
1334
|
const { event } = input;
|
|
1176
1335
|
if (event.type === "server.instance.disposed") {
|
|
1177
1336
|
quotaMonitor.dispose();
|
|
1337
|
+
disposeAccountsWatcher();
|
|
1178
1338
|
return;
|
|
1179
1339
|
}
|
|
1180
1340
|
// Handle TUI account selection events
|
|
@@ -1265,6 +1425,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1265
1425
|
resolveActiveIndex,
|
|
1266
1426
|
getRateLimitResetTimeForFamily,
|
|
1267
1427
|
formatRateLimitEntry,
|
|
1428
|
+
formatQuotaExhaustionEntry,
|
|
1268
1429
|
buildJsonAccountIdentity,
|
|
1269
1430
|
buildRoutingVisibilitySnapshot,
|
|
1270
1431
|
appendRoutingVisibilityText,
|
|
@@ -1328,7 +1489,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1328
1489
|
});
|
|
1329
1490
|
}
|
|
1330
1491
|
applyUiRuntimeFromConfig(pluginConfig);
|
|
1331
|
-
|
|
1492
|
+
let perProjectAccounts = getPerProjectAccounts(pluginConfig);
|
|
1493
|
+
let storageTransition;
|
|
1494
|
+
const activeFetches = new Set();
|
|
1332
1495
|
setStoragePath(perProjectAccounts ? process.cwd() : null);
|
|
1333
1496
|
const authFallback = auth.type === "oauth" ? auth : undefined;
|
|
1334
1497
|
// Prefer multi-account auth metadata when available, but still handle
|
|
@@ -1368,8 +1531,19 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1368
1531
|
}
|
|
1369
1532
|
});
|
|
1370
1533
|
}
|
|
1371
|
-
|
|
1372
|
-
cachedAccountManager
|
|
1534
|
+
const loadedManager = await accountManagerPromise;
|
|
1535
|
+
if (cachedAccountManager && cachedAccountManager !== loadedManager) {
|
|
1536
|
+
// An external reload replaced the cache while this load was
|
|
1537
|
+
// in flight. The loaded snapshot is stale; retiring it keeps
|
|
1538
|
+
// its debounced save from writing full membership over the
|
|
1539
|
+
// successor's state.
|
|
1540
|
+
loadedManager.disposeShutdownHandler();
|
|
1541
|
+
}
|
|
1542
|
+
else {
|
|
1543
|
+
cachedAccountManager = loadedManager;
|
|
1544
|
+
}
|
|
1545
|
+
const accountManager = cachedAccountManager ?? loadedManager;
|
|
1546
|
+
await ensureAccountsWatcher();
|
|
1373
1547
|
const refreshToken = authFallback?.refresh ?? "";
|
|
1374
1548
|
const needsPersist = refreshToken &&
|
|
1375
1549
|
!accountManager.hasRefreshToken(refreshToken);
|
|
@@ -1394,77 +1568,37 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1394
1568
|
global: providerConfig?.options || {},
|
|
1395
1569
|
models: providerConfig?.models || {},
|
|
1396
1570
|
};
|
|
1397
|
-
// Load plugin configuration and determine CODEX_MODE
|
|
1398
|
-
// Priority: CODEX_MODE env var > config file > default (true)
|
|
1399
|
-
const codexMode = getCodexMode(pluginConfig);
|
|
1400
|
-
const requestTransformMode = getRequestTransformMode(pluginConfig);
|
|
1401
|
-
const useLegacyRequestTransform = requestTransformMode === "legacy";
|
|
1402
|
-
const fastSessionEnabled = getFastSession(pluginConfig);
|
|
1403
|
-
const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
|
|
1404
|
-
const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
|
|
1405
|
-
const beginnerSafeMode = getBeginnerSafeMode(pluginConfig);
|
|
1406
|
-
beginnerSafeModeEnabled = beginnerSafeMode;
|
|
1407
|
-
const maskEmailEnabled = getCodexTuiMaskEmail(pluginConfig);
|
|
1408
|
-
const retryProfile = beginnerSafeMode
|
|
1409
|
-
? "conservative"
|
|
1410
|
-
: getRetryProfile(pluginConfig);
|
|
1411
|
-
const retryBudgetOverrides = beginnerSafeMode
|
|
1412
|
-
? {}
|
|
1413
|
-
: getRetryBudgetOverrides(pluginConfig);
|
|
1414
|
-
const retryBudgetLimits = resolveRetryBudgetLimits(retryProfile, retryBudgetOverrides);
|
|
1415
|
-
runtimeMetrics.retryProfile = retryProfile;
|
|
1416
|
-
runtimeMetrics.retryBudgetLimits = { ...retryBudgetLimits };
|
|
1417
|
-
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
1418
|
-
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
1419
|
-
const retryAllAccountsRateLimited = beginnerSafeMode
|
|
1420
|
-
? false
|
|
1421
|
-
: getRetryAllAccountsRateLimited(pluginConfig);
|
|
1422
|
-
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
1423
|
-
const retryAllAccountsMaxRetries = beginnerSafeMode
|
|
1424
|
-
? Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig))
|
|
1425
|
-
: getRetryAllAccountsMaxRetries(pluginConfig);
|
|
1426
|
-
const unsupportedCodexPolicy = getUnsupportedCodexPolicy(pluginConfig);
|
|
1427
|
-
const fallbackOnUnsupportedCodexModel = unsupportedCodexPolicy === "fallback";
|
|
1428
|
-
const fallbackToGpt52OnUnsupportedGpt53 = getFallbackToGpt52OnUnsupportedGpt53(pluginConfig);
|
|
1429
|
-
const unsupportedCodexFallbackChain = getUnsupportedCodexFallbackChain(pluginConfig);
|
|
1430
|
-
const toastDurationMs = getToastDurationMs(pluginConfig);
|
|
1431
|
-
const accountToastsEnabled = getAccountToastsEnabled(pluginConfig);
|
|
1432
|
-
const fetchTimeoutMs = getFetchTimeoutMs(pluginConfig);
|
|
1433
|
-
const streamStallTimeoutMs = getStreamStallTimeoutMs(pluginConfig);
|
|
1434
1571
|
const sessionRecoveryEnabled = getSessionRecovery(pluginConfig);
|
|
1572
|
+
beginnerSafeModeEnabled = getBeginnerSafeMode(pluginConfig);
|
|
1573
|
+
const initialRetryProfile = beginnerSafeModeEnabled ? "conservative" : getRetryProfile(pluginConfig);
|
|
1574
|
+
runtimeMetrics.retryProfile = initialRetryProfile;
|
|
1575
|
+
runtimeMetrics.retryBudgetLimits = resolveRetryBudgetLimits(initialRetryProfile, beginnerSafeModeEnabled ? {} : getRetryBudgetOverrides(pluginConfig));
|
|
1435
1576
|
const autoResumeEnabled = getAutoResume(pluginConfig);
|
|
1436
1577
|
const autoUpdateEnabled = getAutoUpdate(pluginConfig);
|
|
1437
|
-
|
|
1438
|
-
const emptyResponseRetryDelayMs = getEmptyResponseRetryDelayMs(pluginConfig);
|
|
1439
|
-
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
|
|
1440
|
-
const rotationStrategy = getRotationStrategy(pluginConfig);
|
|
1441
|
-
const effectiveUserConfig = fastSessionEnabled
|
|
1442
|
-
? applyFastSessionDefaults(userConfig)
|
|
1443
|
-
: userConfig;
|
|
1444
|
-
if (fastSessionEnabled) {
|
|
1578
|
+
if (getFastSession(pluginConfig)) {
|
|
1445
1579
|
logDebug("Fast session mode enabled", {
|
|
1446
1580
|
reasoningEffort: "none/low",
|
|
1447
1581
|
reasoningSummary: "auto",
|
|
1448
1582
|
textVerbosity: "low",
|
|
1449
|
-
fastSessionStrategy,
|
|
1450
|
-
fastSessionMaxInputItems,
|
|
1583
|
+
fastSessionStrategy: getFastSessionStrategy(pluginConfig),
|
|
1584
|
+
fastSessionMaxInputItems: getFastSessionMaxInputItems(pluginConfig),
|
|
1451
1585
|
});
|
|
1452
1586
|
}
|
|
1453
|
-
if (
|
|
1587
|
+
if (getBeginnerSafeMode(pluginConfig)) {
|
|
1454
1588
|
logInfo("Beginner safe mode enabled", {
|
|
1455
|
-
retryProfile,
|
|
1456
|
-
retryAllAccountsRateLimited,
|
|
1457
|
-
retryAllAccountsMaxRetries,
|
|
1589
|
+
retryProfile: "conservative",
|
|
1590
|
+
retryAllAccountsRateLimited: false,
|
|
1591
|
+
retryAllAccountsMaxRetries: Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig)),
|
|
1458
1592
|
});
|
|
1459
1593
|
}
|
|
1460
1594
|
const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" &&
|
|
1461
1595
|
process.env.VITEST !== "true" &&
|
|
1462
1596
|
process.env.NODE_ENV !== "test";
|
|
1463
|
-
if (!startupPrewarmTriggered && prewarmEnabled &&
|
|
1597
|
+
if (!startupPrewarmTriggered && prewarmEnabled && getRequestTransformMode(pluginConfig) === "legacy") {
|
|
1464
1598
|
startupPrewarmTriggered = true;
|
|
1465
1599
|
const configuredModels = Object.keys(userConfig.models ?? {});
|
|
1466
1600
|
prewarmCodexInstructions(configuredModels);
|
|
1467
|
-
if (
|
|
1601
|
+
if (getCodexMode(pluginConfig)) {
|
|
1468
1602
|
prewarmOpenCodeCodexPrompt();
|
|
1469
1603
|
}
|
|
1470
1604
|
}
|
|
@@ -1497,10 +1631,79 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1497
1631
|
* @returns Response from Codex API
|
|
1498
1632
|
*/
|
|
1499
1633
|
async fetch(input, init) {
|
|
1634
|
+
let finishFetch;
|
|
1635
|
+
let pendingFetch;
|
|
1500
1636
|
try {
|
|
1501
|
-
|
|
1502
|
-
|
|
1637
|
+
const pluginConfig = loadPluginConfig();
|
|
1638
|
+
const currentPerProjectAccounts = getPerProjectAccounts(pluginConfig);
|
|
1639
|
+
while (storageTransition)
|
|
1640
|
+
await storageTransition;
|
|
1641
|
+
if (currentPerProjectAccounts !== perProjectAccounts) {
|
|
1642
|
+
storageTransition = (async () => {
|
|
1643
|
+
await Promise.all(activeFetches);
|
|
1644
|
+
// Drain the old pool's pending write before changing the global path.
|
|
1645
|
+
await cachedAccountManager?.flushPendingSave();
|
|
1646
|
+
setStoragePath(currentPerProjectAccounts ? process.cwd() : null);
|
|
1647
|
+
invalidateAccountManagerCache();
|
|
1648
|
+
perProjectAccounts = currentPerProjectAccounts;
|
|
1649
|
+
})().finally(() => { storageTransition = undefined; });
|
|
1650
|
+
await storageTransition;
|
|
1651
|
+
}
|
|
1652
|
+
pendingFetch = new Promise((resolve) => { finishFetch = resolve; });
|
|
1653
|
+
activeFetches.add(pendingFetch);
|
|
1654
|
+
if (!accountManagerPromise) {
|
|
1655
|
+
const pending = AccountManager.loadFromDisk();
|
|
1656
|
+
accountManagerPromise = pending;
|
|
1657
|
+
void pending.catch(() => {
|
|
1658
|
+
if (accountManagerPromise === pending)
|
|
1659
|
+
accountManagerPromise = null;
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
if (!cachedAccountManager) {
|
|
1663
|
+
const loadedManager = await accountManagerPromise;
|
|
1664
|
+
if (cachedAccountManager && cachedAccountManager !== loadedManager) {
|
|
1665
|
+
// External reload won the race while this load was in
|
|
1666
|
+
// flight: adopt the cache and retire the stale load.
|
|
1667
|
+
loadedManager.disposeShutdownHandler();
|
|
1668
|
+
}
|
|
1669
|
+
else if (!cachedAccountManager) {
|
|
1670
|
+
cachedAccountManager = loadedManager;
|
|
1671
|
+
}
|
|
1503
1672
|
}
|
|
1673
|
+
const codexMode = getCodexMode(pluginConfig);
|
|
1674
|
+
const requestTransformMode = getRequestTransformMode(pluginConfig);
|
|
1675
|
+
const fastSessionEnabled = getFastSession(pluginConfig);
|
|
1676
|
+
const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
|
|
1677
|
+
const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
|
|
1678
|
+
const beginnerSafeMode = getBeginnerSafeMode(pluginConfig);
|
|
1679
|
+
beginnerSafeModeEnabled = beginnerSafeMode;
|
|
1680
|
+
const maskEmailEnabled = getCodexTuiMaskEmail(pluginConfig);
|
|
1681
|
+
const retryProfile = beginnerSafeMode ? "conservative" : getRetryProfile(pluginConfig);
|
|
1682
|
+
const retryBudgetOverrides = beginnerSafeMode ? {} : getRetryBudgetOverrides(pluginConfig);
|
|
1683
|
+
const retryBudgetLimits = resolveRetryBudgetLimits(retryProfile, retryBudgetOverrides);
|
|
1684
|
+
runtimeMetrics.retryProfile = retryProfile;
|
|
1685
|
+
runtimeMetrics.retryBudgetLimits = { ...retryBudgetLimits };
|
|
1686
|
+
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
1687
|
+
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
1688
|
+
const retryAllAccountsRateLimited = beginnerSafeMode ? false : getRetryAllAccountsRateLimited(pluginConfig);
|
|
1689
|
+
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
1690
|
+
const retryAllAccountsMaxRetries = beginnerSafeMode
|
|
1691
|
+
? Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig))
|
|
1692
|
+
: getRetryAllAccountsMaxRetries(pluginConfig);
|
|
1693
|
+
const unsupportedCodexPolicy = getUnsupportedCodexPolicy(pluginConfig);
|
|
1694
|
+
const fallbackOnUnsupportedCodexModel = unsupportedCodexPolicy === "fallback";
|
|
1695
|
+
const fallbackToGpt52OnUnsupportedGpt53 = getFallbackToGpt52OnUnsupportedGpt53(pluginConfig);
|
|
1696
|
+
const unsupportedCodexFallbackChain = getUnsupportedCodexFallbackChain(pluginConfig);
|
|
1697
|
+
const toastDurationMs = getToastDurationMs(pluginConfig);
|
|
1698
|
+
const accountToastsEnabled = getAccountToastsEnabled(pluginConfig);
|
|
1699
|
+
const fetchTimeoutMs = getFetchTimeoutMs(pluginConfig);
|
|
1700
|
+
const streamStallTimeoutMs = getStreamStallTimeoutMs(pluginConfig);
|
|
1701
|
+
const emptyResponseMaxRetries = getEmptyResponseMaxRetries(pluginConfig);
|
|
1702
|
+
const emptyResponseRetryDelayMs = getEmptyResponseRetryDelayMs(pluginConfig);
|
|
1703
|
+
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
|
|
1704
|
+
const rotationStrategy = getRotationStrategy(pluginConfig);
|
|
1705
|
+
const effectiveUserConfig = fastSessionEnabled ? applyFastSessionDefaults(userConfig) : userConfig;
|
|
1706
|
+
let accountManager = cachedAccountManager;
|
|
1504
1707
|
// Step 1: Extract and rewrite URL for Codex backend
|
|
1505
1708
|
const originalUrl = extractRequestUrl(input);
|
|
1506
1709
|
const url = openAIBaseURL ? originalUrl : rewriteUrlForCodex(originalUrl);
|
|
@@ -1638,6 +1841,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1638
1841
|
const startTime = Date.now();
|
|
1639
1842
|
const endTime = startTime + totalMs;
|
|
1640
1843
|
while (Date.now() < endTime) {
|
|
1844
|
+
if (cachedAccountManager !== accountManager)
|
|
1845
|
+
return;
|
|
1641
1846
|
if (abortSignal?.aborted) {
|
|
1642
1847
|
throw abortError();
|
|
1643
1848
|
}
|
|
@@ -1659,7 +1864,124 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1659
1864
|
if (model) {
|
|
1660
1865
|
attemptedUnsupportedFallbackModels.add(model);
|
|
1661
1866
|
}
|
|
1867
|
+
// Degrading the model mid-request touches several coupled pieces:
|
|
1868
|
+
// the attempted-model set, the routing snapshot, the model's own
|
|
1869
|
+
// instructions, the reasoning clamp (only the 5.6 tiers accept
|
|
1870
|
+
// `max`, so an un-clamped sol -> gpt-5.5 hop turns a graceful
|
|
1871
|
+
// fallback into a hard 400) and the per-model body shape. Both the
|
|
1872
|
+
// entitlement fallback and the quota fallback go through here so
|
|
1873
|
+
// the two can never drift apart on any of them.
|
|
1874
|
+
const applyModelFallback = async (previousModel, target, reason) => {
|
|
1875
|
+
attemptedUnsupportedFallbackModels.add(previousModel);
|
|
1876
|
+
attemptedUnsupportedFallbackModels.add(target);
|
|
1877
|
+
model = target;
|
|
1878
|
+
modelFamily = getModelFamily(model);
|
|
1879
|
+
quotaKey = `${modelFamily}:${model}`;
|
|
1880
|
+
fallbackApplied = true;
|
|
1881
|
+
fallbackFrom = previousModel;
|
|
1882
|
+
fallbackTo = model;
|
|
1883
|
+
fallbackReason = reason;
|
|
1884
|
+
const fallbackInstructions = await getCodexInstructions(model);
|
|
1885
|
+
if (transformedBody && typeof transformedBody === "object") {
|
|
1886
|
+
transformedBody = {
|
|
1887
|
+
...transformedBody,
|
|
1888
|
+
model,
|
|
1889
|
+
instructions: fallbackInstructions,
|
|
1890
|
+
input: upsertBackendModelIdentityMessage(transformedBody.input, model),
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
else {
|
|
1894
|
+
let fallbackBody = {
|
|
1895
|
+
model,
|
|
1896
|
+
instructions: fallbackInstructions,
|
|
1897
|
+
};
|
|
1898
|
+
if (requestInit?.body && typeof requestInit.body === "string") {
|
|
1899
|
+
try {
|
|
1900
|
+
const parsed = JSON.parse(requestInit.body);
|
|
1901
|
+
fallbackBody = {
|
|
1902
|
+
...parsed,
|
|
1903
|
+
model,
|
|
1904
|
+
instructions: fallbackInstructions,
|
|
1905
|
+
};
|
|
1906
|
+
if (Array.isArray(fallbackBody.input)) {
|
|
1907
|
+
fallbackBody.input = upsertBackendModelIdentityMessage(fallbackBody.input, model);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
catch {
|
|
1911
|
+
// Keep minimal fallback body if parsing fails.
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
transformedBody = fallbackBody;
|
|
1915
|
+
}
|
|
1916
|
+
const clampedReasoning = clampReasoningForModel(transformedBody.reasoning, model);
|
|
1917
|
+
if (clampedReasoning !== transformedBody.reasoning) {
|
|
1918
|
+
transformedBody = {
|
|
1919
|
+
...transformedBody,
|
|
1920
|
+
reasoning: clampedReasoning,
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
requestInit = {
|
|
1924
|
+
...(requestInit ?? {}),
|
|
1925
|
+
body: JSON.stringify(shapeBodyForModel(transformedBody)),
|
|
1926
|
+
};
|
|
1927
|
+
if (runtimeMetrics.lastSelectionSnapshot) {
|
|
1928
|
+
runtimeMetrics.lastSelectionSnapshot = {
|
|
1929
|
+
...runtimeMetrics.lastSelectionSnapshot,
|
|
1930
|
+
family: modelFamily,
|
|
1931
|
+
model: model ?? null,
|
|
1932
|
+
requestedModel,
|
|
1933
|
+
effectiveModel: model ?? null,
|
|
1934
|
+
quotaKey,
|
|
1935
|
+
fallbackApplied,
|
|
1936
|
+
fallbackFrom,
|
|
1937
|
+
fallbackTo,
|
|
1938
|
+
fallbackReason,
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
// A degraded model must not degrade again without bound, even if a
|
|
1943
|
+
// custom chain is cyclic. The attempted set already prevents
|
|
1944
|
+
// revisiting a model; this caps the total hops per request.
|
|
1945
|
+
const MAX_QUOTA_FALLBACK_SWITCHES = 3;
|
|
1946
|
+
let quotaFallbackSwitches = 0;
|
|
1662
1947
|
while (true) {
|
|
1948
|
+
if (cachedAccountManager && cachedAccountManager !== accountManager) {
|
|
1949
|
+
accountManager = cachedAccountManager;
|
|
1950
|
+
}
|
|
1951
|
+
else if (!cachedAccountManager) {
|
|
1952
|
+
// Reload through the shared accountManagerPromise so
|
|
1953
|
+
// concurrent requests (and the startup path above)
|
|
1954
|
+
// share one load. Racing independent loadFromDisk()
|
|
1955
|
+
// calls here can orphan the slower manager while an
|
|
1956
|
+
// in-flight request still holds it, and an orphaned
|
|
1957
|
+
// manager is never retired, so its next save would
|
|
1958
|
+
// write full membership and drop externally imported
|
|
1959
|
+
// accounts.
|
|
1960
|
+
if (accountManagerPromise === null) {
|
|
1961
|
+
const pending = AccountManager.loadFromDisk();
|
|
1962
|
+
accountManagerPromise = pending;
|
|
1963
|
+
void pending.catch(() => {
|
|
1964
|
+
if (accountManagerPromise === pending)
|
|
1965
|
+
accountManagerPromise = null;
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
const reloaded = await accountManagerPromise;
|
|
1969
|
+
if (cachedAccountManager) {
|
|
1970
|
+
if (cachedAccountManager !== reloaded) {
|
|
1971
|
+
// The cache was repopulated while this load was in
|
|
1972
|
+
// flight (external reload after an invalidation).
|
|
1973
|
+
// The loaded manager is stale; retire it so its
|
|
1974
|
+
// debounced save cannot write full membership over
|
|
1975
|
+
// accounts imported after its snapshot was taken.
|
|
1976
|
+
reloaded.disposeShutdownHandler();
|
|
1977
|
+
}
|
|
1978
|
+
accountManager = cachedAccountManager;
|
|
1979
|
+
}
|
|
1980
|
+
else {
|
|
1981
|
+
cachedAccountManager = reloaded;
|
|
1982
|
+
accountManager = reloaded;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1663
1985
|
let accountCount = accountManager.getAccountCount();
|
|
1664
1986
|
const attempted = new Set();
|
|
1665
1987
|
// Diagnostics for the terminal error message below. The composite key keeps
|
|
@@ -1697,6 +2019,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1697
2019
|
break;
|
|
1698
2020
|
}
|
|
1699
2021
|
attempted.add(account.index);
|
|
2022
|
+
// Hybrid's last-resort result is not necessarily eligible. Requests
|
|
2023
|
+
// must honor active blocks rather than sending it upstream anyway.
|
|
2024
|
+
if (selectionExplainability.some((entry) => entry.index === account.index && !entry.eligible)) {
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
1700
2027
|
runtimeMetrics.lastSelectedAccountIndex = account.index;
|
|
1701
2028
|
runtimeMetrics.lastQuotaKey = quotaKey;
|
|
1702
2029
|
if (runtimeMetrics.lastSelectionSnapshot) {
|
|
@@ -2109,81 +2436,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2109
2436
|
if (fallbackModel) {
|
|
2110
2437
|
const previousModel = model ?? "gpt-5-codex";
|
|
2111
2438
|
const previousModelFamily = modelFamily;
|
|
2112
|
-
attemptedUnsupportedFallbackModels.add(previousModel);
|
|
2113
|
-
attemptedUnsupportedFallbackModels.add(fallbackModel);
|
|
2114
2439
|
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
|
-
}
|
|
2440
|
+
await applyModelFallback(previousModel, fallbackModel, "fallback-unsupported-model-entitlement");
|
|
2187
2441
|
runtimeMetrics.lastError = `Model fallback: ${previousModel} -> ${model}`;
|
|
2188
2442
|
runtimeMetrics.lastErrorCategory = "model-fallback";
|
|
2189
2443
|
logWarn(`Model ${previousModel} is unsupported for this ChatGPT account. Falling back to ${model}.`, {
|
|
@@ -2279,7 +2533,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2279
2533
|
await sleep(addJitter(Math.max(MIN_BACKOFF_MS, delayMs), 0.2));
|
|
2280
2534
|
continue;
|
|
2281
2535
|
}
|
|
2282
|
-
|
|
2536
|
+
// Authoritative subscription exhaustion already has its own block.
|
|
2537
|
+
// Do not duplicate its reset as a model/family transient 429; retain
|
|
2538
|
+
// any genuine transient state written by other in-flight requests.
|
|
2539
|
+
if (!quotaExhausted) {
|
|
2540
|
+
accountManager.markRateLimitedWithReason(account, delayMs, modelFamily, parseRateLimitReason(rateLimit.code), model);
|
|
2541
|
+
}
|
|
2283
2542
|
accountManager.recordRateLimit(account, modelFamily, model);
|
|
2284
2543
|
account.lastSwitchReason = "rate-limit";
|
|
2285
2544
|
runtimeMetrics.accountRotations++;
|
|
@@ -2507,6 +2766,72 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2507
2766
|
const unavailableCount = accountManager
|
|
2508
2767
|
.getAccountsSnapshot()
|
|
2509
2768
|
.filter((account) => !fetchedAccountKeys.has(getAccountDiagnosticsKey(account))).length;
|
|
2769
|
+
const enabledSelection = count > 0 ? accountManager
|
|
2770
|
+
.getSelectionExplainability(modelFamily, model)
|
|
2771
|
+
.filter((entry) => entry.enabled) : [];
|
|
2772
|
+
const upstreamBlocked = enabledSelection.length > 0 && enabledSelection.every((entry) => entry.rateLimitedUntil !== undefined || entry.quotaExhaustedUntil !== undefined);
|
|
2773
|
+
// Every enabled account has an active upstream block. Before waiting out a
|
|
2774
|
+
// block that can run for days (`retryAllAccountsMaxRetries`
|
|
2775
|
+
// defaults to Infinity), degrade to the next chain model that is
|
|
2776
|
+
// actually usable right now. Gated exactly like the entitlement
|
|
2777
|
+
// auto-fallback -- same default-selector entry models, same
|
|
2778
|
+
// opt-out env vars -- even when an entry ID was selected directly.
|
|
2779
|
+
// Local token depletion and auth cooldown alone never trigger it.
|
|
2780
|
+
// An account-wide quota block fails the eligibility test
|
|
2781
|
+
// on every candidate, so it correctly falls through to the wait.
|
|
2782
|
+
if (upstreamBlocked &&
|
|
2783
|
+
waitMs > 0 &&
|
|
2784
|
+
count > 0 &&
|
|
2785
|
+
model &&
|
|
2786
|
+
quotaFallbackSwitches < MAX_QUOTA_FALLBACK_SWITCHES &&
|
|
2787
|
+
isDefaultAutoFallbackModel(model, attemptedUnsupportedFallbackModels)) {
|
|
2788
|
+
const rejected = new Set();
|
|
2789
|
+
let usableFallback;
|
|
2790
|
+
while (true) {
|
|
2791
|
+
const candidate = pickFallbackChainTarget({
|
|
2792
|
+
currentModel: model,
|
|
2793
|
+
attemptedModels: new Set([
|
|
2794
|
+
...attemptedUnsupportedFallbackModels,
|
|
2795
|
+
...rejected,
|
|
2796
|
+
]),
|
|
2797
|
+
customChain: unsupportedCodexFallbackChain,
|
|
2798
|
+
fallbackToGpt52OnUnsupportedGpt53,
|
|
2799
|
+
});
|
|
2800
|
+
if (!candidate || rejected.has(candidate))
|
|
2801
|
+
break;
|
|
2802
|
+
// Only degrade to a model some account can serve NOW,
|
|
2803
|
+
// otherwise the hop just moves the same block sideways.
|
|
2804
|
+
const candidatePool = getModelAccountPool(pluginConfig, candidate);
|
|
2805
|
+
const strictCandidatePool = candidatePool.length > 0 &&
|
|
2806
|
+
getModelAccountPoolMode(pluginConfig, candidate) === "strict";
|
|
2807
|
+
const candidateAccounts = accountManager.getAccountsSnapshot();
|
|
2808
|
+
const candidateEligible = accountManager.getSelectionExplainability(getModelFamily(candidate), candidate).some((entry) => entry.eligible && (!strictCandidatePool ||
|
|
2809
|
+
candidateAccounts.some((account) => account.index === entry.index &&
|
|
2810
|
+
candidatePool.some((key) => matchesModelPoolAccountKey(account, key)))));
|
|
2811
|
+
// A preferred pool may spill into general accounts; a strict
|
|
2812
|
+
// pool must contain an eligible member. This does not select
|
|
2813
|
+
// an account or advance any rotation cursor.
|
|
2814
|
+
if (candidateEligible) {
|
|
2815
|
+
usableFallback = candidate;
|
|
2816
|
+
break;
|
|
2817
|
+
}
|
|
2818
|
+
rejected.add(candidate);
|
|
2819
|
+
}
|
|
2820
|
+
if (usableFallback) {
|
|
2821
|
+
const previousModel = model;
|
|
2822
|
+
quotaFallbackSwitches++;
|
|
2823
|
+
await applyModelFallback(previousModel, usableFallback, "fallback-quota-exhausted");
|
|
2824
|
+
runtimeMetrics.lastError = `Model fallback: ${previousModel} -> ${model}`;
|
|
2825
|
+
runtimeMetrics.lastErrorCategory = "model-fallback";
|
|
2826
|
+
logWarn(`All ${count} account(s) are rate-limited or out of quota for ${previousModel}. Falling back to ${model}.`, {
|
|
2827
|
+
requestedModel: previousModel,
|
|
2828
|
+
effectiveModel: model,
|
|
2829
|
+
fallbackApplied: true,
|
|
2830
|
+
fallbackReason: "fallback-quota-exhausted",
|
|
2831
|
+
});
|
|
2832
|
+
continue;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2510
2835
|
if (retryAllAccountsRateLimited &&
|
|
2511
2836
|
count > 0 &&
|
|
2512
2837
|
waitMs > 0 &&
|
|
@@ -2556,6 +2881,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2556
2881
|
}
|
|
2557
2882
|
}
|
|
2558
2883
|
finally {
|
|
2884
|
+
if (pendingFetch)
|
|
2885
|
+
activeFetches.delete(pendingFetch);
|
|
2886
|
+
finishFetch?.();
|
|
2559
2887
|
clearCorrelationId();
|
|
2560
2888
|
}
|
|
2561
2889
|
},
|