oc-codex-multi-auth 6.0.0
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 +21 -0
- package/README.md +162 -0
- package/assets/opencode-logo-ornate-dark.svg +18 -0
- package/assets/readme-hero.svg +31 -0
- package/config/README.md +96 -0
- package/config/minimal-opencode.json +13 -0
- package/config/opencode-legacy.json +871 -0
- package/config/opencode-modern.json +358 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5008 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/accounts/rate-limits.d.ts +22 -0
- package/dist/lib/accounts/rate-limits.d.ts.map +1 -0
- package/dist/lib/accounts/rate-limits.js +63 -0
- package/dist/lib/accounts/rate-limits.js.map +1 -0
- package/dist/lib/accounts.d.ts +134 -0
- package/dist/lib/accounts.d.ts.map +1 -0
- package/dist/lib/accounts.js +795 -0
- package/dist/lib/accounts.js.map +1 -0
- package/dist/lib/audit.d.ts +45 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/audit.js +154 -0
- package/dist/lib/audit.js.map +1 -0
- package/dist/lib/auth/auth.d.ts +51 -0
- package/dist/lib/auth/auth.d.ts.map +1 -0
- package/dist/lib/auth/auth.js +191 -0
- package/dist/lib/auth/auth.js.map +1 -0
- package/dist/lib/auth/browser.d.ts +17 -0
- package/dist/lib/auth/browser.d.ts.map +1 -0
- package/dist/lib/auth/browser.js +90 -0
- package/dist/lib/auth/browser.js.map +1 -0
- package/dist/lib/auth/device-code.d.ts +28 -0
- package/dist/lib/auth/device-code.d.ts.map +1 -0
- package/dist/lib/auth/device-code.js +215 -0
- package/dist/lib/auth/device-code.js.map +1 -0
- package/dist/lib/auth/login-runner.d.ts +55 -0
- package/dist/lib/auth/login-runner.d.ts.map +1 -0
- package/dist/lib/auth/login-runner.js +631 -0
- package/dist/lib/auth/login-runner.js.map +1 -0
- package/dist/lib/auth/server.d.ts +10 -0
- package/dist/lib/auth/server.d.ts.map +1 -0
- package/dist/lib/auth/server.js +98 -0
- package/dist/lib/auth/server.js.map +1 -0
- package/dist/lib/auth/token-utils.d.ts +60 -0
- package/dist/lib/auth/token-utils.d.ts.map +1 -0
- package/dist/lib/auth/token-utils.js +416 -0
- package/dist/lib/auth/token-utils.js.map +1 -0
- package/dist/lib/auth-rate-limit.d.ts +20 -0
- package/dist/lib/auth-rate-limit.d.ts.map +1 -0
- package/dist/lib/auth-rate-limit.js +91 -0
- package/dist/lib/auth-rate-limit.js.map +1 -0
- package/dist/lib/auto-update-checker.d.ts +10 -0
- package/dist/lib/auto-update-checker.d.ts.map +1 -0
- package/dist/lib/auto-update-checker.js +129 -0
- package/dist/lib/auto-update-checker.js.map +1 -0
- package/dist/lib/circuit-breaker.d.ts +34 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -0
- package/dist/lib/circuit-breaker.js +124 -0
- package/dist/lib/circuit-breaker.js.map +1 -0
- package/dist/lib/cli.d.ts +46 -0
- package/dist/lib/cli.d.ts.map +1 -0
- package/dist/lib/cli.js +192 -0
- package/dist/lib/cli.js.map +1 -0
- package/dist/lib/config.d.ts +42 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +314 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/constants.d.ts +90 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +90 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/context-overflow.d.ts +27 -0
- package/dist/lib/context-overflow.d.ts.map +1 -0
- package/dist/lib/context-overflow.js +124 -0
- package/dist/lib/context-overflow.js.map +1 -0
- package/dist/lib/errors.d.ts +113 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +103 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/health.d.ts +33 -0
- package/dist/lib/health.d.ts.map +1 -0
- package/dist/lib/health.js +70 -0
- package/dist/lib/health.js.map +1 -0
- package/dist/lib/index.d.ts +20 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +20 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logger.d.ts +40 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +337 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/oauth-success.html +712 -0
- package/dist/lib/parallel-probe.d.ts +28 -0
- package/dist/lib/parallel-probe.d.ts.map +1 -0
- package/dist/lib/parallel-probe.js +97 -0
- package/dist/lib/parallel-probe.js.map +1 -0
- package/dist/lib/proactive-refresh.d.ts +67 -0
- package/dist/lib/proactive-refresh.d.ts.map +1 -0
- package/dist/lib/proactive-refresh.js +161 -0
- package/dist/lib/proactive-refresh.js.map +1 -0
- package/dist/lib/prompts/codex-opencode-bridge.d.ts +20 -0
- package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -0
- package/dist/lib/prompts/codex-opencode-bridge.js +126 -0
- package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -0
- package/dist/lib/prompts/codex.d.ts +41 -0
- package/dist/lib/prompts/codex.d.ts.map +1 -0
- package/dist/lib/prompts/codex.js +400 -0
- package/dist/lib/prompts/codex.js.map +1 -0
- package/dist/lib/prompts/opencode-codex.d.ts +25 -0
- package/dist/lib/prompts/opencode-codex.d.ts.map +1 -0
- package/dist/lib/prompts/opencode-codex.js +223 -0
- package/dist/lib/prompts/opencode-codex.js.map +1 -0
- package/dist/lib/recovery/constants.d.ts +12 -0
- package/dist/lib/recovery/constants.d.ts.map +1 -0
- package/dist/lib/recovery/constants.js +25 -0
- package/dist/lib/recovery/constants.js.map +1 -0
- package/dist/lib/recovery/index.d.ts +12 -0
- package/dist/lib/recovery/index.d.ts.map +1 -0
- package/dist/lib/recovery/index.js +12 -0
- package/dist/lib/recovery/index.js.map +1 -0
- package/dist/lib/recovery/storage.d.ts +24 -0
- package/dist/lib/recovery/storage.d.ts.map +1 -0
- package/dist/lib/recovery/storage.js +362 -0
- package/dist/lib/recovery/storage.js.map +1 -0
- package/dist/lib/recovery/types.d.ts +116 -0
- package/dist/lib/recovery/types.d.ts.map +1 -0
- package/dist/lib/recovery/types.js +7 -0
- package/dist/lib/recovery/types.js.map +1 -0
- package/dist/lib/recovery.d.ts +31 -0
- package/dist/lib/recovery.d.ts.map +1 -0
- package/dist/lib/recovery.js +313 -0
- package/dist/lib/recovery.js.map +1 -0
- package/dist/lib/refresh-queue.d.ts +131 -0
- package/dist/lib/refresh-queue.d.ts.map +1 -0
- package/dist/lib/refresh-queue.js +297 -0
- package/dist/lib/refresh-queue.js.map +1 -0
- package/dist/lib/request/fetch-helpers.d.ts +142 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -0
- package/dist/lib/request/fetch-helpers.js +749 -0
- package/dist/lib/request/fetch-helpers.js.map +1 -0
- package/dist/lib/request/helpers/input-utils.d.ts +7 -0
- package/dist/lib/request/helpers/input-utils.d.ts.map +1 -0
- package/dist/lib/request/helpers/input-utils.js +214 -0
- package/dist/lib/request/helpers/input-utils.js.map +1 -0
- package/dist/lib/request/helpers/model-map.d.ts +22 -0
- package/dist/lib/request/helpers/model-map.d.ts.map +1 -0
- package/dist/lib/request/helpers/model-map.js +183 -0
- package/dist/lib/request/helpers/model-map.js.map +1 -0
- package/dist/lib/request/helpers/tool-utils.d.ts +29 -0
- package/dist/lib/request/helpers/tool-utils.d.ts.map +1 -0
- package/dist/lib/request/helpers/tool-utils.js +117 -0
- package/dist/lib/request/helpers/tool-utils.js.map +1 -0
- package/dist/lib/request/rate-limit-backoff.d.ts +17 -0
- package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -0
- package/dist/lib/request/rate-limit-backoff.js +83 -0
- package/dist/lib/request/rate-limit-backoff.js.map +1 -0
- package/dist/lib/request/request-transformer.d.ts +107 -0
- package/dist/lib/request/request-transformer.d.ts.map +1 -0
- package/dist/lib/request/request-transformer.js +930 -0
- package/dist/lib/request/request-transformer.js.map +1 -0
- package/dist/lib/request/response-handler.d.ts +23 -0
- package/dist/lib/request/response-handler.d.ts.map +1 -0
- package/dist/lib/request/response-handler.js +245 -0
- package/dist/lib/request/response-handler.js.map +1 -0
- package/dist/lib/request/retry-budget.d.ts +19 -0
- package/dist/lib/request/retry-budget.d.ts.map +1 -0
- package/dist/lib/request/retry-budget.js +99 -0
- package/dist/lib/request/retry-budget.js.map +1 -0
- package/dist/lib/rotation.d.ts +130 -0
- package/dist/lib/rotation.d.ts.map +1 -0
- package/dist/lib/rotation.js +301 -0
- package/dist/lib/rotation.js.map +1 -0
- package/dist/lib/runtime-contracts.d.ts +16 -0
- package/dist/lib/runtime-contracts.d.ts.map +1 -0
- package/dist/lib/runtime-contracts.js +24 -0
- package/dist/lib/runtime-contracts.js.map +1 -0
- package/dist/lib/schemas.d.ts +457 -0
- package/dist/lib/schemas.d.ts.map +1 -0
- package/dist/lib/schemas.js +283 -0
- package/dist/lib/schemas.js.map +1 -0
- package/dist/lib/shutdown.d.ts +7 -0
- package/dist/lib/shutdown.d.ts.map +1 -0
- package/dist/lib/shutdown.js +43 -0
- package/dist/lib/shutdown.js.map +1 -0
- package/dist/lib/storage/migrations.d.ts +65 -0
- package/dist/lib/storage/migrations.d.ts.map +1 -0
- package/dist/lib/storage/migrations.js +44 -0
- package/dist/lib/storage/migrations.js.map +1 -0
- package/dist/lib/storage/paths.d.ts +16 -0
- package/dist/lib/storage/paths.d.ts.map +1 -0
- package/dist/lib/storage/paths.js +92 -0
- package/dist/lib/storage/paths.js.map +1 -0
- package/dist/lib/storage.d.ts +157 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/lib/storage.js +1169 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/lib/table-formatter.d.ts +32 -0
- package/dist/lib/table-formatter.d.ts.map +1 -0
- package/dist/lib/table-formatter.js +44 -0
- package/dist/lib/table-formatter.js.map +1 -0
- package/dist/lib/types.d.ts +131 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/ui/ansi.d.ts +22 -0
- package/dist/lib/ui/ansi.d.ts.map +1 -0
- package/dist/lib/ui/ansi.js +38 -0
- package/dist/lib/ui/ansi.js.map +1 -0
- package/dist/lib/ui/auth-menu.d.ts +39 -0
- package/dist/lib/ui/auth-menu.d.ts.map +1 -0
- package/dist/lib/ui/auth-menu.js +186 -0
- package/dist/lib/ui/auth-menu.js.map +1 -0
- package/dist/lib/ui/beginner.d.ts +62 -0
- package/dist/lib/ui/beginner.d.ts.map +1 -0
- package/dist/lib/ui/beginner.js +259 -0
- package/dist/lib/ui/beginner.js.map +1 -0
- package/dist/lib/ui/confirm.d.ts +2 -0
- package/dist/lib/ui/confirm.d.ts.map +1 -0
- package/dist/lib/ui/confirm.js +21 -0
- package/dist/lib/ui/confirm.js.map +1 -0
- package/dist/lib/ui/format.d.ts +9 -0
- package/dist/lib/ui/format.d.ts.map +1 -0
- package/dist/lib/ui/format.js +49 -0
- package/dist/lib/ui/format.js.map +1 -0
- package/dist/lib/ui/runtime.d.ts +11 -0
- package/dist/lib/ui/runtime.d.ts.map +1 -0
- package/dist/lib/ui/runtime.js +31 -0
- package/dist/lib/ui/runtime.js.map +1 -0
- package/dist/lib/ui/select.d.ts +20 -0
- package/dist/lib/ui/select.d.ts.map +1 -0
- package/dist/lib/ui/select.js +343 -0
- package/dist/lib/ui/select.js.map +1 -0
- package/dist/lib/ui/theme.d.ts +34 -0
- package/dist/lib/ui/theme.d.ts.map +1 -0
- package/dist/lib/ui/theme.js +84 -0
- package/dist/lib/ui/theme.js.map +1 -0
- package/dist/lib/utils.d.ts +29 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +54 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +113 -0
- package/scripts/audit-dev-allowlist.js +114 -0
- package/scripts/copy-oauth-success.js +37 -0
- package/scripts/install-oc-codex-multi-auth-core.js +381 -0
- package/scripts/install-oc-codex-multi-auth.js +15 -0
- package/scripts/test-all-models.sh +260 -0
- package/scripts/validate-model-map.sh +97 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import { existsSync, promises as fs } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createLogger } from "./logger.js";
|
|
5
|
+
import { loadAccounts, saveAccounts, } from "./storage.js";
|
|
6
|
+
import { MODEL_FAMILIES } from "./prompts/codex.js";
|
|
7
|
+
import { getHealthTracker, getTokenTracker, selectHybridAccount, } from "./rotation.js";
|
|
8
|
+
import { isRecord, nowMs } from "./utils.js";
|
|
9
|
+
import { decodeJWT } from "./auth/auth.js";
|
|
10
|
+
export { extractAccountId, extractAccountEmail, getAccountIdCandidates, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, sanitizeEmail, } from "./auth/token-utils.js";
|
|
11
|
+
export { parseRateLimitReason, getQuotaKey, clampNonNegativeInt, clearExpiredRateLimits, isRateLimitedForQuotaKey, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
|
|
12
|
+
import { extractAccountId, extractAccountEmail, shouldUpdateAccountIdFromToken, sanitizeEmail, } from "./auth/token-utils.js";
|
|
13
|
+
import { clampNonNegativeInt, getQuotaKey, clearExpiredRateLimits, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
|
|
14
|
+
const log = createLogger("accounts");
|
|
15
|
+
const CODEX_CLI_ACCOUNTS_PATH = join(homedir(), ".codex", "accounts.json");
|
|
16
|
+
const CODEX_CLI_CACHE_TTL_MS = 5_000;
|
|
17
|
+
let codexCliTokenCache = null;
|
|
18
|
+
let codexCliTokenCacheLoadedAt = 0;
|
|
19
|
+
function extractExpiresAtFromAccessToken(accessToken) {
|
|
20
|
+
const decoded = decodeJWT(accessToken);
|
|
21
|
+
const exp = decoded?.exp;
|
|
22
|
+
if (typeof exp === "number" && Number.isFinite(exp)) {
|
|
23
|
+
// JWT exp is in seconds since epoch.
|
|
24
|
+
return exp * 1000;
|
|
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;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const raw = await fs.readFile(CODEX_CLI_ACCOUNTS_PATH, "utf-8");
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.accounts)) {
|
|
48
|
+
codexCliTokenCache = null;
|
|
49
|
+
return null;
|
|
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;
|
|
83
|
+
}
|
|
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
|
+
static async loadFromDisk(authFallback) {
|
|
106
|
+
const stored = await loadAccounts();
|
|
107
|
+
const manager = new AccountManager(authFallback, stored);
|
|
108
|
+
await manager.hydrateFromCodexCli();
|
|
109
|
+
return manager;
|
|
110
|
+
}
|
|
111
|
+
hasRefreshToken(refreshToken) {
|
|
112
|
+
return this.accounts.some((account) => account.refreshToken === refreshToken);
|
|
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
|
+
}
|
|
252
|
+
}
|
|
253
|
+
getAccountCount() {
|
|
254
|
+
return this.accounts.length;
|
|
255
|
+
}
|
|
256
|
+
getActiveIndex() {
|
|
257
|
+
return this.getActiveIndexForFamily("codex");
|
|
258
|
+
}
|
|
259
|
+
getActiveIndexForFamily(family) {
|
|
260
|
+
const index = this.currentAccountIndexByFamily[family];
|
|
261
|
+
if (index < 0 || index >= this.accounts.length) {
|
|
262
|
+
return this.accounts.length > 0 ? 0 : -1;
|
|
263
|
+
}
|
|
264
|
+
return index;
|
|
265
|
+
}
|
|
266
|
+
getAccountsSnapshot() {
|
|
267
|
+
return this.accounts.map((account) => ({
|
|
268
|
+
...account,
|
|
269
|
+
rateLimitResetTimes: { ...account.rateLimitResetTimes },
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
getSelectionExplainability(family, model, now = nowMs()) {
|
|
273
|
+
const quotaKey = model ? `${family}:${model}` : family;
|
|
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
|
+
});
|
|
327
|
+
}
|
|
328
|
+
setActiveIndex(index) {
|
|
329
|
+
if (!Number.isFinite(index))
|
|
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;
|
|
345
|
+
}
|
|
346
|
+
getCurrentAccount() {
|
|
347
|
+
return this.getCurrentAccountForFamily("codex");
|
|
348
|
+
}
|
|
349
|
+
getCurrentAccountForFamily(family) {
|
|
350
|
+
const index = this.currentAccountIndexByFamily[family];
|
|
351
|
+
if (index < 0 || index >= this.accounts.length) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
const account = this.accounts[index];
|
|
355
|
+
if (!account || account.enabled === false) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
return account;
|
|
359
|
+
}
|
|
360
|
+
getCurrentOrNext() {
|
|
361
|
+
return this.getCurrentOrNextForFamily("codex");
|
|
362
|
+
}
|
|
363
|
+
getCurrentOrNextForFamily(family, model) {
|
|
364
|
+
const count = this.accounts.length;
|
|
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;
|
|
385
|
+
}
|
|
386
|
+
getNextForFamily(family, model) {
|
|
387
|
+
const count = this.accounts.length;
|
|
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;
|
|
407
|
+
}
|
|
408
|
+
getCurrentOrNextForFamilyHybrid(family, model, options) {
|
|
409
|
+
const count = this.accounts.length;
|
|
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;
|
|
457
|
+
}
|
|
458
|
+
recordSuccess(account, family, model) {
|
|
459
|
+
const quotaKey = model ? `${family}:${model}` : family;
|
|
460
|
+
const healthTracker = getHealthTracker();
|
|
461
|
+
healthTracker.recordSuccess(account.index, quotaKey);
|
|
462
|
+
}
|
|
463
|
+
recordRateLimit(account, family, model) {
|
|
464
|
+
const quotaKey = model ? `${family}:${model}` : family;
|
|
465
|
+
const healthTracker = getHealthTracker();
|
|
466
|
+
const tokenTracker = getTokenTracker();
|
|
467
|
+
healthTracker.recordRateLimit(account.index, quotaKey);
|
|
468
|
+
tokenTracker.drain(account.index, quotaKey);
|
|
469
|
+
}
|
|
470
|
+
recordFailure(account, family, model) {
|
|
471
|
+
const quotaKey = model ? `${family}:${model}` : family;
|
|
472
|
+
const healthTracker = getHealthTracker();
|
|
473
|
+
healthTracker.recordFailure(account.index, quotaKey);
|
|
474
|
+
}
|
|
475
|
+
consumeToken(account, family, model) {
|
|
476
|
+
const quotaKey = model ? `${family}:${model}` : family;
|
|
477
|
+
const tokenTracker = getTokenTracker();
|
|
478
|
+
return tokenTracker.tryConsume(account.index, quotaKey);
|
|
479
|
+
}
|
|
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
|
+
refundToken(account, family, model) {
|
|
486
|
+
const quotaKey = model ? `${family}:${model}` : family;
|
|
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;
|
|
493
|
+
}
|
|
494
|
+
markRateLimited(account, retryAfterMs, family, model) {
|
|
495
|
+
this.markRateLimitedWithReason(account, retryAfterMs, family, "unknown", model);
|
|
496
|
+
}
|
|
497
|
+
markRateLimitedWithReason(account, retryAfterMs, family, reason, model) {
|
|
498
|
+
const retryMs = Math.max(0, Math.floor(retryAfterMs));
|
|
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;
|
|
507
|
+
}
|
|
508
|
+
markAccountCoolingDown(account, cooldownMs, reason) {
|
|
509
|
+
const ms = Math.max(0, Math.floor(cooldownMs));
|
|
510
|
+
account.coolingDownUntil = nowMs() + ms;
|
|
511
|
+
account.cooldownReason = reason;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Mark every in-memory account sharing a refresh token as cooling down.
|
|
515
|
+
* @returns Number of live accounts updated.
|
|
516
|
+
*/
|
|
517
|
+
markAccountsWithRefreshTokenCoolingDown(refreshToken, cooldownMs, reason) {
|
|
518
|
+
const matches = this.accounts.filter((account) => account.refreshToken === refreshToken);
|
|
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
|
+
};
|
|
588
|
+
}
|
|
589
|
+
getMinWaitTime() {
|
|
590
|
+
return this.getMinWaitTimeForFamily("codex");
|
|
591
|
+
}
|
|
592
|
+
getMinWaitTimeForFamily(family, model) {
|
|
593
|
+
const now = nowMs();
|
|
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;
|
|
622
|
+
}
|
|
623
|
+
removeAccount(account) {
|
|
624
|
+
const idx = this.accounts.indexOf(account);
|
|
625
|
+
if (idx < 0) {
|
|
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;
|
|
656
|
+
}
|
|
657
|
+
removeAccountByIndex(index) {
|
|
658
|
+
if (!Number.isFinite(index))
|
|
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);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Remove all accounts that share the same refreshToken as the given account.
|
|
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;
|
|
685
|
+
}
|
|
686
|
+
setAccountEnabled(index, enabled) {
|
|
687
|
+
if (!Number.isFinite(index))
|
|
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;
|
|
696
|
+
}
|
|
697
|
+
async saveToDisk() {
|
|
698
|
+
const activeIndexByFamily = {};
|
|
699
|
+
for (const family of MODEL_FAMILIES) {
|
|
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);
|
|
729
|
+
}
|
|
730
|
+
saveToDiskDebounced(delayMs = 500) {
|
|
731
|
+
if (this.saveDebounceTimer) {
|
|
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);
|
|
752
|
+
}
|
|
753
|
+
async flushPendingSave() {
|
|
754
|
+
if (this.saveDebounceTimer) {
|
|
755
|
+
clearTimeout(this.saveDebounceTimer);
|
|
756
|
+
this.saveDebounceTimer = null;
|
|
757
|
+
await this.saveToDisk();
|
|
758
|
+
}
|
|
759
|
+
if (this.pendingSave) {
|
|
760
|
+
await this.pendingSave;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
export function formatAccountLabel(account, index) {
|
|
765
|
+
const accountLabel = account?.accountLabel?.trim();
|
|
766
|
+
const email = account?.email?.trim();
|
|
767
|
+
const accountId = account?.accountId?.trim();
|
|
768
|
+
const idSuffix = accountId ? (accountId.length > 6 ? accountId.slice(-6) : accountId) : null;
|
|
769
|
+
if (accountLabel && email && idSuffix) {
|
|
770
|
+
return `Account ${index + 1} (${accountLabel}, ${email}, id:${idSuffix})`;
|
|
771
|
+
}
|
|
772
|
+
if (accountLabel && email)
|
|
773
|
+
return `Account ${index + 1} (${accountLabel}, ${email})`;
|
|
774
|
+
if (accountLabel && idSuffix)
|
|
775
|
+
return `Account ${index + 1} (${accountLabel}, id:${idSuffix})`;
|
|
776
|
+
if (accountLabel)
|
|
777
|
+
return `Account ${index + 1} (${accountLabel})`;
|
|
778
|
+
if (email && idSuffix)
|
|
779
|
+
return `Account ${index + 1} (${email}, id:${idSuffix})`;
|
|
780
|
+
if (email)
|
|
781
|
+
return `Account ${index + 1} (${email})`;
|
|
782
|
+
if (idSuffix)
|
|
783
|
+
return `Account ${index + 1} (${idSuffix})`;
|
|
784
|
+
return `Account ${index + 1}`;
|
|
785
|
+
}
|
|
786
|
+
export function formatCooldown(account, now = nowMs()) {
|
|
787
|
+
if (typeof account.coolingDownUntil !== "number")
|
|
788
|
+
return null;
|
|
789
|
+
const remaining = account.coolingDownUntil - now;
|
|
790
|
+
if (remaining <= 0)
|
|
791
|
+
return null;
|
|
792
|
+
const reason = account.cooldownReason ? ` (${account.cooldownReason})` : "";
|
|
793
|
+
return `${formatWaitTime(remaining)}${reason}`;
|
|
794
|
+
}
|
|
795
|
+
//# sourceMappingURL=accounts.js.map
|