oc-chatgpt-multi-auth 4.12.1 → 4.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +414 -303
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts/rate-limits.d.ts +22 -0
- package/dist/lib/accounts/rate-limits.d.ts.map +1 -0
- package/dist/lib/accounts/rate-limits.js +63 -0
- package/dist/lib/accounts/rate-limits.js.map +1 -0
- package/dist/lib/accounts.d.ts +10 -66
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +39 -340
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/audit.js +1 -1
- package/dist/lib/audit.js.map +1 -1
- package/dist/lib/auth/browser.d.ts.map +1 -1
- package/dist/lib/auth/browser.js +9 -2
- package/dist/lib/auth/browser.js.map +1 -1
- package/dist/lib/auth/server.d.ts.map +1 -1
- package/dist/lib/auth/server.js +13 -2
- package/dist/lib/auth/server.js.map +1 -1
- package/dist/lib/auth/token-utils.d.ts +42 -0
- package/dist/lib/auth/token-utils.d.ts.map +1 -0
- package/dist/lib/auth/token-utils.js +288 -0
- package/dist/lib/auth/token-utils.js.map +1 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -1
- package/dist/lib/circuit-breaker.js +7 -0
- package/dist/lib/circuit-breaker.js.map +1 -1
- package/dist/lib/cli.d.ts +6 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +27 -1
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +9 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/errors.d.ts +113 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +103 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/health.d.ts +33 -0
- package/dist/lib/health.d.ts.map +1 -0
- package/dist/lib/health.js +70 -0
- package/dist/lib/health.js.map +1 -0
- package/dist/lib/index.d.ts +5 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +5 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +10 -3
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/parallel-probe.d.ts +28 -0
- package/dist/lib/parallel-probe.d.ts.map +1 -0
- package/dist/lib/parallel-probe.js +97 -0
- package/dist/lib/parallel-probe.js.map +1 -0
- package/dist/lib/prompts/codex.d.ts +5 -0
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +22 -5
- package/dist/lib/prompts/codex.js.map +1 -1
- package/dist/lib/recovery/storage.d.ts.map +1 -1
- package/dist/lib/recovery/storage.js +11 -3
- package/dist/lib/recovery/storage.js.map +1 -1
- package/dist/lib/recovery.js +4 -4
- package/dist/lib/recovery.js.map +1 -1
- package/dist/lib/refresh-queue.d.ts.map +1 -1
- package/dist/lib/refresh-queue.js +3 -1
- package/dist/lib/refresh-queue.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +2 -1
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +23 -9
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/helpers/input-utils.d.ts.map +1 -1
- package/dist/lib/request/helpers/input-utils.js +1 -0
- package/dist/lib/request/helpers/input-utils.js.map +1 -1
- package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
- package/dist/lib/request/helpers/model-map.js +1 -3
- package/dist/lib/request/helpers/model-map.js.map +1 -1
- package/dist/lib/request/helpers/tool-utils.js +5 -1
- package/dist/lib/request/helpers/tool-utils.js.map +1 -1
- package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -1
- package/dist/lib/request/rate-limit-backoff.js +9 -0
- package/dist/lib/request/rate-limit-backoff.js.map +1 -1
- package/dist/lib/request/request-transformer.d.ts.map +1 -1
- package/dist/lib/request/request-transformer.js +2 -1
- package/dist/lib/request/request-transformer.js.map +1 -1
- package/dist/lib/request/response-handler.d.ts.map +1 -1
- package/dist/lib/request/response-handler.js +15 -4
- package/dist/lib/request/response-handler.js.map +1 -1
- package/dist/lib/rotation.d.ts +6 -0
- package/dist/lib/rotation.d.ts.map +1 -1
- package/dist/lib/rotation.js +48 -5
- package/dist/lib/rotation.js.map +1 -1
- package/dist/lib/schemas.d.ts +2 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +2 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/shutdown.d.ts +7 -0
- package/dist/lib/shutdown.d.ts.map +1 -0
- package/dist/lib/shutdown.js +43 -0
- package/dist/lib/shutdown.js.map +1 -0
- package/dist/lib/storage/migrations.d.ts +49 -0
- package/dist/lib/storage/migrations.d.ts.map +1 -0
- package/dist/lib/storage/migrations.js +44 -0
- package/dist/lib/storage/migrations.js.map +1 -0
- package/dist/lib/storage/paths.d.ts +10 -0
- package/dist/lib/storage/paths.d.ts.map +1 -0
- package/dist/lib/storage/paths.js +49 -0
- package/dist/lib/storage/paths.js.map +1 -0
- package/dist/lib/storage.d.ts +4 -44
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +32 -85
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/table-formatter.d.ts +32 -0
- package/dist/lib/table-formatter.d.ts.map +1 -0
- package/dist/lib/table-formatter.js +44 -0
- package/dist/lib/table-formatter.js.map +1 -0
- package/dist/lib/utils.d.ts +29 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +54 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -27,17 +27,18 @@ import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationI
|
|
|
27
27
|
import { queuedRefresh } from "./lib/refresh-queue.js";
|
|
28
28
|
import { openBrowserUrl } from "./lib/auth/browser.js";
|
|
29
29
|
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
|
30
|
-
import {
|
|
30
|
+
import { promptLoginMode } from "./lib/cli.js";
|
|
31
31
|
import { getCodexMode, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getToastDurationMs, getPerProjectAccounts, loadPluginConfig, } from "./lib/config.js";
|
|
32
32
|
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
|
|
33
33
|
import { initLogger, logRequest, logDebug, logInfo, logWarn, logError } from "./lib/logger.js";
|
|
34
34
|
import { checkAndNotify } from "./lib/auto-update-checker.js";
|
|
35
35
|
import { handleContextOverflow } from "./lib/context-overflow.js";
|
|
36
|
-
import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, shouldUpdateAccountIdFromToken, } from "./lib/accounts.js";
|
|
36
|
+
import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, shouldUpdateAccountIdFromToken, parseRateLimitReason, } from "./lib/accounts.js";
|
|
37
37
|
import { getStoragePath, loadAccounts, saveAccounts, setStoragePath, exportAccounts, importAccounts, StorageError, formatStorageErrorHint } from "./lib/storage.js";
|
|
38
38
|
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
39
39
|
import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
|
|
40
40
|
import { addJitter } from "./lib/rotation.js";
|
|
41
|
+
import { buildTableHeader, buildTableRow } from "./lib/table-formatter.js";
|
|
41
42
|
import { getModelFamily, MODEL_FAMILIES } from "./lib/prompts/codex.js";
|
|
42
43
|
import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getRecoveryToastContent, } from "./lib/recovery.js";
|
|
43
44
|
/**
|
|
@@ -56,10 +57,14 @@ import { createSessionRecoveryHook, isRecoverableError, detectErrorType, getReco
|
|
|
56
57
|
* }
|
|
57
58
|
* ```
|
|
58
59
|
*/
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
59
61
|
export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
60
62
|
initLogger(client);
|
|
61
63
|
let cachedAccountManager = null;
|
|
62
|
-
|
|
64
|
+
let accountManagerPromise = null;
|
|
65
|
+
let loaderMutex = null;
|
|
66
|
+
const MIN_BACKOFF_MS = 100;
|
|
67
|
+
const resolveAccountSelection = (tokens) => {
|
|
63
68
|
const override = (process.env.CODEX_AUTH_ACCOUNT_ID ?? "").trim();
|
|
64
69
|
if (override) {
|
|
65
70
|
const suffix = override.length > 6 ? override.slice(-6) : override;
|
|
@@ -76,28 +81,31 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
76
81
|
return tokens;
|
|
77
82
|
}
|
|
78
83
|
if (candidates.length === 1) {
|
|
79
|
-
const candidate = candidates
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
const [candidate] = candidates;
|
|
85
|
+
if (candidate) {
|
|
86
|
+
return {
|
|
87
|
+
...tokens,
|
|
88
|
+
accountIdOverride: candidate.accountId,
|
|
89
|
+
accountIdSource: candidate.source,
|
|
90
|
+
accountLabel: candidate.label,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
86
93
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
// Auto-select the default candidate without prompting
|
|
95
|
+
// The user already selected their workspace on OpenAI's auth page
|
|
96
|
+
// Prompting again breaks TUI mode and is redundant
|
|
97
|
+
const choice = (() => {
|
|
98
|
+
// Prefer org-level default
|
|
99
|
+
const orgDefault = candidates.find((candidate) => candidate.isDefault && candidate.source === "org");
|
|
100
|
+
if (orgDefault)
|
|
101
|
+
return orgDefault;
|
|
102
|
+
// Fall back to token-derived account
|
|
103
|
+
const tokenAccount = candidates.find((candidate) => candidate.source === "token");
|
|
104
|
+
if (tokenAccount)
|
|
105
|
+
return tokenAccount;
|
|
106
|
+
// Otherwise first candidate
|
|
107
|
+
return candidates[0];
|
|
95
108
|
})();
|
|
96
|
-
const selected = await promptAccountSelection(candidates, {
|
|
97
|
-
defaultIndex,
|
|
98
|
-
title: "Multiple workspaces detected for this account:",
|
|
99
|
-
});
|
|
100
|
-
const choice = selected ?? candidates[defaultIndex];
|
|
101
109
|
if (!choice)
|
|
102
110
|
return tokens;
|
|
103
111
|
return {
|
|
@@ -111,6 +119,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
111
119
|
url,
|
|
112
120
|
method: "code",
|
|
113
121
|
instructions: AUTH_LABELS.INSTRUCTIONS_MANUAL,
|
|
122
|
+
validate: (input) => {
|
|
123
|
+
const parsed = parseAuthorizationInput(input);
|
|
124
|
+
if (!parsed.code) {
|
|
125
|
+
return "No authorization code found. Paste the full callback URL (e.g., http://localhost:1455/auth/callback?code=...)";
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
},
|
|
114
129
|
callback: async (input) => {
|
|
115
130
|
const parsed = parseAuthorizationInput(input);
|
|
116
131
|
if (!parsed.code) {
|
|
@@ -118,7 +133,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
118
133
|
}
|
|
119
134
|
const tokens = await exchangeAuthorizationCode(parsed.code, pkce.verifier, REDIRECT_URI);
|
|
120
135
|
if (tokens?.type === "success") {
|
|
121
|
-
const resolved =
|
|
136
|
+
const resolved = resolveAccountSelection(tokens);
|
|
122
137
|
if (onSuccess) {
|
|
123
138
|
await onSuccess(resolved);
|
|
124
139
|
}
|
|
@@ -302,7 +317,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
302
317
|
process.env.OPENCODE_SKIP_EMAIL_HYDRATE === "1";
|
|
303
318
|
if (skipHydrate)
|
|
304
319
|
return storage;
|
|
305
|
-
const
|
|
320
|
+
const accountsCopy = storage.accounts.map((account) => account ? { ...account } : account);
|
|
321
|
+
const accountsToHydrate = accountsCopy.filter((account) => account && !account.email);
|
|
306
322
|
if (accountsToHydrate.length === 0)
|
|
307
323
|
return storage;
|
|
308
324
|
let changed = false;
|
|
@@ -330,10 +346,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
330
346
|
}
|
|
331
347
|
}
|
|
332
348
|
catch {
|
|
333
|
-
|
|
349
|
+
logWarn(`[${PLUGIN_NAME}] Failed to hydrate email for account`);
|
|
334
350
|
}
|
|
335
351
|
}));
|
|
336
352
|
if (changed) {
|
|
353
|
+
storage.accounts = accountsCopy;
|
|
337
354
|
await saveAccounts(storage);
|
|
338
355
|
}
|
|
339
356
|
return storage;
|
|
@@ -368,29 +385,34 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
368
385
|
};
|
|
369
386
|
// Event handler for session recovery and account selection
|
|
370
387
|
const eventHandler = async (input) => {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
event.type === "
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
388
|
+
try {
|
|
389
|
+
const { event } = input;
|
|
390
|
+
// Handle TUI account selection events
|
|
391
|
+
// Accepts generic selection events with an index property
|
|
392
|
+
if (event.type === "account.select" ||
|
|
393
|
+
event.type === "openai.account.select") {
|
|
394
|
+
const props = event.properties;
|
|
395
|
+
// Filter by provider if specified
|
|
396
|
+
if (props.provider && props.provider !== "openai" && props.provider !== PROVIDER_ID) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const index = props.index ?? props.accountIndex;
|
|
400
|
+
if (typeof index === "number" && cachedAccountManager) {
|
|
401
|
+
// Convert 1-based index (UI) to 0-based index (internal) if needed,
|
|
402
|
+
// or handle 0-based directly. Usually UI lists are 0-based in code but 1-based in display.
|
|
403
|
+
// AccountManager.setActiveIndex expects 0-based index.
|
|
404
|
+
// Assuming the event passes the raw index from the list.
|
|
405
|
+
const account = cachedAccountManager.setActiveIndex(index);
|
|
406
|
+
if (account) {
|
|
407
|
+
await cachedAccountManager.saveToDisk();
|
|
408
|
+
await showToast(`Switched to account ${index + 1}`, "info");
|
|
409
|
+
}
|
|
391
410
|
}
|
|
392
411
|
}
|
|
393
412
|
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
logDebug(`[${PLUGIN_NAME}] Event handler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
415
|
+
}
|
|
394
416
|
};
|
|
395
417
|
return {
|
|
396
418
|
event: eventHandler,
|
|
@@ -423,270 +445,332 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
423
445
|
logDebug(`[${PLUGIN_NAME}] Auth is not multi-account, skipping loader`);
|
|
424
446
|
return {};
|
|
425
447
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
!accountManager.hasRefreshToken(refreshToken);
|
|
431
|
-
if (needsPersist) {
|
|
432
|
-
await accountManager.saveToDisk();
|
|
448
|
+
// Acquire mutex for thread-safe initialization
|
|
449
|
+
// Use while loop to handle multiple concurrent waiters correctly
|
|
450
|
+
while (loaderMutex) {
|
|
451
|
+
await loaderMutex;
|
|
433
452
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const cleanup = () => {
|
|
516
|
-
clearTimeout(timeout);
|
|
517
|
-
abortSignal?.removeEventListener("abort", onAbort);
|
|
518
|
-
};
|
|
519
|
-
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
520
|
-
});
|
|
521
|
-
const sleepWithCountdown = async (totalMs, message, intervalMs = 5000) => {
|
|
522
|
-
const startTime = Date.now();
|
|
523
|
-
const endTime = startTime + totalMs;
|
|
524
|
-
while (Date.now() < endTime) {
|
|
525
|
-
if (abortSignal?.aborted) {
|
|
526
|
-
throw new Error("Aborted");
|
|
527
|
-
}
|
|
528
|
-
const remaining = Math.max(0, endTime - Date.now());
|
|
529
|
-
const waitLabel = formatWaitTime(remaining);
|
|
530
|
-
await showToast(`${message} (${waitLabel} remaining)`, "warning", { duration: Math.min(intervalMs + 1000, toastDurationMs) });
|
|
531
|
-
const sleepTime = Math.min(intervalMs, remaining);
|
|
532
|
-
if (sleepTime > 0) {
|
|
533
|
-
await sleep(sleepTime);
|
|
453
|
+
let resolveMutex;
|
|
454
|
+
loaderMutex = new Promise((resolve) => {
|
|
455
|
+
resolveMutex = resolve;
|
|
456
|
+
});
|
|
457
|
+
try {
|
|
458
|
+
if (!accountManagerPromise) {
|
|
459
|
+
accountManagerPromise = AccountManager.loadFromDisk(auth);
|
|
460
|
+
}
|
|
461
|
+
const accountManager = await accountManagerPromise;
|
|
462
|
+
cachedAccountManager = accountManager;
|
|
463
|
+
const refreshToken = auth.type === "oauth" ? auth.refresh : "";
|
|
464
|
+
const needsPersist = refreshToken &&
|
|
465
|
+
!accountManager.hasRefreshToken(refreshToken);
|
|
466
|
+
if (needsPersist) {
|
|
467
|
+
await accountManager.saveToDisk();
|
|
468
|
+
}
|
|
469
|
+
if (accountManager.getAccountCount() === 0) {
|
|
470
|
+
logDebug(`[${PLUGIN_NAME}] No OAuth accounts available (run opencode auth login)`);
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
// Extract user configuration (global + per-model options)
|
|
474
|
+
const providerConfig = provider;
|
|
475
|
+
const userConfig = {
|
|
476
|
+
global: providerConfig?.options || {},
|
|
477
|
+
models: providerConfig?.models || {},
|
|
478
|
+
};
|
|
479
|
+
// Load plugin configuration and determine CODEX_MODE
|
|
480
|
+
// Priority: CODEX_MODE env var > config file > default (true)
|
|
481
|
+
const pluginConfig = loadPluginConfig();
|
|
482
|
+
const codexMode = getCodexMode(pluginConfig);
|
|
483
|
+
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
484
|
+
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
485
|
+
const retryAllAccountsRateLimited = getRetryAllAccountsRateLimited(pluginConfig);
|
|
486
|
+
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
487
|
+
const retryAllAccountsMaxRetries = getRetryAllAccountsMaxRetries(pluginConfig);
|
|
488
|
+
const toastDurationMs = getToastDurationMs(pluginConfig);
|
|
489
|
+
const perProjectAccounts = getPerProjectAccounts(pluginConfig);
|
|
490
|
+
if (perProjectAccounts) {
|
|
491
|
+
setStoragePath(process.cwd());
|
|
492
|
+
}
|
|
493
|
+
const sessionRecoveryEnabled = getSessionRecovery(pluginConfig);
|
|
494
|
+
const autoResumeEnabled = getAutoResume(pluginConfig);
|
|
495
|
+
const recoveryHook = sessionRecoveryEnabled
|
|
496
|
+
? createSessionRecoveryHook({ client, directory: process.cwd() }, { sessionRecovery: true, autoResume: autoResumeEnabled })
|
|
497
|
+
: null;
|
|
498
|
+
checkAndNotify(async (message, variant) => {
|
|
499
|
+
await showToast(message, variant);
|
|
500
|
+
}).catch((err) => {
|
|
501
|
+
logDebug(`Update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
502
|
+
});
|
|
503
|
+
// Return SDK configuration
|
|
504
|
+
return {
|
|
505
|
+
apiKey: DUMMY_API_KEY,
|
|
506
|
+
baseURL: CODEX_BASE_URL,
|
|
507
|
+
/**
|
|
508
|
+
* Custom fetch implementation for Codex API
|
|
509
|
+
*
|
|
510
|
+
* Handles:
|
|
511
|
+
* - Token refresh when expired
|
|
512
|
+
* - URL rewriting for Codex backend
|
|
513
|
+
* - Request body transformation
|
|
514
|
+
* - OAuth header injection
|
|
515
|
+
* - SSE to JSON conversion for non-tool requests
|
|
516
|
+
* - Error handling and logging
|
|
517
|
+
*
|
|
518
|
+
* @param input - Request URL or Request object
|
|
519
|
+
* @param init - Request options
|
|
520
|
+
* @returns Response from Codex API
|
|
521
|
+
*/
|
|
522
|
+
async fetch(input, init) {
|
|
523
|
+
// Step 1: Extract and rewrite URL for Codex backend
|
|
524
|
+
const originalUrl = extractRequestUrl(input);
|
|
525
|
+
const url = rewriteUrlForCodex(originalUrl);
|
|
526
|
+
// Step 3: Transform request body with model-specific Codex instructions
|
|
527
|
+
// Instructions are fetched per model family (codex-max, codex, gpt-5.1)
|
|
528
|
+
// Capture original stream value before transformation
|
|
529
|
+
// generateText() sends no stream field, streamText() sends stream=true
|
|
530
|
+
let originalBody = {};
|
|
531
|
+
if (init?.body) {
|
|
532
|
+
try {
|
|
533
|
+
originalBody = JSON.parse(init.body);
|
|
534
534
|
}
|
|
535
|
-
|
|
536
|
-
|
|
535
|
+
catch {
|
|
536
|
+
logWarn("Failed to parse request body, using empty object");
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
539
|
+
const isStreaming = originalBody.stream === true;
|
|
540
|
+
const transformation = await transformRequestForCodex(init, url, userConfig, codexMode, originalBody);
|
|
541
|
+
const requestInit = transformation?.updatedInit ?? init;
|
|
542
|
+
const promptCacheKey = transformation?.body?.prompt_cache_key;
|
|
543
|
+
const model = transformation?.body.model;
|
|
544
|
+
const modelFamily = model ? getModelFamily(model) : "gpt-5.1";
|
|
545
|
+
const quotaKey = model ? `${modelFamily}:${model}` : modelFamily;
|
|
546
|
+
const abortSignal = requestInit?.signal ?? init?.signal ?? null;
|
|
547
|
+
const sleep = (ms) => new Promise((resolve, reject) => {
|
|
548
|
+
if (abortSignal?.aborted) {
|
|
549
|
+
reject(new Error("Aborted"));
|
|
550
|
+
return;
|
|
548
551
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
552
|
+
const timeout = setTimeout(() => {
|
|
553
|
+
cleanup();
|
|
554
|
+
resolve();
|
|
555
|
+
}, ms);
|
|
556
|
+
const onAbort = () => {
|
|
557
|
+
cleanup();
|
|
558
|
+
reject(new Error("Aborted"));
|
|
559
|
+
};
|
|
560
|
+
const cleanup = () => {
|
|
561
|
+
clearTimeout(timeout);
|
|
562
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
563
|
+
};
|
|
564
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
565
|
+
});
|
|
566
|
+
const sleepWithCountdown = async (totalMs, message, intervalMs = 5000) => {
|
|
567
|
+
const startTime = Date.now();
|
|
568
|
+
const endTime = startTime + totalMs;
|
|
569
|
+
while (Date.now() < endTime) {
|
|
570
|
+
if (abortSignal?.aborted) {
|
|
571
|
+
throw new Error("Aborted");
|
|
572
|
+
}
|
|
573
|
+
const remaining = Math.max(0, endTime - Date.now());
|
|
574
|
+
const waitLabel = formatWaitTime(remaining);
|
|
575
|
+
await showToast(`${message} (${waitLabel} remaining)`, "warning", { duration: Math.min(intervalMs + 1000, toastDurationMs) });
|
|
576
|
+
const sleepTime = Math.min(intervalMs, remaining);
|
|
577
|
+
if (sleepTime > 0) {
|
|
578
|
+
await sleep(sleepTime);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
break;
|
|
559
582
|
}
|
|
560
583
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
584
|
+
};
|
|
585
|
+
let allRateLimitedRetries = 0;
|
|
586
|
+
while (true) {
|
|
587
|
+
const accountCount = accountManager.getAccountCount();
|
|
588
|
+
const attempted = new Set();
|
|
589
|
+
while (attempted.size < Math.max(1, accountCount)) {
|
|
590
|
+
const account = accountManager.getCurrentOrNextForFamilyHybrid(modelFamily, model);
|
|
591
|
+
if (!account || attempted.has(account.index)) {
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
attempted.add(account.index);
|
|
595
|
+
// Log account selection for debugging rotation
|
|
596
|
+
logDebug(`Using account ${account.index + 1}/${accountCount}: ${account.email ?? "unknown"} for ${modelFamily}`);
|
|
597
|
+
let accountAuth = accountManager.toAuthDetails(account);
|
|
598
|
+
try {
|
|
599
|
+
if (shouldRefreshToken(accountAuth, tokenRefreshSkewMs)) {
|
|
600
|
+
accountAuth = (await refreshAndUpdateToken(accountAuth, client));
|
|
601
|
+
accountManager.updateFromAuth(account, accountAuth);
|
|
602
|
+
accountManager.clearAuthFailures(account);
|
|
603
|
+
accountManager.saveToDiskDebounced();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
logDebug(`[${PLUGIN_NAME}] Auth refresh failed for account: ${err?.message ?? String(err)}`);
|
|
608
|
+
const failures = accountManager.incrementAuthFailures(account);
|
|
609
|
+
const accountLabel = formatAccountLabel(account, account.index);
|
|
610
|
+
if (failures >= ACCOUNT_LIMITS.MAX_AUTH_FAILURES_BEFORE_REMOVAL) {
|
|
611
|
+
accountManager.removeAccount(account);
|
|
612
|
+
accountManager.saveToDiskDebounced();
|
|
613
|
+
await showToast(`Removed ${accountLabel} after ${failures} consecutive auth failures. Run 'opencode auth login' to re-add.`, "error", { duration: toastDurationMs * 2 });
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
567
617
|
accountManager.saveToDiskDebounced();
|
|
568
|
-
await showToast(`Removed ${accountLabel} after ${failures} consecutive auth failures. Run 'opencode auth login' to re-add.`, "error", { duration: toastDurationMs * 2 });
|
|
569
618
|
continue;
|
|
570
619
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (!accountId) {
|
|
579
|
-
accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
580
|
-
accountManager.saveToDiskDebounced();
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
account.accountId = accountId;
|
|
584
|
-
if (!hadAccountId) {
|
|
585
|
-
account.accountIdSource = account.accountIdSource ?? "token";
|
|
586
|
-
}
|
|
587
|
-
account.email =
|
|
588
|
-
extractAccountEmail(accountAuth.access) ?? account.email;
|
|
589
|
-
if (accountCount > 1 &&
|
|
590
|
-
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
591
|
-
const accountLabel = formatAccountLabel(account, account.index);
|
|
592
|
-
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
593
|
-
accountManager.markToastShown(account.index);
|
|
594
|
-
}
|
|
595
|
-
const headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
|
|
596
|
-
model,
|
|
597
|
-
promptCacheKey,
|
|
598
|
-
});
|
|
599
|
-
while (true) {
|
|
600
|
-
let response;
|
|
601
|
-
const fetchStart = performance.now();
|
|
602
|
-
try {
|
|
603
|
-
response = await fetch(url, {
|
|
604
|
-
...requestInit,
|
|
605
|
-
headers,
|
|
606
|
-
});
|
|
620
|
+
const hadAccountId = !!account.accountId;
|
|
621
|
+
// Prefer fresh token-derived ID over stored ID (fixes Business plan workspace issues)
|
|
622
|
+
const accountId = extractAccountId(accountAuth.access) ?? account.accountId;
|
|
623
|
+
if (!accountId) {
|
|
624
|
+
accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
625
|
+
accountManager.saveToDiskDebounced();
|
|
626
|
+
continue;
|
|
607
627
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
628
|
+
account.accountId = accountId;
|
|
629
|
+
if (!hadAccountId) {
|
|
630
|
+
account.accountIdSource = account.accountIdSource ?? "token";
|
|
631
|
+
}
|
|
632
|
+
account.email =
|
|
633
|
+
extractAccountEmail(accountAuth.access) ?? account.email;
|
|
634
|
+
if (accountCount > 1 &&
|
|
635
|
+
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
636
|
+
const accountLabel = formatAccountLabel(account, account.index);
|
|
637
|
+
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
638
|
+
accountManager.markToastShown(account.index);
|
|
613
639
|
}
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
ok: response.ok,
|
|
618
|
-
statusText: response.statusText,
|
|
619
|
-
latencyMs: fetchLatencyMs,
|
|
620
|
-
headers: Object.fromEntries(response.headers.entries()),
|
|
640
|
+
const headers = createCodexHeaders(requestInit, accountId, accountAuth.access, {
|
|
641
|
+
model,
|
|
642
|
+
promptCacheKey,
|
|
621
643
|
});
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
644
|
+
// Consume a token before making the request for proactive rate limiting
|
|
645
|
+
accountManager.consumeToken(account, modelFamily, model);
|
|
646
|
+
while (true) {
|
|
647
|
+
let response;
|
|
648
|
+
const fetchStart = performance.now();
|
|
649
|
+
// Merge user AbortSignal with timeout (Node 18 compatible - no AbortSignal.any)
|
|
650
|
+
const fetchController = new AbortController();
|
|
651
|
+
const fetchTimeoutMs = 60000;
|
|
652
|
+
const fetchTimeoutId = setTimeout(() => fetchController.abort(new Error("Request timeout")), fetchTimeoutMs);
|
|
653
|
+
const onUserAbort = abortSignal
|
|
654
|
+
? () => fetchController.abort(abortSignal.reason ?? new Error("Aborted by user"))
|
|
655
|
+
: null;
|
|
656
|
+
if (abortSignal?.aborted) {
|
|
657
|
+
clearTimeout(fetchTimeoutId);
|
|
658
|
+
fetchController.abort(abortSignal.reason ?? new Error("Aborted by user"));
|
|
659
|
+
}
|
|
660
|
+
else if (abortSignal && onUserAbort) {
|
|
661
|
+
abortSignal.addEventListener("abort", onUserAbort, { once: true });
|
|
626
662
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
663
|
+
try {
|
|
664
|
+
response = await fetch(url, {
|
|
665
|
+
...requestInit,
|
|
666
|
+
headers,
|
|
667
|
+
signal: fetchController.signal,
|
|
668
|
+
});
|
|
633
669
|
}
|
|
634
|
-
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
670
|
+
catch (networkError) {
|
|
671
|
+
const errorMsg = networkError instanceof Error ? networkError.message : String(networkError);
|
|
672
|
+
logWarn(`Network error for account ${account.index + 1}: ${errorMsg}`);
|
|
673
|
+
accountManager.refundToken(account, modelFamily, model);
|
|
674
|
+
accountManager.recordFailure(account, modelFamily, model);
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
clearTimeout(fetchTimeoutId);
|
|
679
|
+
if (abortSignal && onUserAbort) {
|
|
680
|
+
abortSignal.removeEventListener("abort", onUserAbort);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const fetchLatencyMs = Math.round(performance.now() - fetchStart);
|
|
684
|
+
logRequest(LOG_STAGES.RESPONSE, {
|
|
685
|
+
status: response.status,
|
|
686
|
+
ok: response.ok,
|
|
687
|
+
statusText: response.statusText,
|
|
688
|
+
latencyMs: fetchLatencyMs,
|
|
689
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
690
|
+
});
|
|
691
|
+
if (!response.ok) {
|
|
692
|
+
const contextOverflowResult = await handleContextOverflow(response, model);
|
|
693
|
+
if (contextOverflowResult.handled) {
|
|
694
|
+
return contextOverflowResult.response;
|
|
695
|
+
}
|
|
696
|
+
const { response: errorResponse, rateLimit, errorBody } = await handleErrorResponse(response);
|
|
697
|
+
if (recoveryHook && errorBody && isRecoverableError(errorBody)) {
|
|
698
|
+
const errorType = detectErrorType(errorBody);
|
|
699
|
+
const toastContent = getRecoveryToastContent(errorType);
|
|
700
|
+
await showToast(`${toastContent.title}: ${toastContent.message}`, "warning", { duration: toastDurationMs });
|
|
701
|
+
logDebug(`[${PLUGIN_NAME}] Recoverable error detected: ${errorType}`);
|
|
702
|
+
}
|
|
703
|
+
// Handle 5xx server errors by rotating to another account
|
|
704
|
+
if (response.status >= 500 && response.status < 600) {
|
|
705
|
+
logWarn(`Server error ${response.status} for account ${account.index + 1}. Rotating to next account.`);
|
|
706
|
+
accountManager.refundToken(account, modelFamily, model);
|
|
707
|
+
accountManager.recordFailure(account, modelFamily, model);
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
if (rateLimit) {
|
|
711
|
+
const { attempt, delayMs } = getRateLimitBackoff(account.index, quotaKey, rateLimit.retryAfterMs);
|
|
712
|
+
const waitLabel = formatWaitTime(delayMs);
|
|
713
|
+
if (delayMs <= RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS) {
|
|
714
|
+
if (accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
715
|
+
await showToast(`Rate limited. Retrying in ${waitLabel} (attempt ${attempt})...`, "warning", { duration: toastDurationMs });
|
|
716
|
+
accountManager.markToastShown(account.index);
|
|
717
|
+
}
|
|
718
|
+
await sleep(addJitter(Math.max(MIN_BACKOFF_MS, delayMs), 0.2));
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
accountManager.markRateLimitedWithReason(account, delayMs, modelFamily, parseRateLimitReason(rateLimit.code), model);
|
|
722
|
+
accountManager.recordRateLimit(account, modelFamily, model);
|
|
723
|
+
account.lastSwitchReason = "rate-limit";
|
|
724
|
+
accountManager.saveToDiskDebounced();
|
|
725
|
+
logWarn(`Rate limited. Rotating account ${account.index + 1} (${account.email ?? "unknown"}).`);
|
|
726
|
+
if (accountManager.getAccountCount() > 1 &&
|
|
727
|
+
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
728
|
+
await showToast(`Rate limited. Switching accounts (retry in ${waitLabel}).`, "warning", { duration: toastDurationMs });
|
|
640
729
|
accountManager.markToastShown(account.index);
|
|
641
730
|
}
|
|
642
|
-
|
|
643
|
-
continue;
|
|
731
|
+
break;
|
|
644
732
|
}
|
|
645
|
-
|
|
646
|
-
accountManager.recordRateLimit(account, modelFamily, model);
|
|
647
|
-
account.lastSwitchReason = "rate-limit";
|
|
648
|
-
accountManager.saveToDiskDebounced();
|
|
649
|
-
logWarn(`Rate limited. Rotating account ${account.index + 1} (${account.email ?? "unknown"}).`);
|
|
650
|
-
if (accountManager.getAccountCount() > 1 &&
|
|
651
|
-
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
652
|
-
await showToast(`Rate limited. Switching accounts (retry in ${waitLabel}).`, "warning", { duration: toastDurationMs });
|
|
653
|
-
accountManager.markToastShown(account.index);
|
|
654
|
-
}
|
|
655
|
-
break;
|
|
733
|
+
return errorResponse;
|
|
656
734
|
}
|
|
657
|
-
|
|
735
|
+
resetRateLimitBackoff(account.index, quotaKey);
|
|
736
|
+
const successResponse = await handleSuccessResponse(response, isStreaming);
|
|
737
|
+
accountManager.recordSuccess(account, modelFamily, model);
|
|
738
|
+
return successResponse;
|
|
658
739
|
}
|
|
659
|
-
resetRateLimitBackoff(account.index, quotaKey);
|
|
660
|
-
accountManager.recordSuccess(account, modelFamily, model);
|
|
661
|
-
return await handleSuccessResponse(response, isStreaming);
|
|
662
740
|
}
|
|
741
|
+
const waitMs = accountManager.getMinWaitTimeForFamily(modelFamily, model);
|
|
742
|
+
const count = accountManager.getAccountCount();
|
|
743
|
+
if (retryAllAccountsRateLimited &&
|
|
744
|
+
count > 0 &&
|
|
745
|
+
waitMs > 0 &&
|
|
746
|
+
(retryAllAccountsMaxWaitMs === 0 ||
|
|
747
|
+
waitMs <= retryAllAccountsMaxWaitMs) &&
|
|
748
|
+
allRateLimitedRetries < retryAllAccountsMaxRetries) {
|
|
749
|
+
const countdownMessage = `All ${count} account(s) rate-limited. Waiting`;
|
|
750
|
+
await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage);
|
|
751
|
+
allRateLimitedRetries++;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const waitLabel = waitMs > 0 ? formatWaitTime(waitMs) : "a bit";
|
|
755
|
+
const message = count === 0
|
|
756
|
+
? "No Codex accounts configured. Run `opencode auth login`."
|
|
757
|
+
: waitMs > 0
|
|
758
|
+
? `All ${count} account(s) are rate-limited. Try again in ${waitLabel} or add another account with \`opencode auth login\`.`
|
|
759
|
+
: `All ${count} account(s) failed (server errors or auth issues). Check account health with \`codex-health\`.`;
|
|
760
|
+
return new Response(JSON.stringify({ error: { message } }), {
|
|
761
|
+
status: waitMs > 0 ? 429 : 503,
|
|
762
|
+
headers: {
|
|
763
|
+
"content-type": "application/json; charset=utf-8",
|
|
764
|
+
},
|
|
765
|
+
});
|
|
663
766
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
allRateLimitedRetries < retryAllAccountsMaxRetries) {
|
|
672
|
-
const countdownMessage = `All ${count} account(s) rate-limited. Waiting`;
|
|
673
|
-
await sleepWithCountdown(addJitter(waitMs, 0.2), countdownMessage);
|
|
674
|
-
allRateLimitedRetries++;
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
const waitLabel = waitMs > 0 ? formatWaitTime(waitMs) : "a bit";
|
|
678
|
-
const message = count === 0
|
|
679
|
-
? "No Codex accounts configured. Run `opencode auth login`."
|
|
680
|
-
: `All ${count} account(s) are rate-limited. Try again in ${waitLabel} or add another account with \`opencode auth login\`.`;
|
|
681
|
-
return new Response(JSON.stringify({ error: { message } }), {
|
|
682
|
-
status: 429,
|
|
683
|
-
headers: {
|
|
684
|
-
"content-type": "application/json; charset=utf-8",
|
|
685
|
-
},
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
},
|
|
689
|
-
};
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
finally {
|
|
771
|
+
resolveMutex?.();
|
|
772
|
+
loaderMutex = null;
|
|
773
|
+
}
|
|
690
774
|
},
|
|
691
775
|
methods: [
|
|
692
776
|
{
|
|
@@ -740,6 +824,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
740
824
|
* @returns Authorization flow configuration
|
|
741
825
|
*/
|
|
742
826
|
authorize: async (inputs) => {
|
|
827
|
+
// Initialize storage path based on config BEFORE any account operations
|
|
828
|
+
// This ensures accounts are saved to the correct location (per-project or global)
|
|
829
|
+
const authPluginConfig = loadPluginConfig();
|
|
830
|
+
const authPerProjectAccounts = getPerProjectAccounts(authPluginConfig);
|
|
831
|
+
if (authPerProjectAccounts) {
|
|
832
|
+
setStoragePath(process.cwd());
|
|
833
|
+
}
|
|
743
834
|
// Always use the multi-account flow regardless of inputs
|
|
744
835
|
// The inputs parameter is only used for noBrowser flag, not for flow selection
|
|
745
836
|
const accounts = [];
|
|
@@ -783,7 +874,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
783
874
|
url: "",
|
|
784
875
|
instructions: "Account limit reached. Remove an account or start fresh.",
|
|
785
876
|
method: "auto",
|
|
786
|
-
callback:
|
|
877
|
+
callback: () => Promise.resolve({
|
|
787
878
|
type: "failed",
|
|
788
879
|
}),
|
|
789
880
|
};
|
|
@@ -809,7 +900,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
809
900
|
const result = await runOAuthFlow(forceNewLogin);
|
|
810
901
|
let resolved = null;
|
|
811
902
|
if (result.type === "success") {
|
|
812
|
-
resolved =
|
|
903
|
+
resolved = resolveAccountSelection(result);
|
|
813
904
|
const email = extractAccountEmail(resolved.access, resolved.idToken);
|
|
814
905
|
const accountId = resolved.accountIdOverride ?? extractAccountId(resolved.access);
|
|
815
906
|
const label = resolved.accountLabel ?? email ?? accountId ?? "Unknown account";
|
|
@@ -829,7 +920,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
829
920
|
url: "",
|
|
830
921
|
instructions: "Authentication failed.",
|
|
831
922
|
method: "auto",
|
|
832
|
-
callback:
|
|
923
|
+
callback: () => Promise.resolve(result),
|
|
833
924
|
};
|
|
834
925
|
}
|
|
835
926
|
logWarn(`[${PLUGIN_NAME}] Skipping failed account ${accounts.length + 1}`);
|
|
@@ -861,7 +952,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
861
952
|
url: "",
|
|
862
953
|
instructions: "Authentication cancelled",
|
|
863
954
|
method: "auto",
|
|
864
|
-
callback:
|
|
955
|
+
callback: () => Promise.resolve({
|
|
865
956
|
type: "failed",
|
|
866
957
|
}),
|
|
867
958
|
};
|
|
@@ -874,13 +965,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
874
965
|
}
|
|
875
966
|
}
|
|
876
967
|
catch (err) {
|
|
877
|
-
|
|
968
|
+
logWarn(`[${PLUGIN_NAME}] Failed to load final account count: ${err?.message ?? String(err)}`);
|
|
878
969
|
}
|
|
879
970
|
return {
|
|
880
971
|
url: "",
|
|
881
972
|
instructions: `Multi-account setup complete (${actualAccountCount} account(s)).`,
|
|
882
973
|
method: "auto",
|
|
883
|
-
callback:
|
|
974
|
+
callback: () => Promise.resolve(primary),
|
|
884
975
|
};
|
|
885
976
|
},
|
|
886
977
|
},
|
|
@@ -888,6 +979,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
888
979
|
label: AUTH_LABELS.OAUTH_MANUAL,
|
|
889
980
|
type: "oauth",
|
|
890
981
|
authorize: async () => {
|
|
982
|
+
// Initialize storage path for manual OAuth flow
|
|
983
|
+
// Must happen BEFORE persistAccountPool to ensure correct storage location
|
|
984
|
+
const manualPluginConfig = loadPluginConfig();
|
|
985
|
+
const manualPerProjectAccounts = getPerProjectAccounts(manualPluginConfig);
|
|
986
|
+
if (manualPerProjectAccounts) {
|
|
987
|
+
setStoragePath(process.cwd());
|
|
988
|
+
}
|
|
891
989
|
const { pkce, url } = await createAuthorizationFlow();
|
|
892
990
|
return buildManualOAuthFlow(pkce, url, async (tokens) => {
|
|
893
991
|
try {
|
|
@@ -924,11 +1022,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
924
1022
|
}
|
|
925
1023
|
const now = Date.now();
|
|
926
1024
|
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
1025
|
+
const listTableOptions = {
|
|
1026
|
+
columns: [
|
|
1027
|
+
{ header: "#", width: 3 },
|
|
1028
|
+
{ header: "Label", width: 42 },
|
|
1029
|
+
{ header: "Status", width: 20 },
|
|
1030
|
+
],
|
|
1031
|
+
};
|
|
927
1032
|
const lines = [
|
|
928
1033
|
`Codex Accounts (${storage.accounts.length}):`,
|
|
929
1034
|
"",
|
|
930
|
-
|
|
931
|
-
"----------------------------------------------- ---------------------",
|
|
1035
|
+
...buildTableHeader(listTableOptions),
|
|
932
1036
|
];
|
|
933
1037
|
storage.accounts.forEach((account, index) => {
|
|
934
1038
|
const label = formatAccountLabel(account, index);
|
|
@@ -944,8 +1048,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
944
1048
|
statuses.push("cooldown");
|
|
945
1049
|
}
|
|
946
1050
|
const statusText = statuses.length > 0 ? statuses.join(", ") : "ok";
|
|
947
|
-
|
|
948
|
-
lines.push(row);
|
|
1051
|
+
lines.push(buildTableRow([String(index + 1), label, statusText], listTableOptions));
|
|
949
1052
|
});
|
|
950
1053
|
lines.push("");
|
|
951
1054
|
lines.push(`Storage: ${storePath}`);
|
|
@@ -1009,22 +1112,30 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1009
1112
|
}
|
|
1010
1113
|
const now = Date.now();
|
|
1011
1114
|
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
1115
|
+
const statusTableOptions = {
|
|
1116
|
+
columns: [
|
|
1117
|
+
{ header: "#", width: 3 },
|
|
1118
|
+
{ header: "Label", width: 42 },
|
|
1119
|
+
{ header: "Active", width: 6 },
|
|
1120
|
+
{ header: "Rate Limit", width: 16 },
|
|
1121
|
+
{ header: "Cooldown", width: 16 },
|
|
1122
|
+
{ header: "Last Used", width: 16 },
|
|
1123
|
+
],
|
|
1124
|
+
};
|
|
1012
1125
|
const lines = [
|
|
1013
1126
|
`Account Status (${storage.accounts.length} total):`,
|
|
1014
1127
|
"",
|
|
1015
|
-
|
|
1016
|
-
"----------------------------------------------- ------ ---------------- ---------------- ----------------",
|
|
1128
|
+
...buildTableHeader(statusTableOptions),
|
|
1017
1129
|
];
|
|
1018
1130
|
storage.accounts.forEach((account, index) => {
|
|
1019
|
-
const label = formatAccountLabel(account, index)
|
|
1131
|
+
const label = formatAccountLabel(account, index);
|
|
1020
1132
|
const active = index === activeIndex ? "Yes" : "No";
|
|
1021
1133
|
const rateLimit = formatRateLimitEntry(account, now) ?? "None";
|
|
1022
1134
|
const cooldown = formatCooldown(account, now) ?? "No";
|
|
1023
1135
|
const lastUsed = typeof account.lastUsed === "number" && account.lastUsed > 0
|
|
1024
1136
|
? `${formatWaitTime(now - account.lastUsed)} ago`
|
|
1025
1137
|
: "-";
|
|
1026
|
-
|
|
1027
|
-
lines.push(row);
|
|
1138
|
+
lines.push(buildTableRow([String(index + 1), label, active, rateLimit, cooldown, lastUsed], statusTableOptions));
|
|
1028
1139
|
});
|
|
1029
1140
|
lines.push("");
|
|
1030
1141
|
lines.push("Active index by model family:");
|