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