codex-multi-auth 0.1.9 → 1.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -9
- package/dist/lib/accounts.d.ts +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +499 -73
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/schemas.d.ts +7 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +1 -1
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/migrations.d.ts +2 -2
- package/dist/lib/storage/migrations.d.ts.map +1 -1
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +5 -2
- package/dist/lib/storage.js.map +1 -1
- package/package.json +1 -1
- package/scripts/codex-routing.js +1 -0
|
@@ -6,7 +6,7 @@ import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationI
|
|
|
6
6
|
import { startLocalOAuthServer } from "./auth/server.js";
|
|
7
7
|
import { copyTextToClipboard, openBrowserUrl } from "./auth/browser.js";
|
|
8
8
|
import { promptAddAnotherAccount, promptLoginMode } from "./cli.js";
|
|
9
|
-
import { extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, getAccountIdCandidates, resolveRequestAccountId, sanitizeEmail, selectBestAccountCandidate, } from "./accounts.js";
|
|
9
|
+
import { extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, getAccountIdCandidates, resolveRequestAccountId, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, } from "./accounts.js";
|
|
10
10
|
import { ACCOUNT_LIMITS } from "./constants.js";
|
|
11
11
|
import { loadDashboardDisplaySettings, DEFAULT_DASHBOARD_DISPLAY_SETTINGS, } from "./dashboard-settings.js";
|
|
12
12
|
import { evaluateForecastAccounts, isHardRefreshFailure, recommendForecastAccount, summarizeForecast, } from "./forecast.js";
|
|
@@ -219,6 +219,7 @@ function printUsage() {
|
|
|
219
219
|
" codex auth list",
|
|
220
220
|
" codex auth status",
|
|
221
221
|
" codex auth switch <index>",
|
|
222
|
+
" codex auth best [--live] [--json] [--model <model>]",
|
|
222
223
|
" codex auth check",
|
|
223
224
|
" codex auth features",
|
|
224
225
|
" codex auth verify-flagged [--dry-run] [--json] [--no-restore]",
|
|
@@ -273,6 +274,7 @@ const IMPLEMENTED_FEATURES = [
|
|
|
273
274
|
{ id: 38, name: "Dashboard display customization" },
|
|
274
275
|
{ id: 39, name: "Unified color/theme runtime (v2 UI)" },
|
|
275
276
|
{ id: 40, name: "OAuth browser-first flow with manual callback fallback" },
|
|
277
|
+
{ id: 41, name: "Auto-switch to best account command" },
|
|
276
278
|
];
|
|
277
279
|
function runFeaturesReport() {
|
|
278
280
|
console.log(`Implemented features (${IMPLEMENTED_FEATURES.length})`);
|
|
@@ -342,6 +344,46 @@ function hasUniqueQuotaAccountId(accounts, account) {
|
|
|
342
344
|
}
|
|
343
345
|
return matchCount === 1;
|
|
344
346
|
}
|
|
347
|
+
function buildQuotaEmailFallbackState(accounts) {
|
|
348
|
+
const stateByEmail = new Map();
|
|
349
|
+
for (const account of accounts) {
|
|
350
|
+
const email = normalizeQuotaEmail(account.email);
|
|
351
|
+
if (!email)
|
|
352
|
+
continue;
|
|
353
|
+
const existing = stateByEmail.get(email);
|
|
354
|
+
if (existing) {
|
|
355
|
+
existing.matchingCount += 1;
|
|
356
|
+
const accountId = normalizeQuotaAccountId(account.accountId);
|
|
357
|
+
if (accountId) {
|
|
358
|
+
existing.distinctAccountIds.add(accountId);
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const distinctAccountIds = new Set();
|
|
363
|
+
const accountId = normalizeQuotaAccountId(account.accountId);
|
|
364
|
+
if (accountId) {
|
|
365
|
+
distinctAccountIds.add(accountId);
|
|
366
|
+
}
|
|
367
|
+
stateByEmail.set(email, {
|
|
368
|
+
matchingCount: 1,
|
|
369
|
+
distinctAccountIds,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return stateByEmail;
|
|
373
|
+
}
|
|
374
|
+
function hasSafeQuotaEmailFallback(emailFallbackState, account) {
|
|
375
|
+
const email = normalizeQuotaEmail(account.email);
|
|
376
|
+
if (!email)
|
|
377
|
+
return false;
|
|
378
|
+
const state = emailFallbackState.get(email);
|
|
379
|
+
if (!state)
|
|
380
|
+
return false;
|
|
381
|
+
// size > 1 only matters when multiple accounts share the same email but
|
|
382
|
+
// disagree on accountId; a single matching account already implies size <= 1.
|
|
383
|
+
if (state.distinctAccountIds.size > 1)
|
|
384
|
+
return false;
|
|
385
|
+
return state.matchingCount === 1;
|
|
386
|
+
}
|
|
345
387
|
function quotaCacheEntryToSnapshot(entry) {
|
|
346
388
|
return {
|
|
347
389
|
status: entry.status,
|
|
@@ -409,20 +451,22 @@ function formatAccountQuotaSummary(entry) {
|
|
|
409
451
|
}
|
|
410
452
|
return formatQuotaSnapshotLine(quotaCacheEntryToSnapshot(entry));
|
|
411
453
|
}
|
|
412
|
-
function getQuotaCacheEntryForAccount(cache, account, accounts) {
|
|
413
|
-
const email = normalizeQuotaEmail(account.email);
|
|
414
|
-
if (email && cache.byEmail[email]) {
|
|
415
|
-
return cache.byEmail[email] ?? null;
|
|
416
|
-
}
|
|
454
|
+
function getQuotaCacheEntryForAccount(cache, account, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
|
|
417
455
|
const accountId = normalizeQuotaAccountId(account.accountId);
|
|
418
456
|
if (accountId &&
|
|
419
457
|
hasUniqueQuotaAccountId(accounts, account) &&
|
|
420
458
|
cache.byAccountId[accountId]) {
|
|
421
459
|
return cache.byAccountId[accountId] ?? null;
|
|
422
460
|
}
|
|
461
|
+
const email = normalizeQuotaEmail(account.email);
|
|
462
|
+
if (email &&
|
|
463
|
+
hasSafeQuotaEmailFallback(emailFallbackState, account) &&
|
|
464
|
+
cache.byEmail[email]) {
|
|
465
|
+
return cache.byEmail[email] ?? null;
|
|
466
|
+
}
|
|
423
467
|
return null;
|
|
424
468
|
}
|
|
425
|
-
function updateQuotaCacheForAccount(cache, account, snapshot, accounts) {
|
|
469
|
+
function updateQuotaCacheForAccount(cache, account, snapshot, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
|
|
426
470
|
const nextEntry = {
|
|
427
471
|
updatedAt: Date.now(),
|
|
428
472
|
status: snapshot.status,
|
|
@@ -440,33 +484,67 @@ function updateQuotaCacheForAccount(cache, account, snapshot, accounts) {
|
|
|
440
484
|
},
|
|
441
485
|
};
|
|
442
486
|
let changed = false;
|
|
487
|
+
const accountId = normalizeQuotaAccountId(account.accountId);
|
|
488
|
+
const hasUniqueAccountId = accountId !== null && hasUniqueQuotaAccountId(accounts, account);
|
|
489
|
+
if (hasUniqueAccountId) {
|
|
490
|
+
cache.byAccountId[accountId] = nextEntry;
|
|
491
|
+
changed = true;
|
|
492
|
+
}
|
|
443
493
|
const email = normalizeQuotaEmail(account.email);
|
|
444
|
-
if (email
|
|
494
|
+
if (email &&
|
|
495
|
+
hasSafeQuotaEmailFallback(emailFallbackState, account) &&
|
|
496
|
+
!hasUniqueAccountId) {
|
|
445
497
|
cache.byEmail[email] = nextEntry;
|
|
446
498
|
changed = true;
|
|
447
|
-
return changed;
|
|
448
499
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
cache.byAccountId[accountId] = nextEntry;
|
|
500
|
+
else if (email && cache.byEmail[email]) {
|
|
501
|
+
delete cache.byEmail[email];
|
|
452
502
|
changed = true;
|
|
453
503
|
}
|
|
454
504
|
return changed;
|
|
455
505
|
}
|
|
506
|
+
function cloneQuotaCacheData(cache) {
|
|
507
|
+
// Shallow spreading is safe because quota cache entries are always replaced,
|
|
508
|
+
// never mutated in-place.
|
|
509
|
+
return {
|
|
510
|
+
byAccountId: { ...cache.byAccountId },
|
|
511
|
+
byEmail: { ...cache.byEmail },
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function pruneUnsafeQuotaEmailCacheEntry(cache, email, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
|
|
515
|
+
const normalizedEmail = normalizeQuotaEmail(email);
|
|
516
|
+
if (!normalizedEmail || !cache.byEmail[normalizedEmail]) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
const hasSafeFallbackAccount = accounts.some((account) => normalizeQuotaEmail(account.email) === normalizedEmail &&
|
|
520
|
+
hasSafeQuotaEmailFallback(emailFallbackState, account));
|
|
521
|
+
if (hasSafeFallbackAccount) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
delete cache.byEmail[normalizedEmail];
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
456
527
|
const DEFAULT_MENU_QUOTA_REFRESH_TTL_MS = 5 * 60_000;
|
|
457
528
|
const MENU_QUOTA_REFRESH_MODEL = "gpt-5-codex";
|
|
458
|
-
function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts) {
|
|
529
|
+
function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts)) {
|
|
459
530
|
if (account.enabled === false)
|
|
460
531
|
return null;
|
|
461
532
|
if (!hasUsableAccessToken(account, now))
|
|
462
533
|
return null;
|
|
463
|
-
const existing = getQuotaCacheEntryForAccount(cache, account, accounts);
|
|
534
|
+
const existing = getQuotaCacheEntryForAccount(cache, account, accounts, emailFallbackState);
|
|
464
535
|
if (existing &&
|
|
465
536
|
typeof existing.updatedAt === "number" &&
|
|
466
537
|
Number.isFinite(existing.updatedAt) &&
|
|
467
538
|
now - existing.updatedAt < maxAgeMs) {
|
|
468
539
|
return null;
|
|
469
540
|
}
|
|
541
|
+
// Menu auto-refresh is cache-backed, so only probe when the result can be
|
|
542
|
+
// written behind a safe lookup key for later reuse.
|
|
543
|
+
const canStore = (normalizeQuotaAccountId(account.accountId) !== null &&
|
|
544
|
+
hasUniqueQuotaAccountId(accounts, account)) ||
|
|
545
|
+
hasSafeQuotaEmailFallback(emailFallbackState, account);
|
|
546
|
+
if (!canStore)
|
|
547
|
+
return null;
|
|
470
548
|
const accessToken = account.accessToken;
|
|
471
549
|
const accountId = accessToken
|
|
472
550
|
? (account.accountId ?? extractAccountId(accessToken))
|
|
@@ -475,10 +553,10 @@ function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts) {
|
|
|
475
553
|
return null;
|
|
476
554
|
return { accountId, accessToken };
|
|
477
555
|
}
|
|
478
|
-
function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
|
|
556
|
+
function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now(), emailFallbackState = buildQuotaEmailFallbackState(storage.accounts)) {
|
|
479
557
|
const targets = [];
|
|
480
558
|
for (const account of storage.accounts) {
|
|
481
|
-
const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts);
|
|
559
|
+
const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts, emailFallbackState);
|
|
482
560
|
if (!probeInput)
|
|
483
561
|
continue;
|
|
484
562
|
targets.push({
|
|
@@ -490,9 +568,10 @@ function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now
|
|
|
490
568
|
return targets;
|
|
491
569
|
}
|
|
492
570
|
function countMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
|
|
571
|
+
const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
|
|
493
572
|
let count = 0;
|
|
494
573
|
for (const account of storage.accounts) {
|
|
495
|
-
if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts)) {
|
|
574
|
+
if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts, emailFallbackState)) {
|
|
496
575
|
count += 1;
|
|
497
576
|
}
|
|
498
577
|
}
|
|
@@ -502,8 +581,10 @@ async function refreshQuotaCacheForMenu(storage, cache, maxAgeMs, onProgress) {
|
|
|
502
581
|
if (storage.accounts.length === 0) {
|
|
503
582
|
return cache;
|
|
504
583
|
}
|
|
584
|
+
const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
|
|
585
|
+
const nextCache = cloneQuotaCacheData(cache);
|
|
505
586
|
const now = Date.now();
|
|
506
|
-
const targets = collectMenuQuotaRefreshTargets(storage,
|
|
587
|
+
const targets = collectMenuQuotaRefreshTargets(storage, nextCache, maxAgeMs, now, emailFallbackState);
|
|
507
588
|
const total = targets.length;
|
|
508
589
|
let processed = 0;
|
|
509
590
|
onProgress?.(processed, total);
|
|
@@ -518,17 +599,16 @@ async function refreshQuotaCacheForMenu(storage, cache, maxAgeMs, onProgress) {
|
|
|
518
599
|
model: MENU_QUOTA_REFRESH_MODEL,
|
|
519
600
|
});
|
|
520
601
|
changed =
|
|
521
|
-
updateQuotaCacheForAccount(
|
|
522
|
-
changed;
|
|
602
|
+
updateQuotaCacheForAccount(nextCache, target.account, snapshot, storage.accounts, emailFallbackState) || changed;
|
|
523
603
|
}
|
|
524
604
|
catch {
|
|
525
605
|
// Keep existing cached values if probing fails.
|
|
526
606
|
}
|
|
527
607
|
}
|
|
528
608
|
if (changed) {
|
|
529
|
-
await saveQuotaCache(
|
|
609
|
+
await saveQuotaCache(nextCache);
|
|
530
610
|
}
|
|
531
|
-
return
|
|
611
|
+
return nextCache;
|
|
532
612
|
}
|
|
533
613
|
const ACCESS_TOKEN_FRESH_WINDOW_MS = 5 * 60 * 1000;
|
|
534
614
|
function hasUsableAccessToken(account, now) {
|
|
@@ -641,9 +721,10 @@ function toExistingAccountInfo(storage, quotaCache, displaySettings) {
|
|
|
641
721
|
const now = Date.now();
|
|
642
722
|
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
643
723
|
const layoutMode = resolveMenuLayoutMode(displaySettings);
|
|
724
|
+
const emailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
|
|
644
725
|
const baseAccounts = storage.accounts.map((account, index) => {
|
|
645
726
|
const entry = quotaCache
|
|
646
|
-
? getQuotaCacheEntryForAccount(quotaCache, account, storage.accounts)
|
|
727
|
+
? getQuotaCacheEntryForAccount(quotaCache, account, storage.accounts, emailFallbackState)
|
|
647
728
|
: null;
|
|
648
729
|
return {
|
|
649
730
|
index,
|
|
@@ -719,6 +800,35 @@ function resolveAccountSelection(tokens) {
|
|
|
719
800
|
accountLabel: best.label,
|
|
720
801
|
};
|
|
721
802
|
}
|
|
803
|
+
function resolveStoredAccountIdentity(storedAccountId, storedAccountIdSource, tokenAccountId) {
|
|
804
|
+
const accountId = resolveRequestAccountId(storedAccountId, storedAccountIdSource, tokenAccountId);
|
|
805
|
+
if (!accountId) {
|
|
806
|
+
return {};
|
|
807
|
+
}
|
|
808
|
+
if (!shouldUpdateAccountIdFromToken(storedAccountIdSource, storedAccountId)) {
|
|
809
|
+
return {
|
|
810
|
+
accountId,
|
|
811
|
+
accountIdSource: storedAccountIdSource,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
accountId,
|
|
816
|
+
accountIdSource: accountId === tokenAccountId ? "token" : storedAccountIdSource,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function applyTokenAccountIdentity(account, tokenAccountId) {
|
|
820
|
+
const nextIdentity = resolveStoredAccountIdentity(account.accountId, account.accountIdSource, tokenAccountId);
|
|
821
|
+
if (!nextIdentity.accountId) {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
if (nextIdentity.accountId === account.accountId
|
|
825
|
+
&& nextIdentity.accountIdSource === account.accountIdSource) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
account.accountId = nextIdentity.accountId;
|
|
829
|
+
account.accountIdSource = nextIdentity.accountIdSource;
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
722
832
|
async function promptManualCallback(state) {
|
|
723
833
|
if (!input.isTTY || !output.isTTY) {
|
|
724
834
|
return null;
|
|
@@ -1222,6 +1332,7 @@ async function runHealthCheck(options = {}) {
|
|
|
1222
1332
|
const probeModel = options.model?.trim() || "gpt-5-codex";
|
|
1223
1333
|
const display = options.display ?? DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
|
|
1224
1334
|
const quotaCache = liveProbe ? await loadQuotaCache() : null;
|
|
1335
|
+
const workingQuotaCache = quotaCache ? cloneQuotaCacheData(quotaCache) : null;
|
|
1225
1336
|
let quotaCacheChanged = false;
|
|
1226
1337
|
setStoragePath(null);
|
|
1227
1338
|
const storage = await loadAccounts();
|
|
@@ -1229,6 +1340,9 @@ async function runHealthCheck(options = {}) {
|
|
|
1229
1340
|
console.log("No accounts configured.");
|
|
1230
1341
|
return;
|
|
1231
1342
|
}
|
|
1343
|
+
let quotaEmailFallbackState = liveProbe && quotaCache
|
|
1344
|
+
? buildQuotaEmailFallbackState(storage.accounts)
|
|
1345
|
+
: null;
|
|
1232
1346
|
let changed = false;
|
|
1233
1347
|
let ok = 0;
|
|
1234
1348
|
let failed = 0;
|
|
@@ -1271,9 +1385,9 @@ async function runHealthCheck(options = {}) {
|
|
|
1271
1385
|
accessToken: currentAccessToken,
|
|
1272
1386
|
model: probeModel,
|
|
1273
1387
|
});
|
|
1274
|
-
if (
|
|
1388
|
+
if (workingQuotaCache) {
|
|
1275
1389
|
quotaCacheChanged =
|
|
1276
|
-
updateQuotaCacheForAccount(
|
|
1390
|
+
updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
|
|
1277
1391
|
}
|
|
1278
1392
|
healthDetail = formatQuotaSnapshotForDashboard(snapshot, display);
|
|
1279
1393
|
}
|
|
@@ -1297,6 +1411,8 @@ async function runHealthCheck(options = {}) {
|
|
|
1297
1411
|
if (result.type === "success") {
|
|
1298
1412
|
const tokenAccountId = extractAccountId(result.access);
|
|
1299
1413
|
const nextEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
|
|
1414
|
+
const previousEmail = account.email;
|
|
1415
|
+
let accountIdentityChanged = false;
|
|
1300
1416
|
if (account.refreshToken !== result.refresh) {
|
|
1301
1417
|
account.refreshToken = result.refresh;
|
|
1302
1418
|
changed = true;
|
|
@@ -1312,16 +1428,21 @@ async function runHealthCheck(options = {}) {
|
|
|
1312
1428
|
if (nextEmail && nextEmail !== account.email) {
|
|
1313
1429
|
account.email = nextEmail;
|
|
1314
1430
|
changed = true;
|
|
1431
|
+
accountIdentityChanged = true;
|
|
1315
1432
|
}
|
|
1316
|
-
if (
|
|
1317
|
-
account.accountId = tokenAccountId;
|
|
1318
|
-
account.accountIdSource = "token";
|
|
1433
|
+
if (applyTokenAccountIdentity(account, tokenAccountId)) {
|
|
1319
1434
|
changed = true;
|
|
1435
|
+
accountIdentityChanged = true;
|
|
1320
1436
|
}
|
|
1321
1437
|
if (account.enabled === false) {
|
|
1322
1438
|
account.enabled = true;
|
|
1323
1439
|
changed = true;
|
|
1324
1440
|
}
|
|
1441
|
+
if (accountIdentityChanged && liveProbe && workingQuotaCache) {
|
|
1442
|
+
quotaEmailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
|
|
1443
|
+
quotaCacheChanged =
|
|
1444
|
+
pruneUnsafeQuotaEmailCacheEntry(workingQuotaCache, previousEmail, storage.accounts, quotaEmailFallbackState) || quotaCacheChanged;
|
|
1445
|
+
}
|
|
1325
1446
|
account.lastUsed = Date.now();
|
|
1326
1447
|
if (i === activeIndex) {
|
|
1327
1448
|
activeAccountRefreshed = true;
|
|
@@ -1341,9 +1462,9 @@ async function runHealthCheck(options = {}) {
|
|
|
1341
1462
|
accessToken: result.access,
|
|
1342
1463
|
model: probeModel,
|
|
1343
1464
|
});
|
|
1344
|
-
if (
|
|
1465
|
+
if (workingQuotaCache) {
|
|
1345
1466
|
quotaCacheChanged =
|
|
1346
|
-
updateQuotaCacheForAccount(
|
|
1467
|
+
updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
|
|
1347
1468
|
}
|
|
1348
1469
|
healthyMessage = formatQuotaSnapshotForDashboard(snapshot, display);
|
|
1349
1470
|
}
|
|
@@ -1377,8 +1498,8 @@ async function runHealthCheck(options = {}) {
|
|
|
1377
1498
|
if (!display.showPerAccountRows) {
|
|
1378
1499
|
console.log(stylePromptText("Per-account lines are hidden in dashboard settings.", "muted"));
|
|
1379
1500
|
}
|
|
1380
|
-
if (
|
|
1381
|
-
await saveQuotaCache(
|
|
1501
|
+
if (workingQuotaCache && quotaCacheChanged) {
|
|
1502
|
+
await saveQuotaCache(workingQuotaCache);
|
|
1382
1503
|
}
|
|
1383
1504
|
if (changed) {
|
|
1384
1505
|
await saveAccounts(storage);
|
|
@@ -1413,6 +1534,21 @@ function printForecastUsage() {
|
|
|
1413
1534
|
" --model, -m Probe model for live mode (default: gpt-5-codex)",
|
|
1414
1535
|
].join("\n"));
|
|
1415
1536
|
}
|
|
1537
|
+
function printBestUsage() {
|
|
1538
|
+
console.log([
|
|
1539
|
+
"Usage:",
|
|
1540
|
+
" codex auth best [--live] [--json] [--model <model>]",
|
|
1541
|
+
"",
|
|
1542
|
+
"Options:",
|
|
1543
|
+
" --live, -l Probe live quota headers via Codex backend before switching",
|
|
1544
|
+
" --json, -j Print machine-readable JSON output",
|
|
1545
|
+
" --model, -m Probe model for live mode (default: gpt-5-codex)",
|
|
1546
|
+
"",
|
|
1547
|
+
"Behavior:",
|
|
1548
|
+
" - Chooses the healthiest account using forecast scoring",
|
|
1549
|
+
" - Switches to the recommended account when it is not already active",
|
|
1550
|
+
].join("\n"));
|
|
1551
|
+
}
|
|
1416
1552
|
function printFixUsage() {
|
|
1417
1553
|
console.log([
|
|
1418
1554
|
"Usage:",
|
|
@@ -1486,6 +1622,48 @@ function parseForecastArgs(args) {
|
|
|
1486
1622
|
}
|
|
1487
1623
|
return { ok: true, options };
|
|
1488
1624
|
}
|
|
1625
|
+
function parseBestArgs(args) {
|
|
1626
|
+
const options = {
|
|
1627
|
+
live: false,
|
|
1628
|
+
json: false,
|
|
1629
|
+
model: "gpt-5-codex",
|
|
1630
|
+
modelProvided: false,
|
|
1631
|
+
};
|
|
1632
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1633
|
+
const arg = args[i];
|
|
1634
|
+
if (!arg)
|
|
1635
|
+
continue;
|
|
1636
|
+
if (arg === "--live" || arg === "-l") {
|
|
1637
|
+
options.live = true;
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
if (arg === "--json" || arg === "-j") {
|
|
1641
|
+
options.json = true;
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
if (arg === "--model" || arg === "-m") {
|
|
1645
|
+
const value = args[i + 1];
|
|
1646
|
+
if (!value) {
|
|
1647
|
+
return { ok: false, message: "Missing value for --model" };
|
|
1648
|
+
}
|
|
1649
|
+
options.model = value;
|
|
1650
|
+
options.modelProvided = true;
|
|
1651
|
+
i += 1;
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (arg.startsWith("--model=")) {
|
|
1655
|
+
const value = arg.slice("--model=".length).trim();
|
|
1656
|
+
if (!value) {
|
|
1657
|
+
return { ok: false, message: "Missing value for --model" };
|
|
1658
|
+
}
|
|
1659
|
+
options.model = value;
|
|
1660
|
+
options.modelProvided = true;
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
return { ok: false, message: `Unknown option: ${arg}` };
|
|
1664
|
+
}
|
|
1665
|
+
return { ok: true, options };
|
|
1666
|
+
}
|
|
1489
1667
|
function parseFixArgs(args) {
|
|
1490
1668
|
const options = {
|
|
1491
1669
|
dryRun: false,
|
|
@@ -1698,6 +1876,7 @@ async function runForecast(args) {
|
|
|
1698
1876
|
const options = parsedArgs.options;
|
|
1699
1877
|
const display = DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
|
|
1700
1878
|
const quotaCache = options.live ? await loadQuotaCache() : null;
|
|
1879
|
+
const workingQuotaCache = quotaCache ? cloneQuotaCacheData(quotaCache) : null;
|
|
1701
1880
|
let quotaCacheChanged = false;
|
|
1702
1881
|
setStoragePath(null);
|
|
1703
1882
|
const storage = await loadAccounts();
|
|
@@ -1705,6 +1884,9 @@ async function runForecast(args) {
|
|
|
1705
1884
|
console.log("No accounts configured.");
|
|
1706
1885
|
return 0;
|
|
1707
1886
|
}
|
|
1887
|
+
const quotaEmailFallbackState = options.live && quotaCache
|
|
1888
|
+
? buildQuotaEmailFallbackState(storage.accounts)
|
|
1889
|
+
: null;
|
|
1708
1890
|
const now = Date.now();
|
|
1709
1891
|
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
1710
1892
|
const refreshFailures = new Map();
|
|
@@ -1741,11 +1923,11 @@ async function runForecast(args) {
|
|
|
1741
1923
|
model: options.model,
|
|
1742
1924
|
});
|
|
1743
1925
|
liveQuotaByIndex.set(i, liveQuota);
|
|
1744
|
-
if (
|
|
1926
|
+
if (workingQuotaCache) {
|
|
1745
1927
|
const account = storage.accounts[i];
|
|
1746
1928
|
if (account) {
|
|
1747
1929
|
quotaCacheChanged =
|
|
1748
|
-
updateQuotaCacheForAccount(
|
|
1930
|
+
updateQuotaCacheForAccount(workingQuotaCache, account, liveQuota, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
|
|
1749
1931
|
}
|
|
1750
1932
|
}
|
|
1751
1933
|
}
|
|
@@ -1766,8 +1948,8 @@ async function runForecast(args) {
|
|
|
1766
1948
|
const summary = summarizeForecast(forecastResults);
|
|
1767
1949
|
const recommendation = recommendForecastAccount(forecastResults);
|
|
1768
1950
|
if (options.json) {
|
|
1769
|
-
if (
|
|
1770
|
-
await saveQuotaCache(
|
|
1951
|
+
if (workingQuotaCache && quotaCacheChanged) {
|
|
1952
|
+
await saveQuotaCache(workingQuotaCache);
|
|
1771
1953
|
}
|
|
1772
1954
|
console.log(JSON.stringify({
|
|
1773
1955
|
command: "forecast",
|
|
@@ -1837,8 +2019,8 @@ async function runForecast(args) {
|
|
|
1837
2019
|
console.log(` ${stylePromptText("-", "warning")} ${stylePromptText(error, "muted")}`);
|
|
1838
2020
|
}
|
|
1839
2021
|
}
|
|
1840
|
-
if (
|
|
1841
|
-
await saveQuotaCache(
|
|
2022
|
+
if (workingQuotaCache && quotaCacheChanged) {
|
|
2023
|
+
await saveQuotaCache(workingQuotaCache);
|
|
1842
2024
|
}
|
|
1843
2025
|
return 0;
|
|
1844
2026
|
}
|
|
@@ -2022,7 +2204,8 @@ function findExistingAccountIndexForFlagged(storage, flagged, nextRefreshToken,
|
|
|
2022
2204
|
}
|
|
2023
2205
|
function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
|
|
2024
2206
|
const nextEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken)) ?? flagged.email;
|
|
2025
|
-
const
|
|
2207
|
+
const tokenAccountId = extractAccountId(refreshResult.access);
|
|
2208
|
+
const { accountId: nextAccountId, accountIdSource: nextAccountIdSource } = resolveStoredAccountIdentity(flagged.accountId, flagged.accountIdSource, tokenAccountId);
|
|
2026
2209
|
const existingIndex = findExistingAccountIndexForFlagged(storage, flagged, refreshResult.refresh, nextAccountId, nextEmail);
|
|
2027
2210
|
if (existingIndex >= 0) {
|
|
2028
2211
|
const existing = storage.accounts[existingIndex];
|
|
@@ -2046,9 +2229,11 @@ function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
|
|
|
2046
2229
|
existing.email = nextEmail;
|
|
2047
2230
|
changed = true;
|
|
2048
2231
|
}
|
|
2049
|
-
if (nextAccountId
|
|
2232
|
+
if (nextAccountId !== undefined &&
|
|
2233
|
+
((nextAccountId !== existing.accountId)
|
|
2234
|
+
|| (nextAccountIdSource !== existing.accountIdSource))) {
|
|
2050
2235
|
existing.accountId = nextAccountId;
|
|
2051
|
-
existing.accountIdSource =
|
|
2236
|
+
existing.accountIdSource = nextAccountIdSource;
|
|
2052
2237
|
changed = true;
|
|
2053
2238
|
}
|
|
2054
2239
|
if (existing.enabled === false) {
|
|
@@ -2078,7 +2263,7 @@ function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
|
|
|
2078
2263
|
accessToken: refreshResult.access,
|
|
2079
2264
|
expiresAt: refreshResult.expires,
|
|
2080
2265
|
accountId: nextAccountId,
|
|
2081
|
-
accountIdSource:
|
|
2266
|
+
accountIdSource: nextAccountIdSource,
|
|
2082
2267
|
accountLabel: flagged.accountLabel,
|
|
2083
2268
|
email: nextEmail,
|
|
2084
2269
|
addedAt: flagged.addedAt ?? now,
|
|
@@ -2146,13 +2331,15 @@ async function runVerifyFlagged(args) {
|
|
|
2146
2331
|
const { index: i, flagged, label, result } = check;
|
|
2147
2332
|
if (result.type === "success") {
|
|
2148
2333
|
if (!options.restore) {
|
|
2334
|
+
const tokenAccountId = extractAccountId(result.access);
|
|
2335
|
+
const nextIdentity = resolveStoredAccountIdentity(flagged.accountId, flagged.accountIdSource, tokenAccountId);
|
|
2149
2336
|
const nextFlagged = {
|
|
2150
2337
|
...flagged,
|
|
2151
2338
|
refreshToken: result.refresh,
|
|
2152
2339
|
accessToken: result.access,
|
|
2153
2340
|
expiresAt: result.expires,
|
|
2154
|
-
accountId:
|
|
2155
|
-
accountIdSource:
|
|
2341
|
+
accountId: nextIdentity.accountId,
|
|
2342
|
+
accountIdSource: nextIdentity.accountIdSource,
|
|
2156
2343
|
email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
|
|
2157
2344
|
lastUsed: now,
|
|
2158
2345
|
lastError: undefined,
|
|
@@ -2181,13 +2368,15 @@ async function runVerifyFlagged(args) {
|
|
|
2181
2368
|
});
|
|
2182
2369
|
continue;
|
|
2183
2370
|
}
|
|
2371
|
+
const tokenAccountId = extractAccountId(result.access);
|
|
2372
|
+
const nextIdentity = resolveStoredAccountIdentity(flagged.accountId, flagged.accountIdSource, tokenAccountId);
|
|
2184
2373
|
const updatedFlagged = {
|
|
2185
2374
|
...flagged,
|
|
2186
2375
|
refreshToken: result.refresh,
|
|
2187
2376
|
accessToken: result.access,
|
|
2188
2377
|
expiresAt: result.expires,
|
|
2189
|
-
accountId:
|
|
2190
|
-
accountIdSource:
|
|
2378
|
+
accountId: nextIdentity.accountId,
|
|
2379
|
+
accountIdSource: nextIdentity.accountIdSource,
|
|
2191
2380
|
email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
|
|
2192
2381
|
lastUsed: now,
|
|
2193
2382
|
lastError: upsertResult.message,
|
|
@@ -2317,6 +2506,7 @@ async function runFix(args) {
|
|
|
2317
2506
|
const options = parsedArgs.options;
|
|
2318
2507
|
const display = DEFAULT_DASHBOARD_DISPLAY_SETTINGS;
|
|
2319
2508
|
const quotaCache = options.live ? await loadQuotaCache() : null;
|
|
2509
|
+
const workingQuotaCache = quotaCache ? cloneQuotaCacheData(quotaCache) : null;
|
|
2320
2510
|
let quotaCacheChanged = false;
|
|
2321
2511
|
setStoragePath(null);
|
|
2322
2512
|
const storage = await loadAccounts();
|
|
@@ -2324,6 +2514,9 @@ async function runFix(args) {
|
|
|
2324
2514
|
console.log("No accounts configured.");
|
|
2325
2515
|
return 0;
|
|
2326
2516
|
}
|
|
2517
|
+
let quotaEmailFallbackState = options.live && quotaCache
|
|
2518
|
+
? buildQuotaEmailFallbackState(storage.accounts)
|
|
2519
|
+
: null;
|
|
2327
2520
|
const now = Date.now();
|
|
2328
2521
|
const activeIndex = resolveActiveIndex(storage, "codex");
|
|
2329
2522
|
let changed = false;
|
|
@@ -2357,9 +2550,9 @@ async function runFix(args) {
|
|
|
2357
2550
|
accessToken: currentAccessToken,
|
|
2358
2551
|
model: options.model,
|
|
2359
2552
|
});
|
|
2360
|
-
if (
|
|
2553
|
+
if (workingQuotaCache) {
|
|
2361
2554
|
quotaCacheChanged =
|
|
2362
|
-
updateQuotaCacheForAccount(
|
|
2555
|
+
updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
|
|
2363
2556
|
}
|
|
2364
2557
|
reports.push({
|
|
2365
2558
|
index: i,
|
|
@@ -2397,7 +2590,9 @@ async function runFix(args) {
|
|
|
2397
2590
|
if (refreshResult.type === "success") {
|
|
2398
2591
|
const nextEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
|
|
2399
2592
|
const nextAccountId = extractAccountId(refreshResult.access);
|
|
2593
|
+
const previousEmail = account.email;
|
|
2400
2594
|
let accountChanged = false;
|
|
2595
|
+
let accountIdentityChanged = false;
|
|
2401
2596
|
if (account.refreshToken !== refreshResult.refresh) {
|
|
2402
2597
|
account.refreshToken = refreshResult.refresh;
|
|
2403
2598
|
accountChanged = true;
|
|
@@ -2413,14 +2608,19 @@ async function runFix(args) {
|
|
|
2413
2608
|
if (nextEmail && nextEmail !== account.email) {
|
|
2414
2609
|
account.email = nextEmail;
|
|
2415
2610
|
accountChanged = true;
|
|
2611
|
+
accountIdentityChanged = true;
|
|
2416
2612
|
}
|
|
2417
|
-
if (
|
|
2418
|
-
account.accountId = nextAccountId;
|
|
2419
|
-
account.accountIdSource = "token";
|
|
2613
|
+
if (applyTokenAccountIdentity(account, nextAccountId)) {
|
|
2420
2614
|
accountChanged = true;
|
|
2615
|
+
accountIdentityChanged = true;
|
|
2421
2616
|
}
|
|
2422
2617
|
if (accountChanged)
|
|
2423
2618
|
changed = true;
|
|
2619
|
+
if (accountIdentityChanged && options.live && workingQuotaCache) {
|
|
2620
|
+
quotaEmailFallbackState = buildQuotaEmailFallbackState(storage.accounts);
|
|
2621
|
+
quotaCacheChanged =
|
|
2622
|
+
pruneUnsafeQuotaEmailCacheEntry(workingQuotaCache, previousEmail, storage.accounts, quotaEmailFallbackState) || quotaCacheChanged;
|
|
2623
|
+
}
|
|
2424
2624
|
if (options.live) {
|
|
2425
2625
|
const probeAccountId = account.accountId ?? nextAccountId;
|
|
2426
2626
|
if (probeAccountId) {
|
|
@@ -2430,9 +2630,9 @@ async function runFix(args) {
|
|
|
2430
2630
|
accessToken: refreshResult.access,
|
|
2431
2631
|
model: options.model,
|
|
2432
2632
|
});
|
|
2433
|
-
if (
|
|
2633
|
+
if (workingQuotaCache) {
|
|
2434
2634
|
quotaCacheChanged =
|
|
2435
|
-
updateQuotaCacheForAccount(
|
|
2635
|
+
updateQuotaCacheForAccount(workingQuotaCache, account, snapshot, storage.accounts, quotaEmailFallbackState ?? undefined) || quotaCacheChanged;
|
|
2436
2636
|
}
|
|
2437
2637
|
reports.push({
|
|
2438
2638
|
index: i,
|
|
@@ -2521,8 +2721,8 @@ async function runFix(args) {
|
|
|
2521
2721
|
await saveAccounts(storage);
|
|
2522
2722
|
}
|
|
2523
2723
|
if (options.json) {
|
|
2524
|
-
if (
|
|
2525
|
-
await saveQuotaCache(
|
|
2724
|
+
if (workingQuotaCache && quotaCacheChanged) {
|
|
2725
|
+
await saveQuotaCache(workingQuotaCache);
|
|
2526
2726
|
}
|
|
2527
2727
|
console.log(JSON.stringify({
|
|
2528
2728
|
command: "fix",
|
|
@@ -2588,8 +2788,8 @@ async function runFix(args) {
|
|
|
2588
2788
|
console.log(`${stylePromptText("Note:", "accent")} ${stylePromptText(recommendation.reason, "muted")}`);
|
|
2589
2789
|
}
|
|
2590
2790
|
}
|
|
2591
|
-
if (
|
|
2592
|
-
await saveQuotaCache(
|
|
2791
|
+
if (workingQuotaCache && quotaCacheChanged) {
|
|
2792
|
+
await saveQuotaCache(workingQuotaCache);
|
|
2593
2793
|
}
|
|
2594
2794
|
if (changed && options.dryRun) {
|
|
2595
2795
|
console.log(`\n${stylePromptText("Preview only: no changes were saved.", "warning")}`);
|
|
@@ -3027,10 +3227,7 @@ async function runDoctor(args) {
|
|
|
3027
3227
|
activeAccount.expiresAt = refreshResult.expires;
|
|
3028
3228
|
if (refreshedEmail)
|
|
3029
3229
|
activeAccount.email = refreshedEmail;
|
|
3030
|
-
|
|
3031
|
-
activeAccount.accountId = refreshedAccountId;
|
|
3032
|
-
activeAccount.accountIdSource = "token";
|
|
3033
|
-
}
|
|
3230
|
+
applyTokenAccountIdentity(activeAccount, refreshedAccountId);
|
|
3034
3231
|
syncAccessToken = refreshResult.access;
|
|
3035
3232
|
syncRefreshToken = refreshResult.refresh;
|
|
3036
3233
|
syncExpiresAt = refreshResult.expires;
|
|
@@ -3349,6 +3546,23 @@ async function runSwitch(args) {
|
|
|
3349
3546
|
console.error(`Account ${parsed} not found.`);
|
|
3350
3547
|
return 1;
|
|
3351
3548
|
}
|
|
3549
|
+
const { synced, wasDisabled } = await persistAndSyncSelectedAccount({
|
|
3550
|
+
storage,
|
|
3551
|
+
targetIndex,
|
|
3552
|
+
parsed,
|
|
3553
|
+
switchReason: "rotation",
|
|
3554
|
+
});
|
|
3555
|
+
if (!synced) {
|
|
3556
|
+
console.warn(`Switched account ${parsed} locally, but Codex auth sync did not complete. Multi-auth routing will still use this account.`);
|
|
3557
|
+
}
|
|
3558
|
+
console.log(`Switched to account ${parsed}: ${formatAccountLabel(account, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
|
|
3559
|
+
return 0;
|
|
3560
|
+
}
|
|
3561
|
+
async function persistAndSyncSelectedAccount({ storage, targetIndex, parsed, switchReason, initialSyncIdToken, }) {
|
|
3562
|
+
const account = storage.accounts[targetIndex];
|
|
3563
|
+
if (!account) {
|
|
3564
|
+
throw new Error(`Account ${parsed} not found.`);
|
|
3565
|
+
}
|
|
3352
3566
|
storage.activeIndex = targetIndex;
|
|
3353
3567
|
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
|
|
3354
3568
|
for (const family of MODEL_FAMILIES) {
|
|
@@ -3362,7 +3576,7 @@ async function runSwitch(args) {
|
|
|
3362
3576
|
let syncAccessToken = account.accessToken;
|
|
3363
3577
|
let syncRefreshToken = account.refreshToken;
|
|
3364
3578
|
let syncExpiresAt = account.expiresAt;
|
|
3365
|
-
let syncIdToken;
|
|
3579
|
+
let syncIdToken = initialSyncIdToken;
|
|
3366
3580
|
if (!hasUsableAccessToken(account, switchNow)) {
|
|
3367
3581
|
const refreshResult = await queuedRefresh(account.refreshToken);
|
|
3368
3582
|
if (refreshResult.type === "success") {
|
|
@@ -3380,10 +3594,7 @@ async function runSwitch(args) {
|
|
|
3380
3594
|
if (nextEmail && nextEmail !== account.email) {
|
|
3381
3595
|
account.email = nextEmail;
|
|
3382
3596
|
}
|
|
3383
|
-
|
|
3384
|
-
account.accountId = tokenAccountId;
|
|
3385
|
-
account.accountIdSource = "token";
|
|
3386
|
-
}
|
|
3597
|
+
applyTokenAccountIdentity(account, tokenAccountId);
|
|
3387
3598
|
syncAccessToken = refreshResult.access;
|
|
3388
3599
|
syncRefreshToken = refreshResult.refresh;
|
|
3389
3600
|
syncExpiresAt = refreshResult.expires;
|
|
@@ -3394,7 +3605,7 @@ async function runSwitch(args) {
|
|
|
3394
3605
|
}
|
|
3395
3606
|
}
|
|
3396
3607
|
account.lastUsed = switchNow;
|
|
3397
|
-
account.lastSwitchReason =
|
|
3608
|
+
account.lastSwitchReason = switchReason;
|
|
3398
3609
|
await saveAccounts(storage);
|
|
3399
3610
|
const synced = await setCodexCliActiveSelection({
|
|
3400
3611
|
accountId: account.accountId,
|
|
@@ -3404,10 +3615,224 @@ async function runSwitch(args) {
|
|
|
3404
3615
|
expiresAt: syncExpiresAt,
|
|
3405
3616
|
...(syncIdToken ? { idToken: syncIdToken } : {}),
|
|
3406
3617
|
});
|
|
3407
|
-
|
|
3408
|
-
|
|
3618
|
+
return { synced, wasDisabled };
|
|
3619
|
+
}
|
|
3620
|
+
async function runBest(args) {
|
|
3621
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3622
|
+
printBestUsage();
|
|
3623
|
+
return 0;
|
|
3624
|
+
}
|
|
3625
|
+
const parsedArgs = parseBestArgs(args);
|
|
3626
|
+
if (!parsedArgs.ok) {
|
|
3627
|
+
console.error(parsedArgs.message);
|
|
3628
|
+
printBestUsage();
|
|
3629
|
+
return 1;
|
|
3630
|
+
}
|
|
3631
|
+
const options = parsedArgs.options;
|
|
3632
|
+
if (options.modelProvided && !options.live) {
|
|
3633
|
+
console.error("--model requires --live for codex auth best");
|
|
3634
|
+
printBestUsage();
|
|
3635
|
+
return 1;
|
|
3636
|
+
}
|
|
3637
|
+
setStoragePath(null);
|
|
3638
|
+
const storage = await loadAccounts();
|
|
3639
|
+
if (!storage || storage.accounts.length === 0) {
|
|
3640
|
+
if (options.json) {
|
|
3641
|
+
console.log(JSON.stringify({ error: "No accounts configured." }, null, 2));
|
|
3642
|
+
}
|
|
3643
|
+
else {
|
|
3644
|
+
console.log("No accounts configured.");
|
|
3645
|
+
}
|
|
3646
|
+
return 1;
|
|
3647
|
+
}
|
|
3648
|
+
const now = Date.now();
|
|
3649
|
+
const refreshFailures = new Map();
|
|
3650
|
+
const liveQuotaByIndex = new Map();
|
|
3651
|
+
const probeIdTokenByIndex = new Map();
|
|
3652
|
+
const probeRefreshedIndices = new Set();
|
|
3653
|
+
const probeErrors = [];
|
|
3654
|
+
let changed = false;
|
|
3655
|
+
const printProbeNotes = () => {
|
|
3656
|
+
if (probeErrors.length === 0)
|
|
3657
|
+
return;
|
|
3658
|
+
console.log(`Live check notes (${probeErrors.length}):`);
|
|
3659
|
+
for (const error of probeErrors) {
|
|
3660
|
+
console.log(` - ${error}`);
|
|
3661
|
+
}
|
|
3662
|
+
};
|
|
3663
|
+
const persistProbeChangesIfNeeded = async () => {
|
|
3664
|
+
if (!changed)
|
|
3665
|
+
return;
|
|
3666
|
+
await saveAccounts(storage);
|
|
3667
|
+
changed = false;
|
|
3668
|
+
};
|
|
3669
|
+
for (let i = 0; i < storage.accounts.length; i += 1) {
|
|
3670
|
+
const account = storage.accounts[i];
|
|
3671
|
+
if (!account || !options.live)
|
|
3672
|
+
continue;
|
|
3673
|
+
if (account.enabled === false)
|
|
3674
|
+
continue;
|
|
3675
|
+
let probeAccessToken = account.accessToken;
|
|
3676
|
+
let probeAccountId = account.accountId ?? extractAccountId(account.accessToken);
|
|
3677
|
+
if (!hasUsableAccessToken(account, now)) {
|
|
3678
|
+
const refreshResult = await queuedRefresh(account.refreshToken);
|
|
3679
|
+
if (refreshResult.type !== "success") {
|
|
3680
|
+
refreshFailures.set(i, {
|
|
3681
|
+
...refreshResult,
|
|
3682
|
+
message: normalizeFailureDetail(refreshResult.message, refreshResult.reason),
|
|
3683
|
+
});
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
const refreshedEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken));
|
|
3687
|
+
const refreshedAccountId = extractAccountId(refreshResult.access);
|
|
3688
|
+
if (account.refreshToken !== refreshResult.refresh) {
|
|
3689
|
+
account.refreshToken = refreshResult.refresh;
|
|
3690
|
+
changed = true;
|
|
3691
|
+
}
|
|
3692
|
+
if (account.accessToken !== refreshResult.access) {
|
|
3693
|
+
account.accessToken = refreshResult.access;
|
|
3694
|
+
changed = true;
|
|
3695
|
+
}
|
|
3696
|
+
if (account.expiresAt !== refreshResult.expires) {
|
|
3697
|
+
account.expiresAt = refreshResult.expires;
|
|
3698
|
+
changed = true;
|
|
3699
|
+
}
|
|
3700
|
+
if (refreshedEmail && refreshedEmail !== account.email) {
|
|
3701
|
+
account.email = refreshedEmail;
|
|
3702
|
+
changed = true;
|
|
3703
|
+
}
|
|
3704
|
+
if (refreshedAccountId && refreshedAccountId !== account.accountId) {
|
|
3705
|
+
account.accountId = refreshedAccountId;
|
|
3706
|
+
account.accountIdSource = "token";
|
|
3707
|
+
changed = true;
|
|
3708
|
+
}
|
|
3709
|
+
if (refreshResult.idToken) {
|
|
3710
|
+
probeIdTokenByIndex.set(i, refreshResult.idToken);
|
|
3711
|
+
}
|
|
3712
|
+
probeRefreshedIndices.add(i);
|
|
3713
|
+
probeAccessToken = account.accessToken;
|
|
3714
|
+
probeAccountId = account.accountId ?? refreshedAccountId;
|
|
3715
|
+
}
|
|
3716
|
+
if (!probeAccessToken || !probeAccountId) {
|
|
3717
|
+
probeErrors.push(`${formatAccountLabel(account, i)}: missing accountId for live probe`);
|
|
3718
|
+
continue;
|
|
3719
|
+
}
|
|
3720
|
+
try {
|
|
3721
|
+
const liveQuota = await fetchCodexQuotaSnapshot({
|
|
3722
|
+
accountId: probeAccountId,
|
|
3723
|
+
accessToken: probeAccessToken,
|
|
3724
|
+
model: options.model,
|
|
3725
|
+
});
|
|
3726
|
+
liveQuotaByIndex.set(i, liveQuota);
|
|
3727
|
+
}
|
|
3728
|
+
catch (error) {
|
|
3729
|
+
const message = normalizeFailureDetail(error instanceof Error ? error.message : String(error), undefined);
|
|
3730
|
+
probeErrors.push(`${formatAccountLabel(account, i)}: ${message}`);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
const forecastInputs = storage.accounts.map((account, index) => ({
|
|
3734
|
+
index,
|
|
3735
|
+
account,
|
|
3736
|
+
isCurrent: index === resolveActiveIndex(storage, "codex"),
|
|
3737
|
+
now,
|
|
3738
|
+
refreshFailure: refreshFailures.get(index),
|
|
3739
|
+
liveQuota: liveQuotaByIndex.get(index),
|
|
3740
|
+
}));
|
|
3741
|
+
const forecastResults = evaluateForecastAccounts(forecastInputs);
|
|
3742
|
+
const recommendation = recommendForecastAccount(forecastResults);
|
|
3743
|
+
if (recommendation.recommendedIndex === null) {
|
|
3744
|
+
await persistProbeChangesIfNeeded();
|
|
3745
|
+
if (options.json) {
|
|
3746
|
+
console.log(JSON.stringify({
|
|
3747
|
+
error: recommendation.reason,
|
|
3748
|
+
...(probeErrors.length > 0 ? { probeErrors } : {}),
|
|
3749
|
+
}, null, 2));
|
|
3750
|
+
}
|
|
3751
|
+
else {
|
|
3752
|
+
console.log(`No best account available: ${recommendation.reason}`);
|
|
3753
|
+
printProbeNotes();
|
|
3754
|
+
}
|
|
3755
|
+
return 1;
|
|
3756
|
+
}
|
|
3757
|
+
const bestIndex = recommendation.recommendedIndex;
|
|
3758
|
+
const bestAccount = storage.accounts[bestIndex];
|
|
3759
|
+
if (!bestAccount) {
|
|
3760
|
+
await persistProbeChangesIfNeeded();
|
|
3761
|
+
if (options.json) {
|
|
3762
|
+
console.log(JSON.stringify({ error: "Best account not found." }, null, 2));
|
|
3763
|
+
}
|
|
3764
|
+
else {
|
|
3765
|
+
console.log("Best account not found.");
|
|
3766
|
+
}
|
|
3767
|
+
return 1;
|
|
3768
|
+
}
|
|
3769
|
+
// Check if already on best account
|
|
3770
|
+
const currentIndex = resolveActiveIndex(storage, "codex");
|
|
3771
|
+
if (currentIndex === bestIndex) {
|
|
3772
|
+
const shouldSyncCurrentBest = probeRefreshedIndices.has(bestIndex) || probeIdTokenByIndex.has(bestIndex);
|
|
3773
|
+
let alreadyBestSynced;
|
|
3774
|
+
if (changed) {
|
|
3775
|
+
bestAccount.lastUsed = now;
|
|
3776
|
+
await persistProbeChangesIfNeeded();
|
|
3777
|
+
}
|
|
3778
|
+
if (shouldSyncCurrentBest) {
|
|
3779
|
+
alreadyBestSynced = await setCodexCliActiveSelection({
|
|
3780
|
+
accountId: bestAccount.accountId,
|
|
3781
|
+
email: bestAccount.email,
|
|
3782
|
+
accessToken: bestAccount.accessToken,
|
|
3783
|
+
refreshToken: bestAccount.refreshToken,
|
|
3784
|
+
expiresAt: bestAccount.expiresAt,
|
|
3785
|
+
...(probeIdTokenByIndex.has(bestIndex)
|
|
3786
|
+
? { idToken: probeIdTokenByIndex.get(bestIndex) }
|
|
3787
|
+
: {}),
|
|
3788
|
+
});
|
|
3789
|
+
if (!alreadyBestSynced && !options.json) {
|
|
3790
|
+
console.warn("Codex auth sync did not complete. Multi-auth routing will still use this account.");
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
if (options.json) {
|
|
3794
|
+
console.log(JSON.stringify({
|
|
3795
|
+
message: `Already on best account: ${formatAccountLabel(bestAccount, bestIndex)}`,
|
|
3796
|
+
accountIndex: bestIndex + 1,
|
|
3797
|
+
reason: recommendation.reason,
|
|
3798
|
+
...(alreadyBestSynced !== undefined ? { synced: alreadyBestSynced } : {}),
|
|
3799
|
+
...(probeErrors.length > 0 ? { probeErrors } : {}),
|
|
3800
|
+
}, null, 2));
|
|
3801
|
+
}
|
|
3802
|
+
else {
|
|
3803
|
+
console.log(`Already on best account ${bestIndex + 1}: ${formatAccountLabel(bestAccount, bestIndex)}`);
|
|
3804
|
+
console.log(`Reason: ${recommendation.reason}`);
|
|
3805
|
+
printProbeNotes();
|
|
3806
|
+
}
|
|
3807
|
+
return 0;
|
|
3808
|
+
}
|
|
3809
|
+
const targetIndex = bestIndex;
|
|
3810
|
+
const parsed = targetIndex + 1;
|
|
3811
|
+
const { synced, wasDisabled } = await persistAndSyncSelectedAccount({
|
|
3812
|
+
storage,
|
|
3813
|
+
targetIndex,
|
|
3814
|
+
parsed,
|
|
3815
|
+
switchReason: "best",
|
|
3816
|
+
initialSyncIdToken: probeIdTokenByIndex.get(bestIndex),
|
|
3817
|
+
});
|
|
3818
|
+
if (options.json) {
|
|
3819
|
+
console.log(JSON.stringify({
|
|
3820
|
+
message: `Switched to best account: ${formatAccountLabel(bestAccount, targetIndex)}`,
|
|
3821
|
+
accountIndex: parsed,
|
|
3822
|
+
reason: recommendation.reason,
|
|
3823
|
+
synced,
|
|
3824
|
+
wasDisabled,
|
|
3825
|
+
...(probeErrors.length > 0 ? { probeErrors } : {}),
|
|
3826
|
+
}, null, 2));
|
|
3827
|
+
}
|
|
3828
|
+
else {
|
|
3829
|
+
console.log(`Switched to best account ${parsed}: ${formatAccountLabel(bestAccount, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
|
|
3830
|
+
console.log(`Reason: ${recommendation.reason}`);
|
|
3831
|
+
printProbeNotes();
|
|
3832
|
+
if (!synced) {
|
|
3833
|
+
console.warn("Codex auth sync did not complete. Multi-auth routing will still use this account.");
|
|
3834
|
+
}
|
|
3409
3835
|
}
|
|
3410
|
-
console.log(`Switched to account ${parsed}: ${formatAccountLabel(account, targetIndex)}${wasDisabled ? " (re-enabled)" : ""}`);
|
|
3411
3836
|
return 0;
|
|
3412
3837
|
}
|
|
3413
3838
|
export async function autoSyncActiveAccountToCodex() {
|
|
@@ -3451,9 +3876,7 @@ export async function autoSyncActiveAccountToCodex() {
|
|
|
3451
3876
|
account.email = nextEmail;
|
|
3452
3877
|
changed = true;
|
|
3453
3878
|
}
|
|
3454
|
-
if (
|
|
3455
|
-
account.accountId = tokenAccountId;
|
|
3456
|
-
account.accountIdSource = "token";
|
|
3879
|
+
if (applyTokenAccountIdentity(account, tokenAccountId)) {
|
|
3457
3880
|
changed = true;
|
|
3458
3881
|
}
|
|
3459
3882
|
syncAccessToken = refreshResult.access;
|
|
@@ -3519,6 +3942,9 @@ export async function runCodexMultiAuthCli(rawArgs) {
|
|
|
3519
3942
|
if (command === "forecast") {
|
|
3520
3943
|
return runForecast(rest);
|
|
3521
3944
|
}
|
|
3945
|
+
if (command === "best") {
|
|
3946
|
+
return runBest(rest);
|
|
3947
|
+
}
|
|
3522
3948
|
if (command === "report") {
|
|
3523
3949
|
return runReport(rest);
|
|
3524
3950
|
}
|