oc-codex-multi-auth 6.0.0 → 6.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +56 -18
- package/assets/readme-hero.svg +10 -10
- package/config/README.md +1 -1
- package/config/minimal-opencode.json +1 -1
- package/config/opencode-legacy.json +1 -1
- package/config/opencode-modern.json +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -2525
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts/persistence.d.ts +34 -0
- package/dist/lib/accounts/persistence.d.ts.map +1 -0
- package/dist/lib/accounts/persistence.js +153 -0
- package/dist/lib/accounts/persistence.js.map +1 -0
- package/dist/lib/accounts/recovery.d.ts +59 -0
- package/dist/lib/accounts/recovery.d.ts.map +1 -0
- package/dist/lib/accounts/recovery.js +229 -0
- package/dist/lib/accounts/recovery.js.map +1 -0
- package/dist/lib/accounts/rotation.d.ts +40 -0
- package/dist/lib/accounts/rotation.d.ts.map +1 -0
- package/dist/lib/accounts/rotation.js +204 -0
- package/dist/lib/accounts/rotation.js.map +1 -0
- package/dist/lib/accounts/state.d.ts +92 -0
- package/dist/lib/accounts/state.d.ts.map +1 -0
- package/dist/lib/accounts/state.js +366 -0
- package/dist/lib/accounts/state.js.map +1 -0
- package/dist/lib/accounts.d.ts +48 -91
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +121 -699
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/auth.d.ts +1 -1
- package/dist/lib/auth/auth.d.ts.map +1 -1
- package/dist/lib/auth/auth.js +5 -2
- package/dist/lib/auth/auth.js.map +1 -1
- package/dist/lib/auth/login-runner.d.ts +32 -0
- package/dist/lib/auth/login-runner.d.ts.map +1 -1
- package/dist/lib/auth/login-runner.js +79 -64
- package/dist/lib/auth/login-runner.js.map +1 -1
- package/dist/lib/auth/server.d.ts.map +1 -1
- package/dist/lib/auth/server.js +3 -8
- package/dist/lib/auth/server.js.map +1 -1
- package/dist/lib/circuit-breaker.d.ts +53 -2
- package/dist/lib/circuit-breaker.d.ts.map +1 -1
- package/dist/lib/circuit-breaker.js +65 -6
- package/dist/lib/circuit-breaker.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +77 -32
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/error-sentinels.d.ts +20 -0
- package/dist/lib/error-sentinels.d.ts.map +1 -0
- package/dist/lib/error-sentinels.js +28 -0
- package/dist/lib/error-sentinels.js.map +1 -0
- package/dist/lib/errors.d.ts +100 -1
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +116 -1
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/logger.d.ts +14 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +65 -5
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/oauth-constants.d.ts +18 -0
- package/dist/lib/oauth-constants.d.ts.map +1 -0
- package/dist/lib/oauth-constants.js +18 -0
- package/dist/lib/oauth-constants.js.map +1 -0
- package/dist/lib/oauth-success.d.ts +2 -0
- package/dist/lib/oauth-success.d.ts.map +1 -0
- package/dist/lib/oauth-success.html +711 -711
- package/dist/lib/oauth-success.js +714 -0
- package/dist/lib/oauth-success.js.map +1 -0
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +12 -3
- package/dist/lib/prompts/codex.js.map +1 -1
- package/dist/lib/prompts/opencode-codex.d.ts.map +1 -1
- package/dist/lib/prompts/opencode-codex.js +3 -2
- package/dist/lib/prompts/opencode-codex.js.map +1 -1
- package/dist/lib/recovery/hook.d.ts +43 -0
- package/dist/lib/recovery/hook.d.ts.map +1 -0
- package/dist/lib/recovery/hook.js +325 -0
- package/dist/lib/recovery/hook.js.map +1 -0
- package/dist/lib/recovery/index.d.ts +1 -0
- package/dist/lib/recovery/index.d.ts.map +1 -1
- package/dist/lib/recovery/index.js +1 -0
- package/dist/lib/recovery/index.js.map +1 -1
- package/dist/lib/recovery/storage.d.ts.map +1 -1
- package/dist/lib/recovery/storage.js +5 -1
- package/dist/lib/recovery/storage.js.map +1 -1
- package/dist/lib/recovery.d.ts +15 -30
- package/dist/lib/recovery.d.ts.map +1 -1
- package/dist/lib/recovery.js +14 -312
- package/dist/lib/recovery.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +1 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +58 -10
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/response-handler.d.ts.map +1 -1
- package/dist/lib/request/response-handler.js +8 -2
- package/dist/lib/request/response-handler.js.map +1 -1
- package/dist/lib/runtime.d.ts +101 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/lib/runtime.js +77 -0
- package/dist/lib/runtime.js.map +1 -0
- package/dist/lib/schemas.d.ts +164 -1
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +251 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/atomic-write.d.ts +31 -0
- package/dist/lib/storage/atomic-write.d.ts.map +1 -0
- package/dist/lib/storage/atomic-write.js +75 -0
- package/dist/lib/storage/atomic-write.js.map +1 -0
- package/dist/lib/storage/backup.d.ts +13 -0
- package/dist/lib/storage/backup.d.ts.map +1 -0
- package/dist/lib/storage/backup.js +59 -0
- package/dist/lib/storage/backup.js.map +1 -0
- package/dist/lib/storage/errors.d.ts +17 -0
- package/dist/lib/storage/errors.d.ts.map +1 -0
- package/dist/lib/storage/errors.js +38 -0
- package/dist/lib/storage/errors.js.map +1 -0
- package/dist/lib/storage/export-import.d.ts +84 -0
- package/dist/lib/storage/export-import.d.ts.map +1 -0
- package/dist/lib/storage/export-import.js +222 -0
- package/dist/lib/storage/export-import.js.map +1 -0
- package/dist/lib/storage/flagged.d.ts +29 -0
- package/dist/lib/storage/flagged.d.ts.map +1 -0
- package/dist/lib/storage/flagged.js +207 -0
- package/dist/lib/storage/flagged.js.map +1 -0
- package/dist/lib/storage/identity.d.ts +72 -0
- package/dist/lib/storage/identity.d.ts.map +1 -0
- package/dist/lib/storage/identity.js +230 -0
- package/dist/lib/storage/identity.js.map +1 -0
- package/dist/lib/storage/keychain.d.ts +146 -0
- package/dist/lib/storage/keychain.d.ts.map +1 -0
- package/dist/lib/storage/keychain.js +278 -0
- package/dist/lib/storage/keychain.js.map +1 -0
- package/dist/lib/storage/load-save.d.ts +61 -0
- package/dist/lib/storage/load-save.d.ts.map +1 -0
- package/dist/lib/storage/load-save.js +606 -0
- package/dist/lib/storage/load-save.js.map +1 -0
- package/dist/lib/storage/migrations.d.ts +35 -0
- package/dist/lib/storage/migrations.d.ts.map +1 -1
- package/dist/lib/storage/migrations.js +37 -0
- package/dist/lib/storage/migrations.js.map +1 -1
- package/dist/lib/storage/normalize.d.ts +22 -0
- package/dist/lib/storage/normalize.d.ts.map +1 -0
- package/dist/lib/storage/normalize.js +114 -0
- package/dist/lib/storage/normalize.js.map +1 -0
- package/dist/lib/storage/paths.d.ts.map +1 -1
- package/dist/lib/storage/paths.js +2 -1
- package/dist/lib/storage/paths.js.map +1 -1
- package/dist/lib/storage/state.d.ts +46 -0
- package/dist/lib/storage/state.d.ts.map +1 -0
- package/dist/lib/storage/state.js +100 -0
- package/dist/lib/storage/state.js.map +1 -0
- package/dist/lib/storage/worktree-lock.d.ts +79 -0
- package/dist/lib/storage/worktree-lock.d.ts.map +1 -0
- package/dist/lib/storage/worktree-lock.js +265 -0
- package/dist/lib/storage/worktree-lock.js.map +1 -0
- package/dist/lib/storage.d.ts +21 -155
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +24 -1167
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/tools/_shared.d.ts +16 -0
- package/dist/lib/tools/_shared.d.ts.map +1 -0
- package/dist/lib/tools/_shared.js +21 -0
- package/dist/lib/tools/_shared.js.map +1 -0
- package/dist/lib/tools/codex-dashboard.d.ts +8 -0
- package/dist/lib/tools/codex-dashboard.d.ts.map +1 -0
- package/dist/lib/tools/codex-dashboard.js +176 -0
- package/dist/lib/tools/codex-dashboard.js.map +1 -0
- package/dist/lib/tools/codex-diag.d.ts +19 -0
- package/dist/lib/tools/codex-diag.d.ts.map +1 -0
- package/dist/lib/tools/codex-diag.js +139 -0
- package/dist/lib/tools/codex-diag.js.map +1 -0
- package/dist/lib/tools/codex-diff.d.ts +44 -0
- package/dist/lib/tools/codex-diff.d.ts.map +1 -0
- package/dist/lib/tools/codex-diff.js +284 -0
- package/dist/lib/tools/codex-diff.js.map +1 -0
- package/dist/lib/tools/codex-doctor.d.ts +8 -0
- package/dist/lib/tools/codex-doctor.d.ts.map +1 -0
- package/dist/lib/tools/codex-doctor.js +284 -0
- package/dist/lib/tools/codex-doctor.js.map +1 -0
- package/dist/lib/tools/codex-export.d.ts +8 -0
- package/dist/lib/tools/codex-export.d.ts.map +1 -0
- package/dist/lib/tools/codex-export.js +63 -0
- package/dist/lib/tools/codex-export.js.map +1 -0
- package/dist/lib/tools/codex-health.d.ts +8 -0
- package/dist/lib/tools/codex-health.d.ts.map +1 -0
- package/dist/lib/tools/codex-health.js +124 -0
- package/dist/lib/tools/codex-health.js.map +1 -0
- package/dist/lib/tools/codex-help.d.ts +8 -0
- package/dist/lib/tools/codex-help.d.ts.map +1 -0
- package/dist/lib/tools/codex-help.js +127 -0
- package/dist/lib/tools/codex-help.js.map +1 -0
- package/dist/lib/tools/codex-import.d.ts +8 -0
- package/dist/lib/tools/codex-import.d.ts.map +1 -0
- package/dist/lib/tools/codex-import.js +97 -0
- package/dist/lib/tools/codex-import.js.map +1 -0
- package/dist/lib/tools/codex-keychain.d.ts +36 -0
- package/dist/lib/tools/codex-keychain.d.ts.map +1 -0
- package/dist/lib/tools/codex-keychain.js +289 -0
- package/dist/lib/tools/codex-keychain.js.map +1 -0
- package/dist/lib/tools/codex-label.d.ts +8 -0
- package/dist/lib/tools/codex-label.d.ts.map +1 -0
- package/dist/lib/tools/codex-label.js +143 -0
- package/dist/lib/tools/codex-label.js.map +1 -0
- package/dist/lib/tools/codex-limits.d.ts +8 -0
- package/dist/lib/tools/codex-limits.d.ts.map +1 -0
- package/dist/lib/tools/codex-limits.js +497 -0
- package/dist/lib/tools/codex-limits.js.map +1 -0
- package/dist/lib/tools/codex-list.d.ts +8 -0
- package/dist/lib/tools/codex-list.d.ts.map +1 -0
- package/dist/lib/tools/codex-list.js +260 -0
- package/dist/lib/tools/codex-list.js.map +1 -0
- package/dist/lib/tools/codex-metrics.d.ts +8 -0
- package/dist/lib/tools/codex-metrics.d.ts.map +1 -0
- package/dist/lib/tools/codex-metrics.js +180 -0
- package/dist/lib/tools/codex-metrics.js.map +1 -0
- package/dist/lib/tools/codex-next.d.ts +8 -0
- package/dist/lib/tools/codex-next.d.ts.map +1 -0
- package/dist/lib/tools/codex-next.js +54 -0
- package/dist/lib/tools/codex-next.js.map +1 -0
- package/dist/lib/tools/codex-note.d.ts +8 -0
- package/dist/lib/tools/codex-note.d.ts.map +1 -0
- package/dist/lib/tools/codex-note.js +79 -0
- package/dist/lib/tools/codex-note.js.map +1 -0
- package/dist/lib/tools/codex-refresh.d.ts +8 -0
- package/dist/lib/tools/codex-refresh.d.ts.map +1 -0
- package/dist/lib/tools/codex-refresh.js +78 -0
- package/dist/lib/tools/codex-refresh.js.map +1 -0
- package/dist/lib/tools/codex-remove.d.ts +8 -0
- package/dist/lib/tools/codex-remove.d.ts.map +1 -0
- package/dist/lib/tools/codex-remove.js +184 -0
- package/dist/lib/tools/codex-remove.js.map +1 -0
- package/dist/lib/tools/codex-setup.d.ts +8 -0
- package/dist/lib/tools/codex-setup.d.ts.map +1 -0
- package/dist/lib/tools/codex-setup.js +26 -0
- package/dist/lib/tools/codex-setup.js.map +1 -0
- package/dist/lib/tools/codex-status.d.ts +8 -0
- package/dist/lib/tools/codex-status.d.ts.map +1 -0
- package/dist/lib/tools/codex-status.js +263 -0
- package/dist/lib/tools/codex-status.js.map +1 -0
- package/dist/lib/tools/codex-switch.d.ts +8 -0
- package/dist/lib/tools/codex-switch.d.ts.map +1 -0
- package/dist/lib/tools/codex-switch.js +122 -0
- package/dist/lib/tools/codex-switch.js.map +1 -0
- package/dist/lib/tools/codex-tag.d.ts +8 -0
- package/dist/lib/tools/codex-tag.d.ts.map +1 -0
- package/dist/lib/tools/codex-tag.js +101 -0
- package/dist/lib/tools/codex-tag.js.map +1 -0
- package/dist/lib/tools/index.d.ts +143 -0
- package/dist/lib/tools/index.d.ts.map +1 -0
- package/dist/lib/tools/index.js +67 -0
- package/dist/lib/tools/index.js.map +1 -0
- package/dist/lib/ui/format.d.ts.map +1 -1
- package/dist/lib/ui/format.js +5 -0
- package/dist/lib/ui/format.js.map +1 -1
- package/dist/lib/ui/runtime.d.ts +10 -0
- package/dist/lib/ui/runtime.d.ts.map +1 -1
- package/dist/lib/ui/runtime.js +16 -11
- package/dist/lib/ui/runtime.js.map +1 -1
- package/dist/lib/ui/select.d.ts.map +1 -1
- package/dist/lib/ui/select.js +12 -3
- package/dist/lib/ui/select.js.map +1 -1
- package/dist/lib/ui/theme.d.ts +19 -0
- package/dist/lib/ui/theme.d.ts.map +1 -1
- package/dist/lib/ui/theme.js +27 -0
- package/dist/lib/ui/theme.js.map +1 -1
- package/package.json +24 -16
- package/scripts/copy-oauth-success.js +16 -6
- package/scripts/install-oc-codex-multi-auth-core.js +462 -381
- package/scripts/install-oc-codex-multi-auth.js +15 -15
- package/scripts/test-all-models.sh +1 -1
- package/dist/lib/audit.d.ts +0 -45
- package/dist/lib/audit.d.ts.map +0 -1
- package/dist/lib/audit.js +0 -154
- package/dist/lib/audit.js.map +0 -1
- package/dist/lib/auth-rate-limit.d.ts +0 -20
- package/dist/lib/auth-rate-limit.d.ts.map +0 -1
- package/dist/lib/auth-rate-limit.js +0 -91
- package/dist/lib/auth-rate-limit.js.map +0 -1
- package/dist/lib/runtime-contracts.d.ts +0 -16
- package/dist/lib/runtime-contracts.d.ts.map +0 -1
- package/dist/lib/runtime-contracts.js +0 -24
- package/dist/lib/runtime-contracts.js.map +0 -1
package/dist/lib/accounts.js
CHANGED
|
@@ -1,771 +1,193 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* AccountManager — slim orchestrator that composes four domain services:
|
|
3
|
+
*
|
|
4
|
+
* - {@link AccountState} — in-memory account registry + per-family indices
|
|
5
|
+
* - {@link AccountPersistence} — debounced disk saves, shutdown-flush lifecycle
|
|
6
|
+
* - {@link AccountRotation} — selection, rate-limit and cooldown management
|
|
7
|
+
* - {@link AccountRecovery} — auth-failure tracking, Codex CLI hydration,
|
|
8
|
+
* merge-safe removal
|
|
9
|
+
*
|
|
10
|
+
* This module preserves the public API that existed before the RC-7 split so
|
|
11
|
+
* that `new AccountManager(...)` and every instance method keeps working for
|
|
12
|
+
* external callers (`index.ts`, `lib/tools/*`, `lib/parallel-probe.ts`, etc.).
|
|
13
|
+
*/
|
|
14
|
+
import { loadAccounts } from "./storage.js";
|
|
15
|
+
import { AccountPersistence, } from "./accounts/persistence.js";
|
|
16
|
+
import { AccountRecovery } from "./accounts/recovery.js";
|
|
17
|
+
import { AccountRotation } from "./accounts/rotation.js";
|
|
18
|
+
import { AccountState, } from "./accounts/state.js";
|
|
19
|
+
import { formatWaitTime } from "./accounts/rate-limits.js";
|
|
20
|
+
import { nowMs } from "./utils.js";
|
|
10
21
|
export { extractAccountId, extractAccountEmail, getAccountIdCandidates, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, sanitizeEmail, } from "./auth/token-utils.js";
|
|
11
22
|
export { parseRateLimitReason, getQuotaKey, clampNonNegativeInt, clearExpiredRateLimits, isRateLimitedForQuotaKey, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
async function getCodexCliTokenCache() {
|
|
29
|
-
const syncEnabled = process.env.CODEX_AUTH_SYNC_CODEX_CLI !== "0";
|
|
30
|
-
const skip = !syncEnabled ||
|
|
31
|
-
process.env.VITEST_WORKER_ID !== undefined ||
|
|
32
|
-
process.env.NODE_ENV === "test";
|
|
33
|
-
if (skip)
|
|
34
|
-
return null;
|
|
35
|
-
const now = nowMs();
|
|
36
|
-
if (codexCliTokenCache && now - codexCliTokenCacheLoadedAt < CODEX_CLI_CACHE_TTL_MS) {
|
|
37
|
-
return codexCliTokenCache;
|
|
38
|
-
}
|
|
39
|
-
codexCliTokenCacheLoadedAt = now;
|
|
40
|
-
if (!existsSync(CODEX_CLI_ACCOUNTS_PATH)) {
|
|
41
|
-
codexCliTokenCache = null;
|
|
42
|
-
return null;
|
|
23
|
+
export { lookupCodexCliTokensByEmail, } from "./accounts/recovery.js";
|
|
24
|
+
export class AccountManager {
|
|
25
|
+
state;
|
|
26
|
+
persistence;
|
|
27
|
+
rotation;
|
|
28
|
+
recovery;
|
|
29
|
+
/**
|
|
30
|
+
* Alias to `state.authFailuresByRefreshToken`. Preserved as a property on
|
|
31
|
+
* the orchestrator so that pre-split tests and diagnostics that reach in
|
|
32
|
+
* via `Reflect.get(manager, "authFailuresByRefreshToken")` continue to see
|
|
33
|
+
* the live map without edits. Internal use only.
|
|
34
|
+
*/
|
|
35
|
+
get authFailuresByRefreshToken() {
|
|
36
|
+
return this.state.authFailuresByRefreshToken;
|
|
43
37
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
const next = new Map();
|
|
52
|
-
for (const entry of parsed.accounts) {
|
|
53
|
-
if (!isRecord(entry))
|
|
54
|
-
continue;
|
|
55
|
-
const email = sanitizeEmail(typeof entry.email === "string" ? entry.email : undefined);
|
|
56
|
-
if (!email)
|
|
57
|
-
continue;
|
|
58
|
-
const accountId = typeof entry.accountId === "string" && entry.accountId.trim() ? entry.accountId.trim() : undefined;
|
|
59
|
-
const auth = entry.auth;
|
|
60
|
-
const tokens = isRecord(auth) ? auth.tokens : undefined;
|
|
61
|
-
const accessToken = isRecord(tokens) && typeof tokens.access_token === "string" && tokens.access_token.trim()
|
|
62
|
-
? tokens.access_token.trim()
|
|
63
|
-
: undefined;
|
|
64
|
-
const refreshToken = isRecord(tokens) && typeof tokens.refresh_token === "string" && tokens.refresh_token.trim()
|
|
65
|
-
? tokens.refresh_token.trim()
|
|
66
|
-
: undefined;
|
|
67
|
-
if (!accessToken)
|
|
68
|
-
continue;
|
|
69
|
-
next.set(email, {
|
|
70
|
-
accessToken,
|
|
71
|
-
expiresAt: extractExpiresAtFromAccessToken(accessToken),
|
|
72
|
-
refreshToken,
|
|
73
|
-
accountId,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
codexCliTokenCache = next;
|
|
77
|
-
return codexCliTokenCache;
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
log.debug("Failed to read Codex CLI accounts cache", { error: String(error) });
|
|
81
|
-
codexCliTokenCache = null;
|
|
82
|
-
return null;
|
|
38
|
+
constructor(authFallback, stored) {
|
|
39
|
+
this.state = new AccountState();
|
|
40
|
+
this.persistence = new AccountPersistence(this.state);
|
|
41
|
+
this.rotation = new AccountRotation(this.state);
|
|
42
|
+
this.recovery = new AccountRecovery(this.state, this.persistence);
|
|
43
|
+
this.state.initializeFromStorage(authFallback, stored);
|
|
83
44
|
}
|
|
84
|
-
}
|
|
85
|
-
export async function lookupCodexCliTokensByEmail(email) {
|
|
86
|
-
const normalized = sanitizeEmail(email);
|
|
87
|
-
if (!normalized)
|
|
88
|
-
return null;
|
|
89
|
-
const cache = await getCodexCliTokenCache();
|
|
90
|
-
const cached = cache?.get(normalized);
|
|
91
|
-
return cached ? { ...cached } : null;
|
|
92
|
-
}
|
|
93
|
-
function initFamilyState(defaultValue) {
|
|
94
|
-
return Object.fromEntries(MODEL_FAMILIES.map((family) => [family, defaultValue]));
|
|
95
|
-
}
|
|
96
|
-
export class AccountManager {
|
|
97
|
-
accounts = [];
|
|
98
|
-
cursorByFamily = initFamilyState(0);
|
|
99
|
-
currentAccountIndexByFamily = initFamilyState(-1);
|
|
100
|
-
lastToastAccountIndex = -1;
|
|
101
|
-
lastToastTime = 0;
|
|
102
|
-
saveDebounceTimer = null;
|
|
103
|
-
pendingSave = null;
|
|
104
|
-
authFailuresByRefreshToken = new Map();
|
|
105
45
|
static async loadFromDisk(authFallback) {
|
|
106
46
|
const stored = await loadAccounts();
|
|
107
47
|
const manager = new AccountManager(authFallback, stored);
|
|
108
|
-
await manager.hydrateFromCodexCli();
|
|
48
|
+
await manager.recovery.hydrateFromCodexCli();
|
|
109
49
|
return manager;
|
|
110
50
|
}
|
|
51
|
+
// ----- state delegations -----
|
|
111
52
|
hasRefreshToken(refreshToken) {
|
|
112
|
-
return this.
|
|
113
|
-
}
|
|
114
|
-
async hydrateFromCodexCli() {
|
|
115
|
-
const cache = await getCodexCliTokenCache();
|
|
116
|
-
if (!cache || cache.size === 0)
|
|
117
|
-
return;
|
|
118
|
-
const now = nowMs();
|
|
119
|
-
let changed = false;
|
|
120
|
-
for (const account of this.accounts) {
|
|
121
|
-
const email = sanitizeEmail(account.email);
|
|
122
|
-
if (!email)
|
|
123
|
-
continue;
|
|
124
|
-
const cached = cache.get(email);
|
|
125
|
-
if (!cached)
|
|
126
|
-
continue;
|
|
127
|
-
if (typeof cached.expiresAt === "number" && cached.expiresAt <= now) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
const missingOrExpired = !account.access || account.expires === undefined || account.expires <= now;
|
|
131
|
-
if (missingOrExpired) {
|
|
132
|
-
account.access = cached.accessToken;
|
|
133
|
-
if (typeof cached.expiresAt === "number") {
|
|
134
|
-
account.expires = cached.expiresAt;
|
|
135
|
-
}
|
|
136
|
-
changed = true;
|
|
137
|
-
}
|
|
138
|
-
if (!account.accountId &&
|
|
139
|
-
cached.accountId &&
|
|
140
|
-
shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId)) {
|
|
141
|
-
account.accountId = cached.accountId;
|
|
142
|
-
account.accountIdSource = account.accountIdSource ?? "token";
|
|
143
|
-
changed = true;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (!changed)
|
|
147
|
-
return;
|
|
148
|
-
try {
|
|
149
|
-
await this.saveToDisk();
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
log.debug("Failed to persist Codex CLI cache hydration", { error: String(error) });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
constructor(authFallback, stored) {
|
|
156
|
-
const fallbackAccountId = extractAccountId(authFallback?.access);
|
|
157
|
-
const fallbackAccountEmail = sanitizeEmail(extractAccountEmail(authFallback?.access));
|
|
158
|
-
if (stored && stored.accounts.length > 0) {
|
|
159
|
-
const baseNow = nowMs();
|
|
160
|
-
this.accounts = stored.accounts
|
|
161
|
-
.map((account, index) => {
|
|
162
|
-
if (!account.refreshToken || typeof account.refreshToken !== "string") {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
const matchesFallback = !!authFallback &&
|
|
166
|
-
((fallbackAccountId && account.accountId === fallbackAccountId) ||
|
|
167
|
-
account.refreshToken === authFallback.refresh ||
|
|
168
|
-
(!!fallbackAccountEmail &&
|
|
169
|
-
sanitizeEmail(account.email) === fallbackAccountEmail));
|
|
170
|
-
const refreshToken = matchesFallback && authFallback ? authFallback.refresh : account.refreshToken;
|
|
171
|
-
return {
|
|
172
|
-
index,
|
|
173
|
-
accountId: matchesFallback ? fallbackAccountId ?? account.accountId : account.accountId,
|
|
174
|
-
organizationId: account.organizationId,
|
|
175
|
-
accountIdSource: account.accountIdSource,
|
|
176
|
-
accountLabel: account.accountLabel,
|
|
177
|
-
accountTags: account.accountTags,
|
|
178
|
-
accountNote: account.accountNote,
|
|
179
|
-
email: matchesFallback
|
|
180
|
-
? fallbackAccountEmail ?? sanitizeEmail(account.email)
|
|
181
|
-
: sanitizeEmail(account.email),
|
|
182
|
-
refreshToken,
|
|
183
|
-
enabled: account.enabled !== false,
|
|
184
|
-
access: matchesFallback && authFallback ? authFallback.access : account.accessToken,
|
|
185
|
-
expires: matchesFallback && authFallback ? authFallback.expires : account.expiresAt,
|
|
186
|
-
addedAt: clampNonNegativeInt(account.addedAt, baseNow),
|
|
187
|
-
lastUsed: clampNonNegativeInt(account.lastUsed, 0),
|
|
188
|
-
lastSwitchReason: account.lastSwitchReason,
|
|
189
|
-
rateLimitResetTimes: account.rateLimitResetTimes ?? {},
|
|
190
|
-
coolingDownUntil: account.coolingDownUntil,
|
|
191
|
-
cooldownReason: account.cooldownReason,
|
|
192
|
-
};
|
|
193
|
-
})
|
|
194
|
-
.filter((account) => account !== null);
|
|
195
|
-
const hasMatchingFallback = !!authFallback &&
|
|
196
|
-
this.accounts.some((account) => account.refreshToken === authFallback.refresh ||
|
|
197
|
-
(fallbackAccountId && account.accountId === fallbackAccountId) ||
|
|
198
|
-
(!!fallbackAccountEmail && account.email === fallbackAccountEmail));
|
|
199
|
-
if (authFallback && !hasMatchingFallback) {
|
|
200
|
-
const now = nowMs();
|
|
201
|
-
this.accounts.push({
|
|
202
|
-
index: this.accounts.length,
|
|
203
|
-
accountId: fallbackAccountId,
|
|
204
|
-
organizationId: undefined,
|
|
205
|
-
accountIdSource: fallbackAccountId ? "token" : undefined,
|
|
206
|
-
email: fallbackAccountEmail,
|
|
207
|
-
refreshToken: authFallback.refresh,
|
|
208
|
-
enabled: true,
|
|
209
|
-
access: authFallback.access,
|
|
210
|
-
expires: authFallback.expires,
|
|
211
|
-
addedAt: now,
|
|
212
|
-
lastUsed: now,
|
|
213
|
-
lastSwitchReason: "initial",
|
|
214
|
-
rateLimitResetTimes: {},
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
if (this.accounts.length > 0) {
|
|
218
|
-
const defaultIndex = clampNonNegativeInt(stored.activeIndex, 0) % this.accounts.length;
|
|
219
|
-
for (const family of MODEL_FAMILIES) {
|
|
220
|
-
const rawIndex = stored.activeIndexByFamily?.[family];
|
|
221
|
-
const nextIndex = clampNonNegativeInt(rawIndex, defaultIndex) % this.accounts.length;
|
|
222
|
-
this.currentAccountIndexByFamily[family] = nextIndex;
|
|
223
|
-
this.cursorByFamily[family] = nextIndex;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (authFallback) {
|
|
229
|
-
const now = nowMs();
|
|
230
|
-
this.accounts = [
|
|
231
|
-
{
|
|
232
|
-
index: 0,
|
|
233
|
-
accountId: fallbackAccountId,
|
|
234
|
-
organizationId: undefined,
|
|
235
|
-
accountIdSource: fallbackAccountId ? "token" : undefined,
|
|
236
|
-
email: fallbackAccountEmail,
|
|
237
|
-
refreshToken: authFallback.refresh,
|
|
238
|
-
enabled: true,
|
|
239
|
-
access: authFallback.access,
|
|
240
|
-
expires: authFallback.expires,
|
|
241
|
-
addedAt: now,
|
|
242
|
-
lastUsed: 0,
|
|
243
|
-
lastSwitchReason: "initial",
|
|
244
|
-
rateLimitResetTimes: {},
|
|
245
|
-
},
|
|
246
|
-
];
|
|
247
|
-
for (const family of MODEL_FAMILIES) {
|
|
248
|
-
this.currentAccountIndexByFamily[family] = 0;
|
|
249
|
-
this.cursorByFamily[family] = 0;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
53
|
+
return this.state.hasRefreshToken(refreshToken);
|
|
252
54
|
}
|
|
253
55
|
getAccountCount() {
|
|
254
|
-
return this.
|
|
56
|
+
return this.state.getAccountCount();
|
|
255
57
|
}
|
|
256
58
|
getActiveIndex() {
|
|
257
|
-
return this.getActiveIndexForFamily("codex");
|
|
59
|
+
return this.state.getActiveIndexForFamily("codex");
|
|
258
60
|
}
|
|
259
61
|
getActiveIndexForFamily(family) {
|
|
260
|
-
|
|
261
|
-
if (index < 0 || index >= this.accounts.length) {
|
|
262
|
-
return this.accounts.length > 0 ? 0 : -1;
|
|
263
|
-
}
|
|
264
|
-
return index;
|
|
62
|
+
return this.state.getActiveIndexForFamily(family);
|
|
265
63
|
}
|
|
266
64
|
getAccountsSnapshot() {
|
|
267
|
-
return this.
|
|
268
|
-
...account,
|
|
269
|
-
rateLimitResetTimes: { ...account.rateLimitResetTimes },
|
|
270
|
-
}));
|
|
65
|
+
return this.state.getAccountsSnapshot();
|
|
271
66
|
}
|
|
272
67
|
getSelectionExplainability(family, model, now = nowMs()) {
|
|
273
|
-
|
|
274
|
-
const baseQuotaKey = getQuotaKey(family);
|
|
275
|
-
const modelQuotaKey = model ? getQuotaKey(family, model) : null;
|
|
276
|
-
const currentIndex = this.currentAccountIndexByFamily[family];
|
|
277
|
-
const healthTracker = getHealthTracker();
|
|
278
|
-
const tokenTracker = getTokenTracker();
|
|
279
|
-
return this.accounts.map((account) => {
|
|
280
|
-
clearExpiredRateLimits(account);
|
|
281
|
-
const enabled = account.enabled !== false;
|
|
282
|
-
const reasons = [];
|
|
283
|
-
let rateLimitedUntil;
|
|
284
|
-
const baseRateLimit = account.rateLimitResetTimes[baseQuotaKey];
|
|
285
|
-
const modelRateLimit = modelQuotaKey ? account.rateLimitResetTimes[modelQuotaKey] : undefined;
|
|
286
|
-
if (typeof baseRateLimit === "number" && baseRateLimit > now) {
|
|
287
|
-
rateLimitedUntil = baseRateLimit;
|
|
288
|
-
}
|
|
289
|
-
if (typeof modelRateLimit === "number" &&
|
|
290
|
-
modelRateLimit > now &&
|
|
291
|
-
(rateLimitedUntil === undefined || modelRateLimit > rateLimitedUntil)) {
|
|
292
|
-
rateLimitedUntil = modelRateLimit;
|
|
293
|
-
}
|
|
294
|
-
const coolingDownUntil = typeof account.coolingDownUntil === "number" && account.coolingDownUntil > now
|
|
295
|
-
? account.coolingDownUntil
|
|
296
|
-
: undefined;
|
|
297
|
-
if (!enabled)
|
|
298
|
-
reasons.push("disabled");
|
|
299
|
-
if (rateLimitedUntil !== undefined)
|
|
300
|
-
reasons.push("rate-limited");
|
|
301
|
-
if (coolingDownUntil !== undefined) {
|
|
302
|
-
reasons.push(account.cooldownReason ? `cooldown:${account.cooldownReason}` : "cooldown");
|
|
303
|
-
}
|
|
304
|
-
const tokensAvailable = tokenTracker.getTokens(account.index, quotaKey);
|
|
305
|
-
if (tokensAvailable < 1)
|
|
306
|
-
reasons.push("token-bucket-empty");
|
|
307
|
-
const eligible = enabled &&
|
|
308
|
-
rateLimitedUntil === undefined &&
|
|
309
|
-
coolingDownUntil === undefined &&
|
|
310
|
-
tokensAvailable >= 1;
|
|
311
|
-
if (reasons.length === 0)
|
|
312
|
-
reasons.push("eligible");
|
|
313
|
-
return {
|
|
314
|
-
index: account.index,
|
|
315
|
-
enabled,
|
|
316
|
-
isCurrentForFamily: currentIndex === account.index,
|
|
317
|
-
eligible,
|
|
318
|
-
reasons,
|
|
319
|
-
healthScore: healthTracker.getScore(account.index, quotaKey),
|
|
320
|
-
tokensAvailable,
|
|
321
|
-
rateLimitedUntil,
|
|
322
|
-
coolingDownUntil,
|
|
323
|
-
cooldownReason: coolingDownUntil !== undefined ? account.cooldownReason : undefined,
|
|
324
|
-
lastUsed: account.lastUsed,
|
|
325
|
-
};
|
|
326
|
-
});
|
|
68
|
+
return this.state.getSelectionExplainability(family, model, now);
|
|
327
69
|
}
|
|
328
70
|
setActiveIndex(index) {
|
|
329
|
-
|
|
330
|
-
return null;
|
|
331
|
-
if (index < 0 || index >= this.accounts.length)
|
|
332
|
-
return null;
|
|
333
|
-
const account = this.accounts[index];
|
|
334
|
-
if (!account)
|
|
335
|
-
return null;
|
|
336
|
-
if (account.enabled === false)
|
|
337
|
-
return null;
|
|
338
|
-
for (const family of MODEL_FAMILIES) {
|
|
339
|
-
this.currentAccountIndexByFamily[family] = index;
|
|
340
|
-
this.cursorByFamily[family] = index;
|
|
341
|
-
}
|
|
342
|
-
account.lastUsed = nowMs();
|
|
343
|
-
account.lastSwitchReason = "rotation";
|
|
344
|
-
return account;
|
|
71
|
+
return this.state.setActiveIndex(index);
|
|
345
72
|
}
|
|
346
73
|
getCurrentAccount() {
|
|
347
|
-
return this.getCurrentAccountForFamily("codex");
|
|
74
|
+
return this.state.getCurrentAccountForFamily("codex");
|
|
348
75
|
}
|
|
349
76
|
getCurrentAccountForFamily(family) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
77
|
+
return this.state.getCurrentAccountForFamily(family);
|
|
78
|
+
}
|
|
79
|
+
shouldShowAccountToast(accountIndex, debounceMs = 30000) {
|
|
80
|
+
return this.state.shouldShowAccountToast(accountIndex, debounceMs);
|
|
81
|
+
}
|
|
82
|
+
markToastShown(accountIndex) {
|
|
83
|
+
this.state.markToastShown(accountIndex);
|
|
84
|
+
}
|
|
85
|
+
updateFromAuth(account, auth) {
|
|
86
|
+
this.state.updateFromAuth(account, auth);
|
|
87
|
+
}
|
|
88
|
+
toAuthDetails(account) {
|
|
89
|
+
return this.state.toAuthDetails(account);
|
|
359
90
|
}
|
|
91
|
+
markSwitched(account, reason, family) {
|
|
92
|
+
this.state.markSwitched(account, reason, family);
|
|
93
|
+
}
|
|
94
|
+
removeAccount(account) {
|
|
95
|
+
return this.state.removeAccount(account);
|
|
96
|
+
}
|
|
97
|
+
removeAccountByIndex(index) {
|
|
98
|
+
return this.state.removeAccountByIndex(index);
|
|
99
|
+
}
|
|
100
|
+
setAccountEnabled(index, enabled) {
|
|
101
|
+
return this.state.setAccountEnabled(index, enabled);
|
|
102
|
+
}
|
|
103
|
+
isAccountCoolingDown(account) {
|
|
104
|
+
return this.state.isAccountCoolingDown(account);
|
|
105
|
+
}
|
|
106
|
+
clearAccountCooldown(account) {
|
|
107
|
+
this.state.clearAccountCooldown(account);
|
|
108
|
+
}
|
|
109
|
+
// ----- rotation delegations -----
|
|
360
110
|
getCurrentOrNext() {
|
|
361
|
-
return this.getCurrentOrNextForFamily("codex");
|
|
111
|
+
return this.rotation.getCurrentOrNextForFamily("codex");
|
|
362
112
|
}
|
|
363
113
|
getCurrentOrNextForFamily(family, model) {
|
|
364
|
-
|
|
365
|
-
if (count === 0)
|
|
366
|
-
return null;
|
|
367
|
-
const cursor = this.cursorByFamily[family];
|
|
368
|
-
for (let i = 0; i < count; i++) {
|
|
369
|
-
const idx = (cursor + i) % count;
|
|
370
|
-
const account = this.accounts[idx];
|
|
371
|
-
if (!account)
|
|
372
|
-
continue;
|
|
373
|
-
if (account.enabled === false)
|
|
374
|
-
continue;
|
|
375
|
-
clearExpiredRateLimits(account);
|
|
376
|
-
if (isRateLimitedForFamily(account, family, model) || this.isAccountCoolingDown(account)) {
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
this.cursorByFamily[family] = (idx + 1) % count;
|
|
380
|
-
this.currentAccountIndexByFamily[family] = idx;
|
|
381
|
-
account.lastUsed = nowMs();
|
|
382
|
-
return account;
|
|
383
|
-
}
|
|
384
|
-
return null;
|
|
114
|
+
return this.rotation.getCurrentOrNextForFamily(family, model);
|
|
385
115
|
}
|
|
386
116
|
getNextForFamily(family, model) {
|
|
387
|
-
|
|
388
|
-
if (count === 0)
|
|
389
|
-
return null;
|
|
390
|
-
const cursor = this.cursorByFamily[family];
|
|
391
|
-
for (let i = 0; i < count; i++) {
|
|
392
|
-
const idx = (cursor + i) % count;
|
|
393
|
-
const account = this.accounts[idx];
|
|
394
|
-
if (!account)
|
|
395
|
-
continue;
|
|
396
|
-
if (account.enabled === false)
|
|
397
|
-
continue;
|
|
398
|
-
clearExpiredRateLimits(account);
|
|
399
|
-
if (isRateLimitedForFamily(account, family, model) || this.isAccountCoolingDown(account)) {
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
this.cursorByFamily[family] = (idx + 1) % count;
|
|
403
|
-
account.lastUsed = nowMs();
|
|
404
|
-
return account;
|
|
405
|
-
}
|
|
406
|
-
return null;
|
|
117
|
+
return this.rotation.getNextForFamily(family, model);
|
|
407
118
|
}
|
|
408
119
|
getCurrentOrNextForFamilyHybrid(family, model, options) {
|
|
409
|
-
|
|
410
|
-
if (count === 0)
|
|
411
|
-
return null;
|
|
412
|
-
const currentIndex = this.currentAccountIndexByFamily[family];
|
|
413
|
-
if (currentIndex >= 0 && currentIndex < count) {
|
|
414
|
-
const currentAccount = this.accounts[currentIndex];
|
|
415
|
-
if (currentAccount) {
|
|
416
|
-
if (currentAccount.enabled === false) {
|
|
417
|
-
// Fall through to hybrid selection.
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
clearExpiredRateLimits(currentAccount);
|
|
421
|
-
if (!isRateLimitedForFamily(currentAccount, family, model) &&
|
|
422
|
-
!this.isAccountCoolingDown(currentAccount)) {
|
|
423
|
-
currentAccount.lastUsed = nowMs();
|
|
424
|
-
return currentAccount;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
const quotaKey = model ? `${family}:${model}` : family;
|
|
430
|
-
const healthTracker = getHealthTracker();
|
|
431
|
-
const tokenTracker = getTokenTracker();
|
|
432
|
-
const accountsWithMetrics = this.accounts
|
|
433
|
-
.map((account) => {
|
|
434
|
-
if (!account)
|
|
435
|
-
return null;
|
|
436
|
-
if (account.enabled === false)
|
|
437
|
-
return null;
|
|
438
|
-
clearExpiredRateLimits(account);
|
|
439
|
-
const isAvailable = !isRateLimitedForFamily(account, family, model) && !this.isAccountCoolingDown(account);
|
|
440
|
-
return {
|
|
441
|
-
index: account.index,
|
|
442
|
-
isAvailable,
|
|
443
|
-
lastUsed: account.lastUsed,
|
|
444
|
-
};
|
|
445
|
-
})
|
|
446
|
-
.filter((a) => a !== null);
|
|
447
|
-
const selected = selectHybridAccount(accountsWithMetrics, healthTracker, tokenTracker, quotaKey, {}, options);
|
|
448
|
-
if (!selected)
|
|
449
|
-
return null;
|
|
450
|
-
const account = this.accounts[selected.index];
|
|
451
|
-
if (!account)
|
|
452
|
-
return null;
|
|
453
|
-
this.currentAccountIndexByFamily[family] = account.index;
|
|
454
|
-
this.cursorByFamily[family] = (account.index + 1) % count;
|
|
455
|
-
account.lastUsed = nowMs();
|
|
456
|
-
return account;
|
|
120
|
+
return this.rotation.getCurrentOrNextForFamilyHybrid(family, model, options);
|
|
457
121
|
}
|
|
458
122
|
recordSuccess(account, family, model) {
|
|
459
|
-
|
|
460
|
-
const healthTracker = getHealthTracker();
|
|
461
|
-
healthTracker.recordSuccess(account.index, quotaKey);
|
|
123
|
+
this.rotation.recordSuccess(account, family, model);
|
|
462
124
|
}
|
|
463
125
|
recordRateLimit(account, family, model) {
|
|
464
|
-
|
|
465
|
-
const healthTracker = getHealthTracker();
|
|
466
|
-
const tokenTracker = getTokenTracker();
|
|
467
|
-
healthTracker.recordRateLimit(account.index, quotaKey);
|
|
468
|
-
tokenTracker.drain(account.index, quotaKey);
|
|
126
|
+
this.rotation.recordRateLimit(account, family, model);
|
|
469
127
|
}
|
|
470
128
|
recordFailure(account, family, model) {
|
|
471
|
-
|
|
472
|
-
const healthTracker = getHealthTracker();
|
|
473
|
-
healthTracker.recordFailure(account.index, quotaKey);
|
|
129
|
+
this.rotation.recordFailure(account, family, model);
|
|
474
130
|
}
|
|
475
131
|
consumeToken(account, family, model) {
|
|
476
|
-
|
|
477
|
-
const tokenTracker = getTokenTracker();
|
|
478
|
-
return tokenTracker.tryConsume(account.index, quotaKey);
|
|
132
|
+
return this.rotation.consumeToken(account, family, model);
|
|
479
133
|
}
|
|
480
|
-
/**
|
|
481
|
-
* Refund a token consumed within the refund window (30 seconds).
|
|
482
|
-
* Use this when a request fails due to network errors (not rate limits).
|
|
483
|
-
* @returns true if refund was successful, false if no valid consumption found
|
|
484
|
-
*/
|
|
485
134
|
refundToken(account, family, model) {
|
|
486
|
-
|
|
487
|
-
const tokenTracker = getTokenTracker();
|
|
488
|
-
return tokenTracker.refundToken(account.index, quotaKey);
|
|
489
|
-
}
|
|
490
|
-
markSwitched(account, reason, family) {
|
|
491
|
-
account.lastSwitchReason = reason;
|
|
492
|
-
this.currentAccountIndexByFamily[family] = account.index;
|
|
135
|
+
return this.rotation.refundToken(account, family, model);
|
|
493
136
|
}
|
|
494
137
|
markRateLimited(account, retryAfterMs, family, model) {
|
|
495
|
-
this.
|
|
138
|
+
this.rotation.markRateLimited(account, retryAfterMs, family, model);
|
|
496
139
|
}
|
|
497
140
|
markRateLimitedWithReason(account, retryAfterMs, family, reason, model) {
|
|
498
|
-
|
|
499
|
-
const resetAt = nowMs() + retryMs;
|
|
500
|
-
const baseKey = getQuotaKey(family);
|
|
501
|
-
account.rateLimitResetTimes[baseKey] = resetAt;
|
|
502
|
-
if (model) {
|
|
503
|
-
const modelKey = getQuotaKey(family, model);
|
|
504
|
-
account.rateLimitResetTimes[modelKey] = resetAt;
|
|
505
|
-
}
|
|
506
|
-
account.lastRateLimitReason = reason;
|
|
141
|
+
this.rotation.markRateLimitedWithReason(account, retryAfterMs, family, reason, model);
|
|
507
142
|
}
|
|
508
143
|
markAccountCoolingDown(account, cooldownMs, reason) {
|
|
509
|
-
|
|
510
|
-
account.coolingDownUntil = nowMs() + ms;
|
|
511
|
-
account.cooldownReason = reason;
|
|
144
|
+
this.rotation.markAccountCoolingDown(account, cooldownMs, reason);
|
|
512
145
|
}
|
|
513
|
-
/**
|
|
514
|
-
* Mark every in-memory account sharing a refresh token as cooling down.
|
|
515
|
-
* @returns Number of live accounts updated.
|
|
516
|
-
*/
|
|
517
146
|
markAccountsWithRefreshTokenCoolingDown(refreshToken, cooldownMs, reason) {
|
|
518
|
-
|
|
519
|
-
for (const account of matches) {
|
|
520
|
-
this.markAccountCoolingDown(account, cooldownMs, reason);
|
|
521
|
-
}
|
|
522
|
-
return matches.length;
|
|
523
|
-
}
|
|
524
|
-
isAccountCoolingDown(account) {
|
|
525
|
-
if (account.coolingDownUntil === undefined)
|
|
526
|
-
return false;
|
|
527
|
-
if (nowMs() >= account.coolingDownUntil) {
|
|
528
|
-
this.clearAccountCooldown(account);
|
|
529
|
-
return false;
|
|
530
|
-
}
|
|
531
|
-
return true;
|
|
532
|
-
}
|
|
533
|
-
clearAccountCooldown(account) {
|
|
534
|
-
delete account.coolingDownUntil;
|
|
535
|
-
delete account.cooldownReason;
|
|
536
|
-
}
|
|
537
|
-
incrementAuthFailures(account) {
|
|
538
|
-
const currentFailures = this.authFailuresByRefreshToken.get(account.refreshToken) ?? 0;
|
|
539
|
-
const newFailures = currentFailures + 1;
|
|
540
|
-
this.authFailuresByRefreshToken.set(account.refreshToken, newFailures);
|
|
541
|
-
return newFailures;
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* Clear the authentication failure counter for the given account's refresh token.
|
|
545
|
-
*
|
|
546
|
-
* Notes:
|
|
547
|
-
* - Failure counts are tracked per refresh token (not per account), so this clears
|
|
548
|
-
* shared failure state for all org variants that reuse the same token.
|
|
549
|
-
* - Failure counts are in-memory only for the current AccountManager instance.
|
|
550
|
-
*/
|
|
551
|
-
clearAuthFailures(account) {
|
|
552
|
-
this.authFailuresByRefreshToken.delete(account.refreshToken);
|
|
553
|
-
}
|
|
554
|
-
shouldShowAccountToast(accountIndex, debounceMs = 30000) {
|
|
555
|
-
const now = nowMs();
|
|
556
|
-
if (accountIndex === this.lastToastAccountIndex && now - this.lastToastTime < debounceMs) {
|
|
557
|
-
return false;
|
|
558
|
-
}
|
|
559
|
-
return true;
|
|
560
|
-
}
|
|
561
|
-
markToastShown(accountIndex) {
|
|
562
|
-
this.lastToastAccountIndex = accountIndex;
|
|
563
|
-
this.lastToastTime = nowMs();
|
|
564
|
-
}
|
|
565
|
-
updateFromAuth(account, auth) {
|
|
566
|
-
const previousRefreshToken = account.refreshToken;
|
|
567
|
-
account.refreshToken = auth.refresh;
|
|
568
|
-
account.access = auth.access;
|
|
569
|
-
account.expires = auth.expires;
|
|
570
|
-
if (previousRefreshToken !== account.refreshToken) {
|
|
571
|
-
this.authFailuresByRefreshToken.delete(previousRefreshToken);
|
|
572
|
-
}
|
|
573
|
-
const tokenAccountId = extractAccountId(auth.access);
|
|
574
|
-
if (tokenAccountId &&
|
|
575
|
-
(shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId))) {
|
|
576
|
-
account.accountId = tokenAccountId;
|
|
577
|
-
account.accountIdSource = "token";
|
|
578
|
-
}
|
|
579
|
-
account.email = sanitizeEmail(extractAccountEmail(auth.access)) ?? account.email;
|
|
580
|
-
}
|
|
581
|
-
toAuthDetails(account) {
|
|
582
|
-
return {
|
|
583
|
-
type: "oauth",
|
|
584
|
-
access: account.access ?? "",
|
|
585
|
-
refresh: account.refreshToken,
|
|
586
|
-
expires: account.expires ?? 0,
|
|
587
|
-
};
|
|
147
|
+
return this.rotation.markAccountsWithRefreshTokenCoolingDown(refreshToken, cooldownMs, reason);
|
|
588
148
|
}
|
|
589
149
|
getMinWaitTime() {
|
|
590
|
-
return this.getMinWaitTimeForFamily("codex");
|
|
150
|
+
return this.rotation.getMinWaitTimeForFamily("codex");
|
|
591
151
|
}
|
|
592
152
|
getMinWaitTimeForFamily(family, model) {
|
|
593
|
-
|
|
594
|
-
const enabledAccounts = this.accounts.filter((account) => account.enabled !== false);
|
|
595
|
-
const available = enabledAccounts.filter((account) => {
|
|
596
|
-
clearExpiredRateLimits(account);
|
|
597
|
-
return !isRateLimitedForFamily(account, family, model) && !this.isAccountCoolingDown(account);
|
|
598
|
-
});
|
|
599
|
-
if (available.length > 0)
|
|
600
|
-
return 0;
|
|
601
|
-
if (enabledAccounts.length === 0)
|
|
602
|
-
return 0;
|
|
603
|
-
const waitTimes = [];
|
|
604
|
-
const baseKey = getQuotaKey(family);
|
|
605
|
-
const modelKey = model ? getQuotaKey(family, model) : null;
|
|
606
|
-
for (const account of enabledAccounts) {
|
|
607
|
-
const baseResetAt = account.rateLimitResetTimes[baseKey];
|
|
608
|
-
if (typeof baseResetAt === "number") {
|
|
609
|
-
waitTimes.push(Math.max(0, baseResetAt - now));
|
|
610
|
-
}
|
|
611
|
-
if (modelKey) {
|
|
612
|
-
const modelResetAt = account.rateLimitResetTimes[modelKey];
|
|
613
|
-
if (typeof modelResetAt === "number") {
|
|
614
|
-
waitTimes.push(Math.max(0, modelResetAt - now));
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (typeof account.coolingDownUntil === "number") {
|
|
618
|
-
waitTimes.push(Math.max(0, account.coolingDownUntil - now));
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return waitTimes.length > 0 ? Math.min(...waitTimes) : 0;
|
|
153
|
+
return this.rotation.getMinWaitTimeForFamily(family, model);
|
|
622
154
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return false;
|
|
627
|
-
}
|
|
628
|
-
this.accounts.splice(idx, 1);
|
|
629
|
-
this.accounts.forEach((acc, index) => {
|
|
630
|
-
acc.index = index;
|
|
631
|
-
});
|
|
632
|
-
if (this.accounts.length === 0) {
|
|
633
|
-
for (const family of MODEL_FAMILIES) {
|
|
634
|
-
this.cursorByFamily[family] = 0;
|
|
635
|
-
this.currentAccountIndexByFamily[family] = -1;
|
|
636
|
-
}
|
|
637
|
-
return true;
|
|
638
|
-
}
|
|
639
|
-
for (const family of MODEL_FAMILIES) {
|
|
640
|
-
if (this.cursorByFamily[family] > idx) {
|
|
641
|
-
this.cursorByFamily[family] = Math.max(0, this.cursorByFamily[family] - 1);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
for (const family of MODEL_FAMILIES) {
|
|
645
|
-
this.cursorByFamily[family] = this.cursorByFamily[family] % this.accounts.length;
|
|
646
|
-
}
|
|
647
|
-
for (const family of MODEL_FAMILIES) {
|
|
648
|
-
if (this.currentAccountIndexByFamily[family] > idx) {
|
|
649
|
-
this.currentAccountIndexByFamily[family] -= 1;
|
|
650
|
-
}
|
|
651
|
-
if (this.currentAccountIndexByFamily[family] >= this.accounts.length) {
|
|
652
|
-
this.currentAccountIndexByFamily[family] = -1;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
return true;
|
|
155
|
+
// ----- persistence delegations -----
|
|
156
|
+
async saveToDisk() {
|
|
157
|
+
await this.persistence.saveToDisk();
|
|
656
158
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
return false;
|
|
660
|
-
if (index < 0 || index >= this.accounts.length)
|
|
661
|
-
return false;
|
|
662
|
-
const account = this.accounts[index];
|
|
663
|
-
if (!account)
|
|
664
|
-
return false;
|
|
665
|
-
return this.removeAccount(account);
|
|
159
|
+
saveToDiskDebounced(delayMs = 500) {
|
|
160
|
+
this.persistence.saveToDiskDebounced(delayMs);
|
|
666
161
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
* This is used when auth refresh fails to remove all org variants together.
|
|
670
|
-
* @returns Number of accounts removed
|
|
671
|
-
*/
|
|
672
|
-
removeAccountsWithSameRefreshToken(account) {
|
|
673
|
-
const refreshToken = account.refreshToken;
|
|
674
|
-
// Snapshot first because removeAccount mutates this.accounts.
|
|
675
|
-
const accountsToRemove = this.accounts.filter((acc) => acc.refreshToken === refreshToken);
|
|
676
|
-
let removedCount = 0;
|
|
677
|
-
for (const accountToRemove of accountsToRemove) {
|
|
678
|
-
if (this.removeAccount(accountToRemove)) {
|
|
679
|
-
removedCount++;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
// Clear stale auth failure state for this refresh token
|
|
683
|
-
this.authFailuresByRefreshToken.delete(refreshToken);
|
|
684
|
-
return removedCount;
|
|
162
|
+
async flushPendingSave() {
|
|
163
|
+
await this.persistence.flushPendingSave();
|
|
685
164
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
return null;
|
|
689
|
-
if (index < 0 || index >= this.accounts.length)
|
|
690
|
-
return null;
|
|
691
|
-
const account = this.accounts[index];
|
|
692
|
-
if (!account)
|
|
693
|
-
return null;
|
|
694
|
-
account.enabled = enabled;
|
|
695
|
-
return account;
|
|
165
|
+
disposeShutdownHandler() {
|
|
166
|
+
this.persistence.disposeShutdownHandler();
|
|
696
167
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const raw = this.currentAccountIndexByFamily[family];
|
|
701
|
-
activeIndexByFamily[family] = clampNonNegativeInt(raw, 0);
|
|
702
|
-
}
|
|
703
|
-
const activeIndex = clampNonNegativeInt(activeIndexByFamily.codex, 0);
|
|
704
|
-
const storage = {
|
|
705
|
-
version: 3,
|
|
706
|
-
accounts: this.accounts.map((account) => ({
|
|
707
|
-
accountId: account.accountId,
|
|
708
|
-
organizationId: account.organizationId,
|
|
709
|
-
accountIdSource: account.accountIdSource,
|
|
710
|
-
accountLabel: account.accountLabel,
|
|
711
|
-
accountTags: account.accountTags,
|
|
712
|
-
accountNote: account.accountNote,
|
|
713
|
-
email: account.email,
|
|
714
|
-
refreshToken: account.refreshToken,
|
|
715
|
-
accessToken: account.access,
|
|
716
|
-
expiresAt: account.expires,
|
|
717
|
-
enabled: account.enabled === false ? false : undefined,
|
|
718
|
-
addedAt: account.addedAt,
|
|
719
|
-
lastUsed: account.lastUsed,
|
|
720
|
-
lastSwitchReason: account.lastSwitchReason,
|
|
721
|
-
rateLimitResetTimes: Object.keys(account.rateLimitResetTimes).length > 0 ? account.rateLimitResetTimes : undefined,
|
|
722
|
-
coolingDownUntil: account.coolingDownUntil,
|
|
723
|
-
cooldownReason: account.cooldownReason,
|
|
724
|
-
})),
|
|
725
|
-
activeIndex,
|
|
726
|
-
activeIndexByFamily,
|
|
727
|
-
};
|
|
728
|
-
await saveAccounts(storage);
|
|
168
|
+
// ----- recovery delegations -----
|
|
169
|
+
async incrementAuthFailures(account) {
|
|
170
|
+
return this.recovery.incrementAuthFailures(account);
|
|
729
171
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
clearTimeout(this.saveDebounceTimer);
|
|
733
|
-
}
|
|
734
|
-
this.saveDebounceTimer = setTimeout(() => {
|
|
735
|
-
this.saveDebounceTimer = null;
|
|
736
|
-
const doSave = async () => {
|
|
737
|
-
try {
|
|
738
|
-
if (this.pendingSave) {
|
|
739
|
-
await this.pendingSave;
|
|
740
|
-
}
|
|
741
|
-
this.pendingSave = this.saveToDisk().finally(() => {
|
|
742
|
-
this.pendingSave = null;
|
|
743
|
-
});
|
|
744
|
-
await this.pendingSave;
|
|
745
|
-
}
|
|
746
|
-
catch (error) {
|
|
747
|
-
log.warn("Debounced save failed", { error: error instanceof Error ? error.message : String(error) });
|
|
748
|
-
}
|
|
749
|
-
};
|
|
750
|
-
void doSave();
|
|
751
|
-
}, delayMs);
|
|
172
|
+
getAuthFailures(account) {
|
|
173
|
+
return this.recovery.getAuthFailures(account);
|
|
752
174
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
}
|
|
759
|
-
if (this.pendingSave) {
|
|
760
|
-
await this.pendingSave;
|
|
761
|
-
}
|
|
175
|
+
clearAuthFailures(account) {
|
|
176
|
+
this.recovery.clearAuthFailures(account);
|
|
177
|
+
}
|
|
178
|
+
removeAccountsWithSameRefreshToken(account) {
|
|
179
|
+
return this.recovery.removeAccountsWithSameRefreshToken(account);
|
|
762
180
|
}
|
|
763
181
|
}
|
|
764
182
|
export function formatAccountLabel(account, index) {
|
|
765
183
|
const accountLabel = account?.accountLabel?.trim();
|
|
766
184
|
const email = account?.email?.trim();
|
|
767
185
|
const accountId = account?.accountId?.trim();
|
|
768
|
-
const idSuffix = accountId
|
|
186
|
+
const idSuffix = accountId
|
|
187
|
+
? accountId.length > 6
|
|
188
|
+
? accountId.slice(-6)
|
|
189
|
+
: accountId
|
|
190
|
+
: null;
|
|
769
191
|
if (accountLabel && email && idSuffix) {
|
|
770
192
|
return `Account ${index + 1} (${accountLabel}, ${email}, id:${idSuffix})`;
|
|
771
193
|
}
|