oc-codex-multi-auth 6.21.0 → 6.23.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 +168 -10
- package/config/README.md +53 -33
- package/config/opencode-legacy.json +129 -279
- package/config/opencode-modern.json +43 -132
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +293 -57
- package/dist/index.js.map +1 -1
- package/dist/lib/account-display.d.ts +46 -0
- package/dist/lib/account-display.d.ts.map +1 -1
- package/dist/lib/account-display.js +260 -0
- package/dist/lib/account-display.js.map +1 -1
- package/dist/lib/accounts/warm-request.d.ts.map +1 -1
- package/dist/lib/accounts/warm-request.js +9 -11
- package/dist/lib/accounts/warm-request.js.map +1 -1
- package/dist/lib/accounts.d.ts +4 -0
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +20 -15
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/login-runner.d.ts.map +1 -1
- package/dist/lib/auth/login-runner.js +79 -2
- package/dist/lib/auth/login-runner.js.map +1 -1
- package/dist/lib/auto-update-checker.d.ts +17 -1
- package/dist/lib/auto-update-checker.d.ts.map +1 -1
- package/dist/lib/auto-update-checker.js +49 -3
- package/dist/lib/auto-update-checker.js.map +1 -1
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +9 -3
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/codex-usage.d.ts +4 -3
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +13 -11
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/config.d.ts +75 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +152 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/constants.d.ts +13 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +13 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/plan-allotment.d.ts +72 -0
- package/dist/lib/plan-allotment.d.ts.map +1 -0
- package/dist/lib/plan-allotment.js +142 -0
- package/dist/lib/plan-allotment.js.map +1 -0
- package/dist/lib/plugin-origin.d.ts +77 -0
- package/dist/lib/plugin-origin.d.ts.map +1 -0
- package/dist/lib/plugin-origin.js +301 -0
- package/dist/lib/plugin-origin.js.map +1 -0
- package/dist/lib/prompts/codex.d.ts +12 -2
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +47 -16
- package/dist/lib/prompts/codex.js.map +1 -1
- package/dist/lib/quota-display.d.ts +40 -0
- package/dist/lib/quota-display.d.ts.map +1 -0
- package/dist/lib/quota-display.js +45 -0
- package/dist/lib/quota-display.js.map +1 -0
- package/dist/lib/quota-notifications.d.ts +2 -1
- package/dist/lib/quota-notifications.d.ts.map +1 -1
- package/dist/lib/quota-notifications.js +34 -14
- package/dist/lib/quota-notifications.js.map +1 -1
- package/dist/lib/quota-overview.d.ts +253 -0
- package/dist/lib/quota-overview.d.ts.map +1 -0
- package/dist/lib/quota-overview.js +707 -0
- package/dist/lib/quota-overview.js.map +1 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +84 -50
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/helpers/model-map.d.ts +10 -0
- package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
- package/dist/lib/request/helpers/model-map.js +39 -11
- package/dist/lib/request/helpers/model-map.js.map +1 -1
- package/dist/lib/request/helpers/responses-lite.d.ts.map +1 -1
- package/dist/lib/request/helpers/responses-lite.js +4 -1
- package/dist/lib/request/helpers/responses-lite.js.map +1 -1
- package/dist/lib/request/request-transformer.d.ts.map +1 -1
- package/dist/lib/request/request-transformer.js +37 -15
- package/dist/lib/request/request-transformer.js.map +1 -1
- package/dist/lib/request/retry-budget.d.ts +26 -0
- package/dist/lib/request/retry-budget.d.ts.map +1 -1
- package/dist/lib/request/retry-budget.js +43 -0
- package/dist/lib/request/retry-budget.js.map +1 -1
- package/dist/lib/schemas.d.ts +73 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +53 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/backup.d.ts +33 -0
- package/dist/lib/storage/backup.d.ts.map +1 -1
- package/dist/lib/storage/backup.js +43 -7
- package/dist/lib/storage/backup.js.map +1 -1
- package/dist/lib/storage/credential-snapshots.d.ts +78 -0
- package/dist/lib/storage/credential-snapshots.d.ts.map +1 -0
- package/dist/lib/storage/credential-snapshots.js +302 -0
- package/dist/lib/storage/credential-snapshots.js.map +1 -0
- package/dist/lib/storage/export-import.d.ts.map +1 -1
- package/dist/lib/storage/export-import.js +7 -0
- package/dist/lib/storage/export-import.js.map +1 -1
- package/dist/lib/storage/flagged.d.ts.map +1 -1
- package/dist/lib/storage/flagged.js +23 -1
- package/dist/lib/storage/flagged.js.map +1 -1
- package/dist/lib/storage/load-save.d.ts +4 -0
- package/dist/lib/storage/load-save.d.ts.map +1 -1
- package/dist/lib/storage/load-save.js +50 -0
- package/dist/lib/storage/load-save.js.map +1 -1
- package/dist/lib/storage/paths.d.ts +1 -0
- package/dist/lib/storage/paths.d.ts.map +1 -1
- package/dist/lib/storage/paths.js +1 -1
- package/dist/lib/storage/paths.js.map +1 -1
- package/dist/lib/storage/test-home-guard.d.ts +35 -0
- package/dist/lib/storage/test-home-guard.d.ts.map +1 -0
- package/dist/lib/storage/test-home-guard.js +59 -0
- package/dist/lib/storage/test-home-guard.js.map +1 -0
- package/dist/lib/tools/codex-dashboard.d.ts.map +1 -1
- package/dist/lib/tools/codex-dashboard.js +3 -2
- package/dist/lib/tools/codex-dashboard.js.map +1 -1
- package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
- package/dist/lib/tools/codex-doctor.js +16 -0
- 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 +10 -2
- package/dist/lib/tools/codex-health.js.map +1 -1
- package/dist/lib/tools/codex-label.d.ts.map +1 -1
- package/dist/lib/tools/codex-label.js +1 -0
- package/dist/lib/tools/codex-label.js.map +1 -1
- package/dist/lib/tools/codex-limits.d.ts.map +1 -1
- package/dist/lib/tools/codex-limits.js +13 -9
- 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 +28 -3
- package/dist/lib/tools/codex-list.js.map +1 -1
- package/dist/lib/tools/codex-note.d.ts.map +1 -1
- package/dist/lib/tools/codex-note.js +4 -1
- package/dist/lib/tools/codex-note.js.map +1 -1
- package/dist/lib/tools/codex-pool.d.ts.map +1 -1
- package/dist/lib/tools/codex-pool.js +6 -2
- package/dist/lib/tools/codex-pool.js.map +1 -1
- package/dist/lib/tools/codex-refresh.d.ts.map +1 -1
- package/dist/lib/tools/codex-refresh.js +4 -1
- package/dist/lib/tools/codex-refresh.js.map +1 -1
- package/dist/lib/tools/codex-remove.d.ts.map +1 -1
- package/dist/lib/tools/codex-remove.js +1 -0
- package/dist/lib/tools/codex-remove.js.map +1 -1
- package/dist/lib/tools/codex-reset.d.ts.map +1 -1
- package/dist/lib/tools/codex-reset.js +13 -7
- 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 +25 -2
- package/dist/lib/tools/codex-status.js.map +1 -1
- package/dist/lib/tools/codex-switch.d.ts.map +1 -1
- package/dist/lib/tools/codex-switch.js +1 -0
- package/dist/lib/tools/codex-switch.js.map +1 -1
- package/dist/lib/tools/codex-tag.d.ts.map +1 -1
- package/dist/lib/tools/codex-tag.js +4 -1
- package/dist/lib/tools/codex-tag.js.map +1 -1
- package/dist/lib/tools/codex-warm.d.ts.map +1 -1
- package/dist/lib/tools/codex-warm.js +1 -0
- package/dist/lib/tools/codex-warm.js.map +1 -1
- package/dist/lib/tools/index.d.ts +9 -0
- 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 +35 -0
- package/dist/lib/tui-quota-cache.d.ts.map +1 -1
- package/dist/lib/tui-quota-cache.js +76 -11
- package/dist/lib/tui-quota-cache.js.map +1 -1
- package/dist/lib/tui-quota-overview.d.ts +55 -0
- package/dist/lib/tui-quota-overview.d.ts.map +1 -0
- package/dist/lib/tui-quota-overview.js +209 -0
- package/dist/lib/tui-quota-overview.js.map +1 -0
- package/dist/lib/tui-status.d.ts +55 -0
- package/dist/lib/tui-status.d.ts.map +1 -1
- package/dist/lib/tui-status.js +155 -14
- package/dist/lib/tui-status.js.map +1 -1
- package/dist/lib/ui/auth-menu.d.ts +2 -0
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +10 -6
- package/dist/lib/ui/auth-menu.js.map +1 -1
- package/dist/tui.d.ts +55 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +511 -29
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/scripts/install-oc-codex-multi-auth-core.js +583 -66
package/dist/index.js
CHANGED
|
@@ -39,15 +39,17 @@ import { buildDeviceCodeInstructions, completeDeviceCodeSession, createDeviceCod
|
|
|
39
39
|
import { applyAccountSelectionFallbacks, persistResolvedAccountSelection, persistAccountPool, resolveAndPersistAccountSelection, resolveAccountSelection, } from "./lib/auth/login-runner.js";
|
|
40
40
|
import { coordinateFlaggedPersistedRefresh, coordinatePersistedRefresh, } from "./lib/storage/coordinated-refresh.js";
|
|
41
41
|
import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
|
|
42
|
-
import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRetryProfile, getRetryBudgetOverrides, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getAutoUpdate, getToastDurationMs, getAccountToastsEnabled, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getRotationStrategy, getModelAccountPool, getModelAccountPoolMode, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getBeginnerSafeMode, getCodexTuiMaskEmail, loadPluginConfig, } from "./lib/config.js";
|
|
43
|
-
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
|
|
42
|
+
import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRetryProfile, getRetryBudgetOverrides, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getAutoUpdate, getToastDurationMs, getAccountToastsEnabled, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getRotationStrategy, getModelAccountPool, getModelAccountPoolMode, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getBeginnerSafeMode, getCodexTuiMaskEmail, getQuotaDisplay, loadPluginConfig, } from "./lib/config.js";
|
|
43
|
+
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, MAX_QUOTA_FALLBACK_SWITCHES, } from "./lib/constants.js";
|
|
44
44
|
import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
|
|
45
45
|
import { createQuotaMonitor } from "./lib/quota-notifications.js";
|
|
46
46
|
import { checkAndNotify } from "./lib/auto-update-checker.js";
|
|
47
|
+
import { describePluginOrigin, getPluginOrigin, recordPluginOrigin } from "./lib/plugin-origin.js";
|
|
47
48
|
import { handleContextOverflow } from "./lib/context-overflow.js";
|
|
48
49
|
import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLabel, formatWaitTime, sanitizeEmail, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
|
|
49
50
|
import { getModelPoolAccountKey, matchesModelPoolAccountKey, } from "./lib/accounts/pool-identity.js";
|
|
50
|
-
import { resolveDisplayEmail } from "./lib/account-display.js";
|
|
51
|
+
import { formatSeatSuffix, maskIdentityValue, resolveDisplayEmail, seatIsDisclosable, } from "./lib/account-display.js";
|
|
52
|
+
import { extractAccountUserId } from "./lib/auth/token-utils.js";
|
|
51
53
|
import { CodexAuthError } from "./lib/errors.js";
|
|
52
54
|
import { getStoragePath, loadAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, loadFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
|
|
53
55
|
import { getWorkspaceIdentityKey } from "./lib/storage/identity.js";
|
|
@@ -185,9 +187,19 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
185
187
|
let accountManagerPromise = null;
|
|
186
188
|
let loaderMutex = null;
|
|
187
189
|
let startupPrewarmTriggered = false;
|
|
190
|
+
let startupOriginRecorded = false;
|
|
188
191
|
let startupPreflightShown = false;
|
|
189
192
|
let beginnerSafeModeEnabled = false;
|
|
190
193
|
const MIN_BACKOFF_MS = 100;
|
|
194
|
+
// An all-accounts rate-limit wait can run for days, and the local accounts
|
|
195
|
+
// file is its only wake-up. A quota reset granted server-side leaves that file
|
|
196
|
+
// untouched, so such a wait would be slept straight through. Long waits
|
|
197
|
+
// therefore re-probe upstream: first after a minute, doubling to a quarter
|
|
198
|
+
// hour, so a multi-day sleep costs a handful of usage requests rather than one
|
|
199
|
+
// per countdown tick.
|
|
200
|
+
const UPSTREAM_REPROBE_MIN_WAIT_MS = 60_000;
|
|
201
|
+
const UPSTREAM_REPROBE_FIRST_DELAY_MS = 60_000;
|
|
202
|
+
const UPSTREAM_REPROBE_MAX_DELAY_MS = 15 * 60_000;
|
|
191
203
|
const runtimeMetrics = {
|
|
192
204
|
startedAt: Date.now(),
|
|
193
205
|
totalRequests: 0,
|
|
@@ -240,17 +252,33 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
240
252
|
selectionExplainability: serializeSelectionExplainability(options.selectionExplainability ?? snapshot?.explainability ?? []),
|
|
241
253
|
};
|
|
242
254
|
};
|
|
243
|
-
const buildJsonAccountIdentity = (index, options = {}) =>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
const buildJsonAccountIdentity = (index, options = {}) => {
|
|
256
|
+
const includeSensitive = options.includeSensitive ?? false;
|
|
257
|
+
const accountUserId = options.account?.accountUserId?.trim() || undefined;
|
|
258
|
+
return {
|
|
259
|
+
index: index + 1,
|
|
260
|
+
zeroBasedIndex: index,
|
|
261
|
+
...(includeSensitive
|
|
262
|
+
? {
|
|
263
|
+
label: options.label ??
|
|
264
|
+
formatCommandAccountLabel(options.account, index, {
|
|
265
|
+
peerAccounts: options.peerAccounts,
|
|
266
|
+
}),
|
|
267
|
+
email: options.account?.email ?? null,
|
|
268
|
+
accountId: options.account?.accountId ?? null,
|
|
269
|
+
}
|
|
270
|
+
: {}),
|
|
271
|
+
// Members of one Business workspace share `accountId`, so the seat is
|
|
272
|
+
// the field a JSON consumer can tell them apart by. It rides in both
|
|
273
|
+
// modes - the member id masked when sensitive output is off, and the
|
|
274
|
+
// suffix withheld when the id is too short to excerpt without
|
|
275
|
+
// disclosing it - under the same field names the standalone CLI emits.
|
|
276
|
+
accountUserId: maskIdentityValue(accountUserId, includeSensitive) ?? null,
|
|
277
|
+
seatSuffix: seatIsDisclosable(accountUserId, includeSensitive)
|
|
278
|
+
? (formatSeatSuffix(accountUserId, options.peerAccounts?.map((peer) => peer?.accountUserId)) ?? null)
|
|
279
|
+
: null,
|
|
280
|
+
};
|
|
281
|
+
};
|
|
254
282
|
const appendRoutingVisibilityText = (lines, routing, options = {}) => {
|
|
255
283
|
lines.push("Routing visibility:");
|
|
256
284
|
lines.push(` Requested model: ${formatRoutingValue(routing.requestedModel)}`);
|
|
@@ -449,14 +477,16 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
449
477
|
logDebug(`[${PLUGIN_NAME}] Failed to clear TUI quota cache: ${error instanceof Error ? error.message : String(error)}`);
|
|
450
478
|
}
|
|
451
479
|
};
|
|
452
|
-
const recordPromptQuotaHeaders = async (response, account, accountCount) => {
|
|
480
|
+
const recordPromptQuotaHeaders = async (response, account, accountCount, peerAccounts) => {
|
|
453
481
|
try {
|
|
454
482
|
const snapshot = parseTuiQuotaSnapshotFromHeaders(response.headers, {
|
|
455
483
|
fingerprint: createUsageAccountFingerprint(account),
|
|
456
484
|
accountIndex: account.index + 1,
|
|
457
485
|
accountCount,
|
|
458
486
|
accountEmail: account.email?.trim() || undefined,
|
|
459
|
-
accountLabel: formatAccountLabel(account, account.index
|
|
487
|
+
accountLabel: formatAccountLabel(account, account.index, {
|
|
488
|
+
peerAccounts,
|
|
489
|
+
}),
|
|
460
490
|
});
|
|
461
491
|
if (!snapshot)
|
|
462
492
|
return;
|
|
@@ -812,6 +842,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
812
842
|
const email = resolveDisplayEmail(account?.email, options.maskEmail ?? false);
|
|
813
843
|
const workspace = account?.accountLabel?.trim();
|
|
814
844
|
const accountId = formatAccountIdForDisplay(account?.accountId);
|
|
845
|
+
// `omitSeat` is for a caller that renders the seat itself in a place
|
|
846
|
+
// a long email cannot push it out of - a table column of its own.
|
|
847
|
+
// Leaving it in the label too would print the seat twice.
|
|
848
|
+
const seat = options.omitSeat
|
|
849
|
+
? undefined
|
|
850
|
+
: formatSeatSuffix(account?.accountUserId, options.peerAccounts?.map((peer) => peer?.accountUserId));
|
|
815
851
|
const tags = Array.isArray(account?.accountTags)
|
|
816
852
|
? account.accountTags
|
|
817
853
|
.filter((tag) => typeof tag === "string")
|
|
@@ -825,6 +861,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
825
861
|
details.push(`workspace:${workspace}`);
|
|
826
862
|
if (accountId)
|
|
827
863
|
details.push(`id:${accountId}`);
|
|
864
|
+
if (seat)
|
|
865
|
+
details.push(`seat:${seat}`);
|
|
828
866
|
if (tags.length > 0)
|
|
829
867
|
details.push(`tags:${tags.join(",")}`);
|
|
830
868
|
if (details.length === 0) {
|
|
@@ -860,7 +898,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
860
898
|
const { select } = await import("./lib/ui/select.js");
|
|
861
899
|
const maskEmail = resolveMaskEmail();
|
|
862
900
|
const selected = await select(storage.accounts.map((account, index) => ({
|
|
863
|
-
label: formatCommandAccountLabel(account, index, {
|
|
901
|
+
label: formatCommandAccountLabel(account, index, {
|
|
902
|
+
maskEmail,
|
|
903
|
+
peerAccounts: storage.accounts,
|
|
904
|
+
}),
|
|
864
905
|
value: index,
|
|
865
906
|
})), {
|
|
866
907
|
message: title,
|
|
@@ -879,7 +920,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
879
920
|
const toBeginnerAccountSnapshots = (storage, activeIndex, now) => {
|
|
880
921
|
return storage.accounts.map((account, index) => ({
|
|
881
922
|
index,
|
|
882
|
-
label: formatCommandAccountLabel(account, index
|
|
923
|
+
label: formatCommandAccountLabel(account, index, {
|
|
924
|
+
peerAccounts: storage.accounts,
|
|
925
|
+
}),
|
|
883
926
|
accountLabel: account.accountLabel,
|
|
884
927
|
enabled: account.enabled !== false,
|
|
885
928
|
isActive: index === activeIndex,
|
|
@@ -1137,6 +1180,44 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1137
1180
|
});
|
|
1138
1181
|
}
|
|
1139
1182
|
};
|
|
1183
|
+
/**
|
|
1184
|
+
* `loadAccounts()` reports a read or parse failure the same way it reports
|
|
1185
|
+
* an absent file - by returning null - and a null load builds a manager
|
|
1186
|
+
* holding zero accounts. Installing that over a working pool makes this
|
|
1187
|
+
* process answer "No Codex accounts configured" while the accounts file on
|
|
1188
|
+
* disk is intact, which cross-process lock contention makes reachable.
|
|
1189
|
+
*
|
|
1190
|
+
* Emptying the pool for real always goes through an explicit action
|
|
1191
|
+
* (`codex-remove`, logout, a storage-mode switch); each installs its own
|
|
1192
|
+
* manager rather than arriving here, so refusing the shrink costs a genuine
|
|
1193
|
+
* deletion nothing.
|
|
1194
|
+
*/
|
|
1195
|
+
const isUntrustworthyEmptyReload = (incumbent, reloaded) => incumbent !== null &&
|
|
1196
|
+
incumbent !== reloaded &&
|
|
1197
|
+
reloaded.getAccountCount() === 0 &&
|
|
1198
|
+
incumbent.getAccountCount() > 0;
|
|
1199
|
+
const EMPTY_RELOAD_RETRY_DELAY_MS = 2000;
|
|
1200
|
+
const EMPTY_RELOAD_MAX_RETRIES = 3;
|
|
1201
|
+
let emptyReloadRetries = 0;
|
|
1202
|
+
let emptyReloadRetryTimer;
|
|
1203
|
+
const cancelEmptyReloadRetry = () => {
|
|
1204
|
+
clearTimeout(emptyReloadRetryTimer);
|
|
1205
|
+
emptyReloadRetryTimer = undefined;
|
|
1206
|
+
emptyReloadRetries = 0;
|
|
1207
|
+
};
|
|
1208
|
+
const scheduleEmptyReloadRetry = (retry) => {
|
|
1209
|
+
if (emptyReloadRetries >= EMPTY_RELOAD_MAX_RETRIES) {
|
|
1210
|
+
emptyReloadRetries = 0;
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
emptyReloadRetries += 1;
|
|
1214
|
+
clearTimeout(emptyReloadRetryTimer);
|
|
1215
|
+
emptyReloadRetryTimer = setTimeout(() => {
|
|
1216
|
+
emptyReloadRetryTimer = undefined;
|
|
1217
|
+
void retry();
|
|
1218
|
+
}, EMPTY_RELOAD_RETRY_DELAY_MS);
|
|
1219
|
+
emptyReloadRetryTimer.unref();
|
|
1220
|
+
};
|
|
1140
1221
|
const reloadCachedAccountManager = async () => {
|
|
1141
1222
|
if (!cachedAccountManager)
|
|
1142
1223
|
return;
|
|
@@ -1155,6 +1236,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1155
1236
|
}
|
|
1156
1237
|
try {
|
|
1157
1238
|
const reloadedManager = await AccountManager.loadFromDisk();
|
|
1239
|
+
if (isUntrustworthyEmptyReload(previous, reloadedManager)) {
|
|
1240
|
+
reloadedManager.disposeShutdownHandler();
|
|
1241
|
+
logWarn(`[${PLUGIN_NAME}] Account reload returned no accounts while ${previous.getAccountCount()} are held; keeping the loaded pool and retrying`);
|
|
1242
|
+
scheduleEmptyReloadRetry(reloadCachedAccountManager);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
cancelEmptyReloadRetry();
|
|
1158
1246
|
cachedAccountManager = reloadedManager;
|
|
1159
1247
|
accountManagerPromise = Promise.resolve(reloadedManager);
|
|
1160
1248
|
// Dispose only after the replacement is installed so we never leak
|
|
@@ -1166,6 +1254,35 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1166
1254
|
logWarn(`Failed to reload account manager: ${error instanceof Error ? error.message : String(error)}`);
|
|
1167
1255
|
}
|
|
1168
1256
|
};
|
|
1257
|
+
/**
|
|
1258
|
+
* Refills the cache after the fetch path refused an empty reload.
|
|
1259
|
+
* `reloadCachedAccountManager` cannot serve here - it compares against
|
|
1260
|
+
* the cached incumbent, which is exactly what is missing - so the
|
|
1261
|
+
* incumbent the refusing request kept serving is passed in instead.
|
|
1262
|
+
*/
|
|
1263
|
+
const repopulateAccountManagerCache = async (incumbent) => {
|
|
1264
|
+
try {
|
|
1265
|
+
const reloaded = await AccountManager.loadFromDisk();
|
|
1266
|
+
if (cachedAccountManager) {
|
|
1267
|
+
// Another actor repopulated first; the late load is stale and
|
|
1268
|
+
// retires for the same reason the fetch path retires it.
|
|
1269
|
+
if (cachedAccountManager !== reloaded)
|
|
1270
|
+
reloaded.disposeShutdownHandler();
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (isUntrustworthyEmptyReload(incumbent, reloaded)) {
|
|
1274
|
+
reloaded.disposeShutdownHandler();
|
|
1275
|
+
scheduleEmptyReloadRetry(() => repopulateAccountManagerCache(incumbent));
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
cancelEmptyReloadRetry();
|
|
1279
|
+
cachedAccountManager = reloaded;
|
|
1280
|
+
accountManagerPromise = Promise.resolve(reloaded);
|
|
1281
|
+
}
|
|
1282
|
+
catch (error) {
|
|
1283
|
+
logWarn(`Failed to reload account manager: ${error instanceof Error ? error.message : String(error)}`);
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1169
1286
|
let watchedAccountsPath;
|
|
1170
1287
|
let observedAccountsDigest;
|
|
1171
1288
|
let accountsReloadTimer;
|
|
@@ -1185,25 +1302,43 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1185
1302
|
accountsWatcherDisposed = true;
|
|
1186
1303
|
unsubscribeAccountsPath?.();
|
|
1187
1304
|
stopAccountsWatcher();
|
|
1305
|
+
cancelEmptyReloadRetry();
|
|
1188
1306
|
unregisterCleanup(disposeAccountsWatcher);
|
|
1189
1307
|
};
|
|
1190
|
-
const
|
|
1308
|
+
const readAccountsFileState = async (path) => {
|
|
1191
1309
|
try {
|
|
1192
1310
|
const content = await readFile(path, "utf8");
|
|
1193
|
-
|
|
1311
|
+
const data = JSON.parse(content);
|
|
1312
|
+
if (!AnyAccountStorageSchema.safeParse(data).success)
|
|
1194
1313
|
return;
|
|
1195
|
-
|
|
1314
|
+
// Counted off the raw document rather than the parsed union so the
|
|
1315
|
+
// count is the same for every storage version.
|
|
1316
|
+
const accounts = data.accounts;
|
|
1317
|
+
return {
|
|
1318
|
+
digest: createHash("sha256").update(content).digest("hex"),
|
|
1319
|
+
accountCount: Array.isArray(accounts) ? accounts.length : 0,
|
|
1320
|
+
};
|
|
1196
1321
|
}
|
|
1197
1322
|
catch {
|
|
1198
1323
|
return;
|
|
1199
1324
|
}
|
|
1200
1325
|
};
|
|
1201
1326
|
const reloadForExternalAccountsChange = async (path, generation, attempt = 0, retired) => {
|
|
1202
|
-
const
|
|
1203
|
-
if (generation !== accountsWatchGeneration || !
|
|
1327
|
+
const observed = await readAccountsFileState(path);
|
|
1328
|
+
if (generation !== accountsWatchGeneration || !observed || path !== getStoragePath())
|
|
1204
1329
|
return;
|
|
1330
|
+
const digest = observed.digest;
|
|
1205
1331
|
if (digest === consumeLastWrittenAccountsDigest(path))
|
|
1206
1332
|
return;
|
|
1333
|
+
const retryLater = () => {
|
|
1334
|
+
if (attempt < 2 && generation === accountsWatchGeneration && !accountsWatcherDisposed) {
|
|
1335
|
+
accountsReloadTimer = setTimeout(() => {
|
|
1336
|
+
accountsReloadTimer = undefined;
|
|
1337
|
+
void reloadForExternalAccountsChange(path, generation, attempt + 1, retired);
|
|
1338
|
+
}, 1500);
|
|
1339
|
+
accountsReloadTimer.unref();
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1207
1342
|
const previous = cachedAccountManager;
|
|
1208
1343
|
try {
|
|
1209
1344
|
// A null cache means an invalidation retired the incumbent; the
|
|
@@ -1224,6 +1359,19 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1224
1359
|
reloaded.disposeShutdownHandler();
|
|
1225
1360
|
return;
|
|
1226
1361
|
}
|
|
1362
|
+
// The file this reload observed carried accounts but the load
|
|
1363
|
+
// produced none, so `loadAccounts()` failed to read it rather than
|
|
1364
|
+
// the accounts having gone away - a failure it reports as an empty
|
|
1365
|
+
// result, never as a throw, so the catch below cannot see it.
|
|
1366
|
+
// Adopting it would answer "No Codex accounts configured" against an
|
|
1367
|
+
// intact file; the retired incumbent still serves its accounts until
|
|
1368
|
+
// a retry lands a real one.
|
|
1369
|
+
if (observed.accountCount > 0 && reloaded.getAccountCount() === 0) {
|
|
1370
|
+
reloaded.disposeShutdownHandler();
|
|
1371
|
+
logWarn(`[${PLUGIN_NAME}] Externally changed accounts file holds ${observed.accountCount} account(s) but loaded as empty; keeping the current pool and retrying`);
|
|
1372
|
+
retryLater();
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1227
1375
|
const outgoing = cachedAccountManager;
|
|
1228
1376
|
if (outgoing && outgoing !== retired) {
|
|
1229
1377
|
// Another actor replaced the cached manager while this reload
|
|
@@ -1240,13 +1388,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1240
1388
|
}
|
|
1241
1389
|
catch {
|
|
1242
1390
|
logWarn("Could not reload externally updated account storage");
|
|
1243
|
-
|
|
1244
|
-
accountsReloadTimer = setTimeout(() => {
|
|
1245
|
-
accountsReloadTimer = undefined;
|
|
1246
|
-
void reloadForExternalAccountsChange(path, generation, attempt + 1, retired);
|
|
1247
|
-
}, 1500);
|
|
1248
|
-
accountsReloadTimer.unref();
|
|
1249
|
-
}
|
|
1391
|
+
retryLater();
|
|
1250
1392
|
return;
|
|
1251
1393
|
}
|
|
1252
1394
|
logDebug("Reloaded cached account manager after external accounts file change");
|
|
@@ -1262,9 +1404,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1262
1404
|
if (!path)
|
|
1263
1405
|
return;
|
|
1264
1406
|
const generation = accountsWatchGeneration;
|
|
1265
|
-
const
|
|
1266
|
-
if (generation !== accountsWatchGeneration || !
|
|
1407
|
+
const observed = await readAccountsFileState(path);
|
|
1408
|
+
if (generation !== accountsWatchGeneration || !observed || observed.digest === observedAccountsDigest)
|
|
1267
1409
|
return;
|
|
1410
|
+
const digest = observed.digest;
|
|
1268
1411
|
observedAccountsDigest = digest;
|
|
1269
1412
|
clearTimeout(accountsReloadTimer);
|
|
1270
1413
|
accountsReloadTimer = undefined;
|
|
@@ -1293,10 +1436,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1293
1436
|
});
|
|
1294
1437
|
watchedAccountsPath = path;
|
|
1295
1438
|
const generation = accountsWatchGeneration;
|
|
1296
|
-
const
|
|
1439
|
+
const initial = await readAccountsFileState(path);
|
|
1297
1440
|
if (generation !== accountsWatchGeneration)
|
|
1298
1441
|
return;
|
|
1299
|
-
observedAccountsDigest =
|
|
1442
|
+
observedAccountsDigest = initial?.digest;
|
|
1300
1443
|
// Stat polling follows the path across the storage writer's temp-file rename.
|
|
1301
1444
|
watchFile(path, { interval: 1500, persistent: false }, onAccountsStatChanged);
|
|
1302
1445
|
unregisterCleanup(disposeAccountsWatcher);
|
|
@@ -1591,9 +1734,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1591
1734
|
retryAllAccountsMaxRetries: Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig)),
|
|
1592
1735
|
});
|
|
1593
1736
|
}
|
|
1594
|
-
const
|
|
1595
|
-
|
|
1596
|
-
process.env.NODE_ENV !== "test";
|
|
1737
|
+
const underTestRunner = process.env.VITEST === "true" || process.env.NODE_ENV === "test";
|
|
1738
|
+
const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" && !underTestRunner;
|
|
1597
1739
|
if (!startupPrewarmTriggered && prewarmEnabled && getRequestTransformMode(pluginConfig) === "legacy") {
|
|
1598
1740
|
startupPrewarmTriggered = true;
|
|
1599
1741
|
const configuredModels = Object.keys(userConfig.models ?? {});
|
|
@@ -1605,9 +1747,24 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1605
1747
|
const recoveryHook = sessionRecoveryEnabled
|
|
1606
1748
|
? createSessionRecoveryHook({ client, directory: process.cwd() }, { sessionRecovery: true, autoResume: autoResumeEnabled })
|
|
1607
1749
|
: null;
|
|
1750
|
+
const pluginOrigin = getPluginOrigin();
|
|
1751
|
+
if (pluginOrigin && !startupOriginRecorded) {
|
|
1752
|
+
startupOriginRecorded = true;
|
|
1753
|
+
if (pluginOrigin.isLocalCheckout) {
|
|
1754
|
+
logInfo(`Running from ${describePluginOrigin(pluginOrigin)}`);
|
|
1755
|
+
}
|
|
1756
|
+
if (!underTestRunner) {
|
|
1757
|
+
recordPluginOrigin(pluginOrigin).catch((err) => {
|
|
1758
|
+
logDebug(`Failed to record plugin origin: ${err instanceof Error ? err.message : String(err)}`);
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1608
1762
|
checkAndNotify(async (message, variant) => {
|
|
1609
1763
|
await showToast(message, variant);
|
|
1610
|
-
}, {
|
|
1764
|
+
}, {
|
|
1765
|
+
autoUpdate: autoUpdateEnabled,
|
|
1766
|
+
localCheckout: pluginOrigin?.isLocalCheckout ?? false,
|
|
1767
|
+
}).catch((err) => {
|
|
1611
1768
|
logDebug(`Update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1612
1769
|
});
|
|
1613
1770
|
await runStartupPreflight();
|
|
@@ -1794,9 +1951,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1794
1951
|
runtimeMetrics.promptCacheMissingRequests++;
|
|
1795
1952
|
}
|
|
1796
1953
|
const retryBudget = new RetryBudgetTracker(retryBudgetLimits);
|
|
1797
|
-
const consumeRetryBudget = (bucket, reason) => {
|
|
1798
|
-
|
|
1799
|
-
|
|
1954
|
+
const consumeRetryBudget = (bucket, reason, waitMs) => {
|
|
1955
|
+
// Pass the wait so the charge scales with how long the retry
|
|
1956
|
+
// blocks. Metrics follow the tracker's own counter rather than
|
|
1957
|
+
// assuming one unit, or a free sub-second wait would report
|
|
1958
|
+
// budget it never spent.
|
|
1959
|
+
const usedBefore = retryBudget.getUsage()[bucket];
|
|
1960
|
+
const granted = waitMs === undefined
|
|
1961
|
+
? retryBudget.consume(bucket)
|
|
1962
|
+
: retryBudget.consumeWait(bucket, waitMs);
|
|
1963
|
+
if (granted) {
|
|
1964
|
+
runtimeMetrics.retryBudgetUsage[bucket] +=
|
|
1965
|
+
retryBudget.getUsage()[bucket] - usedBefore;
|
|
1800
1966
|
return true;
|
|
1801
1967
|
}
|
|
1802
1968
|
runtimeMetrics.retryBudgetExhaustions += 1;
|
|
@@ -1837,27 +2003,69 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1837
2003
|
};
|
|
1838
2004
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1839
2005
|
});
|
|
1840
|
-
const sleepWithCountdown = async (totalMs, message, intervalMs = 5000) => {
|
|
2006
|
+
const sleepWithCountdown = async (totalMs, message, intervalMs = 5000, probeUpstream) => {
|
|
1841
2007
|
const startTime = Date.now();
|
|
1842
2008
|
const endTime = startTime + totalMs;
|
|
2009
|
+
let probeDelayMs = UPSTREAM_REPROBE_FIRST_DELAY_MS;
|
|
2010
|
+
let nextProbeAt = probeUpstream && totalMs >= UPSTREAM_REPROBE_MIN_WAIT_MS
|
|
2011
|
+
? startTime + probeDelayMs
|
|
2012
|
+
: Number.POSITIVE_INFINITY;
|
|
1843
2013
|
while (Date.now() < endTime) {
|
|
1844
2014
|
if (cachedAccountManager !== accountManager)
|
|
1845
2015
|
return;
|
|
1846
2016
|
if (abortSignal?.aborted) {
|
|
1847
2017
|
throw abortError();
|
|
1848
2018
|
}
|
|
2019
|
+
if (probeUpstream && Date.now() >= nextProbeAt) {
|
|
2020
|
+
if (await probeUpstream())
|
|
2021
|
+
return;
|
|
2022
|
+
if (cachedAccountManager !== accountManager)
|
|
2023
|
+
return;
|
|
2024
|
+
if (abortSignal?.aborted) {
|
|
2025
|
+
throw abortError();
|
|
2026
|
+
}
|
|
2027
|
+
probeDelayMs = Math.min(probeDelayMs * 2, UPSTREAM_REPROBE_MAX_DELAY_MS);
|
|
2028
|
+
// Measured from the end of the probe, so a slow usage
|
|
2029
|
+
// request cannot schedule the next one in the past and
|
|
2030
|
+
// collapse the countdown sleep below to zero.
|
|
2031
|
+
nextProbeAt = Date.now() + probeDelayMs;
|
|
2032
|
+
}
|
|
1849
2033
|
const remaining = Math.max(0, endTime - Date.now());
|
|
1850
2034
|
const waitLabel = formatWaitTime(remaining);
|
|
1851
2035
|
await showToast(`${message} (${waitLabel} remaining)`, "warning", { duration: Math.min(intervalMs + 1000, toastDurationMs) });
|
|
1852
|
-
const sleepTime = Math.min(intervalMs, remaining);
|
|
2036
|
+
const sleepTime = Math.min(intervalMs, remaining, nextProbeAt - Date.now());
|
|
1853
2037
|
if (sleepTime > 0) {
|
|
1854
2038
|
await sleep(sleepTime);
|
|
1855
2039
|
}
|
|
1856
2040
|
else {
|
|
1857
|
-
|
|
2041
|
+
continue;
|
|
1858
2042
|
}
|
|
1859
2043
|
}
|
|
1860
2044
|
};
|
|
2045
|
+
/**
|
|
2046
|
+
* True when an all-accounts wait can stop early.
|
|
2047
|
+
*
|
|
2048
|
+
* `runNow` refreshes `/wham/usage` for every account and persists
|
|
2049
|
+
* whatever it finds, so a reset that never touched local disk
|
|
2050
|
+
* becomes visible here. Persisting a recovery also drops the cached
|
|
2051
|
+
* manager, which is what makes the enclosing retry loop re-resolve
|
|
2052
|
+
* one that no longer reports a block.
|
|
2053
|
+
*/
|
|
2054
|
+
const probeUpstreamBlockLifted = async () => {
|
|
2055
|
+
try {
|
|
2056
|
+
await quotaMonitor.runNow();
|
|
2057
|
+
}
|
|
2058
|
+
catch (error) {
|
|
2059
|
+
logDebug(`[${PLUGIN_NAME}] Upstream quota re-probe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2060
|
+
return false;
|
|
2061
|
+
}
|
|
2062
|
+
if (cachedAccountManager !== accountManager)
|
|
2063
|
+
return true;
|
|
2064
|
+
const manager = accountManager;
|
|
2065
|
+
if (!manager)
|
|
2066
|
+
return false;
|
|
2067
|
+
return manager.getMinWaitTimeForFamily(modelFamily, model) === 0;
|
|
2068
|
+
};
|
|
1861
2069
|
let allRateLimitedRetries = 0;
|
|
1862
2070
|
let emptyResponseRetries = 0;
|
|
1863
2071
|
const attemptedUnsupportedFallbackModels = new Set();
|
|
@@ -1940,9 +2148,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1940
2148
|
}
|
|
1941
2149
|
};
|
|
1942
2150
|
// A degraded model must not degrade again without bound, even if a
|
|
1943
|
-
// custom chain is cyclic.
|
|
1944
|
-
// revisiting a model; this caps the total hops per request.
|
|
1945
|
-
const MAX_QUOTA_FALLBACK_SWITCHES = 3;
|
|
2151
|
+
// custom chain is cyclic. See MAX_QUOTA_FALLBACK_SWITCHES.
|
|
1946
2152
|
let quotaFallbackSwitches = 0;
|
|
1947
2153
|
while (true) {
|
|
1948
2154
|
if (cachedAccountManager && cachedAccountManager !== accountManager) {
|
|
@@ -1965,7 +2171,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1965
2171
|
accountManagerPromise = null;
|
|
1966
2172
|
});
|
|
1967
2173
|
}
|
|
1968
|
-
const
|
|
2174
|
+
const loading = accountManagerPromise;
|
|
2175
|
+
const reloaded = await loading;
|
|
1969
2176
|
if (cachedAccountManager) {
|
|
1970
2177
|
if (cachedAccountManager !== reloaded) {
|
|
1971
2178
|
// The cache was repopulated while this load was in
|
|
@@ -1977,6 +2184,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1977
2184
|
}
|
|
1978
2185
|
accountManager = cachedAccountManager;
|
|
1979
2186
|
}
|
|
2187
|
+
else if (accountManager && isUntrustworthyEmptyReload(accountManager, reloaded)) {
|
|
2188
|
+
// The incumbent this request holds still has accounts, so
|
|
2189
|
+
// the empty result is a failed `loadAccounts()` read rather
|
|
2190
|
+
// than a real removal (same refusal as
|
|
2191
|
+
// reloadCachedAccountManager). Keep serving the incumbent
|
|
2192
|
+
// and drop the resolved promise so the next pass re-reads
|
|
2193
|
+
// disk instead of adopting this same empty manager.
|
|
2194
|
+
reloaded.disposeShutdownHandler();
|
|
2195
|
+
if (accountManagerPromise === loading)
|
|
2196
|
+
accountManagerPromise = null;
|
|
2197
|
+
logWarn(`[${PLUGIN_NAME}] Account reload returned no accounts while ${accountManager.getAccountCount()} are held; keeping the loaded pool and retrying`);
|
|
2198
|
+
const incumbent = accountManager;
|
|
2199
|
+
scheduleEmptyReloadRetry(() => repopulateAccountManagerCache(incumbent));
|
|
2200
|
+
}
|
|
1980
2201
|
else {
|
|
1981
2202
|
cachedAccountManager = reloaded;
|
|
1982
2203
|
accountManager = reloaded;
|
|
@@ -2098,6 +2319,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2098
2319
|
const failures = await accountManager.incrementAuthFailures(account);
|
|
2099
2320
|
const accountLabel = formatAccountLabel(account, account.index, {
|
|
2100
2321
|
maskEmail: maskEmailEnabled,
|
|
2322
|
+
peerAccounts: accountManager.getAccountsSnapshot(),
|
|
2101
2323
|
});
|
|
2102
2324
|
if (failures >= ACCOUNT_LIMITS.MAX_AUTH_FAILURES_BEFORE_REMOVAL) {
|
|
2103
2325
|
const removedCount = accountManager.removeAccountsWithSameRefreshToken(account);
|
|
@@ -2147,6 +2369,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2147
2369
|
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
2148
2370
|
const accountLabel = formatAccountLabel(account, account.index, {
|
|
2149
2371
|
maskEmail: maskEmailEnabled,
|
|
2372
|
+
peerAccounts: accountManager.getAccountsSnapshot(),
|
|
2150
2373
|
});
|
|
2151
2374
|
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
2152
2375
|
accountManager.markToastShown(account.index);
|
|
@@ -2334,7 +2557,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2334
2557
|
// only the block would leave the status line reporting "0% left" for
|
|
2335
2558
|
// an account the router considers healthy.
|
|
2336
2559
|
const recordQuotaHeaders = () => {
|
|
2337
|
-
void recordPromptQuotaHeaders(response, account, accountCount);
|
|
2560
|
+
void recordPromptQuotaHeaders(response, account, accountCount, accountManager.getAccountsSnapshot());
|
|
2338
2561
|
return applyQuotaExhaustion(accountManager, response.headers, account, modelFamily, model);
|
|
2339
2562
|
};
|
|
2340
2563
|
if (response.ok) {
|
|
@@ -2354,6 +2577,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2354
2577
|
if (workspaceDeactivated) {
|
|
2355
2578
|
const accountLabel = formatAccountLabel(account, account.index, {
|
|
2356
2579
|
maskEmail: maskEmailEnabled,
|
|
2580
|
+
peerAccounts: accountManager.getAccountsSnapshot(),
|
|
2357
2581
|
});
|
|
2358
2582
|
accountManager.refundToken(account, modelFamily, model);
|
|
2359
2583
|
accountManager.recordFailure(account, modelFamily, model);
|
|
@@ -2567,6 +2791,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2567
2791
|
if (isInvalidatedAuthTokenError(errorBody, response.status)) {
|
|
2568
2792
|
const accountLabel = formatAccountLabel(account, account.index, {
|
|
2569
2793
|
maskEmail: maskEmailEnabled,
|
|
2794
|
+
peerAccounts: accountManager.getAccountsSnapshot(),
|
|
2570
2795
|
});
|
|
2571
2796
|
accountManager.refundToken(account, modelFamily, model);
|
|
2572
2797
|
accountManager.recordFailure(account, modelFamily, model);
|
|
@@ -2838,9 +3063,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2838
3063
|
(retryAllAccountsMaxWaitMs === 0 ||
|
|
2839
3064
|
waitMs <= retryAllAccountsMaxWaitMs) &&
|
|
2840
3065
|
allRateLimitedRetries < retryAllAccountsMaxRetries &&
|
|
2841
|
-
consumeRetryBudget("rateLimitGlobal", `All accounts rate-limited wait ${waitMs}ms
|
|
3066
|
+
consumeRetryBudget("rateLimitGlobal", `All accounts rate-limited wait ${waitMs}ms`, waitMs)) {
|
|
2842
3067
|
const countdownMessage = `All ${count} account(s) rate-limited. Waiting`;
|
|
2843
|
-
await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage);
|
|
3068
|
+
await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage, undefined, probeUpstreamBlockLifted);
|
|
2844
3069
|
allRateLimitedRetries++;
|
|
2845
3070
|
continue;
|
|
2846
3071
|
}
|
|
@@ -2943,19 +3168,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2943
3168
|
message.includes("invalid refresh") ||
|
|
2944
3169
|
message.includes("token has been revoked"));
|
|
2945
3170
|
};
|
|
3171
|
+
const authQuotaDisplay = getQuotaDisplay(authPluginConfig);
|
|
2946
3172
|
const formatCodexQuotaLine = (usage) => {
|
|
2947
3173
|
const parts = [];
|
|
2948
3174
|
for (const window of [usage.primary, usage.secondary]) {
|
|
2949
3175
|
if (!hasUsageWindow(window))
|
|
2950
3176
|
continue;
|
|
2951
|
-
parts.push(`${formatUsageLimitTitle(window.windowMinutes)} ${formatUsageLimitSummary(window)}`);
|
|
3177
|
+
parts.push(`${formatUsageLimitTitle(window.windowMinutes)} ${formatUsageLimitSummary(window, authQuotaDisplay)}`);
|
|
2952
3178
|
}
|
|
2953
3179
|
if (hasUsageWindow(usage.codeReview)) {
|
|
2954
|
-
parts.push(`Code review ${formatUsageLimitSummary(usage.codeReview)}`);
|
|
3180
|
+
parts.push(`Code review ${formatUsageLimitSummary(usage.codeReview, authQuotaDisplay)}`);
|
|
2955
3181
|
}
|
|
2956
3182
|
for (const limit of usage.additionalLimits) {
|
|
2957
3183
|
if (hasUsageWindow(limit.window)) {
|
|
2958
|
-
parts.push(`${limit.name} ${formatUsageLimitSummary(limit.window)}`);
|
|
3184
|
+
parts.push(`${limit.name} ${formatUsageLimitSummary(limit.window, authQuotaDisplay)}`);
|
|
2959
3185
|
}
|
|
2960
3186
|
}
|
|
2961
3187
|
const planLabel = formatPlanType(usage.planType);
|
|
@@ -3127,8 +3353,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
3127
3353
|
}
|
|
3128
3354
|
if (deepProbe) {
|
|
3129
3355
|
ok += 1;
|
|
3130
|
-
|
|
3131
|
-
|
|
3356
|
+
// Both read from the probed token, so the pair is the seat
|
|
3357
|
+
// the credential actually belongs to. The workspace id
|
|
3358
|
+
// alone repeats across every member of a Business
|
|
3359
|
+
// workspace and cannot confirm which seat answered.
|
|
3360
|
+
const tokenSeat = formatSeatSuffix(extractAccountUserId(accessToken), workingStorage.accounts.map((peer) => peer?.accountUserId));
|
|
3361
|
+
const identity = [
|
|
3362
|
+
tokenAccountId ? `id:${tokenAccountId.slice(-6)}` : undefined,
|
|
3363
|
+
tokenSeat ? `seat:${tokenSeat}` : undefined,
|
|
3364
|
+
].filter((part) => part !== undefined);
|
|
3365
|
+
const detail = identity.length > 0
|
|
3366
|
+
? `${authDetail} (${identity.join(", ")})`
|
|
3132
3367
|
: authDetail;
|
|
3133
3368
|
console.log(`[${i + 1}/${total}] ${label}: ${detail}`);
|
|
3134
3369
|
continue;
|
|
@@ -3146,7 +3381,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
3146
3381
|
organizationId: account.organizationId,
|
|
3147
3382
|
normalizeAccountErrors: true,
|
|
3148
3383
|
});
|
|
3149
|
-
const usage = parseCodexUsagePayload(payload);
|
|
3384
|
+
const usage = parseCodexUsagePayload(payload, authQuotaDisplay);
|
|
3150
3385
|
ok += 1;
|
|
3151
3386
|
console.log(`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(usage)}`);
|
|
3152
3387
|
}
|
|
@@ -3422,6 +3657,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
3422
3657
|
}
|
|
3423
3658
|
return {
|
|
3424
3659
|
accountId: account.accountId,
|
|
3660
|
+
accountUserId: account.accountUserId,
|
|
3425
3661
|
accountLabel: account.accountLabel,
|
|
3426
3662
|
email: account.email,
|
|
3427
3663
|
index,
|