oc-codex-multi-auth 6.20.0 → 6.22.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/LICENSE +21 -21
- package/README.md +729 -568
- package/assets/icon.svg +7 -7
- package/assets/opencode-logo-ornate-dark.svg +18 -18
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +177 -177
- package/config/minimal-opencode.json +14 -14
- package/config/opencode-legacy.json +1496 -1496
- package/config/opencode-modern.json +555 -555
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +567 -102
- 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 +263 -1
- package/dist/lib/account-display.js.map +1 -1
- package/dist/lib/accounts/persistence.d.ts +3 -1
- package/dist/lib/accounts/persistence.d.ts.map +1 -1
- package/dist/lib/accounts/persistence.js +27 -3
- package/dist/lib/accounts/persistence.js.map +1 -1
- package/dist/lib/accounts/stale-state.d.ts +1 -0
- package/dist/lib/accounts/stale-state.d.ts.map +1 -1
- package/dist/lib/accounts/stale-state.js +30 -0
- package/dist/lib/accounts/stale-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 +5 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +22 -17
- 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/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/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-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 +7 -4
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +48 -13
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/config.d.ts +77 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +190 -15
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/desktop-notifications.js +6 -6
- package/dist/lib/oauth-success.js +202 -202
- package/dist/lib/parallel-probe.d.ts.map +1 -1
- package/dist/lib/parallel-probe.js +2 -1
- package/dist/lib/parallel-probe.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-opencode-bridge.js +67 -67
- package/dist/lib/prompts/codex.js +75 -75
- 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 +43 -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.js +22 -18
- 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/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/rotation.d.ts +1 -1
- package/dist/lib/schemas.d.ts +67 -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 +5 -0
- package/dist/lib/storage/load-save.d.ts.map +1 -1
- package/dist/lib/storage/load-save.js +61 -0
- package/dist/lib/storage/load-save.js.map +1 -1
- package/dist/lib/storage/normalize.d.ts.map +1 -1
- package/dist/lib/storage/normalize.js +38 -2
- package/dist/lib/storage/normalize.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/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/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 +26 -10
- 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 +53 -8
- 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 +2 -1
- package/dist/lib/tools/codex-warm.d.ts.map +1 -1
- package/dist/lib/tools/codex-warm.js +49 -5
- package/dist/lib/tools/codex-warm.js.map +1 -1
- package/dist/lib/tools/index.d.ts +10 -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 +35 -0
- package/dist/lib/tui-quota-cache.d.ts.map +1 -1
- package/dist/lib/tui-quota-cache.js +79 -12
- 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 +59 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +535 -43
- package/dist/tui.js.map +1 -1
- package/package.json +155 -155
- package/scripts/audit-dev-allowlist.js +114 -114
- package/scripts/clean-dist.js +27 -27
- package/scripts/copy-oauth-success.js +47 -47
- package/scripts/install-oc-codex-multi-auth-core.js +2003 -1469
- package/scripts/install-oc-codex-multi-auth.js +37 -37
- package/scripts/test-all-models.sh +260 -260
- package/scripts/validate-model-map.sh +97 -97
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";
|
|
@@ -33,15 +39,17 @@ import { buildDeviceCodeInstructions, completeDeviceCodeSession, createDeviceCod
|
|
|
33
39
|
import { applyAccountSelectionFallbacks, persistResolvedAccountSelection, persistAccountPool, resolveAndPersistAccountSelection, resolveAccountSelection, } from "./lib/auth/login-runner.js";
|
|
34
40
|
import { coordinateFlaggedPersistedRefresh, coordinatePersistedRefresh, } from "./lib/storage/coordinated-refresh.js";
|
|
35
41
|
import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
|
|
36
|
-
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";
|
|
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";
|
|
37
43
|
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
|
|
38
44
|
import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
|
|
39
45
|
import { createQuotaMonitor } from "./lib/quota-notifications.js";
|
|
40
46
|
import { checkAndNotify } from "./lib/auto-update-checker.js";
|
|
47
|
+
import { describePluginOrigin, getPluginOrigin, recordPluginOrigin } from "./lib/plugin-origin.js";
|
|
41
48
|
import { handleContextOverflow } from "./lib/context-overflow.js";
|
|
42
49
|
import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLabel, formatWaitTime, sanitizeEmail, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
|
|
43
50
|
import { getModelPoolAccountKey, matchesModelPoolAccountKey, } from "./lib/accounts/pool-identity.js";
|
|
44
|
-
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";
|
|
45
53
|
import { CodexAuthError } from "./lib/errors.js";
|
|
46
54
|
import { getStoragePath, loadAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, loadFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
|
|
47
55
|
import { getWorkspaceIdentityKey } from "./lib/storage/identity.js";
|
|
@@ -179,9 +187,19 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
179
187
|
let accountManagerPromise = null;
|
|
180
188
|
let loaderMutex = null;
|
|
181
189
|
let startupPrewarmTriggered = false;
|
|
190
|
+
let startupOriginRecorded = false;
|
|
182
191
|
let startupPreflightShown = false;
|
|
183
192
|
let beginnerSafeModeEnabled = false;
|
|
184
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;
|
|
185
203
|
const runtimeMetrics = {
|
|
186
204
|
startedAt: Date.now(),
|
|
187
205
|
totalRequests: 0,
|
|
@@ -234,17 +252,33 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
234
252
|
selectionExplainability: serializeSelectionExplainability(options.selectionExplainability ?? snapshot?.explainability ?? []),
|
|
235
253
|
};
|
|
236
254
|
};
|
|
237
|
-
const buildJsonAccountIdentity = (index, options = {}) =>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
};
|
|
248
282
|
const appendRoutingVisibilityText = (lines, routing, options = {}) => {
|
|
249
283
|
lines.push("Routing visibility:");
|
|
250
284
|
lines.push(` Requested model: ${formatRoutingValue(routing.requestedModel)}`);
|
|
@@ -443,14 +477,16 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
443
477
|
logDebug(`[${PLUGIN_NAME}] Failed to clear TUI quota cache: ${error instanceof Error ? error.message : String(error)}`);
|
|
444
478
|
}
|
|
445
479
|
};
|
|
446
|
-
const recordPromptQuotaHeaders = async (response, account, accountCount) => {
|
|
480
|
+
const recordPromptQuotaHeaders = async (response, account, accountCount, peerAccounts) => {
|
|
447
481
|
try {
|
|
448
482
|
const snapshot = parseTuiQuotaSnapshotFromHeaders(response.headers, {
|
|
449
483
|
fingerprint: createUsageAccountFingerprint(account),
|
|
450
484
|
accountIndex: account.index + 1,
|
|
451
485
|
accountCount,
|
|
452
486
|
accountEmail: account.email?.trim() || undefined,
|
|
453
|
-
accountLabel: formatAccountLabel(account, account.index
|
|
487
|
+
accountLabel: formatAccountLabel(account, account.index, {
|
|
488
|
+
peerAccounts,
|
|
489
|
+
}),
|
|
454
490
|
});
|
|
455
491
|
if (!snapshot)
|
|
456
492
|
return;
|
|
@@ -806,6 +842,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
806
842
|
const email = resolveDisplayEmail(account?.email, options.maskEmail ?? false);
|
|
807
843
|
const workspace = account?.accountLabel?.trim();
|
|
808
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));
|
|
809
851
|
const tags = Array.isArray(account?.accountTags)
|
|
810
852
|
? account.accountTags
|
|
811
853
|
.filter((tag) => typeof tag === "string")
|
|
@@ -819,6 +861,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
819
861
|
details.push(`workspace:${workspace}`);
|
|
820
862
|
if (accountId)
|
|
821
863
|
details.push(`id:${accountId}`);
|
|
864
|
+
if (seat)
|
|
865
|
+
details.push(`seat:${seat}`);
|
|
822
866
|
if (tags.length > 0)
|
|
823
867
|
details.push(`tags:${tags.join(",")}`);
|
|
824
868
|
if (details.length === 0) {
|
|
@@ -833,6 +877,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
833
877
|
.filter((entry) => entry.length > 0)));
|
|
834
878
|
};
|
|
835
879
|
const supportsInteractiveMenus = () => {
|
|
880
|
+
if (process.env.FORCE_INTERACTIVE_MODE === "1")
|
|
881
|
+
return true;
|
|
836
882
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
837
883
|
return false;
|
|
838
884
|
if (process.env.OPENCODE_TUI === "1")
|
|
@@ -841,6 +887,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
841
887
|
return false;
|
|
842
888
|
if (process.env.TERM_PROGRAM === "opencode")
|
|
843
889
|
return false;
|
|
890
|
+
if (process.env.ELECTRON_RUN_AS_NODE === "1")
|
|
891
|
+
return false;
|
|
844
892
|
return true;
|
|
845
893
|
};
|
|
846
894
|
const promptAccountIndexSelection = async (ui, storage, title) => {
|
|
@@ -850,7 +898,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
850
898
|
const { select } = await import("./lib/ui/select.js");
|
|
851
899
|
const maskEmail = resolveMaskEmail();
|
|
852
900
|
const selected = await select(storage.accounts.map((account, index) => ({
|
|
853
|
-
label: formatCommandAccountLabel(account, index, {
|
|
901
|
+
label: formatCommandAccountLabel(account, index, {
|
|
902
|
+
maskEmail,
|
|
903
|
+
peerAccounts: storage.accounts,
|
|
904
|
+
}),
|
|
854
905
|
value: index,
|
|
855
906
|
})), {
|
|
856
907
|
message: title,
|
|
@@ -869,7 +920,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
869
920
|
const toBeginnerAccountSnapshots = (storage, activeIndex, now) => {
|
|
870
921
|
return storage.accounts.map((account, index) => ({
|
|
871
922
|
index,
|
|
872
|
-
label: formatCommandAccountLabel(account, index
|
|
923
|
+
label: formatCommandAccountLabel(account, index, {
|
|
924
|
+
peerAccounts: storage.accounts,
|
|
925
|
+
}),
|
|
873
926
|
accountLabel: account.accountLabel,
|
|
874
927
|
enabled: account.enabled !== false,
|
|
875
928
|
isActive: index === activeIndex,
|
|
@@ -1112,25 +1165,59 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1112
1165
|
logDebug(`[${PLUGIN_NAME}] Startup preflight skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
1113
1166
|
}
|
|
1114
1167
|
};
|
|
1115
|
-
const invalidateAccountManagerCache = () => {
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
// pointing at a stale instance. Flush first (best-effort, in the
|
|
1119
|
-
// background) so any queued debounced save is not silently dropped.
|
|
1168
|
+
const invalidateAccountManagerCache = (clearedSnapshots) => {
|
|
1169
|
+
// Retire before flushing: keep disk membership authoritative while
|
|
1170
|
+
// publishing queued rate-limit evidence through the volatile merge.
|
|
1120
1171
|
const previous = cachedAccountManager;
|
|
1121
1172
|
cachedAccountManager = null;
|
|
1122
1173
|
accountManagerPromise = null;
|
|
1123
1174
|
if (previous) {
|
|
1175
|
+
previous.disposeShutdownHandler(false, clearedSnapshots);
|
|
1124
1176
|
void previous
|
|
1125
1177
|
.flushPendingSave()
|
|
1126
1178
|
.catch((error) => {
|
|
1127
1179
|
logWarn(`Failed to flush pending save while invalidating account manager: ${error instanceof Error ? error.message : String(error)}`);
|
|
1128
|
-
})
|
|
1129
|
-
.finally(() => {
|
|
1130
|
-
previous.disposeShutdownHandler();
|
|
1131
1180
|
});
|
|
1132
1181
|
}
|
|
1133
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
|
+
};
|
|
1134
1221
|
const reloadCachedAccountManager = async () => {
|
|
1135
1222
|
if (!cachedAccountManager)
|
|
1136
1223
|
return;
|
|
@@ -1149,6 +1236,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1149
1236
|
}
|
|
1150
1237
|
try {
|
|
1151
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();
|
|
1152
1246
|
cachedAccountManager = reloadedManager;
|
|
1153
1247
|
accountManagerPromise = Promise.resolve(reloadedManager);
|
|
1154
1248
|
// Dispose only after the replacement is installed so we never leak
|
|
@@ -1160,6 +1254,197 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1160
1254
|
logWarn(`Failed to reload account manager: ${error instanceof Error ? error.message : String(error)}`);
|
|
1161
1255
|
}
|
|
1162
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
|
+
};
|
|
1286
|
+
let watchedAccountsPath;
|
|
1287
|
+
let observedAccountsDigest;
|
|
1288
|
+
let accountsReloadTimer;
|
|
1289
|
+
let accountsWatcherDisposed = false;
|
|
1290
|
+
let accountsWatchGeneration = 0;
|
|
1291
|
+
let unsubscribeAccountsPath;
|
|
1292
|
+
const stopAccountsWatcher = () => {
|
|
1293
|
+
accountsWatchGeneration += 1;
|
|
1294
|
+
if (watchedAccountsPath)
|
|
1295
|
+
unwatchFile(watchedAccountsPath, onAccountsStatChanged);
|
|
1296
|
+
watchedAccountsPath = undefined;
|
|
1297
|
+
observedAccountsDigest = undefined;
|
|
1298
|
+
clearTimeout(accountsReloadTimer);
|
|
1299
|
+
accountsReloadTimer = undefined;
|
|
1300
|
+
};
|
|
1301
|
+
const disposeAccountsWatcher = () => {
|
|
1302
|
+
accountsWatcherDisposed = true;
|
|
1303
|
+
unsubscribeAccountsPath?.();
|
|
1304
|
+
stopAccountsWatcher();
|
|
1305
|
+
cancelEmptyReloadRetry();
|
|
1306
|
+
unregisterCleanup(disposeAccountsWatcher);
|
|
1307
|
+
};
|
|
1308
|
+
const readAccountsFileState = async (path) => {
|
|
1309
|
+
try {
|
|
1310
|
+
const content = await readFile(path, "utf8");
|
|
1311
|
+
const data = JSON.parse(content);
|
|
1312
|
+
if (!AnyAccountStorageSchema.safeParse(data).success)
|
|
1313
|
+
return;
|
|
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
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
catch {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
const reloadForExternalAccountsChange = async (path, generation, attempt = 0, retired) => {
|
|
1327
|
+
const observed = await readAccountsFileState(path);
|
|
1328
|
+
if (generation !== accountsWatchGeneration || !observed || path !== getStoragePath())
|
|
1329
|
+
return;
|
|
1330
|
+
const digest = observed.digest;
|
|
1331
|
+
if (digest === consumeLastWrittenAccountsDigest(path))
|
|
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
|
+
};
|
|
1342
|
+
const previous = cachedAccountManager;
|
|
1343
|
+
try {
|
|
1344
|
+
// A null cache means an invalidation retired the incumbent; the
|
|
1345
|
+
// reload still must adopt the external change. Bailing here would
|
|
1346
|
+
// strand it: onAccountsFileChanged already marked this digest
|
|
1347
|
+
// observed, and an in-flight load started before the write can
|
|
1348
|
+
// still resolve and install a pre-change snapshot whose
|
|
1349
|
+
// full-membership save then deletes the imported accounts.
|
|
1350
|
+
if (previous) {
|
|
1351
|
+
if (previous !== retired) {
|
|
1352
|
+
previous.disposeShutdownHandler(true);
|
|
1353
|
+
retired = previous;
|
|
1354
|
+
}
|
|
1355
|
+
await previous.flushPendingSave();
|
|
1356
|
+
}
|
|
1357
|
+
const reloaded = await AccountManager.loadFromDisk();
|
|
1358
|
+
if (generation !== accountsWatchGeneration || accountsWatcherDisposed || path !== getStoragePath()) {
|
|
1359
|
+
reloaded.disposeShutdownHandler();
|
|
1360
|
+
return;
|
|
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
|
+
}
|
|
1375
|
+
const outgoing = cachedAccountManager;
|
|
1376
|
+
if (outgoing && outgoing !== retired) {
|
|
1377
|
+
// Another actor replaced the cached manager while this reload
|
|
1378
|
+
// was in flight (concurrent fetch reload or tool mutation).
|
|
1379
|
+
// Retire the incumbent with the same external-reload
|
|
1380
|
+
// semantics, or its queued membership save can clobber the
|
|
1381
|
+
// external change this reload is about to adopt.
|
|
1382
|
+
outgoing.disposeShutdownHandler(true);
|
|
1383
|
+
retired = outgoing;
|
|
1384
|
+
}
|
|
1385
|
+
cachedAccountManager = reloaded;
|
|
1386
|
+
accountManagerPromise = Promise.resolve(reloaded);
|
|
1387
|
+
observedAccountsDigest = digest;
|
|
1388
|
+
}
|
|
1389
|
+
catch {
|
|
1390
|
+
logWarn("Could not reload externally updated account storage");
|
|
1391
|
+
retryLater();
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
logDebug("Reloaded cached account manager after external accounts file change");
|
|
1395
|
+
};
|
|
1396
|
+
const onAccountsFileChanged = async () => {
|
|
1397
|
+
if (accountsWatcherDisposed)
|
|
1398
|
+
return;
|
|
1399
|
+
if (watchedAccountsPath !== getStoragePath()) {
|
|
1400
|
+
await ensureAccountsWatcher();
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const path = watchedAccountsPath;
|
|
1404
|
+
if (!path)
|
|
1405
|
+
return;
|
|
1406
|
+
const generation = accountsWatchGeneration;
|
|
1407
|
+
const observed = await readAccountsFileState(path);
|
|
1408
|
+
if (generation !== accountsWatchGeneration || !observed || observed.digest === observedAccountsDigest)
|
|
1409
|
+
return;
|
|
1410
|
+
const digest = observed.digest;
|
|
1411
|
+
observedAccountsDigest = digest;
|
|
1412
|
+
clearTimeout(accountsReloadTimer);
|
|
1413
|
+
accountsReloadTimer = undefined;
|
|
1414
|
+
if (digest === consumeLastWrittenAccountsDigest(path))
|
|
1415
|
+
return;
|
|
1416
|
+
accountsReloadTimer = setTimeout(() => {
|
|
1417
|
+
accountsReloadTimer = undefined;
|
|
1418
|
+
void reloadForExternalAccountsChange(path, generation);
|
|
1419
|
+
}, 500);
|
|
1420
|
+
accountsReloadTimer.unref();
|
|
1421
|
+
};
|
|
1422
|
+
const onAccountsStatChanged = () => {
|
|
1423
|
+
void onAccountsFileChanged();
|
|
1424
|
+
};
|
|
1425
|
+
const ensureAccountsWatcher = async () => {
|
|
1426
|
+
if (accountsWatcherDisposed)
|
|
1427
|
+
return;
|
|
1428
|
+
const path = getStoragePath();
|
|
1429
|
+
if (path === watchedAccountsPath)
|
|
1430
|
+
return;
|
|
1431
|
+
stopAccountsWatcher();
|
|
1432
|
+
if (isKeychainOptInEnabled())
|
|
1433
|
+
return;
|
|
1434
|
+
unsubscribeAccountsPath ??= subscribeToStoragePathChanges(() => {
|
|
1435
|
+
void ensureAccountsWatcher();
|
|
1436
|
+
});
|
|
1437
|
+
watchedAccountsPath = path;
|
|
1438
|
+
const generation = accountsWatchGeneration;
|
|
1439
|
+
const initial = await readAccountsFileState(path);
|
|
1440
|
+
if (generation !== accountsWatchGeneration)
|
|
1441
|
+
return;
|
|
1442
|
+
observedAccountsDigest = initial?.digest;
|
|
1443
|
+
// Stat polling follows the path across the storage writer's temp-file rename.
|
|
1444
|
+
watchFile(path, { interval: 1500, persistent: false }, onAccountsStatChanged);
|
|
1445
|
+
unregisterCleanup(disposeAccountsWatcher);
|
|
1446
|
+
registerCleanup(disposeAccountsWatcher);
|
|
1447
|
+
};
|
|
1163
1448
|
const persistAuthenticatedSelections = async (results, replaceAll) => {
|
|
1164
1449
|
try {
|
|
1165
1450
|
await persistAccountPool(results, replaceAll);
|
|
@@ -1192,6 +1477,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1192
1477
|
const { event } = input;
|
|
1193
1478
|
if (event.type === "server.instance.disposed") {
|
|
1194
1479
|
quotaMonitor.dispose();
|
|
1480
|
+
disposeAccountsWatcher();
|
|
1195
1481
|
return;
|
|
1196
1482
|
}
|
|
1197
1483
|
// Handle TUI account selection events
|
|
@@ -1346,7 +1632,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1346
1632
|
});
|
|
1347
1633
|
}
|
|
1348
1634
|
applyUiRuntimeFromConfig(pluginConfig);
|
|
1349
|
-
|
|
1635
|
+
let perProjectAccounts = getPerProjectAccounts(pluginConfig);
|
|
1636
|
+
let storageTransition;
|
|
1637
|
+
const activeFetches = new Set();
|
|
1350
1638
|
setStoragePath(perProjectAccounts ? process.cwd() : null);
|
|
1351
1639
|
const authFallback = auth.type === "oauth" ? auth : undefined;
|
|
1352
1640
|
// Prefer multi-account auth metadata when available, but still handle
|
|
@@ -1386,8 +1674,19 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1386
1674
|
}
|
|
1387
1675
|
});
|
|
1388
1676
|
}
|
|
1389
|
-
|
|
1390
|
-
cachedAccountManager
|
|
1677
|
+
const loadedManager = await accountManagerPromise;
|
|
1678
|
+
if (cachedAccountManager && cachedAccountManager !== loadedManager) {
|
|
1679
|
+
// An external reload replaced the cache while this load was
|
|
1680
|
+
// in flight. The loaded snapshot is stale; retiring it keeps
|
|
1681
|
+
// its debounced save from writing full membership over the
|
|
1682
|
+
// successor's state.
|
|
1683
|
+
loadedManager.disposeShutdownHandler();
|
|
1684
|
+
}
|
|
1685
|
+
else {
|
|
1686
|
+
cachedAccountManager = loadedManager;
|
|
1687
|
+
}
|
|
1688
|
+
const accountManager = cachedAccountManager ?? loadedManager;
|
|
1689
|
+
await ensureAccountsWatcher();
|
|
1391
1690
|
const refreshToken = authFallback?.refresh ?? "";
|
|
1392
1691
|
const needsPersist = refreshToken &&
|
|
1393
1692
|
!accountManager.hasRefreshToken(refreshToken);
|
|
@@ -1412,86 +1711,60 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1412
1711
|
global: providerConfig?.options || {},
|
|
1413
1712
|
models: providerConfig?.models || {},
|
|
1414
1713
|
};
|
|
1415
|
-
// Load plugin configuration and determine CODEX_MODE
|
|
1416
|
-
// Priority: CODEX_MODE env var > config file > default (true)
|
|
1417
|
-
const codexMode = getCodexMode(pluginConfig);
|
|
1418
|
-
const requestTransformMode = getRequestTransformMode(pluginConfig);
|
|
1419
|
-
const useLegacyRequestTransform = requestTransformMode === "legacy";
|
|
1420
|
-
const fastSessionEnabled = getFastSession(pluginConfig);
|
|
1421
|
-
const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
|
|
1422
|
-
const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
|
|
1423
|
-
const beginnerSafeMode = getBeginnerSafeMode(pluginConfig);
|
|
1424
|
-
beginnerSafeModeEnabled = beginnerSafeMode;
|
|
1425
|
-
const maskEmailEnabled = getCodexTuiMaskEmail(pluginConfig);
|
|
1426
|
-
const retryProfile = beginnerSafeMode
|
|
1427
|
-
? "conservative"
|
|
1428
|
-
: getRetryProfile(pluginConfig);
|
|
1429
|
-
const retryBudgetOverrides = beginnerSafeMode
|
|
1430
|
-
? {}
|
|
1431
|
-
: getRetryBudgetOverrides(pluginConfig);
|
|
1432
|
-
const retryBudgetLimits = resolveRetryBudgetLimits(retryProfile, retryBudgetOverrides);
|
|
1433
|
-
runtimeMetrics.retryProfile = retryProfile;
|
|
1434
|
-
runtimeMetrics.retryBudgetLimits = { ...retryBudgetLimits };
|
|
1435
|
-
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
1436
|
-
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
1437
|
-
const retryAllAccountsRateLimited = beginnerSafeMode
|
|
1438
|
-
? false
|
|
1439
|
-
: getRetryAllAccountsRateLimited(pluginConfig);
|
|
1440
|
-
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
1441
|
-
const retryAllAccountsMaxRetries = beginnerSafeMode
|
|
1442
|
-
? Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig))
|
|
1443
|
-
: getRetryAllAccountsMaxRetries(pluginConfig);
|
|
1444
|
-
const unsupportedCodexPolicy = getUnsupportedCodexPolicy(pluginConfig);
|
|
1445
|
-
const fallbackOnUnsupportedCodexModel = unsupportedCodexPolicy === "fallback";
|
|
1446
|
-
const fallbackToGpt52OnUnsupportedGpt53 = getFallbackToGpt52OnUnsupportedGpt53(pluginConfig);
|
|
1447
|
-
const unsupportedCodexFallbackChain = getUnsupportedCodexFallbackChain(pluginConfig);
|
|
1448
|
-
const toastDurationMs = getToastDurationMs(pluginConfig);
|
|
1449
|
-
const accountToastsEnabled = getAccountToastsEnabled(pluginConfig);
|
|
1450
|
-
const fetchTimeoutMs = getFetchTimeoutMs(pluginConfig);
|
|
1451
|
-
const streamStallTimeoutMs = getStreamStallTimeoutMs(pluginConfig);
|
|
1452
1714
|
const sessionRecoveryEnabled = getSessionRecovery(pluginConfig);
|
|
1715
|
+
beginnerSafeModeEnabled = getBeginnerSafeMode(pluginConfig);
|
|
1716
|
+
const initialRetryProfile = beginnerSafeModeEnabled ? "conservative" : getRetryProfile(pluginConfig);
|
|
1717
|
+
runtimeMetrics.retryProfile = initialRetryProfile;
|
|
1718
|
+
runtimeMetrics.retryBudgetLimits = resolveRetryBudgetLimits(initialRetryProfile, beginnerSafeModeEnabled ? {} : getRetryBudgetOverrides(pluginConfig));
|
|
1453
1719
|
const autoResumeEnabled = getAutoResume(pluginConfig);
|
|
1454
1720
|
const autoUpdateEnabled = getAutoUpdate(pluginConfig);
|
|
1455
|
-
|
|
1456
|
-
const emptyResponseRetryDelayMs = getEmptyResponseRetryDelayMs(pluginConfig);
|
|
1457
|
-
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
|
|
1458
|
-
const rotationStrategy = getRotationStrategy(pluginConfig);
|
|
1459
|
-
const effectiveUserConfig = fastSessionEnabled
|
|
1460
|
-
? applyFastSessionDefaults(userConfig)
|
|
1461
|
-
: userConfig;
|
|
1462
|
-
if (fastSessionEnabled) {
|
|
1721
|
+
if (getFastSession(pluginConfig)) {
|
|
1463
1722
|
logDebug("Fast session mode enabled", {
|
|
1464
1723
|
reasoningEffort: "none/low",
|
|
1465
1724
|
reasoningSummary: "auto",
|
|
1466
1725
|
textVerbosity: "low",
|
|
1467
|
-
fastSessionStrategy,
|
|
1468
|
-
fastSessionMaxInputItems,
|
|
1726
|
+
fastSessionStrategy: getFastSessionStrategy(pluginConfig),
|
|
1727
|
+
fastSessionMaxInputItems: getFastSessionMaxInputItems(pluginConfig),
|
|
1469
1728
|
});
|
|
1470
1729
|
}
|
|
1471
|
-
if (
|
|
1730
|
+
if (getBeginnerSafeMode(pluginConfig)) {
|
|
1472
1731
|
logInfo("Beginner safe mode enabled", {
|
|
1473
|
-
retryProfile,
|
|
1474
|
-
retryAllAccountsRateLimited,
|
|
1475
|
-
retryAllAccountsMaxRetries,
|
|
1732
|
+
retryProfile: "conservative",
|
|
1733
|
+
retryAllAccountsRateLimited: false,
|
|
1734
|
+
retryAllAccountsMaxRetries: Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig)),
|
|
1476
1735
|
});
|
|
1477
1736
|
}
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
if (!startupPrewarmTriggered && prewarmEnabled && useLegacyRequestTransform) {
|
|
1737
|
+
const underTestRunner = process.env.VITEST === "true" || process.env.NODE_ENV === "test";
|
|
1738
|
+
const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" && !underTestRunner;
|
|
1739
|
+
if (!startupPrewarmTriggered && prewarmEnabled && getRequestTransformMode(pluginConfig) === "legacy") {
|
|
1482
1740
|
startupPrewarmTriggered = true;
|
|
1483
1741
|
const configuredModels = Object.keys(userConfig.models ?? {});
|
|
1484
1742
|
prewarmCodexInstructions(configuredModels);
|
|
1485
|
-
if (
|
|
1743
|
+
if (getCodexMode(pluginConfig)) {
|
|
1486
1744
|
prewarmOpenCodeCodexPrompt();
|
|
1487
1745
|
}
|
|
1488
1746
|
}
|
|
1489
1747
|
const recoveryHook = sessionRecoveryEnabled
|
|
1490
1748
|
? createSessionRecoveryHook({ client, directory: process.cwd() }, { sessionRecovery: true, autoResume: autoResumeEnabled })
|
|
1491
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
|
+
}
|
|
1492
1762
|
checkAndNotify(async (message, variant) => {
|
|
1493
1763
|
await showToast(message, variant);
|
|
1494
|
-
}, {
|
|
1764
|
+
}, {
|
|
1765
|
+
autoUpdate: autoUpdateEnabled,
|
|
1766
|
+
localCheckout: pluginOrigin?.isLocalCheckout ?? false,
|
|
1767
|
+
}).catch((err) => {
|
|
1495
1768
|
logDebug(`Update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1496
1769
|
});
|
|
1497
1770
|
await runStartupPreflight();
|
|
@@ -1515,10 +1788,79 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1515
1788
|
* @returns Response from Codex API
|
|
1516
1789
|
*/
|
|
1517
1790
|
async fetch(input, init) {
|
|
1791
|
+
let finishFetch;
|
|
1792
|
+
let pendingFetch;
|
|
1518
1793
|
try {
|
|
1519
|
-
|
|
1520
|
-
|
|
1794
|
+
const pluginConfig = loadPluginConfig();
|
|
1795
|
+
const currentPerProjectAccounts = getPerProjectAccounts(pluginConfig);
|
|
1796
|
+
while (storageTransition)
|
|
1797
|
+
await storageTransition;
|
|
1798
|
+
if (currentPerProjectAccounts !== perProjectAccounts) {
|
|
1799
|
+
storageTransition = (async () => {
|
|
1800
|
+
await Promise.all(activeFetches);
|
|
1801
|
+
// Drain the old pool's pending write before changing the global path.
|
|
1802
|
+
await cachedAccountManager?.flushPendingSave();
|
|
1803
|
+
setStoragePath(currentPerProjectAccounts ? process.cwd() : null);
|
|
1804
|
+
invalidateAccountManagerCache();
|
|
1805
|
+
perProjectAccounts = currentPerProjectAccounts;
|
|
1806
|
+
})().finally(() => { storageTransition = undefined; });
|
|
1807
|
+
await storageTransition;
|
|
1808
|
+
}
|
|
1809
|
+
pendingFetch = new Promise((resolve) => { finishFetch = resolve; });
|
|
1810
|
+
activeFetches.add(pendingFetch);
|
|
1811
|
+
if (!accountManagerPromise) {
|
|
1812
|
+
const pending = AccountManager.loadFromDisk();
|
|
1813
|
+
accountManagerPromise = pending;
|
|
1814
|
+
void pending.catch(() => {
|
|
1815
|
+
if (accountManagerPromise === pending)
|
|
1816
|
+
accountManagerPromise = null;
|
|
1817
|
+
});
|
|
1521
1818
|
}
|
|
1819
|
+
if (!cachedAccountManager) {
|
|
1820
|
+
const loadedManager = await accountManagerPromise;
|
|
1821
|
+
if (cachedAccountManager && cachedAccountManager !== loadedManager) {
|
|
1822
|
+
// External reload won the race while this load was in
|
|
1823
|
+
// flight: adopt the cache and retire the stale load.
|
|
1824
|
+
loadedManager.disposeShutdownHandler();
|
|
1825
|
+
}
|
|
1826
|
+
else if (!cachedAccountManager) {
|
|
1827
|
+
cachedAccountManager = loadedManager;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const codexMode = getCodexMode(pluginConfig);
|
|
1831
|
+
const requestTransformMode = getRequestTransformMode(pluginConfig);
|
|
1832
|
+
const fastSessionEnabled = getFastSession(pluginConfig);
|
|
1833
|
+
const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
|
|
1834
|
+
const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
|
|
1835
|
+
const beginnerSafeMode = getBeginnerSafeMode(pluginConfig);
|
|
1836
|
+
beginnerSafeModeEnabled = beginnerSafeMode;
|
|
1837
|
+
const maskEmailEnabled = getCodexTuiMaskEmail(pluginConfig);
|
|
1838
|
+
const retryProfile = beginnerSafeMode ? "conservative" : getRetryProfile(pluginConfig);
|
|
1839
|
+
const retryBudgetOverrides = beginnerSafeMode ? {} : getRetryBudgetOverrides(pluginConfig);
|
|
1840
|
+
const retryBudgetLimits = resolveRetryBudgetLimits(retryProfile, retryBudgetOverrides);
|
|
1841
|
+
runtimeMetrics.retryProfile = retryProfile;
|
|
1842
|
+
runtimeMetrics.retryBudgetLimits = { ...retryBudgetLimits };
|
|
1843
|
+
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
1844
|
+
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
1845
|
+
const retryAllAccountsRateLimited = beginnerSafeMode ? false : getRetryAllAccountsRateLimited(pluginConfig);
|
|
1846
|
+
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
1847
|
+
const retryAllAccountsMaxRetries = beginnerSafeMode
|
|
1848
|
+
? Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig))
|
|
1849
|
+
: getRetryAllAccountsMaxRetries(pluginConfig);
|
|
1850
|
+
const unsupportedCodexPolicy = getUnsupportedCodexPolicy(pluginConfig);
|
|
1851
|
+
const fallbackOnUnsupportedCodexModel = unsupportedCodexPolicy === "fallback";
|
|
1852
|
+
const fallbackToGpt52OnUnsupportedGpt53 = getFallbackToGpt52OnUnsupportedGpt53(pluginConfig);
|
|
1853
|
+
const unsupportedCodexFallbackChain = getUnsupportedCodexFallbackChain(pluginConfig);
|
|
1854
|
+
const toastDurationMs = getToastDurationMs(pluginConfig);
|
|
1855
|
+
const accountToastsEnabled = getAccountToastsEnabled(pluginConfig);
|
|
1856
|
+
const fetchTimeoutMs = getFetchTimeoutMs(pluginConfig);
|
|
1857
|
+
const streamStallTimeoutMs = getStreamStallTimeoutMs(pluginConfig);
|
|
1858
|
+
const emptyResponseMaxRetries = getEmptyResponseMaxRetries(pluginConfig);
|
|
1859
|
+
const emptyResponseRetryDelayMs = getEmptyResponseRetryDelayMs(pluginConfig);
|
|
1860
|
+
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
|
|
1861
|
+
const rotationStrategy = getRotationStrategy(pluginConfig);
|
|
1862
|
+
const effectiveUserConfig = fastSessionEnabled ? applyFastSessionDefaults(userConfig) : userConfig;
|
|
1863
|
+
let accountManager = cachedAccountManager;
|
|
1522
1864
|
// Step 1: Extract and rewrite URL for Codex backend
|
|
1523
1865
|
const originalUrl = extractRequestUrl(input);
|
|
1524
1866
|
const url = openAIBaseURL ? originalUrl : rewriteUrlForCodex(originalUrl);
|
|
@@ -1609,9 +1951,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1609
1951
|
runtimeMetrics.promptCacheMissingRequests++;
|
|
1610
1952
|
}
|
|
1611
1953
|
const retryBudget = new RetryBudgetTracker(retryBudgetLimits);
|
|
1612
|
-
const consumeRetryBudget = (bucket, reason) => {
|
|
1613
|
-
|
|
1614
|
-
|
|
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;
|
|
1615
1966
|
return true;
|
|
1616
1967
|
}
|
|
1617
1968
|
runtimeMetrics.retryBudgetExhaustions += 1;
|
|
@@ -1652,25 +2003,69 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1652
2003
|
};
|
|
1653
2004
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1654
2005
|
});
|
|
1655
|
-
const sleepWithCountdown = async (totalMs, message, intervalMs = 5000) => {
|
|
2006
|
+
const sleepWithCountdown = async (totalMs, message, intervalMs = 5000, probeUpstream) => {
|
|
1656
2007
|
const startTime = Date.now();
|
|
1657
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;
|
|
1658
2013
|
while (Date.now() < endTime) {
|
|
2014
|
+
if (cachedAccountManager !== accountManager)
|
|
2015
|
+
return;
|
|
1659
2016
|
if (abortSignal?.aborted) {
|
|
1660
2017
|
throw abortError();
|
|
1661
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
|
+
}
|
|
1662
2033
|
const remaining = Math.max(0, endTime - Date.now());
|
|
1663
2034
|
const waitLabel = formatWaitTime(remaining);
|
|
1664
2035
|
await showToast(`${message} (${waitLabel} remaining)`, "warning", { duration: Math.min(intervalMs + 1000, toastDurationMs) });
|
|
1665
|
-
const sleepTime = Math.min(intervalMs, remaining);
|
|
2036
|
+
const sleepTime = Math.min(intervalMs, remaining, nextProbeAt - Date.now());
|
|
1666
2037
|
if (sleepTime > 0) {
|
|
1667
2038
|
await sleep(sleepTime);
|
|
1668
2039
|
}
|
|
1669
2040
|
else {
|
|
1670
|
-
|
|
2041
|
+
continue;
|
|
1671
2042
|
}
|
|
1672
2043
|
}
|
|
1673
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
|
+
};
|
|
1674
2069
|
let allRateLimitedRetries = 0;
|
|
1675
2070
|
let emptyResponseRetries = 0;
|
|
1676
2071
|
const attemptedUnsupportedFallbackModels = new Set();
|
|
@@ -1758,6 +2153,58 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1758
2153
|
const MAX_QUOTA_FALLBACK_SWITCHES = 3;
|
|
1759
2154
|
let quotaFallbackSwitches = 0;
|
|
1760
2155
|
while (true) {
|
|
2156
|
+
if (cachedAccountManager && cachedAccountManager !== accountManager) {
|
|
2157
|
+
accountManager = cachedAccountManager;
|
|
2158
|
+
}
|
|
2159
|
+
else if (!cachedAccountManager) {
|
|
2160
|
+
// Reload through the shared accountManagerPromise so
|
|
2161
|
+
// concurrent requests (and the startup path above)
|
|
2162
|
+
// share one load. Racing independent loadFromDisk()
|
|
2163
|
+
// calls here can orphan the slower manager while an
|
|
2164
|
+
// in-flight request still holds it, and an orphaned
|
|
2165
|
+
// manager is never retired, so its next save would
|
|
2166
|
+
// write full membership and drop externally imported
|
|
2167
|
+
// accounts.
|
|
2168
|
+
if (accountManagerPromise === null) {
|
|
2169
|
+
const pending = AccountManager.loadFromDisk();
|
|
2170
|
+
accountManagerPromise = pending;
|
|
2171
|
+
void pending.catch(() => {
|
|
2172
|
+
if (accountManagerPromise === pending)
|
|
2173
|
+
accountManagerPromise = null;
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
const loading = accountManagerPromise;
|
|
2177
|
+
const reloaded = await loading;
|
|
2178
|
+
if (cachedAccountManager) {
|
|
2179
|
+
if (cachedAccountManager !== reloaded) {
|
|
2180
|
+
// The cache was repopulated while this load was in
|
|
2181
|
+
// flight (external reload after an invalidation).
|
|
2182
|
+
// The loaded manager is stale; retire it so its
|
|
2183
|
+
// debounced save cannot write full membership over
|
|
2184
|
+
// accounts imported after its snapshot was taken.
|
|
2185
|
+
reloaded.disposeShutdownHandler();
|
|
2186
|
+
}
|
|
2187
|
+
accountManager = cachedAccountManager;
|
|
2188
|
+
}
|
|
2189
|
+
else if (accountManager && isUntrustworthyEmptyReload(accountManager, reloaded)) {
|
|
2190
|
+
// The incumbent this request holds still has accounts, so
|
|
2191
|
+
// the empty result is a failed `loadAccounts()` read rather
|
|
2192
|
+
// than a real removal (same refusal as
|
|
2193
|
+
// reloadCachedAccountManager). Keep serving the incumbent
|
|
2194
|
+
// and drop the resolved promise so the next pass re-reads
|
|
2195
|
+
// disk instead of adopting this same empty manager.
|
|
2196
|
+
reloaded.disposeShutdownHandler();
|
|
2197
|
+
if (accountManagerPromise === loading)
|
|
2198
|
+
accountManagerPromise = null;
|
|
2199
|
+
logWarn(`[${PLUGIN_NAME}] Account reload returned no accounts while ${accountManager.getAccountCount()} are held; keeping the loaded pool and retrying`);
|
|
2200
|
+
const incumbent = accountManager;
|
|
2201
|
+
scheduleEmptyReloadRetry(() => repopulateAccountManagerCache(incumbent));
|
|
2202
|
+
}
|
|
2203
|
+
else {
|
|
2204
|
+
cachedAccountManager = reloaded;
|
|
2205
|
+
accountManager = reloaded;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
1761
2208
|
let accountCount = accountManager.getAccountCount();
|
|
1762
2209
|
const attempted = new Set();
|
|
1763
2210
|
// Diagnostics for the terminal error message below. The composite key keeps
|
|
@@ -1874,6 +2321,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1874
2321
|
const failures = await accountManager.incrementAuthFailures(account);
|
|
1875
2322
|
const accountLabel = formatAccountLabel(account, account.index, {
|
|
1876
2323
|
maskEmail: maskEmailEnabled,
|
|
2324
|
+
peerAccounts: accountManager.getAccountsSnapshot(),
|
|
1877
2325
|
});
|
|
1878
2326
|
if (failures >= ACCOUNT_LIMITS.MAX_AUTH_FAILURES_BEFORE_REMOVAL) {
|
|
1879
2327
|
const removedCount = accountManager.removeAccountsWithSameRefreshToken(account);
|
|
@@ -1923,6 +2371,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1923
2371
|
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
1924
2372
|
const accountLabel = formatAccountLabel(account, account.index, {
|
|
1925
2373
|
maskEmail: maskEmailEnabled,
|
|
2374
|
+
peerAccounts: accountManager.getAccountsSnapshot(),
|
|
1926
2375
|
});
|
|
1927
2376
|
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
1928
2377
|
accountManager.markToastShown(account.index);
|
|
@@ -2110,7 +2559,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2110
2559
|
// only the block would leave the status line reporting "0% left" for
|
|
2111
2560
|
// an account the router considers healthy.
|
|
2112
2561
|
const recordQuotaHeaders = () => {
|
|
2113
|
-
void recordPromptQuotaHeaders(response, account, accountCount);
|
|
2562
|
+
void recordPromptQuotaHeaders(response, account, accountCount, accountManager.getAccountsSnapshot());
|
|
2114
2563
|
return applyQuotaExhaustion(accountManager, response.headers, account, modelFamily, model);
|
|
2115
2564
|
};
|
|
2116
2565
|
if (response.ok) {
|
|
@@ -2130,6 +2579,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2130
2579
|
if (workspaceDeactivated) {
|
|
2131
2580
|
const accountLabel = formatAccountLabel(account, account.index, {
|
|
2132
2581
|
maskEmail: maskEmailEnabled,
|
|
2582
|
+
peerAccounts: accountManager.getAccountsSnapshot(),
|
|
2133
2583
|
});
|
|
2134
2584
|
accountManager.refundToken(account, modelFamily, model);
|
|
2135
2585
|
accountManager.recordFailure(account, modelFamily, model);
|
|
@@ -2343,6 +2793,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2343
2793
|
if (isInvalidatedAuthTokenError(errorBody, response.status)) {
|
|
2344
2794
|
const accountLabel = formatAccountLabel(account, account.index, {
|
|
2345
2795
|
maskEmail: maskEmailEnabled,
|
|
2796
|
+
peerAccounts: accountManager.getAccountsSnapshot(),
|
|
2346
2797
|
});
|
|
2347
2798
|
accountManager.refundToken(account, modelFamily, model);
|
|
2348
2799
|
accountManager.recordFailure(account, modelFamily, model);
|
|
@@ -2614,9 +3065,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2614
3065
|
(retryAllAccountsMaxWaitMs === 0 ||
|
|
2615
3066
|
waitMs <= retryAllAccountsMaxWaitMs) &&
|
|
2616
3067
|
allRateLimitedRetries < retryAllAccountsMaxRetries &&
|
|
2617
|
-
consumeRetryBudget("rateLimitGlobal", `All accounts rate-limited wait ${waitMs}ms
|
|
3068
|
+
consumeRetryBudget("rateLimitGlobal", `All accounts rate-limited wait ${waitMs}ms`, waitMs)) {
|
|
2618
3069
|
const countdownMessage = `All ${count} account(s) rate-limited. Waiting`;
|
|
2619
|
-
await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage);
|
|
3070
|
+
await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage, undefined, probeUpstreamBlockLifted);
|
|
2620
3071
|
allRateLimitedRetries++;
|
|
2621
3072
|
continue;
|
|
2622
3073
|
}
|
|
@@ -2657,6 +3108,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2657
3108
|
}
|
|
2658
3109
|
}
|
|
2659
3110
|
finally {
|
|
3111
|
+
if (pendingFetch)
|
|
3112
|
+
activeFetches.delete(pendingFetch);
|
|
3113
|
+
finishFetch?.();
|
|
2660
3114
|
clearCorrelationId();
|
|
2661
3115
|
}
|
|
2662
3116
|
},
|
|
@@ -2716,19 +3170,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2716
3170
|
message.includes("invalid refresh") ||
|
|
2717
3171
|
message.includes("token has been revoked"));
|
|
2718
3172
|
};
|
|
3173
|
+
const authQuotaDisplay = getQuotaDisplay(authPluginConfig);
|
|
2719
3174
|
const formatCodexQuotaLine = (usage) => {
|
|
2720
3175
|
const parts = [];
|
|
2721
3176
|
for (const window of [usage.primary, usage.secondary]) {
|
|
2722
3177
|
if (!hasUsageWindow(window))
|
|
2723
3178
|
continue;
|
|
2724
|
-
parts.push(`${formatUsageLimitTitle(window.windowMinutes)} ${formatUsageLimitSummary(window)}`);
|
|
3179
|
+
parts.push(`${formatUsageLimitTitle(window.windowMinutes)} ${formatUsageLimitSummary(window, authQuotaDisplay)}`);
|
|
2725
3180
|
}
|
|
2726
3181
|
if (hasUsageWindow(usage.codeReview)) {
|
|
2727
|
-
parts.push(`Code review ${formatUsageLimitSummary(usage.codeReview)}`);
|
|
3182
|
+
parts.push(`Code review ${formatUsageLimitSummary(usage.codeReview, authQuotaDisplay)}`);
|
|
2728
3183
|
}
|
|
2729
3184
|
for (const limit of usage.additionalLimits) {
|
|
2730
3185
|
if (hasUsageWindow(limit.window)) {
|
|
2731
|
-
parts.push(`${limit.name} ${formatUsageLimitSummary(limit.window)}`);
|
|
3186
|
+
parts.push(`${limit.name} ${formatUsageLimitSummary(limit.window, authQuotaDisplay)}`);
|
|
2732
3187
|
}
|
|
2733
3188
|
}
|
|
2734
3189
|
const planLabel = formatPlanType(usage.planType);
|
|
@@ -2900,8 +3355,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2900
3355
|
}
|
|
2901
3356
|
if (deepProbe) {
|
|
2902
3357
|
ok += 1;
|
|
2903
|
-
|
|
2904
|
-
|
|
3358
|
+
// Both read from the probed token, so the pair is the seat
|
|
3359
|
+
// the credential actually belongs to. The workspace id
|
|
3360
|
+
// alone repeats across every member of a Business
|
|
3361
|
+
// workspace and cannot confirm which seat answered.
|
|
3362
|
+
const tokenSeat = formatSeatSuffix(extractAccountUserId(accessToken), workingStorage.accounts.map((peer) => peer?.accountUserId));
|
|
3363
|
+
const identity = [
|
|
3364
|
+
tokenAccountId ? `id:${tokenAccountId.slice(-6)}` : undefined,
|
|
3365
|
+
tokenSeat ? `seat:${tokenSeat}` : undefined,
|
|
3366
|
+
].filter((part) => part !== undefined);
|
|
3367
|
+
const detail = identity.length > 0
|
|
3368
|
+
? `${authDetail} (${identity.join(", ")})`
|
|
2905
3369
|
: authDetail;
|
|
2906
3370
|
console.log(`[${i + 1}/${total}] ${label}: ${detail}`);
|
|
2907
3371
|
continue;
|
|
@@ -2919,7 +3383,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2919
3383
|
organizationId: account.organizationId,
|
|
2920
3384
|
normalizeAccountErrors: true,
|
|
2921
3385
|
});
|
|
2922
|
-
const usage = parseCodexUsagePayload(payload);
|
|
3386
|
+
const usage = parseCodexUsagePayload(payload, authQuotaDisplay);
|
|
2923
3387
|
ok += 1;
|
|
2924
3388
|
console.log(`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(usage)}`);
|
|
2925
3389
|
}
|
|
@@ -3195,6 +3659,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
3195
3659
|
}
|
|
3196
3660
|
return {
|
|
3197
3661
|
accountId: account.accountId,
|
|
3662
|
+
accountUserId: account.accountUserId,
|
|
3198
3663
|
accountLabel: account.accountLabel,
|
|
3199
3664
|
email: account.email,
|
|
3200
3665
|
index,
|