opencode-auth-load-balancer 0.2.1 → 0.3.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/dist/fetch.d.ts +1 -0
- package/dist/index.js +986 -886
- package/dist/providers/anthropic/constants.d.ts +9 -4
- package/dist/providers/anthropic/fallback.d.ts +3 -1
- package/dist/providers/openai/usage.d.ts +14 -8
- package/dist/providers/registry.d.ts +26 -0
- package/dist/providers/types.d.ts +10 -6
- package/dist/scheduler/select.d.ts +5 -2
- package/dist/status.d.ts +1 -1
- package/dist/types.d.ts +55 -1
- package/dist/usage-refresh.d.ts +34 -16
- package/dist/util.d.ts +4 -3
- package/package.json +8 -8
- package/tui/auth-load-balancer-tui.logic.ts +131 -8
- package/tui/auth-load-balancer-tui.view.tsx +254 -33
package/dist/index.js
CHANGED
|
@@ -12341,6 +12341,7 @@ import { dirname as dirname2 } from "node:path";
|
|
|
12341
12341
|
function emptyUsage() {
|
|
12342
12342
|
return { hourly: null, weekly: null, capturedAt: 0 };
|
|
12343
12343
|
}
|
|
12344
|
+
var MANUAL_DISABLED_REASON = "manually disabled";
|
|
12344
12345
|
|
|
12345
12346
|
// src/providers/http-timeouts.ts
|
|
12346
12347
|
var OAUTH_HTTP_TIMEOUT_MS = 30000;
|
|
@@ -12699,6 +12700,14 @@ function normalizeLastSelected(rows) {
|
|
|
12699
12700
|
}
|
|
12700
12701
|
return rows;
|
|
12701
12702
|
}
|
|
12703
|
+
function normalizeRelogin(value, now) {
|
|
12704
|
+
if (!isPlainObject2(value))
|
|
12705
|
+
return;
|
|
12706
|
+
const intent = value;
|
|
12707
|
+
if (typeof intent.accountId !== "string" || typeof intent.providerID !== "string" || !Number.isFinite(intent.expiresAt) || intent.expiresAt <= now)
|
|
12708
|
+
return;
|
|
12709
|
+
return intent;
|
|
12710
|
+
}
|
|
12702
12711
|
async function readRaw() {
|
|
12703
12712
|
let text;
|
|
12704
12713
|
try {
|
|
@@ -12716,6 +12725,7 @@ async function readRaw() {
|
|
|
12716
12725
|
return {
|
|
12717
12726
|
version: 1,
|
|
12718
12727
|
accounts: normalizeAccounts(parsed.accounts),
|
|
12728
|
+
relogin: normalizeRelogin(parsed.relogin, Date.now()),
|
|
12719
12729
|
lastSelected: isPlainObject2(parsed.lastSelected) ? normalizeLastSelected(parsed.lastSelected) : {},
|
|
12720
12730
|
sessions: isPlainObject2(parsed.sessions) ? normalizeSessions(parsed.sessions) : {}
|
|
12721
12731
|
};
|
|
@@ -12788,19 +12798,27 @@ function makeAccount(providerID, label, tokens) {
|
|
|
12788
12798
|
disabledReason: null
|
|
12789
12799
|
};
|
|
12790
12800
|
}
|
|
12801
|
+
function applyTokens(row, tokens) {
|
|
12802
|
+
if (tokens.refresh)
|
|
12803
|
+
row.refresh = tokens.refresh;
|
|
12804
|
+
row.access = tokens.access;
|
|
12805
|
+
row.expires = tokens.expires;
|
|
12806
|
+
row.disabledReason = null;
|
|
12807
|
+
if (tokens.accountId)
|
|
12808
|
+
row.accountId = tokens.accountId;
|
|
12809
|
+
return row;
|
|
12810
|
+
}
|
|
12791
12811
|
async function addAccount(providerID, tokens, label) {
|
|
12792
12812
|
return mutatePool((pool) => {
|
|
12813
|
+
const intent = pool.relogin?.providerID === providerID ? pool.relogin : undefined;
|
|
12814
|
+
if (intent)
|
|
12815
|
+
delete pool.relogin;
|
|
12793
12816
|
const existing = pool.accounts.find((a) => a.providerID === providerID && (tokens.refresh && a.refresh === tokens.refresh || tokens.accountId && a.accountId === tokens.accountId));
|
|
12794
|
-
if (existing)
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
existing.disabledReason = null;
|
|
12800
|
-
if (tokens.accountId)
|
|
12801
|
-
existing.accountId = tokens.accountId;
|
|
12802
|
-
return existing;
|
|
12803
|
-
}
|
|
12817
|
+
if (existing)
|
|
12818
|
+
return applyTokens(existing, tokens);
|
|
12819
|
+
const target = intent ? pool.accounts.find((account2) => account2.id === intent.accountId) : undefined;
|
|
12820
|
+
if (target)
|
|
12821
|
+
return applyTokens(target, tokens);
|
|
12804
12822
|
const used = new Set(pool.accounts.map((a) => a.label));
|
|
12805
12823
|
let n = 1;
|
|
12806
12824
|
while (used.has(`${providerID}-${n}`))
|
|
@@ -13153,26 +13171,31 @@ function loadConfig(env = process.env) {
|
|
|
13153
13171
|
// src/scheduler/select.ts
|
|
13154
13172
|
var IMMINENT_EXHAUSTION_BAND = 0.01;
|
|
13155
13173
|
var PROACTIVE_MIGRATE_MIN_DELTA = 0.02;
|
|
13174
|
+
var INCUMBENT_HYSTERESIS = 0.2;
|
|
13156
13175
|
function weeklyUtil(account) {
|
|
13157
13176
|
return account.usage.weekly?.utilization ?? 0;
|
|
13158
13177
|
}
|
|
13159
13178
|
var NO_EXCLUSIONS = new Set;
|
|
13160
|
-
function selectAccount(accounts, providerID, now, cfg = DEFAULT_CONFIG, exclude = NO_EXCLUSIONS) {
|
|
13161
|
-
let sawAvailable = false;
|
|
13179
|
+
function selectAccount(accounts, providerID, now, cfg = DEFAULT_CONFIG, exclude = NO_EXCLUSIONS, incumbentId = null) {
|
|
13162
13180
|
let bestAvail = null;
|
|
13163
13181
|
let bestAvailScore = Number.NEGATIVE_INFINITY;
|
|
13164
13182
|
let bestFallback = null;
|
|
13165
13183
|
let lowestWeeklyUtil = Number.POSITIVE_INFINITY;
|
|
13184
|
+
let incumbentAccount = null;
|
|
13185
|
+
let incumbentScore = Number.NEGATIVE_INFINITY;
|
|
13166
13186
|
for (const account of accounts) {
|
|
13167
13187
|
if (account.providerID !== providerID || account.disabledReason || exclude.has(account.id))
|
|
13168
13188
|
continue;
|
|
13169
13189
|
if (isAvailable(account, cfg, now)) {
|
|
13170
|
-
sawAvailable = true;
|
|
13171
13190
|
const score = scoreAccount(account, cfg, now);
|
|
13172
13191
|
if (score > bestAvailScore) {
|
|
13173
13192
|
bestAvailScore = score;
|
|
13174
13193
|
bestAvail = account;
|
|
13175
13194
|
}
|
|
13195
|
+
if (account.id === incumbentId) {
|
|
13196
|
+
incumbentAccount = account;
|
|
13197
|
+
incumbentScore = score;
|
|
13198
|
+
}
|
|
13176
13199
|
} else {
|
|
13177
13200
|
const util = weeklyUtil(account);
|
|
13178
13201
|
if (util < lowestWeeklyUtil) {
|
|
@@ -13181,8 +13204,11 @@ function selectAccount(accounts, providerID, now, cfg = DEFAULT_CONFIG, exclude
|
|
|
13181
13204
|
}
|
|
13182
13205
|
}
|
|
13183
13206
|
}
|
|
13184
|
-
if (
|
|
13185
|
-
|
|
13207
|
+
if (bestAvail) {
|
|
13208
|
+
if (incumbentAccount && incumbentScore >= bestAvailScore * (1 - INCUMBENT_HYSTERESIS))
|
|
13209
|
+
return { account: incumbentAccount, degraded: false, sticky: false };
|
|
13210
|
+
return { account: bestAvail, degraded: false, sticky: false };
|
|
13211
|
+
}
|
|
13186
13212
|
return bestFallback ? { account: bestFallback, degraded: true, sticky: false } : null;
|
|
13187
13213
|
}
|
|
13188
13214
|
function findPinned(pool, providerID, sessionKey, exclude) {
|
|
@@ -13206,9 +13232,9 @@ function isCheapMoment(requestBytes, cfg) {
|
|
|
13206
13232
|
function selectForSession(pool, providerID, sessionKey, now, cfg = DEFAULT_CONFIG, exclude = NO_EXCLUSIONS, requestBytes = 0) {
|
|
13207
13233
|
const pinned = sessionKey ? findPinned(pool, providerID, sessionKey, exclude) : null;
|
|
13208
13234
|
if (!pinned)
|
|
13209
|
-
return selectAccount(pool.accounts, providerID, now, cfg, exclude);
|
|
13235
|
+
return selectAccount(pool.accounts, providerID, now, cfg, exclude, pool.lastSelected[providerID] ?? null);
|
|
13210
13236
|
if (!isAvailable(pinned, cfg, now))
|
|
13211
|
-
return selectAccount(pool.accounts, providerID, now, cfg, exclude);
|
|
13237
|
+
return selectAccount(pool.accounts, providerID, now, cfg, exclude, pool.lastSelected[providerID] ?? null);
|
|
13212
13238
|
const weeklyUtilPinned = utilOf(pinned.usage.weekly, now);
|
|
13213
13239
|
const hourlyUtilPinned = utilOf(pinned.usage.hourly, now);
|
|
13214
13240
|
const pinnedOverSoft = weeklyUtilPinned >= cfg.weeklyDrainTarget || hourlyUtilPinned >= cfg.migrateAt;
|
|
@@ -13217,11 +13243,11 @@ function selectForSession(pool, providerID, sessionKey, now, cfg = DEFAULT_CONFI
|
|
|
13217
13243
|
const pinnedImminent = pinnedOverSoft && pinnedUtil >= cfg.exhaustedAt - IMMINENT_EXHAUSTION_BAND;
|
|
13218
13244
|
const cheap = (!pinnedImminent || cfg.drainMigrate) && isCheapMoment(typeof requestBytes === "function" ? requestBytes() : requestBytes, cfg);
|
|
13219
13245
|
if (pinnedOverSoft && (pinnedImminent || cheap) || cfg.drainMigrate && cheap) {
|
|
13220
|
-
const alt = selectAccount(pool.accounts, providerID, now, cfg, withExcluded(exclude, pinned.id));
|
|
13246
|
+
const alt = selectAccount(pool.accounts, providerID, now, cfg, withExcluded(exclude, pinned.id), null);
|
|
13221
13247
|
if (alt && !alt.degraded) {
|
|
13222
13248
|
if (pinnedOverSoft) {
|
|
13223
13249
|
const altUtil = maxUtil(alt.account, now);
|
|
13224
|
-
const proactiveBetter = pinnedImminent ? altUtil < pinnedUtil : pinnedUtil - altUtil >= PROACTIVE_MIGRATE_MIN_DELTA;
|
|
13250
|
+
const proactiveBetter = pinnedImminent ? altUtil < pinnedUtil : pinnedUtil - altUtil >= PROACTIVE_MIGRATE_MIN_DELTA && weeklyUrgency(alt.account, cfg, now) >= weeklyUrgency(pinned, cfg, now);
|
|
13225
13251
|
if (proactiveBetter) {
|
|
13226
13252
|
return { account: alt.account, degraded: false, sticky: false };
|
|
13227
13253
|
}
|
|
@@ -13295,804 +13321,306 @@ function preserveWeeklyAnchor(incoming, stored, now) {
|
|
|
13295
13321
|
return { utilization: incoming.utilization, resetAt: anchor };
|
|
13296
13322
|
}
|
|
13297
13323
|
|
|
13298
|
-
// src/
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
|
|
13303
|
-
|
|
13304
|
-
|
|
13305
|
-
|
|
13306
|
-
|
|
13307
|
-
|
|
13308
|
-
|
|
13309
|
-
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
13313
|
-
lastPoll.set(account.id, now);
|
|
13314
|
-
stale ??= [];
|
|
13315
|
-
stale.push(account);
|
|
13316
|
-
}
|
|
13317
|
-
if (aliveIds) {
|
|
13318
|
-
for (const id of lastPoll.keys())
|
|
13319
|
-
if (!aliveIds.has(id))
|
|
13320
|
-
lastPoll.delete(id);
|
|
13324
|
+
// src/providers/types.ts
|
|
13325
|
+
function classifyHttpStatus(status) {
|
|
13326
|
+
if (status === 429 || status === 402)
|
|
13327
|
+
return "account";
|
|
13328
|
+
if (status === 401 || status === 403)
|
|
13329
|
+
return "auth";
|
|
13330
|
+
if (status >= 500)
|
|
13331
|
+
return "service";
|
|
13332
|
+
return "ok";
|
|
13333
|
+
}
|
|
13334
|
+
function urlFromInput(input) {
|
|
13335
|
+
try {
|
|
13336
|
+
return new URL(input instanceof Request ? input.url : input);
|
|
13337
|
+
} catch {
|
|
13338
|
+
return null;
|
|
13321
13339
|
}
|
|
13322
|
-
if (!stale)
|
|
13323
|
-
return;
|
|
13324
|
-
await Promise.all(stale.map(async (account) => {
|
|
13325
|
-
try {
|
|
13326
|
-
await ensureAccessToken(adapter, account, now);
|
|
13327
|
-
const snapshot = await adapter.fetchUsage(account, now);
|
|
13328
|
-
if (snapshot) {
|
|
13329
|
-
await mutatePool((p) => {
|
|
13330
|
-
const stored = findAccount(p, account.id);
|
|
13331
|
-
if (stored)
|
|
13332
|
-
stored.usage = {
|
|
13333
|
-
hourly: snapshot.hourly ?? stored.usage.hourly,
|
|
13334
|
-
weekly: snapshot.weekly === null ? stored.usage.weekly : preserveWeeklyAnchor(snapshot.weekly, stored.usage.weekly, now),
|
|
13335
|
-
capturedAt: snapshot.weekly === null ? stored.usage.capturedAt : now
|
|
13336
|
-
};
|
|
13337
|
-
});
|
|
13338
|
-
}
|
|
13339
|
-
} catch {}
|
|
13340
|
-
}));
|
|
13341
13340
|
}
|
|
13342
13341
|
|
|
13343
|
-
// src/
|
|
13344
|
-
var
|
|
13345
|
-
var
|
|
13346
|
-
var
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13342
|
+
// src/providers/anthropic/fallback.ts
|
|
13343
|
+
var TIER_RESET_FALLBACK_MS = 60 * 60 * 1000;
|
|
13344
|
+
var TIER_RESET_MAX_MS = MAX_QUOTA_RESET_BOUND_MS;
|
|
13345
|
+
var ALPHA_SEGMENT_RE = /^[a-z]+$/;
|
|
13346
|
+
var DIGIT_SEGMENT_RE = /^\d+$/;
|
|
13347
|
+
var DATE_SEGMENT_RE = /^\d{8}$/;
|
|
13348
|
+
function modelFamily(model) {
|
|
13349
|
+
const lower = model.toLowerCase();
|
|
13350
|
+
if (!lower.startsWith("claude"))
|
|
13351
|
+
return null;
|
|
13352
|
+
for (const segment of lower.split("-")) {
|
|
13353
|
+
if (segment !== "claude" && ALPHA_SEGMENT_RE.test(segment))
|
|
13354
|
+
return segment;
|
|
13355
|
+
}
|
|
13356
|
+
return null;
|
|
13350
13357
|
}
|
|
13351
|
-
|
|
13358
|
+
function requestModelTier(body) {
|
|
13359
|
+
let parsed;
|
|
13352
13360
|
try {
|
|
13353
|
-
|
|
13354
|
-
} catch
|
|
13355
|
-
|
|
13356
|
-
log(`bookkeeping skipped (${what}): ${error45.message}`);
|
|
13357
|
-
return;
|
|
13358
|
-
}
|
|
13359
|
-
throw error45;
|
|
13361
|
+
parsed = JSON.parse(body);
|
|
13362
|
+
} catch {
|
|
13363
|
+
return null;
|
|
13360
13364
|
}
|
|
13365
|
+
const model = parsed.model;
|
|
13366
|
+
return typeof model === "string" ? modelFamily(model) : null;
|
|
13361
13367
|
}
|
|
13362
|
-
function
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
+
function versionVector(model) {
|
|
13369
|
+
const version2 = [];
|
|
13370
|
+
let date5 = -1;
|
|
13371
|
+
for (const segment of model.split("-")) {
|
|
13372
|
+
if (DATE_SEGMENT_RE.test(segment))
|
|
13373
|
+
date5 = Number(segment);
|
|
13374
|
+
else if (DIGIT_SEGMENT_RE.test(segment))
|
|
13375
|
+
version2.push(Number(segment));
|
|
13368
13376
|
}
|
|
13377
|
+
return { version: version2, date: date5 };
|
|
13369
13378
|
}
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
let
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
if (Number.isFinite(secs) && secs > 0) {
|
|
13377
|
-
const delta = secs * 1000;
|
|
13378
|
-
if (Number.isFinite(delta) && delta <= RETRY_AFTER_MAX_MS)
|
|
13379
|
-
until = now + delta;
|
|
13380
|
-
} else {
|
|
13381
|
-
const httpDate = Date.parse(retryAfter);
|
|
13382
|
-
if (Number.isFinite(httpDate) && httpDate > now && httpDate - now <= RETRY_AFTER_MAX_MS)
|
|
13383
|
-
until = httpDate;
|
|
13384
|
-
}
|
|
13379
|
+
function compareVersions(a, b) {
|
|
13380
|
+
const len = Math.max(a.version.length, b.version.length);
|
|
13381
|
+
for (let i = 0;i < len; i++) {
|
|
13382
|
+
const d = (a.version[i] ?? -1) - (b.version[i] ?? -1);
|
|
13383
|
+
if (d !== 0)
|
|
13384
|
+
return d;
|
|
13385
13385
|
}
|
|
13386
|
-
return
|
|
13387
|
-
}
|
|
13388
|
-
async function applyCooldown(accountId, fallbackMs) {
|
|
13389
|
-
const until = cooldownUntilFrom(null, fallbackMs, Date.now());
|
|
13390
|
-
await bestEffort("cooldown", () => mutatePool((pool) => {
|
|
13391
|
-
const account = findAccount(pool, accountId);
|
|
13392
|
-
if (account)
|
|
13393
|
-
account.cooldownUntil = Math.max(account.cooldownUntil, until);
|
|
13394
|
-
}));
|
|
13395
|
-
}
|
|
13396
|
-
async function recordRotation(adapter, accountId, res, fallbackMs, now) {
|
|
13397
|
-
const partial2 = adapter.parseUsageHeaders(res.headers);
|
|
13398
|
-
const until = cooldownUntilFrom(res, fallbackMs, now);
|
|
13399
|
-
await bestEffort("rotation", () => mutatePool((pool) => {
|
|
13400
|
-
const account = findAccount(pool, accountId);
|
|
13401
|
-
if (!account)
|
|
13402
|
-
return;
|
|
13403
|
-
if (partial2)
|
|
13404
|
-
applyUsagePartial(account, partial2, now);
|
|
13405
|
-
account.cooldownUntil = Math.max(account.cooldownUntil, until);
|
|
13406
|
-
}));
|
|
13407
|
-
}
|
|
13408
|
-
async function recordModelCooldown(accountId, tier, until, partial2, now) {
|
|
13409
|
-
await bestEffort("model-cooldown", () => mutatePool((pool) => {
|
|
13410
|
-
const account = findAccount(pool, accountId);
|
|
13411
|
-
if (!account)
|
|
13412
|
-
return;
|
|
13413
|
-
if (partial2)
|
|
13414
|
-
applyUsagePartial(account, partial2, now);
|
|
13415
|
-
const map2 = account.modelCooldownsUntil ??= {};
|
|
13416
|
-
map2[tier] = Math.max(map2[tier] ?? 0, until);
|
|
13417
|
-
}));
|
|
13386
|
+
return a.date - b.date;
|
|
13418
13387
|
}
|
|
13419
|
-
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
|
|
13426
|
-
|
|
13427
|
-
|
|
13428
|
-
|
|
13429
|
-
|
|
13430
|
-
|
|
13431
|
-
|
|
13432
|
-
delete pool.sessions[k];
|
|
13433
|
-
}
|
|
13388
|
+
function bestOfFamily(models, family) {
|
|
13389
|
+
let best = null;
|
|
13390
|
+
let bestVersion = {
|
|
13391
|
+
version: [],
|
|
13392
|
+
date: -1
|
|
13393
|
+
};
|
|
13394
|
+
for (const model of models) {
|
|
13395
|
+
if (modelFamily(model) !== family)
|
|
13396
|
+
continue;
|
|
13397
|
+
const version2 = versionVector(model);
|
|
13398
|
+
if (best === null || compareVersions(version2, bestVersion) > 0) {
|
|
13399
|
+
best = model;
|
|
13400
|
+
bestVersion = version2;
|
|
13434
13401
|
}
|
|
13435
|
-
}
|
|
13402
|
+
}
|
|
13403
|
+
return best;
|
|
13436
13404
|
}
|
|
13437
|
-
|
|
13438
|
-
const
|
|
13439
|
-
|
|
13440
|
-
|
|
13441
|
-
|
|
13442
|
-
|
|
13443
|
-
headers: { "content-type": "application/json" }
|
|
13444
|
-
});
|
|
13405
|
+
var computeFamilyOrder = memoOne((raw) => {
|
|
13406
|
+
const parsed = (raw ?? "").split(",").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
|
|
13407
|
+
return parsed.length > 0 ? parsed : DEFAULT_FAMILY_ORDER;
|
|
13408
|
+
});
|
|
13409
|
+
function resolveFamilyOrder() {
|
|
13410
|
+
return computeFamilyOrder(process.env.OPENCODE_AUTH_LB_ANTHROPIC_FAMILY_ORDER);
|
|
13445
13411
|
}
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
13449
|
-
|
|
13450
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
13453
|
-
|
|
13412
|
+
var computeFallbackSetting = memoOne((raw) => {
|
|
13413
|
+
const trimmed = raw?.trim();
|
|
13414
|
+
return raw === undefined ? { kind: "ladder" } : trimmed ? { kind: "pinned", model: trimmed } : { kind: "disabled" };
|
|
13415
|
+
});
|
|
13416
|
+
function resolveFallbackSetting() {
|
|
13417
|
+
return computeFallbackSetting(process.env.OPENCODE_AUTH_LB_ANTHROPIC_OPUS_FALLBACK_MODEL);
|
|
13418
|
+
}
|
|
13419
|
+
function ladderTargetForFamily(fromFamily, models) {
|
|
13420
|
+
const order = resolveFamilyOrder();
|
|
13421
|
+
const start = order.indexOf(fromFamily) + 1;
|
|
13422
|
+
for (let i = start;i < order.length; i++) {
|
|
13423
|
+
const family = order[i];
|
|
13424
|
+
if (family === undefined)
|
|
13454
13425
|
continue;
|
|
13455
|
-
const
|
|
13456
|
-
if (
|
|
13457
|
-
|
|
13426
|
+
const best = bestOfFamily(models, family);
|
|
13427
|
+
if (best !== null)
|
|
13428
|
+
return best;
|
|
13458
13429
|
}
|
|
13459
|
-
return
|
|
13430
|
+
return null;
|
|
13460
13431
|
}
|
|
13461
|
-
function
|
|
13462
|
-
const
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
if (typeof currentBody === "string" && fallbackPlanFor !== currentBody) {
|
|
13487
|
-
fallbackPlanFor = currentBody;
|
|
13488
|
-
fallbackPlan = adapter.planModelFallback?.(currentBody, models) ?? null;
|
|
13489
|
-
}
|
|
13490
|
-
return fallbackPlan;
|
|
13491
|
-
};
|
|
13492
|
-
let fallbackInfo = null;
|
|
13493
|
-
let pendingTierFallback = null;
|
|
13494
|
-
let tierSkipped = null;
|
|
13495
|
-
let usageSeeded = false;
|
|
13496
|
-
const waitDeadline = Date.now() + cfg.maxWaitMs;
|
|
13497
|
-
for (;; ) {
|
|
13498
|
-
const pool = await readPool();
|
|
13499
|
-
const now = Date.now();
|
|
13500
|
-
if (!usageSeeded) {
|
|
13501
|
-
usageSeeded = true;
|
|
13502
|
-
refreshUsageInBackground(adapter, now, pool).catch(ignore);
|
|
13503
|
-
}
|
|
13504
|
-
const selection = selectForSession(pool, adapter.id, sessionKey, now, cfg, tried, requestBytes);
|
|
13505
|
-
if (!selection) {
|
|
13506
|
-
if (tried.size === 0)
|
|
13507
|
-
return noUsableAccountResponse(adapter.id);
|
|
13508
|
-
if (pendingTierFallback !== null) {
|
|
13509
|
-
const adopted = pendingTierFallback;
|
|
13510
|
-
pendingTierFallback = null;
|
|
13511
|
-
currentBody = adopted.body;
|
|
13512
|
-
currentTier = adapter.requestModelTier?.(adopted.body) ?? null;
|
|
13513
|
-
fallbackInfo = fallbackInfo === null ? adopted : { ...adopted, fromModel: fallbackInfo.fromModel };
|
|
13514
|
-
if (tierSkipped !== null)
|
|
13515
|
-
for (const id of tierSkipped)
|
|
13516
|
-
tried.delete(id);
|
|
13517
|
-
tierSkipped = null;
|
|
13518
|
-
if (DEBUG)
|
|
13519
|
-
log(`~~ every ${adapter.id} account is tier-limited for ${adopted.fromModel} -> downgraded to ${adopted.toModel}`);
|
|
13520
|
-
continue;
|
|
13521
|
-
}
|
|
13522
|
-
if (cfg.maxWaitMs > 0 && waitableCooldownIds) {
|
|
13523
|
-
const resumeAt = soonestCooldownUntil(pool.accounts, now, waitableCooldownIds);
|
|
13524
|
-
if (resumeAt !== null && resumeAt <= waitDeadline) {
|
|
13525
|
-
let remaining = resumeAt - Date.now();
|
|
13526
|
-
while (remaining > 0) {
|
|
13527
|
-
await sleepAbortable(remaining, signal);
|
|
13528
|
-
remaining = resumeAt - Date.now();
|
|
13529
|
-
}
|
|
13530
|
-
tried.clear();
|
|
13531
|
-
waitableCooldownIds.clear();
|
|
13532
|
-
continue;
|
|
13533
|
-
}
|
|
13534
|
-
}
|
|
13535
|
-
break;
|
|
13536
|
-
}
|
|
13537
|
-
const { account, degraded, sticky } = selection;
|
|
13538
|
-
tried.add(account.id);
|
|
13539
|
-
if (currentTier !== null && (account.modelCooldownsUntil?.[currentTier] ?? 0) > now) {
|
|
13540
|
-
const plan = planDowngrade();
|
|
13541
|
-
if (plan !== null) {
|
|
13542
|
-
pendingTierFallback = plan;
|
|
13543
|
-
tierSkipped ??= new Set;
|
|
13544
|
-
tierSkipped.add(account.id);
|
|
13545
|
-
if (DEBUG)
|
|
13546
|
-
log(`~~ ${account.label} ${currentTier} tier exhausted -> skipped for ${currentTier}`);
|
|
13547
|
-
continue;
|
|
13548
|
-
}
|
|
13549
|
-
}
|
|
13550
|
-
try {
|
|
13551
|
-
await ensureAccessToken(adapter, account, now);
|
|
13552
|
-
const headers = new Headers(baseHeaders);
|
|
13553
|
-
adapter.applyAuth(headers, account);
|
|
13554
|
-
if (DEBUG)
|
|
13555
|
-
log(`-> ${adapter.id} via ${account.label}${sticky ? " (sticky)" : ""}${degraded ? " (degraded)" : ""}`);
|
|
13556
|
-
const res = await fetch(transformedUrl, {
|
|
13557
|
-
...init,
|
|
13558
|
-
body: currentBody,
|
|
13559
|
-
headers
|
|
13560
|
-
});
|
|
13561
|
-
const cls = adapter.classifyError(res.status);
|
|
13562
|
-
if (cls === "account" || cls === "auth") {
|
|
13563
|
-
await res.body?.cancel().catch(ignore);
|
|
13564
|
-
if (cls === "account" && typeof currentBody === "string") {
|
|
13565
|
-
const reactiveNow = Date.now();
|
|
13566
|
-
const plan = adapter.planReactiveFallback?.(res, currentBody, reactiveNow, models);
|
|
13567
|
-
if (plan) {
|
|
13568
|
-
await recordModelCooldown(account.id, plan.tier, plan.resetAt, adapter.parseUsageHeaders(res.headers), reactiveNow);
|
|
13569
|
-
const local = account.modelCooldownsUntil ??= {};
|
|
13570
|
-
local[plan.tier] = Math.max(local[plan.tier] ?? 0, plan.resetAt);
|
|
13571
|
-
pendingTierFallback = plan.fallback;
|
|
13572
|
-
tierSkipped ??= new Set;
|
|
13573
|
-
tierSkipped.add(account.id);
|
|
13574
|
-
lastError = new Error(`${adapter.id} account "${account.label}" hit its "${plan.tier}" model-tier cap`);
|
|
13575
|
-
if (DEBUG)
|
|
13576
|
-
log(`~~ ${account.label} ${plan.tier} tier limited -> rotating (fallback ${plan.fallback.toModel} pending)`);
|
|
13577
|
-
continue;
|
|
13578
|
-
}
|
|
13579
|
-
}
|
|
13580
|
-
const ms = cls === "auth" ? AUTH_COOLDOWN_MS : ACCOUNT_COOLDOWN_MS;
|
|
13581
|
-
await recordRotation(adapter, account.id, res, ms, Date.now());
|
|
13582
|
-
if (cls === "account") {
|
|
13583
|
-
waitableCooldownIds ??= new Set;
|
|
13584
|
-
waitableCooldownIds.add(account.id);
|
|
13585
|
-
}
|
|
13586
|
-
lastError = new Error(`${adapter.id} account "${account.label}" returned ${res.status}`);
|
|
13587
|
-
if (DEBUG)
|
|
13588
|
-
log(`!! ${account.label} ${res.status} (${cls}) -> rotating`);
|
|
13589
|
-
continue;
|
|
13590
|
-
}
|
|
13591
|
-
const successNow = Date.now();
|
|
13592
|
-
const partial2 = adapter.parseUsageHeaders(res.headers);
|
|
13593
|
-
if (partial2)
|
|
13594
|
-
applyUsagePartial(account, partial2, successNow);
|
|
13595
|
-
if (fallbackInfo)
|
|
13596
|
-
hooks.onModelFallback?.(adapter.id, account, fallbackInfo.fromModel, fallbackInfo.toModel, fallbackInfo.fromTier);
|
|
13597
|
-
hooks.onUse?.(adapter.id, account);
|
|
13598
|
-
await recordSuccess(adapter, account.id, partial2, sessionKey, successNow, cfg.sessionTtlMs);
|
|
13599
|
-
return adapter.transformResponse(res);
|
|
13600
|
-
} catch (error45) {
|
|
13601
|
-
lastError = error45;
|
|
13602
|
-
const aborted2 = signal?.aborted === true || error45 instanceof Error && error45.name === "AbortError";
|
|
13603
|
-
if (aborted2)
|
|
13604
|
-
throw error45;
|
|
13605
|
-
if (!account.disabledReason)
|
|
13606
|
-
await applyCooldown(account.id, AUTH_COOLDOWN_MS);
|
|
13607
|
-
if (DEBUG)
|
|
13608
|
-
log(`!! ${account.label} threw: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
13609
|
-
continue;
|
|
13610
|
-
}
|
|
13611
|
-
}
|
|
13612
|
-
throw lastError ?? new Error(`${adapter.id}: no usable account in the load-balancer pool`);
|
|
13432
|
+
function downgradeModel(body, models) {
|
|
13433
|
+
const setting = resolveFallbackSetting();
|
|
13434
|
+
if (setting.kind === "disabled")
|
|
13435
|
+
return null;
|
|
13436
|
+
let parsed;
|
|
13437
|
+
try {
|
|
13438
|
+
parsed = JSON.parse(body);
|
|
13439
|
+
} catch {
|
|
13440
|
+
return null;
|
|
13441
|
+
}
|
|
13442
|
+
const from = parsed.model;
|
|
13443
|
+
if (typeof from !== "string")
|
|
13444
|
+
return null;
|
|
13445
|
+
const fromFamily = modelFamily(from);
|
|
13446
|
+
if (fromFamily === null)
|
|
13447
|
+
return null;
|
|
13448
|
+
const target = setting.kind === "pinned" ? setting.model : ladderTargetForFamily(fromFamily, models) ?? DEFAULT_OPUS_FALLBACK_MODEL;
|
|
13449
|
+
if (modelFamily(target) === fromFamily)
|
|
13450
|
+
return null;
|
|
13451
|
+
parsed.model = target;
|
|
13452
|
+
return {
|
|
13453
|
+
body: JSON.stringify(parsed),
|
|
13454
|
+
fromModel: from,
|
|
13455
|
+
toModel: target,
|
|
13456
|
+
fromTier: fromFamily
|
|
13613
13457
|
};
|
|
13614
13458
|
}
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
13620
|
-
};
|
|
13621
|
-
function compareAscii(a, b) {
|
|
13622
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
13623
|
-
}
|
|
13624
|
-
function byProviderId(a, b) {
|
|
13625
|
-
return compareAscii(a[0], b[0]);
|
|
13459
|
+
function parseTierReset(res, now) {
|
|
13460
|
+
const ms = secondsToMs(Number(res.headers.get(UNIFIED_RESET_HEADER)));
|
|
13461
|
+
if (ms > now && ms - now <= TIER_RESET_MAX_MS)
|
|
13462
|
+
return ms;
|
|
13463
|
+
return now + TIER_RESET_FALLBACK_MS;
|
|
13626
13464
|
}
|
|
13627
|
-
function
|
|
13628
|
-
|
|
13465
|
+
function planReactiveFallback(res, body, now, models) {
|
|
13466
|
+
const claim = res.headers.get(REPRESENTATIVE_CLAIM_HEADER);
|
|
13467
|
+
if (!claim || !MODEL_TIER_CLAIM_RE.test(claim))
|
|
13468
|
+
return null;
|
|
13469
|
+
const fallback = downgradeModel(body, models);
|
|
13470
|
+
if (!fallback)
|
|
13471
|
+
return null;
|
|
13472
|
+
return {
|
|
13473
|
+
tier: fallback.fromTier,
|
|
13474
|
+
resetAt: parseTierReset(res, now),
|
|
13475
|
+
fallback
|
|
13476
|
+
};
|
|
13629
13477
|
}
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
|
|
13478
|
+
|
|
13479
|
+
// src/providers/oauth-callback.ts
|
|
13480
|
+
function toTokenSet(json2, previousRefresh) {
|
|
13633
13481
|
return {
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
|
|
13637
|
-
hourlyUtil: displayUtil(account.usage.hourly, now),
|
|
13638
|
-
weeklyResetAt: weekly && !weeklyExpired ? weekly.resetAt : 0,
|
|
13639
|
-
available,
|
|
13640
|
-
cooldownUntil: account.cooldownUntil,
|
|
13641
|
-
modelCooldownsUntil: account.modelCooldownsUntil ?? {},
|
|
13642
|
-
disabledReason: account.disabledReason,
|
|
13643
|
-
current,
|
|
13644
|
-
rank
|
|
13482
|
+
access: json2.access_token,
|
|
13483
|
+
refresh: json2.refresh_token || previousRefresh,
|
|
13484
|
+
expires: Date.now() + json2.expires_in * 1000
|
|
13645
13485
|
};
|
|
13646
13486
|
}
|
|
13647
|
-
function
|
|
13648
|
-
const
|
|
13649
|
-
|
|
13650
|
-
|
|
13651
|
-
|
|
13652
|
-
|
|
13653
|
-
|
|
13654
|
-
|
|
13487
|
+
async function readTokenResponse(res) {
|
|
13488
|
+
const json2 = await res.json().catch(() => null);
|
|
13489
|
+
if (!json2 || typeof json2.access_token !== "string" || json2.access_token === "" || !Number.isFinite(json2.expires_in) || json2.expires_in <= 0 || !Number.isFinite(json2.expires_in * 1000) || json2.refresh_token !== undefined && json2.refresh_token !== null && typeof json2.refresh_token !== "string")
|
|
13490
|
+
return null;
|
|
13491
|
+
return json2;
|
|
13492
|
+
}
|
|
13493
|
+
async function readExchangeResponse(res) {
|
|
13494
|
+
if (!res.ok) {
|
|
13495
|
+
await res.body?.cancel().catch(ignore);
|
|
13496
|
+
return null;
|
|
13655
13497
|
}
|
|
13656
|
-
|
|
13657
|
-
return sorted.map(([providerID, providerAccounts]) => {
|
|
13658
|
-
const currentAccountId = pool.lastSelected[providerID] ?? null;
|
|
13659
|
-
const ranked = providerAccounts.map((a) => {
|
|
13660
|
-
const available = isAvailable(a, cfg, now);
|
|
13661
|
-
return {
|
|
13662
|
-
a,
|
|
13663
|
-
available,
|
|
13664
|
-
score: available ? scoreAccount(a, cfg, now) : Number.NEGATIVE_INFINITY,
|
|
13665
|
-
weeklyUtil: a.usage.weekly?.utilization ?? 0
|
|
13666
|
-
};
|
|
13667
|
-
}).sort(compareRanked);
|
|
13668
|
-
const accounts = ranked.map(({ a, available }, i) => toStatus(a, now, a.id === currentAccountId, available, i + 1));
|
|
13669
|
-
return { providerID, accounts };
|
|
13670
|
-
});
|
|
13498
|
+
return readTokenResponse(res);
|
|
13671
13499
|
}
|
|
13672
|
-
async function
|
|
13673
|
-
|
|
13500
|
+
async function readRefreshResponse(res) {
|
|
13501
|
+
if (!res.ok)
|
|
13502
|
+
throw new Error(`Token refresh failed: ${res.status} — ${await res.text()}`);
|
|
13503
|
+
const json2 = await readTokenResponse(res);
|
|
13504
|
+
if (!json2)
|
|
13505
|
+
throw new Error(`Token refresh failed: ${res.status} — malformed token response body`);
|
|
13506
|
+
return json2;
|
|
13674
13507
|
}
|
|
13675
|
-
function
|
|
13676
|
-
return
|
|
13508
|
+
function generateState() {
|
|
13509
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
13677
13510
|
}
|
|
13678
|
-
function
|
|
13679
|
-
|
|
13680
|
-
|
|
13681
|
-
|
|
13682
|
-
const
|
|
13683
|
-
|
|
13684
|
-
|
|
13685
|
-
|
|
13686
|
-
|
|
13687
|
-
|
|
13688
|
-
|
|
13689
|
-
|
|
13690
|
-
|
|
13691
|
-
|
|
13692
|
-
|
|
13693
|
-
return "-";
|
|
13694
|
-
const mins = Math.max(1, Math.round((at - now) / 60000));
|
|
13695
|
-
if (mins < 60)
|
|
13696
|
-
return `${mins}m`;
|
|
13697
|
-
const hrs = Math.floor(mins / 60);
|
|
13698
|
-
if (hrs < 24)
|
|
13699
|
-
return `${hrs}h${mins % 60}m`;
|
|
13700
|
-
return `${Math.floor(hrs / 24)}d${hrs % 24}h`;
|
|
13701
|
-
}
|
|
13702
|
-
function stateOf(a, now) {
|
|
13703
|
-
if (a.disabledReason)
|
|
13704
|
-
return "re-login";
|
|
13705
|
-
if (a.cooldownUntil > now)
|
|
13706
|
-
return `cooldown ${relTime(a.cooldownUntil, now)}`;
|
|
13707
|
-
if (!a.available)
|
|
13708
|
-
return "exhausted";
|
|
13709
|
-
const base = a.current ? "in use" : "ready";
|
|
13710
|
-
const tiers = Object.entries(a.modelCooldownsUntil).filter(([, until]) => until > now).sort(([x], [y]) => compareAscii(x, y)).map(([tier, until]) => `${tier} ${relTime(until, now)}`);
|
|
13711
|
-
if (tiers.length > 0)
|
|
13712
|
-
return `${base} · ${tiers.join(" · ")}`;
|
|
13713
|
-
return base;
|
|
13714
|
-
}
|
|
13715
|
-
function providerName(providerID) {
|
|
13716
|
-
return PROVIDER_NAMES[providerID] ?? providerID;
|
|
13717
|
-
}
|
|
13718
|
-
function renderStatus(providers, now = Date.now()) {
|
|
13719
|
-
if (providers.length === 0)
|
|
13720
|
-
return "auth-load-balancer: no accounts registered.";
|
|
13721
|
-
const lines = [];
|
|
13722
|
-
for (const p of providers) {
|
|
13723
|
-
const current = p.accounts.find((a) => a.current);
|
|
13724
|
-
lines.push(`${providerName(p.providerID)} — in use: ${current ? current.label : "(none yet)"}`);
|
|
13725
|
-
const width = p.accounts.reduce((w, a) => Math.max(w, displayWidth(a.label)), 16);
|
|
13726
|
-
lines.push(` # ${"account".padEnd(width + 3)}weekly 5h resets state`);
|
|
13727
|
-
for (const a of p.accounts) {
|
|
13728
|
-
const mark = a.current ? "▶" : " ";
|
|
13729
|
-
lines.push(` ${String(a.rank).padEnd(2)} ${mark} ${padDisplayEnd(a.label, width)} ${pct(a.weeklyUtil).padStart(5)} ${pct(a.hourlyUtil).padStart(5)} ${relTime(a.weeklyResetAt, now).padStart(7)} ${stateOf(a, now)}`);
|
|
13511
|
+
function parseCallbackInput(input, options = {}) {
|
|
13512
|
+
const trimmed = input.trim();
|
|
13513
|
+
let isUrl = false;
|
|
13514
|
+
try {
|
|
13515
|
+
const url2 = new URL(trimmed);
|
|
13516
|
+
isUrl = true;
|
|
13517
|
+
const code2 = url2.searchParams.get("code");
|
|
13518
|
+
const state2 = url2.searchParams.get("state");
|
|
13519
|
+
if (code2 && state2)
|
|
13520
|
+
return { code: code2, state: state2 };
|
|
13521
|
+
} catch {}
|
|
13522
|
+
if (options.allowHashFormat && !isUrl) {
|
|
13523
|
+
const hashSplits = trimmed.split("#");
|
|
13524
|
+
if (hashSplits.length === 2 && hashSplits[0] && hashSplits[1]) {
|
|
13525
|
+
return { code: hashSplits[0], state: hashSplits[1] };
|
|
13730
13526
|
}
|
|
13731
|
-
lines.push("");
|
|
13732
13527
|
}
|
|
13733
|
-
|
|
13734
|
-
|
|
13528
|
+
const params = new URLSearchParams(trimmed);
|
|
13529
|
+
const code = params.get("code");
|
|
13530
|
+
const state = params.get("state");
|
|
13531
|
+
if (code && state)
|
|
13532
|
+
return { code, state };
|
|
13533
|
+
return null;
|
|
13735
13534
|
}
|
|
13736
13535
|
|
|
13737
|
-
// src/
|
|
13738
|
-
|
|
13739
|
-
|
|
13740
|
-
}
|
|
13741
|
-
var lastToasted = new Map;
|
|
13742
|
-
async function notifyOnSwitch(client, providerID, account) {
|
|
13743
|
-
if (lastToasted.get(providerID) === account.id)
|
|
13744
|
-
return;
|
|
13745
|
-
lastToasted.set(providerID, account.id);
|
|
13746
|
-
const now = Date.now();
|
|
13747
|
-
const message = `▶ ${account.label} · weekly ${pct(displayUtil(account.usage.weekly, now))} · 5h ${pct(displayUtil(account.usage.hourly, now))}`;
|
|
13748
|
-
await postToast(client, {
|
|
13749
|
-
title: `${providerName(providerID)} account`,
|
|
13750
|
-
message,
|
|
13751
|
-
variant: "info",
|
|
13752
|
-
duration: 4000
|
|
13753
|
-
});
|
|
13536
|
+
// src/providers/pkce.ts
|
|
13537
|
+
function base64UrlEncode(bytes) {
|
|
13538
|
+
return Buffer.from(bytes).toString("base64url");
|
|
13754
13539
|
}
|
|
13755
|
-
|
|
13756
|
-
|
|
13757
|
-
|
|
13758
|
-
const
|
|
13759
|
-
const
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
latestWindow = triggeringWindow;
|
|
13765
|
-
} else if (tiers) {
|
|
13766
|
-
for (const until of Object.values(tiers)) {
|
|
13767
|
-
if (until > now && until > latestWindow)
|
|
13768
|
-
latestWindow = until;
|
|
13769
|
-
}
|
|
13770
|
-
}
|
|
13771
|
-
const window = `${fromModel}@${latestWindow}`;
|
|
13772
|
-
if (lastFallbackToasted.get(key) === window)
|
|
13773
|
-
return;
|
|
13774
|
-
setBounded(lastFallbackToasted, key, window, LAST_FALLBACK_TOASTED_MAX);
|
|
13775
|
-
await postToast(client, {
|
|
13776
|
-
title: `${providerName(providerID)} model fallback`,
|
|
13777
|
-
message: `▶ ${account.label} · ${fromModel} → ${toModel} (model-tier weekly limit)`,
|
|
13778
|
-
variant: "warning",
|
|
13779
|
-
duration: 6000
|
|
13780
|
-
});
|
|
13540
|
+
async function generatePKCE() {
|
|
13541
|
+
const buf = new Uint8Array(64);
|
|
13542
|
+
crypto.getRandomValues(buf);
|
|
13543
|
+
const verifier = base64UrlEncode(buf);
|
|
13544
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
13545
|
+
return {
|
|
13546
|
+
verifier,
|
|
13547
|
+
challenge: base64UrlEncode(new Uint8Array(digest))
|
|
13548
|
+
};
|
|
13781
13549
|
}
|
|
13782
13550
|
|
|
13783
|
-
// src/
|
|
13784
|
-
async function
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
if (selection)
|
|
13795
|
-
pool.lastSelected[providerID] = selection.account.id;
|
|
13551
|
+
// src/providers/anthropic/oauth.ts
|
|
13552
|
+
async function postToken(body) {
|
|
13553
|
+
return fetch(TOKEN_URL, {
|
|
13554
|
+
method: "POST",
|
|
13555
|
+
headers: {
|
|
13556
|
+
"Content-Type": "application/json",
|
|
13557
|
+
Accept: "application/json, text/plain, */*",
|
|
13558
|
+
"User-Agent": "axios/1.13.6"
|
|
13559
|
+
},
|
|
13560
|
+
body: JSON.stringify(body),
|
|
13561
|
+
signal: AbortSignal.timeout(OAUTH_HTTP_TIMEOUT_MS)
|
|
13796
13562
|
});
|
|
13797
13563
|
}
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
13805
|
-
|
|
13806
|
-
|
|
13807
|
-
|
|
13564
|
+
async function authorize() {
|
|
13565
|
+
const pkce = await generatePKCE();
|
|
13566
|
+
const state = generateState();
|
|
13567
|
+
const url2 = new URL(AUTHORIZE_URL);
|
|
13568
|
+
url2.searchParams.set("code", "true");
|
|
13569
|
+
url2.searchParams.set("client_id", CLIENT_ID);
|
|
13570
|
+
url2.searchParams.set("response_type", "code");
|
|
13571
|
+
url2.searchParams.set("redirect_uri", CODE_CALLBACK_URL);
|
|
13572
|
+
url2.searchParams.set("scope", OAUTH_SCOPES.join(" "));
|
|
13573
|
+
url2.searchParams.set("code_challenge", pkce.challenge);
|
|
13574
|
+
url2.searchParams.set("code_challenge_method", "S256");
|
|
13575
|
+
url2.searchParams.set("state", state);
|
|
13576
|
+
return {
|
|
13577
|
+
url: url2.toString(),
|
|
13578
|
+
redirectUri: CODE_CALLBACK_URL,
|
|
13579
|
+
state,
|
|
13580
|
+
verifier: pkce.verifier
|
|
13581
|
+
};
|
|
13808
13582
|
}
|
|
13809
|
-
function
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
} catch {
|
|
13583
|
+
async function exchange(input, verifier, redirectUri, expectedState) {
|
|
13584
|
+
const callback = parseCallbackInput(input, { allowHashFormat: true });
|
|
13585
|
+
if (!callback)
|
|
13813
13586
|
return null;
|
|
13814
|
-
|
|
13815
|
-
}
|
|
13816
|
-
|
|
13817
|
-
// src/providers/anthropic/fallback.ts
|
|
13818
|
-
var TIER_RESET_FALLBACK_MS = 60 * 60 * 1000;
|
|
13819
|
-
var TIER_RESET_MAX_MS = MAX_QUOTA_RESET_BOUND_MS;
|
|
13820
|
-
var ALPHA_SEGMENT_RE = /^[a-z]+$/;
|
|
13821
|
-
var DIGIT_SEGMENT_RE = /^\d+$/;
|
|
13822
|
-
var DATE_SEGMENT_RE = /^\d{8}$/;
|
|
13823
|
-
function modelFamily(model) {
|
|
13824
|
-
const lower = model.toLowerCase();
|
|
13825
|
-
if (!lower.startsWith("claude"))
|
|
13587
|
+
if (expectedState && callback.state !== expectedState)
|
|
13826
13588
|
return null;
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
} catch {
|
|
13589
|
+
const result = await postToken({
|
|
13590
|
+
code: callback.code,
|
|
13591
|
+
state: callback.state,
|
|
13592
|
+
grant_type: "authorization_code",
|
|
13593
|
+
client_id: CLIENT_ID,
|
|
13594
|
+
redirect_uri: redirectUri,
|
|
13595
|
+
code_verifier: verifier
|
|
13596
|
+
});
|
|
13597
|
+
const json2 = await readExchangeResponse(result);
|
|
13598
|
+
if (!json2)
|
|
13838
13599
|
return null;
|
|
13839
|
-
|
|
13840
|
-
const model = parsed.model;
|
|
13841
|
-
return typeof model === "string" ? modelFamily(model) : null;
|
|
13600
|
+
return toTokenSet(json2, "");
|
|
13842
13601
|
}
|
|
13843
|
-
function
|
|
13844
|
-
const
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
13850
|
-
|
|
13851
|
-
}
|
|
13852
|
-
return { version: version2, date: date5 };
|
|
13602
|
+
async function refresh(refreshToken) {
|
|
13603
|
+
const response = await postToken({
|
|
13604
|
+
grant_type: "refresh_token",
|
|
13605
|
+
refresh_token: refreshToken,
|
|
13606
|
+
client_id: CLIENT_ID
|
|
13607
|
+
});
|
|
13608
|
+
const json2 = await readRefreshResponse(response);
|
|
13609
|
+
return toTokenSet(json2, refreshToken);
|
|
13853
13610
|
}
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
13611
|
+
|
|
13612
|
+
// src/providers/anthropic/cch.ts
|
|
13613
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
13614
|
+
function messageText({ content }) {
|
|
13615
|
+
if (typeof content === "string")
|
|
13616
|
+
return content;
|
|
13617
|
+
if (Array.isArray(content)) {
|
|
13618
|
+
for (const block of content) {
|
|
13619
|
+
if (block?.type === "text")
|
|
13620
|
+
return typeof block.text === "string" ? block.text : "";
|
|
13621
|
+
}
|
|
13860
13622
|
}
|
|
13861
|
-
return
|
|
13862
|
-
}
|
|
13863
|
-
function bestOfFamily(models, family) {
|
|
13864
|
-
let best = null;
|
|
13865
|
-
let bestVersion = {
|
|
13866
|
-
version: [],
|
|
13867
|
-
date: -1
|
|
13868
|
-
};
|
|
13869
|
-
for (const model of models) {
|
|
13870
|
-
if (modelFamily(model) !== family)
|
|
13871
|
-
continue;
|
|
13872
|
-
const version2 = versionVector(model);
|
|
13873
|
-
if (best === null || compareVersions(version2, bestVersion) > 0) {
|
|
13874
|
-
best = model;
|
|
13875
|
-
bestVersion = version2;
|
|
13876
|
-
}
|
|
13877
|
-
}
|
|
13878
|
-
return best;
|
|
13879
|
-
}
|
|
13880
|
-
var computeFamilyOrder = memoOne((raw) => {
|
|
13881
|
-
const parsed = (raw ?? "").split(",").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
|
|
13882
|
-
return parsed.length > 0 ? parsed : DEFAULT_FAMILY_ORDER;
|
|
13883
|
-
});
|
|
13884
|
-
function resolveFamilyOrder() {
|
|
13885
|
-
return computeFamilyOrder(process.env.OPENCODE_AUTH_LB_ANTHROPIC_FAMILY_ORDER);
|
|
13886
|
-
}
|
|
13887
|
-
var computeFallbackSetting = memoOne((raw) => {
|
|
13888
|
-
const trimmed = raw?.trim();
|
|
13889
|
-
return raw === undefined ? { kind: "ladder" } : trimmed ? { kind: "pinned", model: trimmed } : { kind: "disabled" };
|
|
13890
|
-
});
|
|
13891
|
-
function resolveFallbackSetting() {
|
|
13892
|
-
return computeFallbackSetting(process.env.OPENCODE_AUTH_LB_ANTHROPIC_OPUS_FALLBACK_MODEL);
|
|
13893
|
-
}
|
|
13894
|
-
function ladderTargetForFamily(fromFamily, models) {
|
|
13895
|
-
const order = resolveFamilyOrder();
|
|
13896
|
-
const start = order.indexOf(fromFamily) + 1;
|
|
13897
|
-
for (let i = start;i < order.length; i++) {
|
|
13898
|
-
const family = order[i];
|
|
13899
|
-
if (family === undefined)
|
|
13900
|
-
continue;
|
|
13901
|
-
const best = bestOfFamily(models, family);
|
|
13902
|
-
if (best !== null)
|
|
13903
|
-
return best;
|
|
13904
|
-
}
|
|
13905
|
-
return null;
|
|
13906
|
-
}
|
|
13907
|
-
function downgradeModel(body, models) {
|
|
13908
|
-
const setting = resolveFallbackSetting();
|
|
13909
|
-
if (setting.kind === "disabled")
|
|
13910
|
-
return null;
|
|
13911
|
-
let parsed;
|
|
13912
|
-
try {
|
|
13913
|
-
parsed = JSON.parse(body);
|
|
13914
|
-
} catch {
|
|
13915
|
-
return null;
|
|
13916
|
-
}
|
|
13917
|
-
const from = parsed.model;
|
|
13918
|
-
if (typeof from !== "string")
|
|
13919
|
-
return null;
|
|
13920
|
-
const fromFamily = modelFamily(from);
|
|
13921
|
-
if (fromFamily === null)
|
|
13922
|
-
return null;
|
|
13923
|
-
const target = setting.kind === "pinned" ? setting.model : ladderTargetForFamily(fromFamily, models) ?? DEFAULT_OPUS_FALLBACK_MODEL;
|
|
13924
|
-
if (modelFamily(target) === fromFamily)
|
|
13925
|
-
return null;
|
|
13926
|
-
parsed.model = target;
|
|
13927
|
-
return {
|
|
13928
|
-
body: JSON.stringify(parsed),
|
|
13929
|
-
fromModel: from,
|
|
13930
|
-
toModel: target,
|
|
13931
|
-
fromTier: fromFamily
|
|
13932
|
-
};
|
|
13933
|
-
}
|
|
13934
|
-
function parseTierReset(res, now) {
|
|
13935
|
-
const ms = secondsToMs(Number(res.headers.get(UNIFIED_RESET_HEADER)));
|
|
13936
|
-
if (ms > now && ms - now <= TIER_RESET_MAX_MS)
|
|
13937
|
-
return ms;
|
|
13938
|
-
return now + TIER_RESET_FALLBACK_MS;
|
|
13939
|
-
}
|
|
13940
|
-
function planReactiveFallback(res, body, now, models) {
|
|
13941
|
-
const claim = res.headers.get(REPRESENTATIVE_CLAIM_HEADER);
|
|
13942
|
-
const tier = claim ? MODEL_TIER_CLAIM_RE.exec(claim)?.[1] : undefined;
|
|
13943
|
-
if (!tier)
|
|
13944
|
-
return null;
|
|
13945
|
-
const fallback = downgradeModel(body, models);
|
|
13946
|
-
if (!fallback)
|
|
13947
|
-
return null;
|
|
13948
|
-
return { tier, resetAt: parseTierReset(res, now), fallback };
|
|
13949
|
-
}
|
|
13950
|
-
|
|
13951
|
-
// src/providers/oauth-callback.ts
|
|
13952
|
-
function toTokenSet(json2, previousRefresh) {
|
|
13953
|
-
return {
|
|
13954
|
-
access: json2.access_token,
|
|
13955
|
-
refresh: json2.refresh_token || previousRefresh,
|
|
13956
|
-
expires: Date.now() + json2.expires_in * 1000
|
|
13957
|
-
};
|
|
13958
|
-
}
|
|
13959
|
-
async function readTokenResponse(res) {
|
|
13960
|
-
const json2 = await res.json().catch(() => null);
|
|
13961
|
-
if (!json2 || typeof json2.access_token !== "string" || json2.access_token === "" || !Number.isFinite(json2.expires_in) || json2.expires_in <= 0 || !Number.isFinite(json2.expires_in * 1000) || json2.refresh_token !== undefined && json2.refresh_token !== null && typeof json2.refresh_token !== "string")
|
|
13962
|
-
return null;
|
|
13963
|
-
return json2;
|
|
13964
|
-
}
|
|
13965
|
-
async function readExchangeResponse(res) {
|
|
13966
|
-
if (!res.ok) {
|
|
13967
|
-
await res.body?.cancel().catch(ignore);
|
|
13968
|
-
return null;
|
|
13969
|
-
}
|
|
13970
|
-
return readTokenResponse(res);
|
|
13971
|
-
}
|
|
13972
|
-
async function readRefreshResponse(res) {
|
|
13973
|
-
if (!res.ok)
|
|
13974
|
-
throw new Error(`Token refresh failed: ${res.status} — ${await res.text()}`);
|
|
13975
|
-
const json2 = await readTokenResponse(res);
|
|
13976
|
-
if (!json2)
|
|
13977
|
-
throw new Error(`Token refresh failed: ${res.status} — malformed token response body`);
|
|
13978
|
-
return json2;
|
|
13979
|
-
}
|
|
13980
|
-
function generateState() {
|
|
13981
|
-
return crypto.randomUUID().replaceAll("-", "");
|
|
13982
|
-
}
|
|
13983
|
-
function parseCallbackInput(input, options = {}) {
|
|
13984
|
-
const trimmed = input.trim();
|
|
13985
|
-
let isUrl = false;
|
|
13986
|
-
try {
|
|
13987
|
-
const url2 = new URL(trimmed);
|
|
13988
|
-
isUrl = true;
|
|
13989
|
-
const code2 = url2.searchParams.get("code");
|
|
13990
|
-
const state2 = url2.searchParams.get("state");
|
|
13991
|
-
if (code2 && state2)
|
|
13992
|
-
return { code: code2, state: state2 };
|
|
13993
|
-
} catch {}
|
|
13994
|
-
if (options.allowHashFormat && !isUrl) {
|
|
13995
|
-
const hashSplits = trimmed.split("#");
|
|
13996
|
-
if (hashSplits.length === 2 && hashSplits[0] && hashSplits[1]) {
|
|
13997
|
-
return { code: hashSplits[0], state: hashSplits[1] };
|
|
13998
|
-
}
|
|
13999
|
-
}
|
|
14000
|
-
const params = new URLSearchParams(trimmed);
|
|
14001
|
-
const code = params.get("code");
|
|
14002
|
-
const state = params.get("state");
|
|
14003
|
-
if (code && state)
|
|
14004
|
-
return { code, state };
|
|
14005
|
-
return null;
|
|
14006
|
-
}
|
|
14007
|
-
|
|
14008
|
-
// src/providers/pkce.ts
|
|
14009
|
-
function base64UrlEncode(bytes) {
|
|
14010
|
-
return Buffer.from(bytes).toString("base64url");
|
|
14011
|
-
}
|
|
14012
|
-
async function generatePKCE() {
|
|
14013
|
-
const buf = new Uint8Array(64);
|
|
14014
|
-
crypto.getRandomValues(buf);
|
|
14015
|
-
const verifier = base64UrlEncode(buf);
|
|
14016
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
14017
|
-
return {
|
|
14018
|
-
verifier,
|
|
14019
|
-
challenge: base64UrlEncode(new Uint8Array(digest))
|
|
14020
|
-
};
|
|
14021
|
-
}
|
|
14022
|
-
|
|
14023
|
-
// src/providers/anthropic/oauth.ts
|
|
14024
|
-
async function postToken(body) {
|
|
14025
|
-
return fetch(TOKEN_URL, {
|
|
14026
|
-
method: "POST",
|
|
14027
|
-
headers: {
|
|
14028
|
-
"Content-Type": "application/json",
|
|
14029
|
-
Accept: "application/json, text/plain, */*",
|
|
14030
|
-
"User-Agent": "axios/1.13.6"
|
|
14031
|
-
},
|
|
14032
|
-
body: JSON.stringify(body),
|
|
14033
|
-
signal: AbortSignal.timeout(OAUTH_HTTP_TIMEOUT_MS)
|
|
14034
|
-
});
|
|
14035
|
-
}
|
|
14036
|
-
async function authorize() {
|
|
14037
|
-
const pkce = await generatePKCE();
|
|
14038
|
-
const state = generateState();
|
|
14039
|
-
const url2 = new URL(AUTHORIZE_URL);
|
|
14040
|
-
url2.searchParams.set("code", "true");
|
|
14041
|
-
url2.searchParams.set("client_id", CLIENT_ID);
|
|
14042
|
-
url2.searchParams.set("response_type", "code");
|
|
14043
|
-
url2.searchParams.set("redirect_uri", CODE_CALLBACK_URL);
|
|
14044
|
-
url2.searchParams.set("scope", OAUTH_SCOPES.join(" "));
|
|
14045
|
-
url2.searchParams.set("code_challenge", pkce.challenge);
|
|
14046
|
-
url2.searchParams.set("code_challenge_method", "S256");
|
|
14047
|
-
url2.searchParams.set("state", state);
|
|
14048
|
-
return {
|
|
14049
|
-
url: url2.toString(),
|
|
14050
|
-
redirectUri: CODE_CALLBACK_URL,
|
|
14051
|
-
state,
|
|
14052
|
-
verifier: pkce.verifier
|
|
14053
|
-
};
|
|
14054
|
-
}
|
|
14055
|
-
async function exchange(input, verifier, redirectUri, expectedState) {
|
|
14056
|
-
const callback = parseCallbackInput(input, { allowHashFormat: true });
|
|
14057
|
-
if (!callback)
|
|
14058
|
-
return null;
|
|
14059
|
-
if (expectedState && callback.state !== expectedState)
|
|
14060
|
-
return null;
|
|
14061
|
-
const result = await postToken({
|
|
14062
|
-
code: callback.code,
|
|
14063
|
-
state: callback.state,
|
|
14064
|
-
grant_type: "authorization_code",
|
|
14065
|
-
client_id: CLIENT_ID,
|
|
14066
|
-
redirect_uri: redirectUri,
|
|
14067
|
-
code_verifier: verifier
|
|
14068
|
-
});
|
|
14069
|
-
const json2 = await readExchangeResponse(result);
|
|
14070
|
-
if (!json2)
|
|
14071
|
-
return null;
|
|
14072
|
-
return toTokenSet(json2, "");
|
|
14073
|
-
}
|
|
14074
|
-
async function refresh(refreshToken) {
|
|
14075
|
-
const response = await postToken({
|
|
14076
|
-
grant_type: "refresh_token",
|
|
14077
|
-
refresh_token: refreshToken,
|
|
14078
|
-
client_id: CLIENT_ID
|
|
14079
|
-
});
|
|
14080
|
-
const json2 = await readRefreshResponse(response);
|
|
14081
|
-
return toTokenSet(json2, refreshToken);
|
|
14082
|
-
}
|
|
14083
|
-
|
|
14084
|
-
// src/providers/anthropic/cch.ts
|
|
14085
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
14086
|
-
function messageText({ content }) {
|
|
14087
|
-
if (typeof content === "string")
|
|
14088
|
-
return content;
|
|
14089
|
-
if (Array.isArray(content)) {
|
|
14090
|
-
for (const block of content) {
|
|
14091
|
-
if (block?.type === "text")
|
|
14092
|
-
return typeof block.text === "string" ? block.text : "";
|
|
14093
|
-
}
|
|
14094
|
-
}
|
|
14095
|
-
return "";
|
|
13623
|
+
return "";
|
|
14096
13624
|
}
|
|
14097
13625
|
function computeCCH(messageText2) {
|
|
14098
13626
|
return createHash2("sha256").update(messageText2).digest("hex").slice(0, 5);
|
|
@@ -14572,131 +14100,681 @@ async function exchange2(input, verifier, redirectUri, expectedState) {
|
|
|
14572
14100
|
return null;
|
|
14573
14101
|
return toTokenSet2(json2, "");
|
|
14574
14102
|
}
|
|
14575
|
-
async function refresh2(refreshToken) {
|
|
14576
|
-
const body = new URLSearchParams({
|
|
14577
|
-
grant_type: "refresh_token",
|
|
14578
|
-
refresh_token: refreshToken,
|
|
14579
|
-
client_id: CLIENT_ID2
|
|
14103
|
+
async function refresh2(refreshToken) {
|
|
14104
|
+
const body = new URLSearchParams({
|
|
14105
|
+
grant_type: "refresh_token",
|
|
14106
|
+
refresh_token: refreshToken,
|
|
14107
|
+
client_id: CLIENT_ID2
|
|
14108
|
+
});
|
|
14109
|
+
const json2 = await readRefreshResponse(await postToken2(body));
|
|
14110
|
+
return toTokenSet2(json2, refreshToken);
|
|
14111
|
+
}
|
|
14112
|
+
|
|
14113
|
+
// src/providers/openai/transform.ts
|
|
14114
|
+
function applyAuth(headers, account) {
|
|
14115
|
+
headers.delete("x-api-key");
|
|
14116
|
+
headers.set("authorization", `Bearer ${account.access}`);
|
|
14117
|
+
const accountId = resolveAccountId(account);
|
|
14118
|
+
if (accountId)
|
|
14119
|
+
headers.set("chatgpt-account-id", accountId);
|
|
14120
|
+
headers.set("openai-beta", OPENAI_BETA);
|
|
14121
|
+
headers.set("originator", ORIGINATOR);
|
|
14122
|
+
headers.set("user-agent", USER_AGENT2);
|
|
14123
|
+
headers.set("session-id", SESSION_ID);
|
|
14124
|
+
}
|
|
14125
|
+
function rewriteUrl2(input) {
|
|
14126
|
+
const url2 = urlFromInput(input);
|
|
14127
|
+
if (!url2)
|
|
14128
|
+
return input;
|
|
14129
|
+
if (!url2.pathname.includes("/responses"))
|
|
14130
|
+
return input;
|
|
14131
|
+
if (url2.href === CODEX_RESPONSES_URL)
|
|
14132
|
+
return input;
|
|
14133
|
+
return input instanceof Request ? new Request(CODEX_RESPONSES_URL, input) : CODEX_RESPONSES_URL;
|
|
14134
|
+
}
|
|
14135
|
+
function applyInstructions(obj) {
|
|
14136
|
+
let instructions = typeof obj.instructions === "string" ? obj.instructions : "";
|
|
14137
|
+
if (Array.isArray(obj.input)) {
|
|
14138
|
+
const input = obj.input;
|
|
14139
|
+
const systemTexts = [];
|
|
14140
|
+
let remaining = null;
|
|
14141
|
+
let i = 0;
|
|
14142
|
+
for (const raw of input) {
|
|
14143
|
+
if (raw && raw.type === "message" && raw.role === "system") {
|
|
14144
|
+
if (remaining === null)
|
|
14145
|
+
remaining = input.slice(0, i);
|
|
14146
|
+
const text = joinBlockTexts(raw.content, `
|
|
14147
|
+
`);
|
|
14148
|
+
if (text)
|
|
14149
|
+
systemTexts.push(text);
|
|
14150
|
+
} else if (remaining !== null) {
|
|
14151
|
+
remaining.push(raw);
|
|
14152
|
+
}
|
|
14153
|
+
i++;
|
|
14154
|
+
}
|
|
14155
|
+
if (systemTexts.length > 0) {
|
|
14156
|
+
instructions = [instructions, ...systemTexts].filter(Boolean).join(`
|
|
14157
|
+
|
|
14158
|
+
`);
|
|
14159
|
+
}
|
|
14160
|
+
if (remaining !== null)
|
|
14161
|
+
obj.input = remaining;
|
|
14162
|
+
}
|
|
14163
|
+
obj.instructions = instructions.trim() || "You are a helpful assistant.";
|
|
14164
|
+
}
|
|
14165
|
+
function rewriteRequestBody2(body) {
|
|
14166
|
+
try {
|
|
14167
|
+
const obj = JSON.parse(body);
|
|
14168
|
+
obj.store = false;
|
|
14169
|
+
const include = Array.isArray(obj.include) ? obj.include : [];
|
|
14170
|
+
obj.include = include.includes("reasoning.encrypted_content") && new Set(include).size === include.length ? include : [...new Set([...include, "reasoning.encrypted_content"])];
|
|
14171
|
+
applyInstructions(obj);
|
|
14172
|
+
delete obj.max_output_tokens;
|
|
14173
|
+
return JSON.stringify(obj);
|
|
14174
|
+
} catch {
|
|
14175
|
+
return body;
|
|
14176
|
+
}
|
|
14177
|
+
}
|
|
14178
|
+
|
|
14179
|
+
// src/providers/openai/usage.ts
|
|
14180
|
+
var WEEK_MINUTES = 7 * 24 * 60;
|
|
14181
|
+
function isWeeklyMinutes(minutes) {
|
|
14182
|
+
return minutes >= WEEK_MINUTES * 0.95 && minutes <= WEEK_MINUTES * 1.05;
|
|
14183
|
+
}
|
|
14184
|
+
var PRIMARY_WINDOW_MINUTES_HEADER = "x-codex-primary-window-minutes";
|
|
14185
|
+
var HEADER_SPEC2 = {
|
|
14186
|
+
hourlyUtil: "x-codex-primary-used-percent",
|
|
14187
|
+
hourlyReset: "x-codex-primary-reset-at",
|
|
14188
|
+
weeklyUtil: "x-codex-secondary-used-percent",
|
|
14189
|
+
weeklyReset: "x-codex-secondary-reset-at",
|
|
14190
|
+
divisor: 100
|
|
14191
|
+
};
|
|
14192
|
+
var WEEKLY_PRIMARY_HEADER_SPEC = {
|
|
14193
|
+
hourlyUtil: HEADER_SPEC2.weeklyUtil,
|
|
14194
|
+
hourlyReset: HEADER_SPEC2.weeklyReset,
|
|
14195
|
+
weeklyUtil: HEADER_SPEC2.hourlyUtil,
|
|
14196
|
+
weeklyReset: HEADER_SPEC2.hourlyReset,
|
|
14197
|
+
divisor: HEADER_SPEC2.divisor
|
|
14198
|
+
};
|
|
14199
|
+
function parseUsageHeaders2(headers) {
|
|
14200
|
+
const primaryWindowMinutes = Number(headers.get(PRIMARY_WINDOW_MINUTES_HEADER));
|
|
14201
|
+
const spec = isWeeklyMinutes(primaryWindowMinutes) ? WEEKLY_PRIMARY_HEADER_SPEC : HEADER_SPEC2;
|
|
14202
|
+
return parseWindowPairHeaders(headers, spec);
|
|
14203
|
+
}
|
|
14204
|
+
function endpointWindow2(w) {
|
|
14205
|
+
return endpointWindowFrom(w, (win) => win.used_percent, 100, (win) => secondsToMs(Number(win.reset_at ?? 0)));
|
|
14206
|
+
}
|
|
14207
|
+
async function fetchUsage2(account, now) {
|
|
14208
|
+
const headers = {
|
|
14209
|
+
authorization: `Bearer ${account.access}`,
|
|
14210
|
+
"user-agent": USAGE_USER_AGENT2,
|
|
14211
|
+
accept: "application/json"
|
|
14212
|
+
};
|
|
14213
|
+
const accountId = resolveAccountId(account);
|
|
14214
|
+
if (accountId)
|
|
14215
|
+
headers["chatgpt-account-id"] = accountId;
|
|
14216
|
+
const json2 = await fetchUsageJson(USAGE_URL2, headers, USAGE_HTTP_TIMEOUT_MS);
|
|
14217
|
+
const rateLimit = json2?.rate_limit;
|
|
14218
|
+
if (!isPlainObject2(rateLimit))
|
|
14219
|
+
return null;
|
|
14220
|
+
const { primary_window: primary, secondary_window: secondary } = rateLimit;
|
|
14221
|
+
const primaryIsWeekly = isWeeklyMinutes(Math.ceil(Number(primary?.limit_window_seconds) / 60));
|
|
14222
|
+
return {
|
|
14223
|
+
hourly: endpointWindow2(primaryIsWeekly ? secondary : primary),
|
|
14224
|
+
weekly: endpointWindow2(primaryIsWeekly ? primary : secondary),
|
|
14225
|
+
capturedAt: now
|
|
14226
|
+
};
|
|
14227
|
+
}
|
|
14228
|
+
|
|
14229
|
+
// src/providers/openai/adapter.ts
|
|
14230
|
+
var openaiAdapter = {
|
|
14231
|
+
id: PROVIDER_ID2,
|
|
14232
|
+
authorize: authorize2,
|
|
14233
|
+
exchange: exchange2,
|
|
14234
|
+
refresh: refresh2,
|
|
14235
|
+
applyAuth,
|
|
14236
|
+
transformUrl: rewriteUrl2,
|
|
14237
|
+
transformBody: rewriteRequestBody2,
|
|
14238
|
+
transformResponse(response) {
|
|
14239
|
+
return response;
|
|
14240
|
+
},
|
|
14241
|
+
parseUsageHeaders: parseUsageHeaders2,
|
|
14242
|
+
fetchUsage: fetchUsage2,
|
|
14243
|
+
classifyError: classifyHttpStatus
|
|
14244
|
+
};
|
|
14245
|
+
|
|
14246
|
+
// src/providers/registry.ts
|
|
14247
|
+
var ADAPTERS = [
|
|
14248
|
+
anthropicAdapter,
|
|
14249
|
+
openaiAdapter
|
|
14250
|
+
];
|
|
14251
|
+
function adapterFor(adapters, providerID) {
|
|
14252
|
+
for (const adapter of adapters)
|
|
14253
|
+
if (adapter.id === providerID)
|
|
14254
|
+
return adapter;
|
|
14255
|
+
return;
|
|
14256
|
+
}
|
|
14257
|
+
|
|
14258
|
+
// src/usage-refresh.ts
|
|
14259
|
+
var SEED_TTL_MS = 5 * 60 * 1000;
|
|
14260
|
+
var MAX_TRANSIENT_COOLDOWN_MS = 5 * 60 * 1000;
|
|
14261
|
+
var lastPoll = new Map;
|
|
14262
|
+
async function refreshUsage(adapters, now, poolSnapshot) {
|
|
14263
|
+
const pool = poolSnapshot ?? await readPool();
|
|
14264
|
+
const aliveIds = lastPoll.size > 0 ? new Set : undefined;
|
|
14265
|
+
let stale;
|
|
14266
|
+
for (const account of pool.accounts) {
|
|
14267
|
+
aliveIds?.add(account.id);
|
|
14268
|
+
if (account.disabledReason)
|
|
14269
|
+
continue;
|
|
14270
|
+
if (account.usage.capturedAt !== 0 && now - account.usage.capturedAt <= SEED_TTL_MS)
|
|
14271
|
+
continue;
|
|
14272
|
+
const adapter = adapterFor(adapters, account.providerID);
|
|
14273
|
+
if (!adapter)
|
|
14274
|
+
continue;
|
|
14275
|
+
if ((lastPoll.get(account.id) ?? 0) > now - SEED_TTL_MS)
|
|
14276
|
+
continue;
|
|
14277
|
+
lastPoll.set(account.id, now);
|
|
14278
|
+
stale ??= [];
|
|
14279
|
+
stale.push({ adapter, account });
|
|
14280
|
+
}
|
|
14281
|
+
if (aliveIds) {
|
|
14282
|
+
for (const id of lastPoll.keys())
|
|
14283
|
+
if (!aliveIds.has(id))
|
|
14284
|
+
lastPoll.delete(id);
|
|
14285
|
+
}
|
|
14286
|
+
if (!stale)
|
|
14287
|
+
return;
|
|
14288
|
+
const scoreCfg = loadScoreConfig();
|
|
14289
|
+
await Promise.all(stale.map(async ({ adapter, account }) => {
|
|
14290
|
+
try {
|
|
14291
|
+
await ensureAccessToken(adapter, account, now);
|
|
14292
|
+
const snapshot = await adapter.fetchUsage(account, now);
|
|
14293
|
+
if (snapshot) {
|
|
14294
|
+
await mutatePool((p) => {
|
|
14295
|
+
const stored = findAccount(p, account.id);
|
|
14296
|
+
if (stored) {
|
|
14297
|
+
stored.usage = {
|
|
14298
|
+
hourly: snapshot.hourly ?? stored.usage.hourly,
|
|
14299
|
+
weekly: snapshot.weekly === null ? stored.usage.weekly : preserveWeeklyAnchor(snapshot.weekly, stored.usage.weekly, now),
|
|
14300
|
+
capturedAt: snapshot.weekly === null ? stored.usage.capturedAt : now
|
|
14301
|
+
};
|
|
14302
|
+
if (snapshot.weekly !== null && stored.cooldownUntil > now + MAX_TRANSIENT_COOLDOWN_MS && !isExhausted(stored, scoreCfg, now))
|
|
14303
|
+
stored.cooldownUntil = 0;
|
|
14304
|
+
}
|
|
14305
|
+
});
|
|
14306
|
+
}
|
|
14307
|
+
} catch {}
|
|
14308
|
+
}));
|
|
14309
|
+
}
|
|
14310
|
+
function refreshUsageInBackground(adapter, now, poolSnapshot) {
|
|
14311
|
+
return refreshUsage([adapter], now, poolSnapshot);
|
|
14312
|
+
}
|
|
14313
|
+
function refreshAllUsageInBackground(now, poolSnapshot) {
|
|
14314
|
+
return refreshUsage(ADAPTERS, now, poolSnapshot);
|
|
14315
|
+
}
|
|
14316
|
+
|
|
14317
|
+
// src/fetch.ts
|
|
14318
|
+
var ACCOUNT_COOLDOWN_MS = 5 * 60 * 1000;
|
|
14319
|
+
var AUTH_COOLDOWN_MS = 2 * 60 * 1000;
|
|
14320
|
+
var DEBUG = process.env.OPENCODE_AUTH_LB_DEBUG === "1" || process.env.OPENCODE_AUTH_LB_DEBUG === "true";
|
|
14321
|
+
function log(message) {
|
|
14322
|
+
if (DEBUG)
|
|
14323
|
+
console.error(`[auth-lb] ${message}`);
|
|
14324
|
+
}
|
|
14325
|
+
async function bestEffort(what, op) {
|
|
14326
|
+
try {
|
|
14327
|
+
await op();
|
|
14328
|
+
} catch (error45) {
|
|
14329
|
+
if (error45 instanceof LockTimeoutError || error45 instanceof PoolReadError || error45 instanceof PoolWriteError) {
|
|
14330
|
+
log(`bookkeeping skipped (${what}): ${error45.message}`);
|
|
14331
|
+
return;
|
|
14332
|
+
}
|
|
14333
|
+
throw error45;
|
|
14334
|
+
}
|
|
14335
|
+
}
|
|
14336
|
+
function applyUsagePartial(account, partial2, now) {
|
|
14337
|
+
if (partial2.hourly !== undefined)
|
|
14338
|
+
account.usage.hourly = partial2.hourly;
|
|
14339
|
+
if (partial2.weekly !== undefined) {
|
|
14340
|
+
account.usage.weekly = preserveWeeklyAnchor(partial2.weekly, account.usage.weekly, now);
|
|
14341
|
+
account.usage.capturedAt = now;
|
|
14342
|
+
}
|
|
14343
|
+
}
|
|
14344
|
+
var RETRY_AFTER_MAX_MS = MAX_QUOTA_RESET_BOUND_MS;
|
|
14345
|
+
function cooldownUntilFrom(res, fallbackMs, now) {
|
|
14346
|
+
let until = now + fallbackMs;
|
|
14347
|
+
const retryAfter = res?.headers.get("retry-after");
|
|
14348
|
+
if (retryAfter) {
|
|
14349
|
+
const secs = Number(retryAfter);
|
|
14350
|
+
if (Number.isFinite(secs) && secs > 0) {
|
|
14351
|
+
const delta = secs * 1000;
|
|
14352
|
+
if (Number.isFinite(delta) && delta <= RETRY_AFTER_MAX_MS)
|
|
14353
|
+
until = now + delta;
|
|
14354
|
+
} else {
|
|
14355
|
+
const httpDate = Date.parse(retryAfter);
|
|
14356
|
+
if (Number.isFinite(httpDate) && httpDate > now && httpDate - now <= RETRY_AFTER_MAX_MS)
|
|
14357
|
+
until = httpDate;
|
|
14358
|
+
}
|
|
14359
|
+
}
|
|
14360
|
+
return until;
|
|
14361
|
+
}
|
|
14362
|
+
async function applyCooldown(accountId, fallbackMs) {
|
|
14363
|
+
const until = cooldownUntilFrom(null, fallbackMs, Date.now());
|
|
14364
|
+
await bestEffort("cooldown", () => mutatePool((pool) => {
|
|
14365
|
+
const account = findAccount(pool, accountId);
|
|
14366
|
+
if (account)
|
|
14367
|
+
account.cooldownUntil = Math.max(account.cooldownUntil, until);
|
|
14368
|
+
}));
|
|
14369
|
+
}
|
|
14370
|
+
async function recordRotation(adapter, accountId, res, fallbackMs, now) {
|
|
14371
|
+
const partial2 = adapter.parseUsageHeaders(res.headers);
|
|
14372
|
+
const until = cooldownUntilFrom(res, fallbackMs, now);
|
|
14373
|
+
await bestEffort("rotation", () => mutatePool((pool) => {
|
|
14374
|
+
const account = findAccount(pool, accountId);
|
|
14375
|
+
if (!account)
|
|
14376
|
+
return;
|
|
14377
|
+
if (partial2)
|
|
14378
|
+
applyUsagePartial(account, partial2, now);
|
|
14379
|
+
account.cooldownUntil = Math.max(account.cooldownUntil, until);
|
|
14380
|
+
}));
|
|
14381
|
+
}
|
|
14382
|
+
async function recordModelCooldown(accountId, tier, until, partial2, now) {
|
|
14383
|
+
await bestEffort("model-cooldown", () => mutatePool((pool) => {
|
|
14384
|
+
const account = findAccount(pool, accountId);
|
|
14385
|
+
if (!account)
|
|
14386
|
+
return;
|
|
14387
|
+
if (partial2)
|
|
14388
|
+
applyUsagePartial(account, partial2, now);
|
|
14389
|
+
const map2 = account.modelCooldownsUntil ??= {};
|
|
14390
|
+
map2[tier] = Math.max(map2[tier] ?? 0, until);
|
|
14391
|
+
}));
|
|
14392
|
+
}
|
|
14393
|
+
async function recordSuccess(adapter, accountId, partial2, sessionKey, now, cfg, fallback) {
|
|
14394
|
+
await bestEffort("record-success", () => mutatePool((pool) => {
|
|
14395
|
+
const account = findAccount(pool, accountId);
|
|
14396
|
+
if (account) {
|
|
14397
|
+
if (partial2)
|
|
14398
|
+
applyUsagePartial(account, partial2, now);
|
|
14399
|
+
if (partial2 && account.cooldownUntil > now + ACCOUNT_COOLDOWN_MS && !isExhausted(account, cfg, now))
|
|
14400
|
+
account.cooldownUntil = 0;
|
|
14401
|
+
pool.lastSelected[adapter.id] = accountId;
|
|
14402
|
+
}
|
|
14403
|
+
if (sessionKey) {
|
|
14404
|
+
pool.sessions[sessionKey] = fallback ? {
|
|
14405
|
+
accountId,
|
|
14406
|
+
updatedAt: now,
|
|
14407
|
+
fallback: { from: fallback.fromModel, to: fallback.toModel }
|
|
14408
|
+
} : { accountId, updatedAt: now };
|
|
14409
|
+
for (const k of Object.keys(pool.sessions)) {
|
|
14410
|
+
const pin = pool.sessions[k];
|
|
14411
|
+
if (pin && now - pin.updatedAt > cfg.sessionTtlMs)
|
|
14412
|
+
delete pool.sessions[k];
|
|
14413
|
+
}
|
|
14414
|
+
}
|
|
14415
|
+
}));
|
|
14416
|
+
}
|
|
14417
|
+
function noUsableAccountResponse(providerID) {
|
|
14418
|
+
const message = `No usable ${providerID} account in the auth-load-balancer pool. Run "opencode auth login" to add or re-login an account.`;
|
|
14419
|
+
const error45 = { type: "authentication_error", message };
|
|
14420
|
+
const body = providerID === PROVIDER_ID ? { type: "error", error: error45 } : { error: error45 };
|
|
14421
|
+
return new Response(JSON.stringify(body), {
|
|
14422
|
+
status: 401,
|
|
14423
|
+
headers: { "content-type": "application/json" }
|
|
14424
|
+
});
|
|
14425
|
+
}
|
|
14426
|
+
function soonestCooldownUntil(accounts, now, accountIds) {
|
|
14427
|
+
if (accountIds.size === 0)
|
|
14428
|
+
return null;
|
|
14429
|
+
let soonest = Number.POSITIVE_INFINITY;
|
|
14430
|
+
for (const account of accounts) {
|
|
14431
|
+
if (account.disabledReason)
|
|
14432
|
+
continue;
|
|
14433
|
+
if (!accountIds.has(account.id))
|
|
14434
|
+
continue;
|
|
14435
|
+
const until = account.cooldownUntil > now ? account.cooldownUntil : now;
|
|
14436
|
+
if (until < soonest)
|
|
14437
|
+
soonest = until;
|
|
14438
|
+
}
|
|
14439
|
+
return Number.isFinite(soonest) ? soonest : null;
|
|
14440
|
+
}
|
|
14441
|
+
function createLoadBalancedFetch(adapter, hooks = {}, models = []) {
|
|
14442
|
+
const cfg = loadConfig();
|
|
14443
|
+
return async (input, init) => {
|
|
14444
|
+
const bodyStr = typeof init?.body === "string" ? init.body : undefined;
|
|
14445
|
+
const baseHeaders = mergeHeaders(input, init);
|
|
14446
|
+
const signal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
|
|
14447
|
+
const baseKey = deriveSessionKey(baseHeaders, bodyStr);
|
|
14448
|
+
const sessionKey = baseKey ? `${adapter.id}:${baseKey}` : null;
|
|
14449
|
+
baseHeaders.delete(SESSION_HEADER);
|
|
14450
|
+
const tried = new Set;
|
|
14451
|
+
let waitableCooldownIds = null;
|
|
14452
|
+
let lastError = null;
|
|
14453
|
+
let cachedRequestBytes = -1;
|
|
14454
|
+
const requestBytes = cfg.cheapSwitchMaxBytes > 0 && sessionKey !== null && bodyStr ? () => {
|
|
14455
|
+
if (cachedRequestBytes < 0)
|
|
14456
|
+
cachedRequestBytes = Buffer.byteLength(bodyStr, "utf8");
|
|
14457
|
+
return cachedRequestBytes;
|
|
14458
|
+
} : 0;
|
|
14459
|
+
const transformedBody = bodyStr !== undefined ? adapter.transformBody(bodyStr) : init?.body;
|
|
14460
|
+
const transformedUrl = adapter.transformUrl(input);
|
|
14461
|
+
let currentBody = transformedBody;
|
|
14462
|
+
let currentTier = typeof bodyStr === "string" ? adapter.requestModelTier?.(bodyStr) ?? null : null;
|
|
14463
|
+
let fallbackPlan = null;
|
|
14464
|
+
let fallbackPlanFor = null;
|
|
14465
|
+
const planDowngrade = () => {
|
|
14466
|
+
if (typeof currentBody === "string" && fallbackPlanFor !== currentBody) {
|
|
14467
|
+
fallbackPlanFor = currentBody;
|
|
14468
|
+
fallbackPlan = adapter.planModelFallback?.(currentBody, models) ?? null;
|
|
14469
|
+
}
|
|
14470
|
+
return fallbackPlan;
|
|
14471
|
+
};
|
|
14472
|
+
let fallbackInfo = null;
|
|
14473
|
+
let pendingTierFallback = null;
|
|
14474
|
+
let tierSkipped = null;
|
|
14475
|
+
let usageSeeded = false;
|
|
14476
|
+
const waitDeadline = Date.now() + cfg.maxWaitMs;
|
|
14477
|
+
for (;; ) {
|
|
14478
|
+
const pool = await readPool();
|
|
14479
|
+
const now = Date.now();
|
|
14480
|
+
if (!usageSeeded) {
|
|
14481
|
+
usageSeeded = true;
|
|
14482
|
+
refreshAllUsageInBackground(now, pool).catch(ignore);
|
|
14483
|
+
}
|
|
14484
|
+
const selection = selectForSession(pool, adapter.id, sessionKey, now, cfg, tried, requestBytes);
|
|
14485
|
+
if (!selection) {
|
|
14486
|
+
if (tried.size === 0)
|
|
14487
|
+
return noUsableAccountResponse(adapter.id);
|
|
14488
|
+
if (pendingTierFallback !== null) {
|
|
14489
|
+
const adopted = pendingTierFallback;
|
|
14490
|
+
pendingTierFallback = null;
|
|
14491
|
+
currentBody = adopted.body;
|
|
14492
|
+
currentTier = adapter.requestModelTier?.(adopted.body) ?? null;
|
|
14493
|
+
fallbackInfo = fallbackInfo === null ? adopted : { ...adopted, fromModel: fallbackInfo.fromModel };
|
|
14494
|
+
if (tierSkipped !== null)
|
|
14495
|
+
for (const id of tierSkipped)
|
|
14496
|
+
tried.delete(id);
|
|
14497
|
+
tierSkipped = null;
|
|
14498
|
+
if (DEBUG)
|
|
14499
|
+
log(`~~ every ${adapter.id} account is tier-limited for ${adopted.fromModel} -> downgraded to ${adopted.toModel}`);
|
|
14500
|
+
continue;
|
|
14501
|
+
}
|
|
14502
|
+
if (cfg.maxWaitMs > 0 && waitableCooldownIds) {
|
|
14503
|
+
const resumeAt = soonestCooldownUntil(pool.accounts, now, waitableCooldownIds);
|
|
14504
|
+
if (resumeAt !== null && resumeAt <= waitDeadline) {
|
|
14505
|
+
let remaining = resumeAt - Date.now();
|
|
14506
|
+
while (remaining > 0) {
|
|
14507
|
+
await sleepAbortable(remaining, signal);
|
|
14508
|
+
remaining = resumeAt - Date.now();
|
|
14509
|
+
}
|
|
14510
|
+
tried.clear();
|
|
14511
|
+
waitableCooldownIds.clear();
|
|
14512
|
+
continue;
|
|
14513
|
+
}
|
|
14514
|
+
}
|
|
14515
|
+
break;
|
|
14516
|
+
}
|
|
14517
|
+
const { account, degraded, sticky } = selection;
|
|
14518
|
+
tried.add(account.id);
|
|
14519
|
+
if (currentTier !== null && (account.modelCooldownsUntil?.[currentTier] ?? 0) > now) {
|
|
14520
|
+
const plan = planDowngrade();
|
|
14521
|
+
if (plan !== null) {
|
|
14522
|
+
pendingTierFallback = plan;
|
|
14523
|
+
tierSkipped ??= new Set;
|
|
14524
|
+
tierSkipped.add(account.id);
|
|
14525
|
+
if (DEBUG)
|
|
14526
|
+
log(`~~ ${account.label} ${currentTier} tier exhausted -> skipped for ${currentTier}`);
|
|
14527
|
+
continue;
|
|
14528
|
+
}
|
|
14529
|
+
}
|
|
14530
|
+
try {
|
|
14531
|
+
await ensureAccessToken(adapter, account, now);
|
|
14532
|
+
const headers = new Headers(baseHeaders);
|
|
14533
|
+
adapter.applyAuth(headers, account);
|
|
14534
|
+
if (DEBUG)
|
|
14535
|
+
log(`-> ${adapter.id} via ${account.label}${sticky ? " (sticky)" : ""}${degraded ? " (degraded)" : ""}`);
|
|
14536
|
+
const res = await fetch(transformedUrl, {
|
|
14537
|
+
...init,
|
|
14538
|
+
body: currentBody,
|
|
14539
|
+
headers
|
|
14540
|
+
});
|
|
14541
|
+
const cls = adapter.classifyError(res.status);
|
|
14542
|
+
if (cls === "account" || cls === "auth") {
|
|
14543
|
+
await res.body?.cancel().catch(ignore);
|
|
14544
|
+
if (cls === "account" && typeof currentBody === "string") {
|
|
14545
|
+
const reactiveNow = Date.now();
|
|
14546
|
+
const plan = adapter.planReactiveFallback?.(res, currentBody, reactiveNow, models);
|
|
14547
|
+
if (plan) {
|
|
14548
|
+
await recordModelCooldown(account.id, plan.tier, plan.resetAt, adapter.parseUsageHeaders(res.headers), reactiveNow);
|
|
14549
|
+
const local = account.modelCooldownsUntil ??= {};
|
|
14550
|
+
local[plan.tier] = Math.max(local[plan.tier] ?? 0, plan.resetAt);
|
|
14551
|
+
pendingTierFallback = plan.fallback;
|
|
14552
|
+
tierSkipped ??= new Set;
|
|
14553
|
+
tierSkipped.add(account.id);
|
|
14554
|
+
lastError = new Error(`${adapter.id} account "${account.label}" hit its "${plan.tier}" model-tier cap`);
|
|
14555
|
+
if (DEBUG)
|
|
14556
|
+
log(`~~ ${account.label} ${plan.tier} tier limited -> rotating (fallback ${plan.fallback.toModel} pending)`);
|
|
14557
|
+
continue;
|
|
14558
|
+
}
|
|
14559
|
+
}
|
|
14560
|
+
const ms = cls === "auth" ? AUTH_COOLDOWN_MS : ACCOUNT_COOLDOWN_MS;
|
|
14561
|
+
await recordRotation(adapter, account.id, res, ms, Date.now());
|
|
14562
|
+
if (cls === "account") {
|
|
14563
|
+
waitableCooldownIds ??= new Set;
|
|
14564
|
+
waitableCooldownIds.add(account.id);
|
|
14565
|
+
}
|
|
14566
|
+
lastError = new Error(`${adapter.id} account "${account.label}" returned ${res.status}`);
|
|
14567
|
+
if (DEBUG)
|
|
14568
|
+
log(`!! ${account.label} ${res.status} (${cls}) -> rotating`);
|
|
14569
|
+
continue;
|
|
14570
|
+
}
|
|
14571
|
+
const successNow = Date.now();
|
|
14572
|
+
const partial2 = adapter.parseUsageHeaders(res.headers);
|
|
14573
|
+
if (partial2)
|
|
14574
|
+
applyUsagePartial(account, partial2, successNow);
|
|
14575
|
+
if (fallbackInfo)
|
|
14576
|
+
hooks.onModelFallback?.(adapter.id, account, fallbackInfo.fromModel, fallbackInfo.toModel, fallbackInfo.fromTier);
|
|
14577
|
+
hooks.onUse?.(adapter.id, account);
|
|
14578
|
+
await recordSuccess(adapter, account.id, partial2, sessionKey, successNow, cfg, fallbackInfo);
|
|
14579
|
+
return adapter.transformResponse(res);
|
|
14580
|
+
} catch (error45) {
|
|
14581
|
+
lastError = error45;
|
|
14582
|
+
const aborted2 = signal?.aborted === true || error45 instanceof Error && error45.name === "AbortError";
|
|
14583
|
+
if (aborted2)
|
|
14584
|
+
throw error45;
|
|
14585
|
+
if (!account.disabledReason)
|
|
14586
|
+
await applyCooldown(account.id, AUTH_COOLDOWN_MS);
|
|
14587
|
+
if (DEBUG)
|
|
14588
|
+
log(`!! ${account.label} threw: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
14589
|
+
continue;
|
|
14590
|
+
}
|
|
14591
|
+
}
|
|
14592
|
+
throw lastError ?? new Error(`${adapter.id}: no usable account in the load-balancer pool`);
|
|
14593
|
+
};
|
|
14594
|
+
}
|
|
14595
|
+
|
|
14596
|
+
// src/status.ts
|
|
14597
|
+
var PROVIDER_NAMES = {
|
|
14598
|
+
anthropic: "Claude",
|
|
14599
|
+
openai: "Codex"
|
|
14600
|
+
};
|
|
14601
|
+
function compareAscii(a, b) {
|
|
14602
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
14603
|
+
}
|
|
14604
|
+
function byProviderId(a, b) {
|
|
14605
|
+
return compareAscii(a[0], b[0]);
|
|
14606
|
+
}
|
|
14607
|
+
function isExpired(window, now) {
|
|
14608
|
+
return !!window && window.resetAt > 0 && window.resetAt <= now;
|
|
14609
|
+
}
|
|
14610
|
+
function toStatus(account, now, current, available, rank) {
|
|
14611
|
+
const weekly = account.usage.weekly;
|
|
14612
|
+
const weeklyExpired = isExpired(weekly, now);
|
|
14613
|
+
return {
|
|
14614
|
+
id: account.id,
|
|
14615
|
+
label: account.label,
|
|
14616
|
+
weeklyUtil: displayUtil(weekly, now),
|
|
14617
|
+
hourlyUtil: displayUtil(account.usage.hourly, now),
|
|
14618
|
+
weeklyResetAt: weekly && !weeklyExpired ? weekly.resetAt : 0,
|
|
14619
|
+
available,
|
|
14620
|
+
cooldownUntil: account.cooldownUntil,
|
|
14621
|
+
modelCooldownsUntil: account.modelCooldownsUntil ?? {},
|
|
14622
|
+
disabledReason: account.disabledReason,
|
|
14623
|
+
current,
|
|
14624
|
+
rank
|
|
14625
|
+
};
|
|
14626
|
+
}
|
|
14627
|
+
function buildStatus(pool, now, cfg = DEFAULT_CONFIG) {
|
|
14628
|
+
const byProvider = new Map;
|
|
14629
|
+
for (const a of pool.accounts) {
|
|
14630
|
+
const list = byProvider.get(a.providerID);
|
|
14631
|
+
if (list)
|
|
14632
|
+
list.push(a);
|
|
14633
|
+
else
|
|
14634
|
+
byProvider.set(a.providerID, [a]);
|
|
14635
|
+
}
|
|
14636
|
+
const sorted = [...byProvider.entries()].sort(byProviderId);
|
|
14637
|
+
return sorted.map(([providerID, providerAccounts]) => {
|
|
14638
|
+
const currentAccountId = pool.lastSelected[providerID] ?? null;
|
|
14639
|
+
const ranked = providerAccounts.map((a) => {
|
|
14640
|
+
const available = isAvailable(a, cfg, now);
|
|
14641
|
+
return {
|
|
14642
|
+
a,
|
|
14643
|
+
available,
|
|
14644
|
+
score: available ? scoreAccount(a, cfg, now) : Number.NEGATIVE_INFINITY,
|
|
14645
|
+
weeklyUtil: a.usage.weekly?.utilization ?? 0
|
|
14646
|
+
};
|
|
14647
|
+
}).sort(compareRanked);
|
|
14648
|
+
const accounts = ranked.map(({ a, available }, i) => toStatus(a, now, a.id === currentAccountId, available, i + 1));
|
|
14649
|
+
return { providerID, accounts };
|
|
14580
14650
|
});
|
|
14581
|
-
const json2 = await readRefreshResponse(await postToken2(body));
|
|
14582
|
-
return toTokenSet2(json2, refreshToken);
|
|
14583
14651
|
}
|
|
14584
|
-
|
|
14585
|
-
|
|
14586
|
-
function applyAuth(headers, account) {
|
|
14587
|
-
headers.delete("x-api-key");
|
|
14588
|
-
headers.set("authorization", `Bearer ${account.access}`);
|
|
14589
|
-
const accountId = resolveAccountId(account);
|
|
14590
|
-
if (accountId)
|
|
14591
|
-
headers.set("chatgpt-account-id", accountId);
|
|
14592
|
-
headers.set("openai-beta", OPENAI_BETA);
|
|
14593
|
-
headers.set("originator", ORIGINATOR);
|
|
14594
|
-
headers.set("user-agent", USER_AGENT2);
|
|
14595
|
-
headers.set("session-id", SESSION_ID);
|
|
14652
|
+
async function readStatus(now = Date.now(), cfg = loadConfig()) {
|
|
14653
|
+
return buildStatus(await readPool(), now, cfg);
|
|
14596
14654
|
}
|
|
14597
|
-
function
|
|
14598
|
-
|
|
14599
|
-
if (!url2)
|
|
14600
|
-
return input;
|
|
14601
|
-
if (!url2.pathname.includes("/responses"))
|
|
14602
|
-
return input;
|
|
14603
|
-
if (url2.href === CODEX_RESPONSES_URL)
|
|
14604
|
-
return input;
|
|
14605
|
-
return input instanceof Request ? new Request(CODEX_RESPONSES_URL, input) : CODEX_RESPONSES_URL;
|
|
14655
|
+
function pct(u) {
|
|
14656
|
+
return typeof u === "number" ? `${Math.round(u * 100)}%` : "-";
|
|
14606
14657
|
}
|
|
14607
|
-
function
|
|
14608
|
-
let
|
|
14609
|
-
|
|
14610
|
-
const
|
|
14611
|
-
const
|
|
14612
|
-
|
|
14613
|
-
let i = 0;
|
|
14614
|
-
for (const raw of input) {
|
|
14615
|
-
if (raw && raw.type === "message" && raw.role === "system") {
|
|
14616
|
-
if (remaining === null)
|
|
14617
|
-
remaining = input.slice(0, i);
|
|
14618
|
-
const text = joinBlockTexts(raw.content, `
|
|
14619
|
-
`);
|
|
14620
|
-
if (text)
|
|
14621
|
-
systemTexts.push(text);
|
|
14622
|
-
} else if (remaining !== null) {
|
|
14623
|
-
remaining.push(raw);
|
|
14624
|
-
}
|
|
14625
|
-
i++;
|
|
14626
|
-
}
|
|
14627
|
-
if (systemTexts.length > 0) {
|
|
14628
|
-
instructions = [instructions, ...systemTexts].filter(Boolean).join(`
|
|
14629
|
-
|
|
14630
|
-
`);
|
|
14631
|
-
}
|
|
14632
|
-
if (remaining !== null)
|
|
14633
|
-
obj.input = remaining;
|
|
14658
|
+
function displayWidth(s) {
|
|
14659
|
+
let width = 0;
|
|
14660
|
+
for (const ch of s) {
|
|
14661
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
14662
|
+
const wide = cp >= 4352 && cp <= 4447 || cp >= 11904 && cp <= 42191 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 131072 && cp <= 262141;
|
|
14663
|
+
width += wide ? 2 : 1;
|
|
14634
14664
|
}
|
|
14635
|
-
|
|
14665
|
+
return width;
|
|
14636
14666
|
}
|
|
14637
|
-
function
|
|
14638
|
-
|
|
14639
|
-
|
|
14640
|
-
|
|
14641
|
-
|
|
14642
|
-
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14667
|
+
function padDisplayEnd(s, width) {
|
|
14668
|
+
const pad = width - displayWidth(s);
|
|
14669
|
+
return pad > 0 ? s + " ".repeat(pad) : s;
|
|
14670
|
+
}
|
|
14671
|
+
function relTime(at, now) {
|
|
14672
|
+
if (at <= now)
|
|
14673
|
+
return "-";
|
|
14674
|
+
const mins = Math.max(1, Math.round((at - now) / 60000));
|
|
14675
|
+
if (mins <= 120)
|
|
14676
|
+
return `${mins}m`;
|
|
14677
|
+
const hrs = Math.floor(mins / 60);
|
|
14678
|
+
if (mins <= 48 * 60)
|
|
14679
|
+
return `${hrs}h${mins % 60}m`;
|
|
14680
|
+
return `${Math.floor(hrs / 24)}d${hrs % 24}h`;
|
|
14681
|
+
}
|
|
14682
|
+
function stateOf(a, now) {
|
|
14683
|
+
if (a.disabledReason)
|
|
14684
|
+
return a.disabledReason === MANUAL_DISABLED_REASON ? "disabled" : "re-login";
|
|
14685
|
+
if (a.cooldownUntil > now)
|
|
14686
|
+
return `cooldown ${relTime(a.cooldownUntil, now)}`;
|
|
14687
|
+
if (!a.available)
|
|
14688
|
+
return "exhausted";
|
|
14689
|
+
const base = a.current ? "in use" : "ready";
|
|
14690
|
+
const tiers = Object.entries(a.modelCooldownsUntil).filter(([, until]) => until > now).sort(([x], [y]) => compareAscii(x, y)).map(([tier, until]) => `${tier} ${relTime(until, now)}`);
|
|
14691
|
+
if (tiers.length > 0)
|
|
14692
|
+
return `${base} · ${tiers.join(" · ")}`;
|
|
14693
|
+
return base;
|
|
14694
|
+
}
|
|
14695
|
+
function providerName(providerID) {
|
|
14696
|
+
return PROVIDER_NAMES[providerID] ?? providerID;
|
|
14697
|
+
}
|
|
14698
|
+
function renderStatus(providers, now = Date.now()) {
|
|
14699
|
+
if (providers.length === 0)
|
|
14700
|
+
return "auth-load-balancer: no accounts registered.";
|
|
14701
|
+
const lines = [];
|
|
14702
|
+
for (const p of providers) {
|
|
14703
|
+
const current = p.accounts.find((a) => a.current);
|
|
14704
|
+
lines.push(`${providerName(p.providerID)} — in use: ${current ? current.label : "(none yet)"}`);
|
|
14705
|
+
const width = p.accounts.reduce((w, a) => Math.max(w, displayWidth(a.label)), 16);
|
|
14706
|
+
lines.push(` # ${"account".padEnd(width + 3)}weekly 5h resets state`);
|
|
14707
|
+
for (const a of p.accounts) {
|
|
14708
|
+
const mark = a.current ? "▶" : " ";
|
|
14709
|
+
lines.push(` ${String(a.rank).padEnd(2)} ${mark} ${padDisplayEnd(a.label, width)} ${pct(a.weeklyUtil).padStart(5)} ${pct(a.hourlyUtil).padStart(5)} ${relTime(a.weeklyResetAt, now).padStart(7)} ${stateOf(a, now)}`);
|
|
14710
|
+
}
|
|
14711
|
+
lines.push("");
|
|
14648
14712
|
}
|
|
14713
|
+
return lines.join(`
|
|
14714
|
+
`).trimEnd();
|
|
14649
14715
|
}
|
|
14650
14716
|
|
|
14651
|
-
// src/
|
|
14652
|
-
|
|
14653
|
-
|
|
14654
|
-
hourlyReset: "x-codex-primary-reset-at",
|
|
14655
|
-
weeklyUtil: "x-codex-secondary-used-percent",
|
|
14656
|
-
weeklyReset: "x-codex-secondary-reset-at",
|
|
14657
|
-
divisor: 100
|
|
14658
|
-
};
|
|
14659
|
-
function parseUsageHeaders2(headers) {
|
|
14660
|
-
return parseWindowPairHeaders(headers, HEADER_SPEC2);
|
|
14717
|
+
// src/notify.ts
|
|
14718
|
+
async function postToast(client, body) {
|
|
14719
|
+
await client.tui.showToast({ body }).catch(ignore);
|
|
14661
14720
|
}
|
|
14662
|
-
|
|
14663
|
-
|
|
14721
|
+
var lastToasted = new Map;
|
|
14722
|
+
async function notifyOnSwitch(client, providerID, account) {
|
|
14723
|
+
if (lastToasted.get(providerID) === account.id)
|
|
14724
|
+
return;
|
|
14725
|
+
lastToasted.set(providerID, account.id);
|
|
14726
|
+
const now = Date.now();
|
|
14727
|
+
const message = `▶ ${account.label} · weekly ${pct(displayUtil(account.usage.weekly, now))} · 5h ${pct(displayUtil(account.usage.hourly, now))}`;
|
|
14728
|
+
await postToast(client, {
|
|
14729
|
+
title: `${providerName(providerID)} account`,
|
|
14730
|
+
message,
|
|
14731
|
+
variant: "info",
|
|
14732
|
+
duration: 4000
|
|
14733
|
+
});
|
|
14664
14734
|
}
|
|
14665
|
-
|
|
14666
|
-
|
|
14667
|
-
|
|
14668
|
-
|
|
14669
|
-
|
|
14670
|
-
|
|
14671
|
-
|
|
14672
|
-
|
|
14673
|
-
|
|
14674
|
-
|
|
14675
|
-
if (
|
|
14676
|
-
|
|
14677
|
-
|
|
14678
|
-
|
|
14679
|
-
|
|
14680
|
-
|
|
14681
|
-
}
|
|
14735
|
+
var lastFallbackToasted = new Map;
|
|
14736
|
+
var LAST_FALLBACK_TOASTED_MAX = 256;
|
|
14737
|
+
async function notifyModelFallback(client, providerID, account, fromModel, toModel, fromTier) {
|
|
14738
|
+
const key = `${providerID}:${account.id}`;
|
|
14739
|
+
const now = Date.now();
|
|
14740
|
+
const tiers = account.modelCooldownsUntil;
|
|
14741
|
+
let latestWindow = 0;
|
|
14742
|
+
const triggeringWindow = fromTier ? tiers?.[fromTier] : undefined;
|
|
14743
|
+
if (triggeringWindow !== undefined && triggeringWindow > now) {
|
|
14744
|
+
latestWindow = triggeringWindow;
|
|
14745
|
+
} else if (tiers) {
|
|
14746
|
+
for (const until of Object.values(tiers)) {
|
|
14747
|
+
if (until > now && until > latestWindow)
|
|
14748
|
+
latestWindow = until;
|
|
14749
|
+
}
|
|
14750
|
+
}
|
|
14751
|
+
const window = `${fromModel}@${latestWindow}`;
|
|
14752
|
+
if (lastFallbackToasted.get(key) === window)
|
|
14753
|
+
return;
|
|
14754
|
+
setBounded(lastFallbackToasted, key, window, LAST_FALLBACK_TOASTED_MAX);
|
|
14755
|
+
await postToast(client, {
|
|
14756
|
+
title: `${providerName(providerID)} model fallback`,
|
|
14757
|
+
message: `▶ ${account.label} · ${fromModel} → ${toModel} (model-tier weekly limit)`,
|
|
14758
|
+
variant: "warning",
|
|
14759
|
+
duration: 6000
|
|
14760
|
+
});
|
|
14682
14761
|
}
|
|
14683
14762
|
|
|
14684
|
-
// src/
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
};
|
|
14763
|
+
// src/prime.ts
|
|
14764
|
+
async function primeInUse(providerID, now) {
|
|
14765
|
+
const snapshot = await readPool();
|
|
14766
|
+
if (!snapshot.accounts.some((a) => a.providerID === providerID))
|
|
14767
|
+
return;
|
|
14768
|
+
const cfg = loadConfig();
|
|
14769
|
+
const preview = selectAccount(snapshot.accounts, providerID, now, cfg);
|
|
14770
|
+
if (!preview || snapshot.lastSelected[providerID] === preview.account.id)
|
|
14771
|
+
return;
|
|
14772
|
+
await mutatePool((pool) => {
|
|
14773
|
+
const selection = selectAccount(pool.accounts, providerID, now, cfg);
|
|
14774
|
+
if (selection)
|
|
14775
|
+
pool.lastSelected[providerID] = selection.account.id;
|
|
14776
|
+
});
|
|
14777
|
+
}
|
|
14700
14778
|
|
|
14701
14779
|
// src/index.ts
|
|
14702
14780
|
var PROVIDER_LABELS = {
|
|
@@ -14786,7 +14864,7 @@ var AuthLoadBalancerStatusPlugin = async () => ({
|
|
|
14786
14864
|
execute: async () => {
|
|
14787
14865
|
const now = Date.now();
|
|
14788
14866
|
const pool = await readPool();
|
|
14789
|
-
await
|
|
14867
|
+
await refreshAllUsageInBackground(now, pool).catch(ignore);
|
|
14790
14868
|
const renderedAt = Date.now();
|
|
14791
14869
|
return lbResult(renderStatus(await readStatus(renderedAt), renderedAt));
|
|
14792
14870
|
}
|
|
@@ -14819,6 +14897,28 @@ var AuthLoadBalancerStatusPlugin = async () => ({
|
|
|
14819
14897
|
return lbResult(result.reason === "taken" ? `Label "${trimmed}" is already used by another account.` : `No account matching "${account}". Available: ${result.labels.join(", ") || "(none)"}`);
|
|
14820
14898
|
return lbResult(`Renamed "${result.previous}" → "${trimmed}".`);
|
|
14821
14899
|
}
|
|
14900
|
+
}),
|
|
14901
|
+
auth_lb_disable: tool({
|
|
14902
|
+
description: "Disable a pooled account so the load balancer stops selecting it (match by current label or id), or re-enable a previously disabled one with `enable: true`. A disabled account is skipped by scheduling and shows as `disabled` in the auth_lb_status dashboard — separate from an account that needs re-login.",
|
|
14903
|
+
args: {
|
|
14904
|
+
account: tool.schema.string().describe("Current label or id of the account to disable/enable"),
|
|
14905
|
+
enable: tool.schema.boolean().optional().describe("Set true to re-enable a previously disabled account; omit or false to disable it")
|
|
14906
|
+
},
|
|
14907
|
+
execute: async ({ account, enable }) => {
|
|
14908
|
+
const result = await mutatePool((pool) => {
|
|
14909
|
+
const target = pool.accounts.find((a) => a.id === account || a.label === account);
|
|
14910
|
+
if (!target)
|
|
14911
|
+
return {
|
|
14912
|
+
ok: false,
|
|
14913
|
+
labels: pool.accounts.map((a) => `${a.label} (${a.providerID})`)
|
|
14914
|
+
};
|
|
14915
|
+
target.disabledReason = enable ? null : MANUAL_DISABLED_REASON;
|
|
14916
|
+
return { ok: true, label: target.label, enabled: !!enable };
|
|
14917
|
+
});
|
|
14918
|
+
if (!result.ok)
|
|
14919
|
+
return lbResult(`No account matching "${account}". Available: ${result.labels.join(", ") || "(none)"}`);
|
|
14920
|
+
return lbResult(result.enabled ? `Enabled "${result.label}" — back in the load-balancer rotation.` : `Disabled "${result.label}" — the load balancer will skip it until you re-enable it.`);
|
|
14921
|
+
}
|
|
14822
14922
|
})
|
|
14823
14923
|
}
|
|
14824
14924
|
});
|