codex-multi-auth 1.2.1 → 1.2.2
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 +19 -8
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +10 -0
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +312 -69
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/circuit-breaker.d.ts +4 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -1
- package/dist/lib/circuit-breaker.js +33 -1
- package/dist/lib/circuit-breaker.js.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.d.ts +2 -1
- package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.js +92 -2
- package/dist/lib/codex-manager/commands/forecast.js.map +1 -1
- package/dist/lib/codex-manager/commands/report.d.ts +2 -1
- package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/report.js +87 -2
- package/dist/lib/codex-manager/commands/report.js.map +1 -1
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +2 -0
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/health.d.ts +1 -0
- package/dist/lib/health.d.ts.map +1 -1
- package/dist/lib/health.js +6 -2
- package/dist/lib/health.js.map +1 -1
- package/dist/lib/parallel-probe.d.ts +1 -1
- package/dist/lib/parallel-probe.d.ts.map +1 -1
- package/dist/lib/parallel-probe.js +5 -2
- package/dist/lib/parallel-probe.js.map +1 -1
- package/dist/lib/proactive-refresh.d.ts +1 -1
- package/dist/lib/proactive-refresh.d.ts.map +1 -1
- package/dist/lib/proactive-refresh.js +14 -5
- package/dist/lib/proactive-refresh.js.map +1 -1
- package/dist/lib/refresh-guardian.d.ts +3 -0
- package/dist/lib/refresh-guardian.d.ts.map +1 -1
- package/dist/lib/refresh-guardian.js +142 -52
- package/dist/lib/refresh-guardian.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +64 -13
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/rotation.d.ts +14 -11
- package/dist/lib/rotation.d.ts.map +1 -1
- package/dist/lib/rotation.js +36 -30
- package/dist/lib/rotation.js.map +1 -1
- package/dist/lib/storage/identity.d.ts +6 -0
- package/dist/lib/storage/identity.d.ts.map +1 -1
- package/dist/lib/storage/identity.js +14 -3
- package/dist/lib/storage/identity.js.map +1 -1
- package/package.json +1 -1
- package/dist/lib/codex-manager/best.d.ts +0 -26
- package/dist/lib/codex-manager/best.d.ts.map +0 -1
- package/dist/lib/codex-manager/best.js +0 -249
- package/dist/lib/codex-manager/best.js.map +0 -1
- package/dist/lib/codex-manager/config-explain.d.ts +0 -2
- package/dist/lib/codex-manager/config-explain.d.ts.map +0 -1
- package/dist/lib/codex-manager/config-explain.js +0 -62
- package/dist/lib/codex-manager/config-explain.js.map +0 -1
- package/dist/lib/codex-manager/config-template.d.ts +0 -2
- package/dist/lib/codex-manager/config-template.d.ts.map +0 -1
- package/dist/lib/codex-manager/config-template.js +0 -112
- package/dist/lib/codex-manager/config-template.js.map +0 -1
- package/dist/lib/codex-manager/debug-bundle.d.ts +0 -2
- package/dist/lib/codex-manager/debug-bundle.d.ts.map +0 -1
- package/dist/lib/codex-manager/debug-bundle.js +0 -128
- package/dist/lib/codex-manager/debug-bundle.js.map +0 -1
- package/dist/lib/codex-manager/doctor.d.ts +0 -20
- package/dist/lib/codex-manager/doctor.d.ts.map +0 -1
- package/dist/lib/codex-manager/doctor.js +0 -546
- package/dist/lib/codex-manager/doctor.js.map +0 -1
- package/dist/lib/codex-manager/fix.d.ts +0 -26
- package/dist/lib/codex-manager/fix.d.ts.map +0 -1
- package/dist/lib/codex-manager/fix.js +0 -366
- package/dist/lib/codex-manager/fix.js.map +0 -1
- package/dist/lib/codex-manager/forecast.d.ts +0 -26
- package/dist/lib/codex-manager/forecast.d.ts.map +0 -1
- package/dist/lib/codex-manager/forecast.js +0 -218
- package/dist/lib/codex-manager/forecast.js.map +0 -1
- package/dist/lib/codex-manager/info.d.ts +0 -11
- package/dist/lib/codex-manager/info.d.ts.map +0 -1
- package/dist/lib/codex-manager/info.js +0 -91
- package/dist/lib/codex-manager/info.js.map +0 -1
- package/dist/lib/codex-manager/login.d.ts +0 -40
- package/dist/lib/codex-manager/login.d.ts.map +0 -1
- package/dist/lib/codex-manager/login.js +0 -1041
- package/dist/lib/codex-manager/login.js.map +0 -1
- package/dist/lib/codex-manager/report.d.ts +0 -39
- package/dist/lib/codex-manager/report.d.ts.map +0 -1
- package/dist/lib/codex-manager/report.js +0 -243
- package/dist/lib/codex-manager/report.js.map +0 -1
- package/dist/lib/codex-manager/switch.d.ts +0 -31
- package/dist/lib/codex-manager/switch.d.ts.map +0 -1
- package/dist/lib/codex-manager/switch.js +0 -103
- package/dist/lib/codex-manager/switch.js.map +0 -1
- package/dist/lib/codex-manager/verify-flagged.d.ts +0 -20
- package/dist/lib/codex-manager/verify-flagged.d.ts.map +0 -1
- package/dist/lib/codex-manager/verify-flagged.js +0 -316
- package/dist/lib/codex-manager/verify-flagged.js.map +0 -1
- package/dist/lib/request/trace-mode.d.ts +0 -9
- package/dist/lib/request/trace-mode.d.ts.map +0 -1
- package/dist/lib/request/trace-mode.js +0 -14
- package/dist/lib/request/trace-mode.js.map +0 -1
package/dist/lib/accounts.js
CHANGED
|
@@ -1,20 +1,112 @@
|
|
|
1
1
|
import { createLogger } from "./logger.js";
|
|
2
|
-
import { loadAccounts, saveAccounts, findMatchingAccountIndex, } from "./storage.js";
|
|
2
|
+
import { loadAccounts, saveAccounts, findMatchingAccountIndex, withAccountStorageTransaction, } from "./storage.js";
|
|
3
3
|
import { MODEL_FAMILIES } from "./prompts/codex.js";
|
|
4
4
|
import { getHealthTracker, getTokenTracker, selectHybridAccount, } from "./rotation.js";
|
|
5
5
|
import { nowMs } from "./utils.js";
|
|
6
|
+
import { ERROR_MESSAGES, HTTP_STATUS } from "./constants.js";
|
|
7
|
+
import { CodexAuthError } from "./errors.js";
|
|
6
8
|
import { loadCodexCliState, } from "./codex-cli/state.js";
|
|
7
9
|
import { syncAccountStorageFromCodexCli } from "./codex-cli/sync.js";
|
|
8
10
|
import { setCodexCliActiveSelection } from "./codex-cli/writer.js";
|
|
11
|
+
import { getAccountIdentityKey, getRuntimeAccountIdentityKey, } from "./storage/identity.js";
|
|
12
|
+
import { getCircuitBreaker } from "./circuit-breaker.js";
|
|
9
13
|
export { extractAccountId, extractAccountEmail, getAccountIdCandidates, selectBestAccountCandidate, resolveRuntimeRequestIdentity, shouldUpdateAccountIdFromToken, resolveRequestAccountId, sanitizeEmail, } from "./auth/token-utils.js";
|
|
10
14
|
export { parseRateLimitReason, getQuotaKey, clampNonNegativeInt, clearExpiredRateLimits, isRateLimitedForQuotaKey, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
|
|
11
15
|
export { lookupCodexCliTokensByEmail, isCodexCliSyncEnabled, } from "./codex-cli/state.js";
|
|
12
16
|
import { extractAccountId, extractAccountEmail, shouldUpdateAccountIdFromToken, sanitizeEmail, } from "./auth/token-utils.js";
|
|
13
17
|
import { clampNonNegativeInt, getQuotaKey, clearExpiredRateLimits, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
|
|
14
18
|
const log = createLogger("accounts");
|
|
19
|
+
let nextRuntimeCircuitKeyId = 0;
|
|
20
|
+
function getAccountCircuitKey(account) {
|
|
21
|
+
if (!account.circuitKeyId) {
|
|
22
|
+
account.circuitKeyId =
|
|
23
|
+
getAccountIdentityKey(account) ?? `circuit:${nextRuntimeCircuitKeyId++}`;
|
|
24
|
+
}
|
|
25
|
+
return account.circuitKeyId;
|
|
26
|
+
}
|
|
27
|
+
export function getRuntimeTrackerKey(account) {
|
|
28
|
+
if (account._runtimeTrackerKey !== undefined) {
|
|
29
|
+
return account._runtimeTrackerKey;
|
|
30
|
+
}
|
|
31
|
+
const trackerKey = getRuntimeAccountIdentityKey(account) ?? account.index;
|
|
32
|
+
account._runtimeTrackerKey = trackerKey;
|
|
33
|
+
return trackerKey;
|
|
34
|
+
}
|
|
15
35
|
function initFamilyState(defaultValue) {
|
|
16
36
|
return Object.fromEntries(MODEL_FAMILIES.map((family) => [family, defaultValue]));
|
|
17
37
|
}
|
|
38
|
+
function getAuthIdentityCandidate(auth) {
|
|
39
|
+
const accountId = extractAccountId(auth?.access)?.trim() || undefined;
|
|
40
|
+
const email = sanitizeEmail(extractAccountEmail(auth?.access));
|
|
41
|
+
return {
|
|
42
|
+
accountId,
|
|
43
|
+
email,
|
|
44
|
+
refreshToken: auth?.refresh,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function buildAccountIdentityCandidates(source, auth) {
|
|
48
|
+
const derived = getAuthIdentityCandidate(auth);
|
|
49
|
+
const candidates = [];
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const pushCandidate = (candidate) => {
|
|
52
|
+
const key = `${candidate.accountId ?? ""}|${candidate.email ?? ""}|${candidate.refreshToken ?? ""}`;
|
|
53
|
+
if (seen.has(key))
|
|
54
|
+
return;
|
|
55
|
+
seen.add(key);
|
|
56
|
+
candidates.push(candidate);
|
|
57
|
+
};
|
|
58
|
+
pushCandidate(source);
|
|
59
|
+
pushCandidate({
|
|
60
|
+
accountId: source.accountId ?? derived.accountId,
|
|
61
|
+
email: source.email ?? derived.email,
|
|
62
|
+
refreshToken: source.refreshToken,
|
|
63
|
+
index: source.index,
|
|
64
|
+
});
|
|
65
|
+
pushCandidate({
|
|
66
|
+
accountId: derived.accountId ?? source.accountId,
|
|
67
|
+
email: derived.email ?? source.email,
|
|
68
|
+
refreshToken: source.refreshToken,
|
|
69
|
+
index: source.index,
|
|
70
|
+
});
|
|
71
|
+
pushCandidate({
|
|
72
|
+
accountId: derived.accountId ?? source.accountId,
|
|
73
|
+
email: derived.email ?? source.email,
|
|
74
|
+
refreshToken: derived.refreshToken ?? source.refreshToken,
|
|
75
|
+
index: source.index,
|
|
76
|
+
});
|
|
77
|
+
return candidates;
|
|
78
|
+
}
|
|
79
|
+
function findAccountIndexByIdentity(accounts, source, auth) {
|
|
80
|
+
for (const candidate of buildAccountIdentityCandidates(source, auth)) {
|
|
81
|
+
const matchIndex = findMatchingAccountIndex(accounts, candidate, {
|
|
82
|
+
allowUniqueAccountIdFallbackWithoutEmail: true,
|
|
83
|
+
});
|
|
84
|
+
if (matchIndex !== undefined) {
|
|
85
|
+
return matchIndex;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const RETRYABLE_AUTH_PERSISTENCE_CODES = new Set(["EAGAIN", "EBUSY", "EPERM"]);
|
|
91
|
+
function isRetryableAuthPersistenceError(error) {
|
|
92
|
+
if (!error || typeof error !== "object") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const candidate = error;
|
|
96
|
+
const code = typeof candidate.code === "string"
|
|
97
|
+
? candidate.code.toUpperCase()
|
|
98
|
+
: undefined;
|
|
99
|
+
if (code && RETRYABLE_AUTH_PERSISTENCE_CODES.has(code)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (candidate.status === HTTP_STATUS.TOO_MANY_REQUESTS) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (candidate.cause && candidate.cause !== error) {
|
|
106
|
+
return isRetryableAuthPersistenceError(candidate.cause);
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
18
110
|
export class AccountManager {
|
|
19
111
|
accounts = [];
|
|
20
112
|
cursorByFamily = initFamilyState(0);
|
|
@@ -227,10 +319,16 @@ export class AccountManager {
|
|
|
227
319
|
return index;
|
|
228
320
|
}
|
|
229
321
|
getAccountsSnapshot() {
|
|
230
|
-
return this.accounts.map((account) =>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
322
|
+
return this.accounts.map((account) => {
|
|
323
|
+
const trackerKey = getRuntimeTrackerKey(account);
|
|
324
|
+
const circuitKeyId = getAccountCircuitKey(account);
|
|
325
|
+
return {
|
|
326
|
+
...account,
|
|
327
|
+
_runtimeTrackerKey: trackerKey,
|
|
328
|
+
circuitKeyId,
|
|
329
|
+
rateLimitResetTimes: { ...account.rateLimitResetTimes },
|
|
330
|
+
};
|
|
331
|
+
});
|
|
234
332
|
}
|
|
235
333
|
getAccountByIndex(index) {
|
|
236
334
|
if (!Number.isFinite(index))
|
|
@@ -247,7 +345,9 @@ export class AccountManager {
|
|
|
247
345
|
if (account.enabled === false)
|
|
248
346
|
return false;
|
|
249
347
|
clearExpiredRateLimits(account);
|
|
250
|
-
return !isRateLimitedForFamily(account, family, model) &&
|
|
348
|
+
return (!isRateLimitedForFamily(account, family, model) &&
|
|
349
|
+
!this.isAccountCoolingDown(account) &&
|
|
350
|
+
this.isCircuitAvailable(account));
|
|
251
351
|
}
|
|
252
352
|
setActiveIndex(index) {
|
|
253
353
|
if (!Number.isFinite(index))
|
|
@@ -314,7 +414,9 @@ export class AccountManager {
|
|
|
314
414
|
if (account.enabled === false)
|
|
315
415
|
continue;
|
|
316
416
|
clearExpiredRateLimits(account);
|
|
317
|
-
if (isRateLimitedForFamily(account, family, model) ||
|
|
417
|
+
if (isRateLimitedForFamily(account, family, model) ||
|
|
418
|
+
this.isAccountCoolingDown(account) ||
|
|
419
|
+
!this.isCircuitAvailable(account)) {
|
|
318
420
|
continue;
|
|
319
421
|
}
|
|
320
422
|
this.cursorByFamily[family] = (idx + 1) % count;
|
|
@@ -337,7 +439,9 @@ export class AccountManager {
|
|
|
337
439
|
if (account.enabled === false)
|
|
338
440
|
continue;
|
|
339
441
|
clearExpiredRateLimits(account);
|
|
340
|
-
if (isRateLimitedForFamily(account, family, model) ||
|
|
442
|
+
if (isRateLimitedForFamily(account, family, model) ||
|
|
443
|
+
this.isAccountCoolingDown(account) ||
|
|
444
|
+
!this.isCircuitAvailable(account)) {
|
|
341
445
|
continue;
|
|
342
446
|
}
|
|
343
447
|
this.cursorByFamily[family] = (idx + 1) % count;
|
|
@@ -350,23 +454,6 @@ export class AccountManager {
|
|
|
350
454
|
const count = this.accounts.length;
|
|
351
455
|
if (count === 0)
|
|
352
456
|
return null;
|
|
353
|
-
const currentIndex = this.currentAccountIndexByFamily[family];
|
|
354
|
-
if (currentIndex >= 0 && currentIndex < count) {
|
|
355
|
-
const currentAccount = this.accounts[currentIndex];
|
|
356
|
-
if (currentAccount) {
|
|
357
|
-
if (currentAccount.enabled === false) {
|
|
358
|
-
// Fall through to hybrid selection.
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
clearExpiredRateLimits(currentAccount);
|
|
362
|
-
if (!isRateLimitedForFamily(currentAccount, family, model) &&
|
|
363
|
-
!this.isAccountCoolingDown(currentAccount)) {
|
|
364
|
-
currentAccount.lastUsed = nowMs();
|
|
365
|
-
return currentAccount;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
457
|
const quotaKey = model ? `${family}:${model}` : family;
|
|
371
458
|
const healthTracker = getHealthTracker();
|
|
372
459
|
const tokenTracker = getTokenTracker();
|
|
@@ -377,9 +464,12 @@ export class AccountManager {
|
|
|
377
464
|
if (account.enabled === false)
|
|
378
465
|
return null;
|
|
379
466
|
clearExpiredRateLimits(account);
|
|
380
|
-
const isAvailable = !isRateLimitedForFamily(account, family, model) &&
|
|
467
|
+
const isAvailable = !isRateLimitedForFamily(account, family, model) &&
|
|
468
|
+
!this.isAccountCoolingDown(account) &&
|
|
469
|
+
this.isCircuitAvailable(account);
|
|
381
470
|
return {
|
|
382
471
|
index: account.index,
|
|
472
|
+
trackerKey: getRuntimeTrackerKey(account),
|
|
383
473
|
isAvailable,
|
|
384
474
|
lastUsed: account.lastUsed,
|
|
385
475
|
};
|
|
@@ -399,24 +489,53 @@ export class AccountManager {
|
|
|
399
489
|
recordSuccess(account, family, model) {
|
|
400
490
|
const quotaKey = model ? `${family}:${model}` : family;
|
|
401
491
|
const healthTracker = getHealthTracker();
|
|
402
|
-
healthTracker.recordSuccess(account
|
|
492
|
+
healthTracker.recordSuccess(getRuntimeTrackerKey(account), quotaKey);
|
|
493
|
+
const hadCooldownMetadata = account.coolingDownUntil !== undefined || account.cooldownReason !== undefined;
|
|
494
|
+
const hadAuthFailures = (account.consecutiveAuthFailures ?? 0) > 0;
|
|
495
|
+
const isCoolingDown = this.isAccountCoolingDown(account);
|
|
496
|
+
let healed = false;
|
|
497
|
+
if (!isCoolingDown && hadCooldownMetadata) {
|
|
498
|
+
this.clearAccountCooldown(account);
|
|
499
|
+
healed = true;
|
|
500
|
+
}
|
|
501
|
+
if (!isCoolingDown && hadAuthFailures) {
|
|
502
|
+
this.clearAuthFailures(account);
|
|
503
|
+
healed = true;
|
|
504
|
+
}
|
|
505
|
+
if (healed) {
|
|
506
|
+
this.saveToDiskDebounced();
|
|
507
|
+
}
|
|
508
|
+
getCircuitBreaker(getAccountCircuitKey(account)).recordSuccess();
|
|
403
509
|
}
|
|
404
510
|
recordRateLimit(account, family, model) {
|
|
405
511
|
const quotaKey = model ? `${family}:${model}` : family;
|
|
406
512
|
const healthTracker = getHealthTracker();
|
|
407
513
|
const tokenTracker = getTokenTracker();
|
|
408
|
-
|
|
409
|
-
|
|
514
|
+
const trackerKey = getRuntimeTrackerKey(account);
|
|
515
|
+
healthTracker.recordRateLimit(trackerKey, quotaKey);
|
|
516
|
+
tokenTracker.drain(trackerKey, quotaKey);
|
|
410
517
|
}
|
|
411
518
|
recordFailure(account, family, model) {
|
|
412
519
|
const quotaKey = model ? `${family}:${model}` : family;
|
|
413
520
|
const healthTracker = getHealthTracker();
|
|
414
|
-
healthTracker.recordFailure(account
|
|
521
|
+
healthTracker.recordFailure(getRuntimeTrackerKey(account), quotaKey);
|
|
522
|
+
getCircuitBreaker(getAccountCircuitKey(account)).recordFailure();
|
|
415
523
|
}
|
|
416
524
|
consumeToken(account, family, model) {
|
|
417
525
|
const quotaKey = model ? `${family}:${model}` : family;
|
|
418
526
|
const tokenTracker = getTokenTracker();
|
|
419
|
-
|
|
527
|
+
const trackerKey = getRuntimeTrackerKey(account);
|
|
528
|
+
if (!tokenTracker.tryConsume(trackerKey, quotaKey)) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
getCircuitBreaker(getAccountCircuitKey(account)).canExecute();
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
tokenTracker.refundToken(trackerKey, quotaKey);
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
420
539
|
}
|
|
421
540
|
/**
|
|
422
541
|
* Refund a token consumed within the refund window (30 seconds).
|
|
@@ -426,7 +545,7 @@ export class AccountManager {
|
|
|
426
545
|
refundToken(account, family, model) {
|
|
427
546
|
const quotaKey = model ? `${family}:${model}` : family;
|
|
428
547
|
const tokenTracker = getTokenTracker();
|
|
429
|
-
return tokenTracker.refundToken(account
|
|
548
|
+
return tokenTracker.refundToken(getRuntimeTrackerKey(account), quotaKey);
|
|
430
549
|
}
|
|
431
550
|
markSwitched(account, reason, family) {
|
|
432
551
|
account.lastSwitchReason = reason;
|
|
@@ -439,8 +558,11 @@ export class AccountManager {
|
|
|
439
558
|
const retryMs = Math.max(0, Math.floor(retryAfterMs));
|
|
440
559
|
const resetAt = nowMs() + retryMs;
|
|
441
560
|
const baseKey = getQuotaKey(family);
|
|
442
|
-
|
|
443
|
-
|
|
561
|
+
if (!model || reason === "quota" || reason === "unknown") {
|
|
562
|
+
account.rateLimitResetTimes[baseKey] = resetAt;
|
|
563
|
+
}
|
|
564
|
+
if (model &&
|
|
565
|
+
(reason === "tokens" || reason === "concurrent" || reason === "unknown")) {
|
|
444
566
|
const modelKey = getQuotaKey(family, model);
|
|
445
567
|
account.rateLimitResetTimes[modelKey] = resetAt;
|
|
446
568
|
}
|
|
@@ -464,6 +586,9 @@ export class AccountManager {
|
|
|
464
586
|
delete account.coolingDownUntil;
|
|
465
587
|
delete account.cooldownReason;
|
|
466
588
|
}
|
|
589
|
+
isCircuitAvailable(account) {
|
|
590
|
+
return getCircuitBreaker(getAccountCircuitKey(account)).isAvailable();
|
|
591
|
+
}
|
|
467
592
|
incrementAuthFailures(account) {
|
|
468
593
|
account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
|
|
469
594
|
return account.consecutiveAuthFailures;
|
|
@@ -471,6 +596,13 @@ export class AccountManager {
|
|
|
471
596
|
clearAuthFailures(account) {
|
|
472
597
|
account.consecutiveAuthFailures = 0;
|
|
473
598
|
}
|
|
599
|
+
getAccountByIdentity(candidate, auth) {
|
|
600
|
+
const index = findAccountIndexByIdentity(this.accounts, candidate, auth);
|
|
601
|
+
if (index === undefined) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
return this.accounts[index] ?? null;
|
|
605
|
+
}
|
|
474
606
|
shouldShowAccountToast(accountIndex, debounceMs = 30000) {
|
|
475
607
|
const now = nowMs();
|
|
476
608
|
if (accountIndex === this.lastToastAccountIndex && now - this.lastToastTime < debounceMs) {
|
|
@@ -486,7 +618,7 @@ export class AccountManager {
|
|
|
486
618
|
account.refreshToken = auth.refresh;
|
|
487
619
|
account.access = auth.access;
|
|
488
620
|
account.expires = auth.expires;
|
|
489
|
-
const tokenAccountId = extractAccountId(auth.access);
|
|
621
|
+
const tokenAccountId = extractAccountId(auth.access)?.trim() || undefined;
|
|
490
622
|
if (tokenAccountId &&
|
|
491
623
|
(shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId))) {
|
|
492
624
|
account.accountId = tokenAccountId;
|
|
@@ -494,6 +626,134 @@ export class AccountManager {
|
|
|
494
626
|
}
|
|
495
627
|
account.email = sanitizeEmail(extractAccountEmail(auth.access)) ?? account.email;
|
|
496
628
|
}
|
|
629
|
+
buildStorageSnapshot() {
|
|
630
|
+
const activeIndexByFamily = {};
|
|
631
|
+
for (const family of MODEL_FAMILIES) {
|
|
632
|
+
const raw = this.currentAccountIndexByFamily[family];
|
|
633
|
+
activeIndexByFamily[family] = clampNonNegativeInt(raw, 0);
|
|
634
|
+
}
|
|
635
|
+
const activeIndex = clampNonNegativeInt(activeIndexByFamily.codex, 0);
|
|
636
|
+
return {
|
|
637
|
+
version: 3,
|
|
638
|
+
accounts: this.accounts.map((account) => ({
|
|
639
|
+
accountId: account.accountId,
|
|
640
|
+
accountIdSource: account.accountIdSource,
|
|
641
|
+
accountLabel: account.accountLabel,
|
|
642
|
+
email: account.email,
|
|
643
|
+
refreshToken: account.refreshToken,
|
|
644
|
+
accessToken: account.access,
|
|
645
|
+
expiresAt: account.expires,
|
|
646
|
+
enabled: account.enabled === false ? false : undefined,
|
|
647
|
+
addedAt: account.addedAt,
|
|
648
|
+
lastUsed: account.lastUsed,
|
|
649
|
+
lastSwitchReason: account.lastSwitchReason,
|
|
650
|
+
rateLimitResetTimes: Object.keys(account.rateLimitResetTimes).length > 0 ? account.rateLimitResetTimes : undefined,
|
|
651
|
+
coolingDownUntil: account.coolingDownUntil,
|
|
652
|
+
cooldownReason: account.cooldownReason,
|
|
653
|
+
workspaces: account.workspaces,
|
|
654
|
+
currentWorkspaceIndex: account.currentWorkspaceIndex,
|
|
655
|
+
})),
|
|
656
|
+
activeIndex,
|
|
657
|
+
activeIndexByFamily,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
async commitRefreshedAuth(source, auth) {
|
|
661
|
+
const nextAccountId = extractAccountId(auth.access)?.trim() || undefined;
|
|
662
|
+
const nextEmail = sanitizeEmail(extractAccountEmail(auth.access));
|
|
663
|
+
try {
|
|
664
|
+
return await withAccountStorageTransaction(async (_current, persist) => {
|
|
665
|
+
// Snapshot the live in-memory pool under the storage lock so refresh
|
|
666
|
+
// persistence merges against the latest account state.
|
|
667
|
+
const nextStorage = structuredClone(this.buildStorageSnapshot());
|
|
668
|
+
const storageIndex = findAccountIndexByIdentity(nextStorage.accounts, source, auth);
|
|
669
|
+
if (storageIndex === undefined) {
|
|
670
|
+
log.warn("Unable to resolve refreshed account for persistence", {
|
|
671
|
+
sourceIndex: source.index,
|
|
672
|
+
});
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
const storedAccount = nextStorage.accounts[storageIndex];
|
|
676
|
+
if (!storedAccount) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
storedAccount.refreshToken = auth.refresh;
|
|
680
|
+
storedAccount.accessToken = auth.access;
|
|
681
|
+
storedAccount.expiresAt = auth.expires;
|
|
682
|
+
if (nextAccountId &&
|
|
683
|
+
shouldUpdateAccountIdFromToken(storedAccount.accountIdSource, storedAccount.accountId)) {
|
|
684
|
+
storedAccount.accountId = nextAccountId;
|
|
685
|
+
storedAccount.accountIdSource = "token";
|
|
686
|
+
}
|
|
687
|
+
if (nextEmail) {
|
|
688
|
+
storedAccount.email = nextEmail;
|
|
689
|
+
}
|
|
690
|
+
storedAccount.enabled = undefined;
|
|
691
|
+
delete storedAccount.coolingDownUntil;
|
|
692
|
+
delete storedAccount.cooldownReason;
|
|
693
|
+
const liveAccount = this.getAccountByIdentity(source, auth);
|
|
694
|
+
if (liveAccount) {
|
|
695
|
+
const previousLiveAccountState = {
|
|
696
|
+
access: liveAccount.access,
|
|
697
|
+
refreshToken: liveAccount.refreshToken,
|
|
698
|
+
expires: liveAccount.expires,
|
|
699
|
+
accountId: liveAccount.accountId,
|
|
700
|
+
accountIdSource: liveAccount.accountIdSource,
|
|
701
|
+
email: liveAccount.email,
|
|
702
|
+
enabled: liveAccount.enabled,
|
|
703
|
+
coolingDownUntil: liveAccount.coolingDownUntil,
|
|
704
|
+
cooldownReason: liveAccount.cooldownReason,
|
|
705
|
+
consecutiveAuthFailures: liveAccount.consecutiveAuthFailures,
|
|
706
|
+
};
|
|
707
|
+
this.updateFromAuth(liveAccount, auth);
|
|
708
|
+
liveAccount.enabled = true;
|
|
709
|
+
this.clearAccountCooldown(liveAccount);
|
|
710
|
+
this.clearAuthFailures(liveAccount);
|
|
711
|
+
try {
|
|
712
|
+
await persist(nextStorage);
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
liveAccount.access = previousLiveAccountState.access;
|
|
716
|
+
liveAccount.refreshToken = previousLiveAccountState.refreshToken;
|
|
717
|
+
liveAccount.expires = previousLiveAccountState.expires;
|
|
718
|
+
liveAccount.accountId = previousLiveAccountState.accountId;
|
|
719
|
+
liveAccount.accountIdSource =
|
|
720
|
+
previousLiveAccountState.accountIdSource;
|
|
721
|
+
liveAccount.email = previousLiveAccountState.email;
|
|
722
|
+
liveAccount.enabled = previousLiveAccountState.enabled;
|
|
723
|
+
liveAccount.consecutiveAuthFailures =
|
|
724
|
+
previousLiveAccountState.consecutiveAuthFailures;
|
|
725
|
+
if (previousLiveAccountState.coolingDownUntil === undefined) {
|
|
726
|
+
delete liveAccount.coolingDownUntil;
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
liveAccount.coolingDownUntil =
|
|
730
|
+
previousLiveAccountState.coolingDownUntil;
|
|
731
|
+
}
|
|
732
|
+
if (previousLiveAccountState.cooldownReason === undefined) {
|
|
733
|
+
delete liveAccount.cooldownReason;
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
liveAccount.cooldownReason =
|
|
737
|
+
previousLiveAccountState.cooldownReason;
|
|
738
|
+
}
|
|
739
|
+
throw error;
|
|
740
|
+
}
|
|
741
|
+
return liveAccount;
|
|
742
|
+
}
|
|
743
|
+
await persist(nextStorage);
|
|
744
|
+
log.warn("Unable to resolve refreshed live account after persistence", {
|
|
745
|
+
sourceIndex: source.index,
|
|
746
|
+
});
|
|
747
|
+
return null;
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
throw new CodexAuthError(ERROR_MESSAGES.TOKEN_REFRESH_FAILED, {
|
|
752
|
+
retryable: isRetryableAuthPersistenceError(error),
|
|
753
|
+
cause: error,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
497
757
|
toAuthDetails(account) {
|
|
498
758
|
return {
|
|
499
759
|
type: "oauth",
|
|
@@ -510,7 +770,9 @@ export class AccountManager {
|
|
|
510
770
|
const enabledAccounts = this.accounts.filter((account) => account.enabled !== false);
|
|
511
771
|
const available = enabledAccounts.filter((account) => {
|
|
512
772
|
clearExpiredRateLimits(account);
|
|
513
|
-
return !isRateLimitedForFamily(account, family, model) &&
|
|
773
|
+
return (!isRateLimitedForFamily(account, family, model) &&
|
|
774
|
+
!this.isAccountCoolingDown(account) &&
|
|
775
|
+
this.isCircuitAvailable(account));
|
|
514
776
|
});
|
|
515
777
|
if (available.length > 0)
|
|
516
778
|
return 0;
|
|
@@ -520,18 +782,26 @@ export class AccountManager {
|
|
|
520
782
|
const baseKey = getQuotaKey(family);
|
|
521
783
|
const modelKey = model ? getQuotaKey(family, model) : null;
|
|
522
784
|
for (const account of enabledAccounts) {
|
|
785
|
+
const perAccountWaitTimes = [];
|
|
523
786
|
const baseResetAt = account.rateLimitResetTimes[baseKey];
|
|
524
787
|
if (typeof baseResetAt === "number") {
|
|
525
|
-
|
|
788
|
+
perAccountWaitTimes.push(Math.max(0, baseResetAt - now));
|
|
526
789
|
}
|
|
527
790
|
if (modelKey) {
|
|
528
791
|
const modelResetAt = account.rateLimitResetTimes[modelKey];
|
|
529
792
|
if (typeof modelResetAt === "number") {
|
|
530
|
-
|
|
793
|
+
perAccountWaitTimes.push(Math.max(0, modelResetAt - now));
|
|
531
794
|
}
|
|
532
795
|
}
|
|
533
796
|
if (typeof account.coolingDownUntil === "number") {
|
|
534
|
-
|
|
797
|
+
perAccountWaitTimes.push(Math.max(0, account.coolingDownUntil - now));
|
|
798
|
+
}
|
|
799
|
+
const breakerWait = getCircuitBreaker(getAccountCircuitKey(account)).getTimeUntilAvailable();
|
|
800
|
+
if (breakerWait > 0) {
|
|
801
|
+
perAccountWaitTimes.push(breakerWait);
|
|
802
|
+
}
|
|
803
|
+
if (perAccountWaitTimes.length > 0) {
|
|
804
|
+
waitTimes.push(Math.max(...perAccountWaitTimes));
|
|
535
805
|
}
|
|
536
806
|
}
|
|
537
807
|
return waitTimes.length > 0 ? Math.min(...waitTimes) : 0;
|
|
@@ -596,36 +866,9 @@ export class AccountManager {
|
|
|
596
866
|
return account;
|
|
597
867
|
}
|
|
598
868
|
async saveToDisk() {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
activeIndexByFamily[family] = clampNonNegativeInt(raw, 0);
|
|
603
|
-
}
|
|
604
|
-
const activeIndex = clampNonNegativeInt(activeIndexByFamily.codex, 0);
|
|
605
|
-
const storage = {
|
|
606
|
-
version: 3,
|
|
607
|
-
accounts: this.accounts.map((account) => ({
|
|
608
|
-
accountId: account.accountId,
|
|
609
|
-
accountIdSource: account.accountIdSource,
|
|
610
|
-
accountLabel: account.accountLabel,
|
|
611
|
-
email: account.email,
|
|
612
|
-
refreshToken: account.refreshToken,
|
|
613
|
-
accessToken: account.access,
|
|
614
|
-
expiresAt: account.expires,
|
|
615
|
-
enabled: account.enabled === false ? false : undefined,
|
|
616
|
-
addedAt: account.addedAt,
|
|
617
|
-
lastUsed: account.lastUsed,
|
|
618
|
-
lastSwitchReason: account.lastSwitchReason,
|
|
619
|
-
rateLimitResetTimes: Object.keys(account.rateLimitResetTimes).length > 0 ? account.rateLimitResetTimes : undefined,
|
|
620
|
-
coolingDownUntil: account.coolingDownUntil,
|
|
621
|
-
cooldownReason: account.cooldownReason,
|
|
622
|
-
workspaces: account.workspaces,
|
|
623
|
-
currentWorkspaceIndex: account.currentWorkspaceIndex,
|
|
624
|
-
})),
|
|
625
|
-
activeIndex,
|
|
626
|
-
activeIndexByFamily,
|
|
627
|
-
};
|
|
628
|
-
await saveAccounts(storage);
|
|
869
|
+
await withAccountStorageTransaction(async (_current, persist) => {
|
|
870
|
+
await persist(this.buildStorageSnapshot());
|
|
871
|
+
});
|
|
629
872
|
}
|
|
630
873
|
saveToDiskDebounced(delayMs = 500) {
|
|
631
874
|
if (this.saveDebounceTimer) {
|