dsh-plugin-subscriptions 0.5.2 → 0.6.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/README.md +79 -5
- package/README.zh.md +78 -4
- package/lib/auth/rpc.d.ts +64 -13
- package/lib/auth/rpc.js +75 -10
- package/lib/auth/store.d.ts +75 -17
- package/lib/auth/store.js +148 -27
- package/lib/client/ImageGenerateToolview.d.ts +1 -1
- package/lib/client/SpeedSelect.d.ts +25 -2
- package/lib/client/SpeedSelect.js +10 -6
- package/lib/client/SubscriptionsSection.d.ts +83 -3
- package/lib/client/SubscriptionsSection.js +411 -62
- package/lib/client/VideoGenerateToolview.d.ts +1 -1
- package/lib/client/index.d.ts +1 -9
- package/lib/client/index.js +7 -4
- package/lib/client/locales.d.ts +46 -10
- package/lib/client/locales.js +46 -10
- package/lib/client.js +703 -132
- package/lib/client.js.map +1 -1
- package/lib/compat.d.ts +36 -0
- package/lib/compat.js +20 -0
- package/lib/index.d.ts +26 -1
- package/lib/index.js +2377 -309
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +237 -0
- package/lib/providers/accounts.d.ts +102 -0
- package/lib/providers/accounts.js +123 -0
- package/lib/providers/claude.d.ts +46 -7
- package/lib/providers/claude.js +125 -34
- package/lib/providers/codex.d.ts +45 -3
- package/lib/providers/codex.js +152 -26
- package/lib/providers/common.d.ts +87 -6
- package/lib/providers/common.js +185 -22
- package/lib/providers/copilot.d.ts +32 -3
- package/lib/providers/copilot.js +111 -19
- package/lib/providers/grok.d.ts +45 -4
- package/lib/providers/grok.js +136 -20
- package/lib/providers/pool-family.d.ts +56 -0
- package/lib/providers/pool-family.js +45 -0
- package/lib/providers/pool-health.d.ts +74 -0
- package/lib/providers/pool-health.js +148 -0
- package/lib/providers/pool-usage.d.ts +78 -0
- package/lib/providers/pool-usage.js +185 -0
- package/lib/providers/pool.d.ts +107 -0
- package/lib/providers/pool.js +371 -0
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/tools/image-generate.d.ts +3 -3
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.d.ts +2 -2
- package/lib/tools/video-generate.js +2 -1
- package/lib/tools/x-search.d.ts +2 -2
- package/lib/tools/x-search.js +2 -1
- package/lib/translate/anthropic.js +5 -4
- package/lib/translate/chat-completions.js +5 -4
- package/lib/translate/responses.js +5 -4
- package/package.json +21 -21
- package/lib/providers/antigravity.d.ts +0 -90
- package/lib/providers/antigravity.js +0 -392
- package/lib/translate/antigravity.d.ts +0 -110
- package/lib/translate/antigravity.js +0 -303
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import
|
|
2
|
+
import * as llm from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, errorChain, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
3
4
|
import { createServer } from "node:http";
|
|
4
5
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
6
|
import { ProxyAgent, fetch as fetch$1 } from "undici";
|
|
@@ -271,13 +272,13 @@ const DISABLED = {
|
|
|
271
272
|
bypass: []
|
|
272
273
|
};
|
|
273
274
|
/** Current config; updated by every load/apply/save. */
|
|
274
|
-
let current = DISABLED;
|
|
275
|
+
let current$1 = DISABLED;
|
|
275
276
|
/** The live dispatcher, or undefined when proxies are off/errored. */
|
|
276
277
|
let agent;
|
|
277
278
|
/** Last load/apply failure, surfaced by the config view. */
|
|
278
279
|
let configError;
|
|
279
280
|
/** One lazy load of the on-disk config (module-import cheap; file read once). */
|
|
280
|
-
let ready;
|
|
281
|
+
let ready$1;
|
|
281
282
|
/** Absolute path of the proxy config file. */
|
|
282
283
|
function proxyFilePath() {
|
|
283
284
|
return dshHomePath("plugins", "subscriptions", "proxy.json");
|
|
@@ -392,7 +393,7 @@ async function applyConfig(cfg) {
|
|
|
392
393
|
withError(error);
|
|
393
394
|
next = void 0;
|
|
394
395
|
}
|
|
395
|
-
current = cfg;
|
|
396
|
+
current$1 = cfg;
|
|
396
397
|
}
|
|
397
398
|
const previous = agent;
|
|
398
399
|
agent = next;
|
|
@@ -432,16 +433,16 @@ async function loadConfigFile(path) {
|
|
|
432
433
|
});
|
|
433
434
|
}
|
|
434
435
|
/** Resolve the module state once from disk; failures disable the proxy. */
|
|
435
|
-
async function ensureReady() {
|
|
436
|
-
ready ??= loadConfigFile(proxyFilePath()).then(async (cfg) => {
|
|
436
|
+
async function ensureReady$1() {
|
|
437
|
+
ready$1 ??= loadConfigFile(proxyFilePath()).then(async (cfg) => {
|
|
437
438
|
await applyConfig(cfg);
|
|
438
|
-
return current;
|
|
439
|
+
return current$1;
|
|
439
440
|
}, async (error) => {
|
|
440
441
|
withError(error);
|
|
441
442
|
await applyConfig(void 0);
|
|
442
|
-
return current;
|
|
443
|
+
return current$1;
|
|
443
444
|
});
|
|
444
|
-
return ready;
|
|
445
|
+
return ready$1;
|
|
445
446
|
}
|
|
446
447
|
/** Persist a config atomically with owner-only permissions, then apply it. */
|
|
447
448
|
async function persistConfig(cfg, path) {
|
|
@@ -462,13 +463,13 @@ async function persistConfig(cfg, path) {
|
|
|
462
463
|
* load/apply failure when the stored config is unusable.
|
|
463
464
|
*/
|
|
464
465
|
async function proxyGetConfig() {
|
|
465
|
-
await ensureReady();
|
|
466
|
+
await ensureReady$1();
|
|
466
467
|
return {
|
|
467
|
-
enabled: current.enabled,
|
|
468
|
-
url: current.url,
|
|
469
|
-
...current.username === void 0 ? {} : { username: current.username },
|
|
470
|
-
passwordSet: current.password !== void 0 && current.password !== "",
|
|
471
|
-
bypass: [...current.bypass],
|
|
468
|
+
enabled: current$1.enabled,
|
|
469
|
+
url: current$1.url,
|
|
470
|
+
...current$1.username === void 0 ? {} : { username: current$1.username },
|
|
471
|
+
passwordSet: current$1.password !== void 0 && current$1.password !== "",
|
|
472
|
+
bypass: [...current$1.bypass],
|
|
472
473
|
...configError === void 0 ? {} : { error: configError }
|
|
473
474
|
};
|
|
474
475
|
}
|
|
@@ -479,14 +480,14 @@ async function proxyGetConfig() {
|
|
|
479
480
|
* @returns the resulting view (secrets omitted).
|
|
480
481
|
*/
|
|
481
482
|
async function proxySetConfig(input) {
|
|
482
|
-
await ensureReady();
|
|
483
|
-
const password = input.password === void 0 ? current.password : input.password === null || input.password === "" ? void 0 : input.password;
|
|
483
|
+
await ensureReady$1();
|
|
484
|
+
const password = input.password === void 0 ? current$1.password : input.password === null || input.password === "" ? void 0 : input.password;
|
|
484
485
|
const next = normalizeConfig({
|
|
485
486
|
enabled: input.enabled,
|
|
486
487
|
url: input.url,
|
|
487
488
|
...input.username === void 0 ? {} : { username: input.username },
|
|
488
489
|
...password === void 0 ? {} : { password },
|
|
489
|
-
bypass: input.bypass ?? current.bypass
|
|
490
|
+
bypass: input.bypass ?? current$1.bypass
|
|
490
491
|
});
|
|
491
492
|
await persistConfig(next, proxyFilePath());
|
|
492
493
|
await applyConfig(next);
|
|
@@ -502,16 +503,16 @@ async function proxySetConfig(input) {
|
|
|
502
503
|
* host's global fetch.
|
|
503
504
|
*/
|
|
504
505
|
async function proxiedFetch(input, init = {}) {
|
|
505
|
-
await ensureReady();
|
|
506
|
+
await ensureReady$1();
|
|
506
507
|
let dispatcher;
|
|
507
|
-
if (current.enabled && agent !== void 0) {
|
|
508
|
+
if (current$1.enabled && agent !== void 0) {
|
|
508
509
|
let hostname = "";
|
|
509
510
|
try {
|
|
510
511
|
hostname = (typeof input === "string" ? new URL(input) : input instanceof URL ? input : new URL(input.url)).hostname;
|
|
511
512
|
} catch {
|
|
512
513
|
hostname = "";
|
|
513
514
|
}
|
|
514
|
-
if (!matchesBypass(hostname, current.bypass)) dispatcher = agent;
|
|
515
|
+
if (!matchesBypass(hostname, current$1.bypass)) dispatcher = agent;
|
|
515
516
|
}
|
|
516
517
|
if (dispatcher === void 0) return fetch(input, init);
|
|
517
518
|
return dispatchFetch(input, {
|
|
@@ -544,7 +545,7 @@ async function proxyTestConnection(target = DEFAULT_PROXY_TEST_URL, draft) {
|
|
|
544
545
|
error: errorMessage(error)
|
|
545
546
|
};
|
|
546
547
|
}
|
|
547
|
-
await ensureReady();
|
|
548
|
+
await ensureReady$1();
|
|
548
549
|
let probeAgent;
|
|
549
550
|
let viaProxy;
|
|
550
551
|
let closeProbe = false;
|
|
@@ -566,7 +567,7 @@ async function proxyTestConnection(target = DEFAULT_PROXY_TEST_URL, draft) {
|
|
|
566
567
|
};
|
|
567
568
|
}
|
|
568
569
|
else {
|
|
569
|
-
viaProxy = current.enabled && agent !== void 0 && !matchesBypass(parsed.hostname, current.bypass);
|
|
570
|
+
viaProxy = current$1.enabled && agent !== void 0 && !matchesBypass(parsed.hostname, current$1.bypass);
|
|
570
571
|
probeAgent = viaProxy ? agent : void 0;
|
|
571
572
|
}
|
|
572
573
|
const started = Date.now();
|
|
@@ -939,6 +940,31 @@ const PROVIDER_IDS = [
|
|
|
939
940
|
"copilot"
|
|
940
941
|
];
|
|
941
942
|
/**
|
|
943
|
+
* The stable identity of one session's account: codex keys on the always
|
|
944
|
+
* present `accountId` claim, the others on their display identity, falling
|
|
945
|
+
* back to a refresh-token hash for sessions stored before identity fields
|
|
946
|
+
* existed. Logging the same account in again lands on the same key, so a
|
|
947
|
+
* re-login updates in place instead of duplicating. (The hash fallback can
|
|
948
|
+
* miss that dedup once for a legacy session re-logged with a now-known
|
|
949
|
+
* identity — the duplicate is visible on the Settings page and can simply
|
|
950
|
+
* be logged out.)
|
|
951
|
+
* @param provider - the provider route.
|
|
952
|
+
* @param session - the session to key.
|
|
953
|
+
* @returns the account map key.
|
|
954
|
+
*/
|
|
955
|
+
function accountKeyOf(provider, session) {
|
|
956
|
+
switch (provider) {
|
|
957
|
+
case "codex": return session.accountId;
|
|
958
|
+
case "claude": return session.emailAddress ?? tokenHash(session.refreshToken);
|
|
959
|
+
case "grok": return session.account ?? tokenHash(session.refreshToken);
|
|
960
|
+
case "copilot": return session.account ?? tokenHash(session.refreshToken);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/** Short stable hash for sessions without an identity field. */
|
|
964
|
+
function tokenHash(refreshToken) {
|
|
965
|
+
return `token-${createHash("sha256").update(refreshToken).digest("hex").slice(0, 16)}`;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
942
968
|
* Absolute path of the auth store file.
|
|
943
969
|
* @returns `dshHomePath('plugins', 'subscriptions', 'auth.json')`.
|
|
944
970
|
*/
|
|
@@ -949,16 +975,17 @@ function authFilePath() {
|
|
|
949
975
|
function legacyAuthFilePath() {
|
|
950
976
|
return dshHomePath("plugins", "router", "auth.json");
|
|
951
977
|
}
|
|
952
|
-
/** Check that one durable
|
|
953
|
-
function assertSessionShape(provider, value) {
|
|
954
|
-
if (typeof value !== "object" || value === null) throw new Error(`subscriptions auth store: entry "${provider}" is not an object; fix or delete the store file`);
|
|
978
|
+
/** Check that one durable session carries the fields every session needs. */
|
|
979
|
+
function assertSessionShape(provider, account, value) {
|
|
980
|
+
if (typeof value !== "object" || value === null) throw new Error(`subscriptions auth store: entry "${provider}/${account}" is not an object; fix or delete the store file`);
|
|
955
981
|
const entry = value;
|
|
956
|
-
if (typeof entry.accessToken !== "string" || entry.accessToken.length === 0 || typeof entry.refreshToken !== "string" || entry.refreshToken.length === 0 || typeof entry.expiresAt !== "number" || !Number.isFinite(entry.expiresAt)) throw new Error(`subscriptions auth store: entry "${provider}" is missing accessToken/refreshToken/expiresAt; fix or delete the store file`);
|
|
982
|
+
if (typeof entry.accessToken !== "string" || entry.accessToken.length === 0 || typeof entry.refreshToken !== "string" || entry.refreshToken.length === 0 || typeof entry.expiresAt !== "number" || !Number.isFinite(entry.expiresAt)) throw new Error(`subscriptions auth store: entry "${provider}/${account}" is missing accessToken/refreshToken/expiresAt; fix or delete the store file`);
|
|
957
983
|
}
|
|
958
984
|
/**
|
|
959
985
|
* Read the whole store. A missing file is an empty store; malformed JSON or a
|
|
960
986
|
* malformed entry throws, because silently discarding tokens would strand the
|
|
961
|
-
* user without a diagnosis.
|
|
987
|
+
* user without a diagnosis. Single-account entries are migrated in memory;
|
|
988
|
+
* the next write persists the new shape.
|
|
962
989
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
963
990
|
* @returns the parsed session map.
|
|
964
991
|
*/
|
|
@@ -982,7 +1009,7 @@ async function loadStore(path = authFilePath()) {
|
|
|
982
1009
|
}
|
|
983
1010
|
return parseStore(text, path);
|
|
984
1011
|
}
|
|
985
|
-
/** Parse and
|
|
1012
|
+
/** Parse, validate, and migrate store JSON read from `path`. */
|
|
986
1013
|
function parseStore(text, path) {
|
|
987
1014
|
let parsed;
|
|
988
1015
|
try {
|
|
@@ -991,10 +1018,28 @@ function parseStore(text, path) {
|
|
|
991
1018
|
throw new Error(`subscriptions auth store at ${path} is not valid JSON; fix or delete the file`);
|
|
992
1019
|
}
|
|
993
1020
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`subscriptions auth store at ${path} must be a JSON object keyed by provider; fix or delete the file`);
|
|
994
|
-
const
|
|
1021
|
+
const raw = parsed;
|
|
1022
|
+
const store = {};
|
|
995
1023
|
for (const provider of PROVIDER_IDS) {
|
|
996
|
-
const entry =
|
|
997
|
-
if (entry
|
|
1024
|
+
const entry = raw[provider];
|
|
1025
|
+
if (entry === void 0) continue;
|
|
1026
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new Error(`subscriptions auth store: entry "${provider}" is not an object; fix or delete the store file`);
|
|
1027
|
+
const record = entry;
|
|
1028
|
+
if (typeof record.accessToken === "string") {
|
|
1029
|
+
assertSessionShape(provider, "(legacy)", record);
|
|
1030
|
+
const session = record;
|
|
1031
|
+
const key = accountKeyOf(provider, session);
|
|
1032
|
+
store[provider] = {
|
|
1033
|
+
default: key,
|
|
1034
|
+
accounts: { [key]: session }
|
|
1035
|
+
};
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
const accounts = record.accounts;
|
|
1039
|
+
if (typeof accounts !== "object" || accounts === null || Array.isArray(accounts)) throw new Error(`subscriptions auth store: entry "${provider}" has no accounts map; fix or delete the store file`);
|
|
1040
|
+
if (record.default !== void 0 && typeof record.default !== "string") throw new Error(`subscriptions auth store: entry "${provider}" default is not a string; fix or delete the store file`);
|
|
1041
|
+
for (const [account, session] of Object.entries(accounts)) assertSessionShape(provider, account, session);
|
|
1042
|
+
store[provider] = record;
|
|
998
1043
|
}
|
|
999
1044
|
return store;
|
|
1000
1045
|
}
|
|
@@ -1014,8 +1059,8 @@ async function writeStore(store, path) {
|
|
|
1014
1059
|
/**
|
|
1015
1060
|
* One write chain per store path. Every mutation is a read-modify-write of a
|
|
1016
1061
|
* single JSON file, and the plugin has several independent writers — a login,
|
|
1017
|
-
* a logout, and one token refresh per provider
|
|
1018
|
-
* schedule. Overlapping them unserialized costs whichever
|
|
1062
|
+
* a logout, and one token refresh per provider account, each on its own
|
|
1063
|
+
* schedule. Overlapping them unserialized costs whichever account read the
|
|
1019
1064
|
* store first its entry.
|
|
1020
1065
|
*
|
|
1021
1066
|
* A chain is dropped once nothing is queued behind it, so the map holds an
|
|
@@ -1040,37 +1085,94 @@ async function serialize(path, action) {
|
|
|
1040
1085
|
}
|
|
1041
1086
|
}
|
|
1042
1087
|
/**
|
|
1043
|
-
*
|
|
1088
|
+
* List one provider's accounts, default first.
|
|
1089
|
+
* @param provider - the provider route.
|
|
1090
|
+
* @param path - store file path; defaults to {@link authFilePath}.
|
|
1091
|
+
* @returns the account entries in stable order (empty when logged out).
|
|
1092
|
+
*/
|
|
1093
|
+
async function listAccounts(provider, path = authFilePath()) {
|
|
1094
|
+
const entry = (await loadStore(path))[provider];
|
|
1095
|
+
if (entry === void 0) return [];
|
|
1096
|
+
const accounts = Object.entries(entry.accounts).map(([key, session]) => ({
|
|
1097
|
+
key,
|
|
1098
|
+
session
|
|
1099
|
+
}));
|
|
1100
|
+
accounts.sort((a, b) => Number(b.key === entry.default) - Number(a.key === entry.default));
|
|
1101
|
+
return accounts;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Read one account's session.
|
|
1044
1105
|
* @param provider - the provider route.
|
|
1106
|
+
* @param account - the account key; defaults to the provider's default account.
|
|
1045
1107
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
1046
|
-
* @returns the stored session, or `undefined` when
|
|
1108
|
+
* @returns the stored session, or `undefined` when absent.
|
|
1047
1109
|
*/
|
|
1048
|
-
async function
|
|
1049
|
-
|
|
1110
|
+
async function getAccountSession(provider, account, path = authFilePath()) {
|
|
1111
|
+
const entry = (await loadStore(path))[provider];
|
|
1112
|
+
if (entry === void 0) return void 0;
|
|
1113
|
+
const key = account ?? entry.default;
|
|
1114
|
+
if (key === void 0) return void 0;
|
|
1115
|
+
return entry.accounts[key];
|
|
1050
1116
|
}
|
|
1051
1117
|
/**
|
|
1052
|
-
* Write one
|
|
1118
|
+
* Write one account's session, preserving the others. The first account of a
|
|
1119
|
+
* provider becomes its default.
|
|
1053
1120
|
* @param provider - the provider route.
|
|
1121
|
+
* @param account - the account key (see {@link accountKeyOf}).
|
|
1054
1122
|
* @param session - the fresh session from a login or refresh.
|
|
1055
1123
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
1056
1124
|
*/
|
|
1057
|
-
async function
|
|
1125
|
+
async function saveAccountSession(provider, account, session, path = authFilePath()) {
|
|
1058
1126
|
return serialize(path, async () => {
|
|
1059
1127
|
const store = await loadStore(path);
|
|
1060
|
-
store[provider]
|
|
1128
|
+
const entry = store[provider];
|
|
1129
|
+
store[provider] = {
|
|
1130
|
+
default: entry?.default ?? account,
|
|
1131
|
+
accounts: {
|
|
1132
|
+
...entry?.accounts,
|
|
1133
|
+
[account]: session
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
await writeStore(store, path);
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Delete one account's session (logout). Deleting the default moves the badge
|
|
1141
|
+
* to the next remaining account.
|
|
1142
|
+
* @param provider - the provider route.
|
|
1143
|
+
* @param account - the account key.
|
|
1144
|
+
* @param path - store file path; defaults to {@link authFilePath}.
|
|
1145
|
+
*/
|
|
1146
|
+
async function deleteAccountSession(provider, account, path = authFilePath()) {
|
|
1147
|
+
return serialize(path, async () => {
|
|
1148
|
+
const store = await loadStore(path);
|
|
1149
|
+
const entry = store[provider];
|
|
1150
|
+
if (entry === void 0 || !(account in entry.accounts)) return;
|
|
1151
|
+
const accounts = { ...entry.accounts };
|
|
1152
|
+
delete accounts[account];
|
|
1153
|
+
if (Object.keys(accounts).length === 0) delete store[provider];
|
|
1154
|
+
else store[provider] = {
|
|
1155
|
+
...entry.default === account ? { default: Object.keys(accounts)[0] } : { default: entry.default },
|
|
1156
|
+
accounts
|
|
1157
|
+
};
|
|
1061
1158
|
await writeStore(store, path);
|
|
1062
1159
|
});
|
|
1063
1160
|
}
|
|
1064
1161
|
/**
|
|
1065
|
-
*
|
|
1162
|
+
* Pin the account direct (non-pool) routes serve.
|
|
1066
1163
|
* @param provider - the provider route.
|
|
1164
|
+
* @param account - the account key; must exist.
|
|
1067
1165
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
1068
1166
|
*/
|
|
1069
|
-
async function
|
|
1167
|
+
async function setDefaultAccount(provider, account, path = authFilePath()) {
|
|
1070
1168
|
return serialize(path, async () => {
|
|
1071
1169
|
const store = await loadStore(path);
|
|
1072
|
-
|
|
1073
|
-
|
|
1170
|
+
const entry = store[provider];
|
|
1171
|
+
if (entry === void 0 || !(account in entry.accounts)) throw new Error(`no ${provider} account "${account}" is logged in`);
|
|
1172
|
+
store[provider] = {
|
|
1173
|
+
...entry,
|
|
1174
|
+
default: account
|
|
1175
|
+
};
|
|
1074
1176
|
await writeStore(store, path);
|
|
1075
1177
|
});
|
|
1076
1178
|
}
|
|
@@ -1126,6 +1228,30 @@ function readString(payload, field) {
|
|
|
1126
1228
|
if (typeof value !== "string" || value.length === 0) throw new BadRequest(`payload.${field} must be a non-empty string`);
|
|
1127
1229
|
return value;
|
|
1128
1230
|
}
|
|
1231
|
+
/** Validate the `setModelDefault` endpoint's payload. */
|
|
1232
|
+
function readModelDefaultInput(payload) {
|
|
1233
|
+
const provider = readProvider(payload);
|
|
1234
|
+
const model = readString(payload, "model");
|
|
1235
|
+
const record = payload;
|
|
1236
|
+
let effort;
|
|
1237
|
+
if (record.effort !== void 0) {
|
|
1238
|
+
if (typeof record.effort !== "string" || record.effort.length === 0) throw new BadRequest("payload.effort must be a non-empty string when present");
|
|
1239
|
+
effort = record.effort;
|
|
1240
|
+
}
|
|
1241
|
+
return {
|
|
1242
|
+
provider,
|
|
1243
|
+
model,
|
|
1244
|
+
...effort === void 0 ? {} : { effort }
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
/** Validate the optional Claude login method. */
|
|
1248
|
+
function readLoginMethod(payload, provider) {
|
|
1249
|
+
const method = payload.method;
|
|
1250
|
+
if (method === void 0) return void 0;
|
|
1251
|
+
if (provider !== "claude") throw new BadRequest("payload.method is only valid for claude");
|
|
1252
|
+
if (method !== "oauth" && method !== "keychain") throw new BadRequest("payload.method must be \"oauth\" or \"keychain\"");
|
|
1253
|
+
return method;
|
|
1254
|
+
}
|
|
1129
1255
|
/** Validate the `setSpeed` endpoint's tier. */
|
|
1130
1256
|
function readSpeedTier(payload) {
|
|
1131
1257
|
const tier = payload.tier;
|
|
@@ -1170,6 +1296,14 @@ function readVideoName(payload) {
|
|
|
1170
1296
|
if (typeof name$1 !== "string" || !VIDEO_NAME_PATTERN.test(name$1)) throw new BadRequest("payload.name must be a bare .mp4 file name");
|
|
1171
1297
|
return name$1;
|
|
1172
1298
|
}
|
|
1299
|
+
/** Validate the `usage` endpoint's optional force flag. */
|
|
1300
|
+
function readForce(payload) {
|
|
1301
|
+
if (typeof payload !== "object" || payload === null) return false;
|
|
1302
|
+
const force = payload.force;
|
|
1303
|
+
if (force === void 0) return false;
|
|
1304
|
+
if (typeof force !== "boolean") throw new BadRequest("payload.force must be a boolean when present");
|
|
1305
|
+
return force;
|
|
1306
|
+
}
|
|
1173
1307
|
/** Validate the session id both speed endpoints carry. */
|
|
1174
1308
|
function readSessionId(payload) {
|
|
1175
1309
|
if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
|
|
@@ -1237,13 +1371,16 @@ function readProxyTestPayload(payload) {
|
|
|
1237
1371
|
...proxy === void 0 ? {} : { proxy }
|
|
1238
1372
|
};
|
|
1239
1373
|
}
|
|
1240
|
-
async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
1374
|
+
async function dispatch(controller, speed, proxy, modelDefaults, endpoint, payload, signal) {
|
|
1241
1375
|
switch (endpoint) {
|
|
1242
1376
|
case "status": {
|
|
1243
1377
|
const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider)]));
|
|
1244
1378
|
return ok({ providers: Object.fromEntries(entries) });
|
|
1245
1379
|
}
|
|
1246
|
-
case "login":
|
|
1380
|
+
case "login": {
|
|
1381
|
+
const provider = readProvider(payload);
|
|
1382
|
+
return ok(await controller.login(provider, readLoginMethod(payload, provider)));
|
|
1383
|
+
}
|
|
1247
1384
|
case "manual": {
|
|
1248
1385
|
const provider = readProvider(payload);
|
|
1249
1386
|
await controller.manual(provider, readString(payload, "input"));
|
|
@@ -1252,10 +1389,20 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
|
1252
1389
|
case "cancel":
|
|
1253
1390
|
await controller.cancel(readProvider(payload));
|
|
1254
1391
|
return ok({ ok: true });
|
|
1255
|
-
case "logout":
|
|
1256
|
-
|
|
1392
|
+
case "logout": {
|
|
1393
|
+
const provider = readProvider(payload);
|
|
1394
|
+
await controller.logout(provider, readString(payload, "account"));
|
|
1395
|
+
return ok({ ok: true });
|
|
1396
|
+
}
|
|
1397
|
+
case "setDefault": {
|
|
1398
|
+
const provider = readProvider(payload);
|
|
1399
|
+
await controller.setDefault(provider, readString(payload, "account"));
|
|
1257
1400
|
return ok({ ok: true });
|
|
1258
|
-
|
|
1401
|
+
}
|
|
1402
|
+
case "usage": {
|
|
1403
|
+
const provider = readProvider(payload);
|
|
1404
|
+
return ok(await controller.usage(provider, readString(payload, "account"), signal, readForce(payload)));
|
|
1405
|
+
}
|
|
1259
1406
|
case "image": return ok(await controller.readImage(readImageRef(payload), signal));
|
|
1260
1407
|
case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
|
|
1261
1408
|
case "speed": return ok(await speed.speed(readSessionId(payload)));
|
|
@@ -1271,6 +1418,16 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
|
1271
1418
|
case "proxyTest":
|
|
1272
1419
|
if (proxy === void 0) throw new BadRequest("proxy configuration is unavailable");
|
|
1273
1420
|
return ok(await proxy.test(readProxyTestPayload(payload)));
|
|
1421
|
+
case "modelDefaults":
|
|
1422
|
+
if (modelDefaults === void 0) throw new BadRequest("model defaults are unavailable");
|
|
1423
|
+
return ok(await modelDefaults.catalog());
|
|
1424
|
+
case "setModelDefault":
|
|
1425
|
+
if (modelDefaults === void 0) throw new BadRequest("model defaults are unavailable");
|
|
1426
|
+
{
|
|
1427
|
+
const input = readModelDefaultInput(payload);
|
|
1428
|
+
await modelDefaults.set(input.provider, input.model, input.effort);
|
|
1429
|
+
}
|
|
1430
|
+
return ok({ ok: true });
|
|
1274
1431
|
default: throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
|
|
1275
1432
|
}
|
|
1276
1433
|
}
|
|
@@ -1280,13 +1437,14 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
|
1280
1437
|
* @param controller - the auth operations backing the endpoints.
|
|
1281
1438
|
* @param speed - the per-session speed-tier state backing the Speed toggle.
|
|
1282
1439
|
* @param proxy - optional proxy-config controller backing `proxyGet`/`proxySet`/`proxyTest`.
|
|
1440
|
+
* @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
|
|
1283
1441
|
*/
|
|
1284
|
-
function registerAuthRpc(ctx, controller, speed, proxy = void 0) {
|
|
1442
|
+
function registerAuthRpc(ctx, controller, speed, proxy = void 0, modelDefaults = void 0) {
|
|
1285
1443
|
ctx.inject(["connection"], (ctx$1) => {
|
|
1286
1444
|
const connection = ctx$1.get("connection");
|
|
1287
1445
|
ctx$1.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload, signal) => {
|
|
1288
1446
|
try {
|
|
1289
|
-
return await dispatch(controller, speed, proxy, endpoint, payload, signal);
|
|
1447
|
+
return await dispatch(controller, speed, proxy, modelDefaults, endpoint, payload, signal);
|
|
1290
1448
|
} catch (error) {
|
|
1291
1449
|
return failure(error);
|
|
1292
1450
|
}
|
|
@@ -1294,6 +1452,442 @@ function registerAuthRpc(ctx, controller, speed, proxy = void 0) {
|
|
|
1294
1452
|
});
|
|
1295
1453
|
}
|
|
1296
1454
|
|
|
1455
|
+
//#endregion
|
|
1456
|
+
//#region src/model-defaults.ts
|
|
1457
|
+
/** Absolute path of the defaults file. */
|
|
1458
|
+
function modelDefaultsFilePath() {
|
|
1459
|
+
return dshHomePath("plugins", "subscriptions", "model-defaults.json");
|
|
1460
|
+
}
|
|
1461
|
+
const EMPTY = Object.freeze({});
|
|
1462
|
+
/** In-memory snapshot read by every consumer (adapters, RPC). */
|
|
1463
|
+
let current = EMPTY;
|
|
1464
|
+
/** One lazy load of the on-disk file (read once per process). */
|
|
1465
|
+
let ready;
|
|
1466
|
+
/** Last load failure, surfaced to callers that care; defaults stay empty. */
|
|
1467
|
+
let loadError;
|
|
1468
|
+
/**
|
|
1469
|
+
* Serialises every write: the read-modify-write sequence must not interleave,
|
|
1470
|
+
* or a fast second save would compute its snapshot from the stale `current`
|
|
1471
|
+
* and silently drop the first update (the UI disables only the row being
|
|
1472
|
+
* saved, so overlaps are reachable).
|
|
1473
|
+
*/
|
|
1474
|
+
let writeChain = Promise.resolve();
|
|
1475
|
+
/**
|
|
1476
|
+
* Validate one persisted provider section: a string→string map, or undefined.
|
|
1477
|
+
* Malformed *entries* are skipped, not the whole section: one bad value (a
|
|
1478
|
+
* hand edit losing its quotes) must not silently un-configure every model in
|
|
1479
|
+
* that provider. What was dropped is reported so the caller can surface it
|
|
1480
|
+
* instead of the loss disappearing.
|
|
1481
|
+
*/
|
|
1482
|
+
function sanitizeProvider(value, dropped) {
|
|
1483
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1484
|
+
const entries = {};
|
|
1485
|
+
for (const [model, effort] of Object.entries(value)) {
|
|
1486
|
+
if (typeof effort !== "string" || effort.length === 0) {
|
|
1487
|
+
dropped.push(model);
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
entries[model] = effort;
|
|
1491
|
+
}
|
|
1492
|
+
if (Object.keys(entries).length === 0) return void 0;
|
|
1493
|
+
return Object.freeze(entries);
|
|
1494
|
+
}
|
|
1495
|
+
/** Validate the raw document: only known providers, malformed sections dropped. */
|
|
1496
|
+
function sanitizeDefaults(value) {
|
|
1497
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {
|
|
1498
|
+
defaults: EMPTY,
|
|
1499
|
+
dropped: []
|
|
1500
|
+
};
|
|
1501
|
+
const record = value;
|
|
1502
|
+
const result = {};
|
|
1503
|
+
const dropped = [];
|
|
1504
|
+
for (const provider of PROVIDER_IDS) {
|
|
1505
|
+
const section = sanitizeProvider(record[provider], dropped);
|
|
1506
|
+
if (section !== void 0) result[provider] = section;
|
|
1507
|
+
}
|
|
1508
|
+
return {
|
|
1509
|
+
defaults: Object.freeze(result),
|
|
1510
|
+
dropped
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
/** Read and validate the on-disk file; a missing file reads as empty. */
|
|
1514
|
+
async function loadFile(path) {
|
|
1515
|
+
let text;
|
|
1516
|
+
try {
|
|
1517
|
+
text = await readFile(path, "utf8");
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
if (error.code === "ENOENT") return EMPTY;
|
|
1520
|
+
throw error;
|
|
1521
|
+
}
|
|
1522
|
+
try {
|
|
1523
|
+
const { defaults, dropped } = sanitizeDefaults(JSON.parse(text));
|
|
1524
|
+
if (dropped.length > 0) loadError = /* @__PURE__ */ new Error(`subscriptions model defaults: ${dropped.length} malformed entr${dropped.length === 1 ? "y" : "ies"} skipped (${dropped.join(", ")}); fix or delete the file`);
|
|
1525
|
+
return defaults;
|
|
1526
|
+
} catch {
|
|
1527
|
+
throw new Error(`subscriptions model defaults at ${path} are not valid JSON; fix or delete the file`);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
/** Resolve the module state once from disk; failures leave the defaults empty. */
|
|
1531
|
+
async function ensureReady() {
|
|
1532
|
+
ready ??= loadFile(modelDefaultsFilePath()).then((loaded) => {
|
|
1533
|
+
current = loaded;
|
|
1534
|
+
}, (error) => {
|
|
1535
|
+
loadError = error;
|
|
1536
|
+
current = EMPTY;
|
|
1537
|
+
});
|
|
1538
|
+
return ready;
|
|
1539
|
+
}
|
|
1540
|
+
/** Persist a snapshot atomically with owner-only permissions. */
|
|
1541
|
+
async function atomicPersist(defaults, path) {
|
|
1542
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1543
|
+
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
1544
|
+
try {
|
|
1545
|
+
await writeFile(tmp, JSON.stringify(defaults, null, 2), { mode: 384 });
|
|
1546
|
+
await chmod(tmp, 384);
|
|
1547
|
+
await rename(tmp, path);
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
await rm(tmp, { force: true });
|
|
1550
|
+
throw error;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
let persistDefaults = atomicPersist;
|
|
1554
|
+
/**
|
|
1555
|
+
* Clone one provider section, or undefined when nothing is configured for it.
|
|
1556
|
+
* The clone is prototype-less: model ids are provider-supplied catalog data
|
|
1557
|
+
* used as object keys, and consumers index the section directly (the RPC
|
|
1558
|
+
* catalog in index.ts does), so an id like `toString` would otherwise yield an
|
|
1559
|
+
* inherited *function* where a string is declared.
|
|
1560
|
+
*/
|
|
1561
|
+
function sectionOf(defaults, provider) {
|
|
1562
|
+
const section = defaults[provider];
|
|
1563
|
+
if (section === void 0) return void 0;
|
|
1564
|
+
return Object.assign(Object.create(null), section);
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Ready the defaults store.
|
|
1568
|
+
* @internal Exported for tests; index.ts calls it at apply time so every
|
|
1569
|
+
* later synchronous read sees the persisted state.
|
|
1570
|
+
*/
|
|
1571
|
+
async function loadModelDefaults() {
|
|
1572
|
+
await ensureReady();
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* The configured default effort for one model, or undefined when none (the
|
|
1576
|
+
* picker then follows the provider's own default).
|
|
1577
|
+
* @internal Exported for the adapters' `defaultEffortOf` options.
|
|
1578
|
+
*/
|
|
1579
|
+
function defaultEffortOf(provider, model) {
|
|
1580
|
+
const section = current[provider];
|
|
1581
|
+
if (section === void 0) return void 0;
|
|
1582
|
+
return Object.prototype.hasOwnProperty.call(section, model) ? section[model] : void 0;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Set or clear one model's configured default effort, then persist. The
|
|
1586
|
+
* memory snapshot updates only after the atomic write succeeds, so a failed
|
|
1587
|
+
* write never leaves the live state ahead of the file.
|
|
1588
|
+
* @param provider - the subscription provider route.
|
|
1589
|
+
* @param model - the wire model id.
|
|
1590
|
+
* @param effort - the effort id, or undefined to clear the override.
|
|
1591
|
+
*/
|
|
1592
|
+
function setDefaultEffort(provider, model, effort) {
|
|
1593
|
+
const run = writeChain.then(async () => {
|
|
1594
|
+
await ensureReady();
|
|
1595
|
+
const section = { ...sectionOf(current, provider) ?? {} };
|
|
1596
|
+
if (effort === void 0) delete section[model];
|
|
1597
|
+
else section[model] = effort;
|
|
1598
|
+
const next = { ...current };
|
|
1599
|
+
if (Object.keys(section).length === 0) delete next[provider];
|
|
1600
|
+
else next[provider] = Object.freeze(section);
|
|
1601
|
+
const frozen = Object.freeze(next);
|
|
1602
|
+
await persistDefaults(frozen, modelDefaultsFilePath());
|
|
1603
|
+
current = frozen;
|
|
1604
|
+
});
|
|
1605
|
+
writeChain = run.catch(() => void 0);
|
|
1606
|
+
return run;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
//#endregion
|
|
1610
|
+
//#region src/providers/rate-limit.ts
|
|
1611
|
+
/**
|
|
1612
|
+
* Extra time added to every provider-disclosed wait. Absorbs clock skew
|
|
1613
|
+
* between the harness and the provider, so a retry does not land a moment
|
|
1614
|
+
* before the window actually reopens and burn an attempt on a second 429.
|
|
1615
|
+
*/
|
|
1616
|
+
const RESET_GRACE_MS = 2e3;
|
|
1617
|
+
/** Shortest wait ever scheduled, including for a reset instant already in the past. */
|
|
1618
|
+
const MIN_WAIT_MS = 1e3;
|
|
1619
|
+
/** Below this a bare number is a delay in seconds rather than an epoch stamp. */
|
|
1620
|
+
const EPOCH_SECONDS_FLOOR = 1e9;
|
|
1621
|
+
/** At or above this a bare epoch stamp is already in milliseconds. */
|
|
1622
|
+
const EPOCH_MILLIS_FLOOR = 0xe8d4a51000;
|
|
1623
|
+
/** Node's maximum timer delay; a longer wait cannot be scheduled at all. */
|
|
1624
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
1625
|
+
/** Default ceiling on a rate-limit wait: six hours covers a five-hour session window with slack. */
|
|
1626
|
+
const DEFAULT_RATE_LIMIT_MAX_WAIT_MS = 360 * 60 * 1e3;
|
|
1627
|
+
/**
|
|
1628
|
+
* Interpret a bare numeric rate-limit value, which providers write in three
|
|
1629
|
+
* shapes: epoch milliseconds, epoch seconds, or a delay in seconds. The
|
|
1630
|
+
* magnitude separates them unambiguously for any plausible value — an epoch in
|
|
1631
|
+
* seconds is ~1.8e9 today, while a delay of even a full week is ~6e5.
|
|
1632
|
+
* @param value - the raw numeric value.
|
|
1633
|
+
* @param now - the current epoch milliseconds.
|
|
1634
|
+
* @returns epoch milliseconds of the reset, or undefined when the value is unusable.
|
|
1635
|
+
*/
|
|
1636
|
+
function resetInstantFromNumber(value, now) {
|
|
1637
|
+
if (!Number.isFinite(value) || value <= 0) return void 0;
|
|
1638
|
+
if (value >= EPOCH_MILLIS_FLOOR) return value;
|
|
1639
|
+
if (value >= EPOCH_SECONDS_FLOOR) return value * 1e3;
|
|
1640
|
+
return now + value * 1e3;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Parse a Go-style duration (`6m0s`, `1h2m3.5s`, `150ms`) into milliseconds —
|
|
1644
|
+
* the form OpenAI-compatible `x-ratelimit-reset-*` headers use.
|
|
1645
|
+
* @param text - the raw header value.
|
|
1646
|
+
* @returns the duration in milliseconds, or undefined when the text is not one.
|
|
1647
|
+
*/
|
|
1648
|
+
function durationMs(text) {
|
|
1649
|
+
const trimmed = text.trim();
|
|
1650
|
+
if (trimmed.length === 0) return void 0;
|
|
1651
|
+
const pattern = /(\d+(?:\.\d+)?)(ms|h|m|s)/y;
|
|
1652
|
+
const units = {
|
|
1653
|
+
h: 36e5,
|
|
1654
|
+
m: 6e4,
|
|
1655
|
+
s: 1e3,
|
|
1656
|
+
ms: 1
|
|
1657
|
+
};
|
|
1658
|
+
let total = 0;
|
|
1659
|
+
let matched = false;
|
|
1660
|
+
let index = 0;
|
|
1661
|
+
let previousUnit = Number.POSITIVE_INFINITY;
|
|
1662
|
+
for (;;) {
|
|
1663
|
+
pattern.lastIndex = index;
|
|
1664
|
+
const match = pattern.exec(trimmed);
|
|
1665
|
+
if (match === null) break;
|
|
1666
|
+
const unit = units[match[2]];
|
|
1667
|
+
if (unit >= previousUnit) return void 0;
|
|
1668
|
+
previousUnit = unit;
|
|
1669
|
+
total += Number(match[1]) * unit;
|
|
1670
|
+
index = pattern.lastIndex;
|
|
1671
|
+
matched = true;
|
|
1672
|
+
}
|
|
1673
|
+
if (!matched || index !== trimmed.length) return void 0;
|
|
1674
|
+
return total > 0 ? total : void 0;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Interpret any single rate-limit value — a number, a numeric string, a
|
|
1678
|
+
* duration (`6m0s`), or a date — as the instant a window reopens. One reader
|
|
1679
|
+
* for every shape, so a provider that changes the encoding of a field it
|
|
1680
|
+
* already sends does not need a code change here.
|
|
1681
|
+
* @param value - the raw header value or JSON field.
|
|
1682
|
+
* @param now - the current epoch milliseconds.
|
|
1683
|
+
* @returns epoch milliseconds of the reset, or undefined when the value is unusable.
|
|
1684
|
+
*/
|
|
1685
|
+
function resetInstantFromValue(value, now) {
|
|
1686
|
+
if (typeof value === "number") return resetInstantFromNumber(value, now);
|
|
1687
|
+
if (typeof value !== "string") return void 0;
|
|
1688
|
+
const trimmed = value.trim();
|
|
1689
|
+
if (trimmed.length === 0) return void 0;
|
|
1690
|
+
const numeric = Number(trimmed);
|
|
1691
|
+
if (Number.isFinite(numeric)) return resetInstantFromNumber(numeric, now);
|
|
1692
|
+
const duration = durationMs(trimmed);
|
|
1693
|
+
if (duration !== void 0) return now + duration;
|
|
1694
|
+
const parsed = Date.parse(trimmed);
|
|
1695
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Read a header carrying any of the {@link resetInstantFromValue} shapes.
|
|
1699
|
+
* @param response - the failed response.
|
|
1700
|
+
* @param name - the header to read.
|
|
1701
|
+
* @param now - the current epoch milliseconds.
|
|
1702
|
+
* @returns epoch milliseconds of the reset, or undefined when absent or unusable.
|
|
1703
|
+
*/
|
|
1704
|
+
function resetInstantFromHeader(response, name$1, now) {
|
|
1705
|
+
return resetInstantFromValue(response.headers.get(name$1), now);
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Read the RFC 7231 `retry-after` header in both its forms: a delay in seconds
|
|
1709
|
+
* (never an epoch stamp, whatever its magnitude) or an HTTP-date.
|
|
1710
|
+
* @param response - the failed response.
|
|
1711
|
+
* @param now - the current epoch milliseconds.
|
|
1712
|
+
* @returns epoch milliseconds of the reset, or undefined when absent or unusable.
|
|
1713
|
+
*/
|
|
1714
|
+
function retryAfterInstant(response, now) {
|
|
1715
|
+
const raw = response.headers.get("retry-after");
|
|
1716
|
+
if (raw === null) return void 0;
|
|
1717
|
+
const trimmed = raw.trim();
|
|
1718
|
+
if (trimmed.length === 0) return void 0;
|
|
1719
|
+
const seconds = Number(trimmed);
|
|
1720
|
+
if (Number.isFinite(seconds)) return seconds > 0 ? now + seconds * 1e3 : void 0;
|
|
1721
|
+
const parsed = Date.parse(trimmed);
|
|
1722
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Parse a response body as JSON without throwing on the non-JSON bodies
|
|
1726
|
+
* providers occasionally return under load (an HTML gateway page, say).
|
|
1727
|
+
* @param body - the complete response body.
|
|
1728
|
+
* @returns the parsed value, or undefined when the body is not JSON.
|
|
1729
|
+
*/
|
|
1730
|
+
function jsonBody(body) {
|
|
1731
|
+
if (body.length === 0) return void 0;
|
|
1732
|
+
try {
|
|
1733
|
+
return JSON.parse(body);
|
|
1734
|
+
} catch {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
/** How deep {@link resetFromFields} walks; every observed payload nests one or two levels. */
|
|
1739
|
+
const MAX_BODY_DEPTH = 4;
|
|
1740
|
+
/**
|
|
1741
|
+
* Find a reset instant under any of the named keys, anywhere in a parsed body.
|
|
1742
|
+
*
|
|
1743
|
+
* The search is by key rather than by path on purpose: providers move the same
|
|
1744
|
+
* field between containers (`detail`, `error`, top level) across endpoints and
|
|
1745
|
+
* versions, and a path-shaped reader silently stops working when they do. Only
|
|
1746
|
+
* the key list is provider-specific.
|
|
1747
|
+
* @param value - the parsed body, or any nested value.
|
|
1748
|
+
* @param keys - field names this provider uses for a reset or delay.
|
|
1749
|
+
* @param now - the current epoch milliseconds.
|
|
1750
|
+
* @param depth - remaining recursion depth.
|
|
1751
|
+
* @returns the earliest instant found, or undefined when no key matched.
|
|
1752
|
+
*/
|
|
1753
|
+
function resetFromFields(value, keys, now, depth = MAX_BODY_DEPTH) {
|
|
1754
|
+
if (depth <= 0 || value === null || typeof value !== "object") return void 0;
|
|
1755
|
+
let earliest;
|
|
1756
|
+
const consider = (candidate) => {
|
|
1757
|
+
if (candidate !== void 0 && (earliest === void 0 || candidate < earliest)) earliest = candidate;
|
|
1758
|
+
};
|
|
1759
|
+
if (Array.isArray(value)) {
|
|
1760
|
+
for (const item of value) consider(resetFromFields(item, keys, now, depth - 1));
|
|
1761
|
+
return earliest;
|
|
1762
|
+
}
|
|
1763
|
+
for (const [key, nested] of Object.entries(value)) if (keys.includes(key)) consider(resetInstantFromValue(nested, now));
|
|
1764
|
+
else consider(resetFromFields(nested, keys, now, depth - 1));
|
|
1765
|
+
return earliest;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* The earliest of several candidate reset instants, ignoring absent ones. The
|
|
1769
|
+
* earliest is the one that matters: it is the first moment any of the reported
|
|
1770
|
+
* limits allows a request again.
|
|
1771
|
+
* @param candidates - reset instants in no particular order.
|
|
1772
|
+
* @returns the earliest instant, or undefined when every candidate is absent.
|
|
1773
|
+
*/
|
|
1774
|
+
function earliestReset(...candidates) {
|
|
1775
|
+
let earliest;
|
|
1776
|
+
for (const candidate of candidates) {
|
|
1777
|
+
if (candidate === void 0) continue;
|
|
1778
|
+
if (earliest === void 0 || candidate < earliest) earliest = candidate;
|
|
1779
|
+
}
|
|
1780
|
+
return earliest;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Turn a reset instant into the wait to report as `providerRetryAfterMs`.
|
|
1784
|
+
*
|
|
1785
|
+
* Deliberately not capped: a reset beyond the policy's `maxDelayMs` makes the
|
|
1786
|
+
* retry plugin delegate immediately, failing the turn at once with the real
|
|
1787
|
+
* reset in the message, rather than clamping the wait down and burning the
|
|
1788
|
+
* retry budget against a window that is still closed.
|
|
1789
|
+
* @param instant - epoch milliseconds the window reopens.
|
|
1790
|
+
* @param now - the current epoch milliseconds.
|
|
1791
|
+
* @returns the wait in milliseconds, never below {@link MIN_WAIT_MS}.
|
|
1792
|
+
*/
|
|
1793
|
+
function waitFromReset(instant, now) {
|
|
1794
|
+
return Math.max(MIN_WAIT_MS, instant - now + RESET_GRACE_MS);
|
|
1795
|
+
}
|
|
1796
|
+
/** Header names worth showing when a 429 disclosed no reset this code recognizes. */
|
|
1797
|
+
const DIAGNOSTIC_HEADER = /rate-?limit|retry|reset|^x-codex-/i;
|
|
1798
|
+
/**
|
|
1799
|
+
* Render the rate-limit-shaped headers and the head of the body of a 429 whose
|
|
1800
|
+
* reset instant nothing parsed. Emitted through the adapter's `onWarn`, this is
|
|
1801
|
+
* how an unrecognized provider field gets named from live traffic instead of
|
|
1802
|
+
* being guessed at.
|
|
1803
|
+
*
|
|
1804
|
+
* It is also where the per-bucket rollover snapshots land by design — no reader
|
|
1805
|
+
* parks a turn on one, because on a 429 they cannot say which bucket refused —
|
|
1806
|
+
* so the operator still sees what the provider disclosed.
|
|
1807
|
+
* @param response - the failed response.
|
|
1808
|
+
* @param body - the complete response body.
|
|
1809
|
+
* @returns a one-line diagnostic.
|
|
1810
|
+
*/
|
|
1811
|
+
function rateLimitDiagnostics(response, body) {
|
|
1812
|
+
const headers = [];
|
|
1813
|
+
response.headers.forEach((value, key) => {
|
|
1814
|
+
if (DIAGNOSTIC_HEADER.test(key)) headers.push(`${key}: ${value}`);
|
|
1815
|
+
});
|
|
1816
|
+
headers.sort();
|
|
1817
|
+
const rendered = headers.length > 0 ? headers.join("; ") : "(none)";
|
|
1818
|
+
const head = body.slice(0, 200);
|
|
1819
|
+
return `429 disclosed no reset time; headers [${rendered}]; body ${head.length > 0 ? head : "(empty)"}`;
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* The retry shape every subscription route starts from: Claude Code's own SDK
|
|
1823
|
+
* numbers — ten retries after the first attempt, exponential backoff from 1s
|
|
1824
|
+
* doubling per attempt, capped at 60s, plus 20% jitter.
|
|
1825
|
+
*
|
|
1826
|
+
* Shared across all four routes rather than kept to claude, because what these
|
|
1827
|
+
* numbers are tuned for is the shape of a subscription endpoint — a consumer
|
|
1828
|
+
* plan behind a session window, which sheds load in bursts and rewards an
|
|
1829
|
+
* attempt that outlasts them — and that is the same on all four. The dsh-llm
|
|
1830
|
+
* defaults (5 retries from 500ms to 10s) give up after about fifteen seconds,
|
|
1831
|
+
* which is short for that.
|
|
1832
|
+
*
|
|
1833
|
+
* The 60s cap governs local backoff only: a disclosed rate-limit reset is
|
|
1834
|
+
* accepted up to the configured wait ceiling instead.
|
|
1835
|
+
*/
|
|
1836
|
+
const DEFAULT_RETRY = Object.freeze({
|
|
1837
|
+
maxRetries: 10,
|
|
1838
|
+
initialDelayMs: 1e3,
|
|
1839
|
+
maxDelayMs: 6e4,
|
|
1840
|
+
jitterRatio: .2
|
|
1841
|
+
});
|
|
1842
|
+
/** Waiting behavior a route falls back to when the plugin passed none (waiting on, six-hour ceiling). */
|
|
1843
|
+
const DEFAULT_RATE_LIMIT_WAIT = Object.freeze({
|
|
1844
|
+
wait: true,
|
|
1845
|
+
maxWaitMs: DEFAULT_RATE_LIMIT_MAX_WAIT_MS
|
|
1846
|
+
});
|
|
1847
|
+
/**
|
|
1848
|
+
* Validate and default the rate-limit waiting config.
|
|
1849
|
+
* @param config - the raw plugin config section, when present.
|
|
1850
|
+
* @param path - diagnostic path naming the config that owns the value.
|
|
1851
|
+
* @returns the resolved, immutable behavior.
|
|
1852
|
+
*/
|
|
1853
|
+
function resolveRateLimitWait(config, path) {
|
|
1854
|
+
const wait = config?.wait ?? true;
|
|
1855
|
+
const maxWaitMs = config?.maxWaitMs ?? DEFAULT_RATE_LIMIT_MAX_WAIT_MS;
|
|
1856
|
+
if (!Number.isFinite(maxWaitMs) || maxWaitMs <= 0) throw new Error(`${path}.maxWaitMs must be a positive finite number of milliseconds`);
|
|
1857
|
+
if (maxWaitMs > MAX_TIMER_DELAY_MS) throw new Error(`${path}.maxWaitMs must be no greater than ${String(MAX_TIMER_DELAY_MS)} (the maximum schedulable delay)`);
|
|
1858
|
+
return Object.freeze({
|
|
1859
|
+
wait,
|
|
1860
|
+
maxWaitMs
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Resolve one route's retry policy, widening the delay ceiling to the
|
|
1865
|
+
* configured wait so a disclosed reset hours out is accepted rather than
|
|
1866
|
+
* refused.
|
|
1867
|
+
*
|
|
1868
|
+
* The ceiling is shared with local exponential backoff, so widening it also
|
|
1869
|
+
* raises how long an unrelated transient failure may back off for. That stays
|
|
1870
|
+
* bounded by the finite retry budget — the claude route's ten retries reach
|
|
1871
|
+
* 512 s per attempt at most — and it only governs when the provider disclosed
|
|
1872
|
+
* nothing, which is exactly the case where a longer wait is the safer guess.
|
|
1873
|
+
* @param defaults - the route's retry shape.
|
|
1874
|
+
* @param rateLimit - resolved waiting behavior.
|
|
1875
|
+
* @param path - diagnostic path naming the provider route.
|
|
1876
|
+
* @returns the policy to report from `providerRetryPolicy`.
|
|
1877
|
+
*/
|
|
1878
|
+
function subscriptionRetryPolicy(defaults, rateLimit, path) {
|
|
1879
|
+
const maxDelayMs = rateLimit.wait ? Math.max(defaults.maxDelayMs, rateLimit.maxWaitMs) : defaults.maxDelayMs;
|
|
1880
|
+
return resolveRetryPolicy({
|
|
1881
|
+
mode: "normal",
|
|
1882
|
+
maxRetries: defaults.maxRetries,
|
|
1883
|
+
backoff: {
|
|
1884
|
+
initialDelayMs: defaults.initialDelayMs,
|
|
1885
|
+
maxDelayMs,
|
|
1886
|
+
jitterRatio: defaults.jitterRatio
|
|
1887
|
+
}
|
|
1888
|
+
}, path);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1297
1891
|
//#endregion
|
|
1298
1892
|
//#region src/providers/common.ts
|
|
1299
1893
|
/**
|
|
@@ -1324,38 +1918,56 @@ function validateModels(models, label) {
|
|
|
1324
1918
|
});
|
|
1325
1919
|
}
|
|
1326
1920
|
/**
|
|
1327
|
-
* Build an LlmError from a non-2xx provider response,
|
|
1328
|
-
*
|
|
1921
|
+
* Build an LlmError from a non-2xx provider response, mapping the status to a
|
|
1922
|
+
* stable code and, for a rate-limited request, the disclosed reset instant to
|
|
1923
|
+
* the `providerRetryAfterMs` the retry plugin waits out.
|
|
1924
|
+
*
|
|
1925
|
+
* A 429 classifies as `RATE_LIMIT` on the strength of the status alone, ahead
|
|
1926
|
+
* of the quota-wording check. On these routes there is no terminal quota to
|
|
1927
|
+
* distinguish: a subscription has no balance to top up, only a window that
|
|
1928
|
+
* reopens, and providers announce an exhausted window with wording
|
|
1929
|
+
* (`usage_limit_reached`) the shared classifier reads as permanent.
|
|
1329
1930
|
* @param response - the failed response.
|
|
1330
1931
|
* @param label - diagnostic prefix naming the provider API.
|
|
1932
|
+
* @param options - the calling provider's rate-limit reader and warning sink.
|
|
1331
1933
|
* @returns the classified error.
|
|
1332
1934
|
*/
|
|
1333
|
-
async function httpLlmError(response, label) {
|
|
1935
|
+
async function httpLlmError(response, label, options = {}) {
|
|
1334
1936
|
let body = "";
|
|
1335
1937
|
try {
|
|
1336
|
-
body =
|
|
1938
|
+
body = await response.text();
|
|
1337
1939
|
} catch {}
|
|
1338
|
-
const
|
|
1940
|
+
const shown = body.slice(0, 500);
|
|
1941
|
+
const message = shown.length > 0 ? `${label} error (HTTP ${String(response.status)}): ${shown}` : `${label} error (HTTP ${String(response.status)})`;
|
|
1339
1942
|
let code;
|
|
1340
1943
|
if (response.status === 401 || response.status === 403) code = "AUTH";
|
|
1341
|
-
else if (isQuotaExceededError(body)) code = QUOTA_EXCEEDED_CODE;
|
|
1342
1944
|
else if (response.status === 429) code = "RATE_LIMIT";
|
|
1343
|
-
else if (
|
|
1945
|
+
else if (isQuotaExceededError(shown)) code = QUOTA_EXCEEDED_CODE;
|
|
1946
|
+
else if (response.status === 400 && isContextWindowExceededError(shown)) code = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
1344
1947
|
else if (response.status === 408 || response.status === 504) code = "TIMEOUT";
|
|
1345
1948
|
else if (response.status >= 500) code = "SERVER";
|
|
1346
1949
|
else code = `HTTP_${String(response.status)}`;
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
if (Number.isFinite(seconds) && seconds > 0) providerRetryAfterMs = seconds * 1e3;
|
|
1352
|
-
}
|
|
1950
|
+
const now = Date.now();
|
|
1951
|
+
const rateLimited = response.status === 429;
|
|
1952
|
+
const reset = rateLimited ? options.rateLimitReset?.(response, body, now) ?? retryAfterInstant(response, now) : retryAfterInstant(response, now);
|
|
1953
|
+
if (reset === void 0 && rateLimited) options.onWarn?.(`${label}: ${rateLimitDiagnostics(response, body)}`);
|
|
1353
1954
|
return new LlmError(message, code, {
|
|
1354
1955
|
status: response.status,
|
|
1355
|
-
...
|
|
1956
|
+
...reset === void 0 ? {} : { providerRetryAfterMs: waitFromReset(reset, now) }
|
|
1356
1957
|
});
|
|
1357
1958
|
}
|
|
1358
1959
|
/**
|
|
1960
|
+
* Parse a response's `retry-after` header (seconds) into milliseconds.
|
|
1961
|
+
* @param response - the failed response.
|
|
1962
|
+
* @returns the delay in ms, or undefined when absent/unusable.
|
|
1963
|
+
*/
|
|
1964
|
+
function parseRetryAfterMs(response) {
|
|
1965
|
+
const retryAfter = response.headers.get("retry-after");
|
|
1966
|
+
if (retryAfter === null) return void 0;
|
|
1967
|
+
const seconds = Number(retryAfter);
|
|
1968
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1359
1971
|
* Create an idle watchdog chained to the caller's signal.
|
|
1360
1972
|
* @param caller - the request's own abort signal, when present.
|
|
1361
1973
|
* @param timeoutMs - maximum idle interval while a stream read is outstanding.
|
|
@@ -1409,11 +2021,20 @@ var OAuthEndpointError = class extends Error {
|
|
|
1409
2021
|
status;
|
|
1410
2022
|
/** The provider's OAuth `error` code (e.g. `invalid_grant`), when present. */
|
|
1411
2023
|
oauthCode;
|
|
1412
|
-
|
|
2024
|
+
/**
|
|
2025
|
+
* The endpoint's `retry-after`, in ms, when it sent one. Usage/models
|
|
2026
|
+
* endpoints reuse this error type and can rate-limit progressively (each
|
|
2027
|
+
* hit within the window extends the next one), so a caller retrying on a
|
|
2028
|
+
* fixed schedule instead of honoring this can keep an account locked out
|
|
2029
|
+
* indefinitely.
|
|
2030
|
+
*/
|
|
2031
|
+
retryAfterMs;
|
|
2032
|
+
constructor(message, status, oauthCode, retryAfterMs$1) {
|
|
1413
2033
|
super(message);
|
|
1414
2034
|
this.name = "OAuthEndpointError";
|
|
1415
2035
|
this.status = status;
|
|
1416
2036
|
this.oauthCode = oauthCode;
|
|
2037
|
+
this.retryAfterMs = retryAfterMs$1;
|
|
1417
2038
|
}
|
|
1418
2039
|
};
|
|
1419
2040
|
/**
|
|
@@ -1430,7 +2051,7 @@ async function oauthEndpointError(response, label) {
|
|
|
1430
2051
|
oauthCode = typeof parsed.error === "string" ? parsed.error : void 0;
|
|
1431
2052
|
detail = typeof parsed.error_description === "string" ? parsed.error_description : oauthCode ?? "";
|
|
1432
2053
|
} catch {}
|
|
1433
|
-
return new OAuthEndpointError(detail.length > 0 ? `${label} token endpoint error (HTTP ${String(response.status)}): ${detail}` : `${label} token endpoint error (HTTP ${String(response.status)})`, response.status, oauthCode);
|
|
2054
|
+
return new OAuthEndpointError(detail.length > 0 ? `${label} token endpoint error (HTTP ${String(response.status)}): ${detail}` : `${label} token endpoint error (HTTP ${String(response.status)})`, response.status, oauthCode, parseRetryAfterMs(response));
|
|
1434
2055
|
}
|
|
1435
2056
|
/**
|
|
1436
2057
|
* Per-provider session freshness: loads the stored session, refreshes
|
|
@@ -1487,13 +2108,94 @@ var TokenManager = class {
|
|
|
1487
2108
|
}
|
|
1488
2109
|
}
|
|
1489
2110
|
async doRefresh(session) {
|
|
1490
|
-
const current$
|
|
1491
|
-
if (current$
|
|
1492
|
-
const next = await this.options.refresh(current$
|
|
2111
|
+
const current$2 = await this.options.load();
|
|
2112
|
+
if (current$2 !== void 0 && current$2.accessToken !== session.accessToken && current$2.expiresAt - Date.now() > this.options.preemptMs) return current$2;
|
|
2113
|
+
const next = await this.options.refresh(current$2 ?? session);
|
|
1493
2114
|
await this.options.save(next);
|
|
1494
2115
|
return next;
|
|
1495
2116
|
}
|
|
1496
2117
|
};
|
|
2118
|
+
/** Bound on one account catalog fetch or usage poll — a hang must not block the picker. */
|
|
2119
|
+
const DISCOVERY_TIMEOUT_MS = 1e4;
|
|
2120
|
+
/**
|
|
2121
|
+
* Run `work` with an aborting signal. Resolves undefined when the timeout
|
|
2122
|
+
* fires (the fetch is aborted); other failures propagate.
|
|
2123
|
+
*/
|
|
2124
|
+
function withTimeout(work, timeoutMs) {
|
|
2125
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
2126
|
+
const aborted = new Promise((resolve) => {
|
|
2127
|
+
if (signal.aborted) resolve(void 0);
|
|
2128
|
+
else signal.addEventListener("abort", () => resolve(void 0), { once: true });
|
|
2129
|
+
});
|
|
2130
|
+
return Promise.race([work(signal).then((value) => signal.aborted ? void 0 : value, (error) => {
|
|
2131
|
+
if (signal.aborted) return void 0;
|
|
2132
|
+
throw error;
|
|
2133
|
+
}), aborted]);
|
|
2134
|
+
}
|
|
2135
|
+
/** Display name for a wire reasoning-effort identifier. */
|
|
2136
|
+
function effortDisplayName(effort) {
|
|
2137
|
+
return effort === "xhigh" ? "Extra High" : effort.charAt(0).toUpperCase() + effort.slice(1);
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Fold a configured per-model default effort into a reasoning block, keeping
|
|
2141
|
+
* the DSH runtime invariant `defaultEffort ∈ efforts` (the runtime rejects an
|
|
2142
|
+
* unknown default with `INVALID_MODEL_REASONING`).
|
|
2143
|
+
*
|
|
2144
|
+
* A configured level the base set does not advertise is *dropped*, not
|
|
2145
|
+
* appended: for claude/grok/copilot the base is the provider's live catalog,
|
|
2146
|
+
* i.e. the truth about what the model accepts, so honouring a stale override
|
|
2147
|
+
* would put an unsupported effort on every single request instead of letting
|
|
2148
|
+
* the harness reject it before provider I/O. The override then simply falls
|
|
2149
|
+
* back to the provider's own default until the user picks a level the catalog
|
|
2150
|
+
* still lists.
|
|
2151
|
+
*
|
|
2152
|
+
* `extendable` opts into the opposite rule for a base that is a *built-in
|
|
2153
|
+
* fallback* rather than discovered truth (codex, whose static effort list is
|
|
2154
|
+
* known to trail the backend): there, appending the configured level is how a
|
|
2155
|
+
* newly shipped tier becomes selectable at all.
|
|
2156
|
+
* @param configuredDefault - the user-configured default effort id, or undefined.
|
|
2157
|
+
* @param base - the discovered/built-in reasoning block, or undefined.
|
|
2158
|
+
* @param options - `extendable` marks the base as a fallback that may be extended.
|
|
2159
|
+
* @returns the merged block, or undefined when neither side contributes one.
|
|
2160
|
+
*/
|
|
2161
|
+
function mergeReasoning(configuredDefault, base, options) {
|
|
2162
|
+
const detached = base === void 0 ? void 0 : {
|
|
2163
|
+
efforts: [...base.efforts],
|
|
2164
|
+
...base.defaultEffort === void 0 ? {} : { defaultEffort: base.defaultEffort }
|
|
2165
|
+
};
|
|
2166
|
+
if (configuredDefault === void 0) return detached;
|
|
2167
|
+
const effort = ReasoningEffortId(configuredDefault);
|
|
2168
|
+
if (base === void 0) return options?.extendable === true ? {
|
|
2169
|
+
efforts: [{
|
|
2170
|
+
id: effort,
|
|
2171
|
+
name: effortDisplayName(effort)
|
|
2172
|
+
}],
|
|
2173
|
+
defaultEffort: effort
|
|
2174
|
+
} : void 0;
|
|
2175
|
+
if (base.efforts.some((entry) => entry.id === effort)) return {
|
|
2176
|
+
efforts: [...base.efforts],
|
|
2177
|
+
defaultEffort: effort
|
|
2178
|
+
};
|
|
2179
|
+
if (options?.extendable !== true) return detached;
|
|
2180
|
+
return {
|
|
2181
|
+
efforts: [...base.efforts, {
|
|
2182
|
+
id: effort,
|
|
2183
|
+
name: effortDisplayName(effort)
|
|
2184
|
+
}],
|
|
2185
|
+
defaultEffort: effort
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* First account catalog that lists `model` (callers pass default-first).
|
|
2190
|
+
* One failing lookup sits that account out so a sibling's metadata still
|
|
2191
|
+
* resolves — the same isolation as the picker catalog union.
|
|
2192
|
+
*/
|
|
2193
|
+
async function discoverAcrossAccounts(accounts, lookup) {
|
|
2194
|
+
for (const account of accounts) try {
|
|
2195
|
+
const found = await lookup(account);
|
|
2196
|
+
if (found !== void 0) return found;
|
|
2197
|
+
} catch {}
|
|
2198
|
+
}
|
|
1497
2199
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
1498
2200
|
const DISCOVERY_TTL_MS = 5 * 6e4;
|
|
1499
2201
|
/**
|
|
@@ -1515,6 +2217,8 @@ var ModelCatalogCache = class {
|
|
|
1515
2217
|
seeded;
|
|
1516
2218
|
/** Set by {@link invalidate} so an in-flight disk read cannot resurrect dropped state. */
|
|
1517
2219
|
seedDisabled = false;
|
|
2220
|
+
/** Bumped by {@link invalidate} so a loser in-flight fetch cannot write back. */
|
|
2221
|
+
generation = 0;
|
|
1518
2222
|
constructor(persistence, ttlMs = DISCOVERY_TTL_MS) {
|
|
1519
2223
|
this.persistence = persistence;
|
|
1520
2224
|
this.ttlMs = ttlMs;
|
|
@@ -1545,7 +2249,10 @@ var ModelCatalogCache = class {
|
|
|
1545
2249
|
}
|
|
1546
2250
|
/** Run (or join) the single in-flight fetch, updating memory and disk on success. */
|
|
1547
2251
|
refresh(fetcher) {
|
|
1548
|
-
this.inflight
|
|
2252
|
+
if (this.inflight !== void 0) return this.inflight;
|
|
2253
|
+
const gen = this.generation;
|
|
2254
|
+
const pending = fetcher().then((models) => {
|
|
2255
|
+
if (this.generation !== gen) return models;
|
|
1549
2256
|
const snapshot = {
|
|
1550
2257
|
at: Date.now(),
|
|
1551
2258
|
models
|
|
@@ -1554,9 +2261,10 @@ var ModelCatalogCache = class {
|
|
|
1554
2261
|
this.persistence?.save(snapshot).catch(() => void 0);
|
|
1555
2262
|
return models;
|
|
1556
2263
|
}).finally(() => {
|
|
1557
|
-
this.inflight = void 0;
|
|
2264
|
+
if (this.generation === gen) this.inflight = void 0;
|
|
1558
2265
|
});
|
|
1559
|
-
|
|
2266
|
+
this.inflight = pending;
|
|
2267
|
+
return pending;
|
|
1560
2268
|
}
|
|
1561
2269
|
/**
|
|
1562
2270
|
* Return the cached catalog when fresh, otherwise fetch and cache it.
|
|
@@ -1594,7 +2302,9 @@ var ModelCatalogCache = class {
|
|
|
1594
2302
|
}
|
|
1595
2303
|
/** Drop the cached catalog (e.g. after a 401 proved the credential changed). */
|
|
1596
2304
|
invalidate() {
|
|
2305
|
+
this.generation += 1;
|
|
1597
2306
|
this.entry = void 0;
|
|
2307
|
+
this.inflight = void 0;
|
|
1598
2308
|
this.seedDisabled = true;
|
|
1599
2309
|
this.persistence?.clear().catch(() => void 0);
|
|
1600
2310
|
}
|
|
@@ -1603,6 +2313,11 @@ var ModelCatalogCache = class {
|
|
|
1603
2313
|
function isMissingOrInvalidCredential(error) {
|
|
1604
2314
|
return error instanceof LlmError && (error.code === "MISSING_CREDENTIAL" || error.code === "INVALID_CREDENTIAL");
|
|
1605
2315
|
}
|
|
2316
|
+
/** Whether discovery stopped because the caller cancelled or the timeout fired. */
|
|
2317
|
+
function isDiscoveryAborted(error, signal) {
|
|
2318
|
+
if (signal?.aborted === true) return true;
|
|
2319
|
+
return signal !== void 0 && error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
2320
|
+
}
|
|
1606
2321
|
/** Whether discovery failed because the access token was rejected. */
|
|
1607
2322
|
function isDiscoveryAuthFailure(error) {
|
|
1608
2323
|
return error instanceof OAuthEndpointError && error.status === 401 || error instanceof LlmError && error.code === "AUTH";
|
|
@@ -1628,6 +2343,109 @@ async function discoverOrRetryAuth(session, catalog, run) {
|
|
|
1628
2343
|
}
|
|
1629
2344
|
}
|
|
1630
2345
|
|
|
2346
|
+
//#endregion
|
|
2347
|
+
//#region src/providers/accounts.ts
|
|
2348
|
+
/** Catalog sort hint when the provider advertised one (Codex `priority`). */
|
|
2349
|
+
function catalogPriority(model) {
|
|
2350
|
+
const ranked = model;
|
|
2351
|
+
return typeof ranked.priority === "number" ? ranked.priority : Number.MAX_SAFE_INTEGER;
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Merge per-account catalogs, keeping the first occurrence of each model id.
|
|
2355
|
+
* Rows that carry a numeric `priority` (Codex discovery) are then ordered by
|
|
2356
|
+
* it so a model only the second account lists — e.g. `gpt-5.6-sol` — still
|
|
2357
|
+
* sits with its generation instead of being appended after the default
|
|
2358
|
+
* account's older ids.
|
|
2359
|
+
*/
|
|
2360
|
+
async function unionAccountCatalogs(accounts, listOne, options) {
|
|
2361
|
+
const timeoutMs = options?.timeoutMs;
|
|
2362
|
+
const caller = options?.signal;
|
|
2363
|
+
const catalogs = await Promise.all(accounts.map(async (account) => {
|
|
2364
|
+
try {
|
|
2365
|
+
if (timeoutMs === void 0) return await listOne(account, caller);
|
|
2366
|
+
return await withTimeout((timeoutSignal) => listOne(account, caller === void 0 ? timeoutSignal : AbortSignal.any([timeoutSignal, caller])), timeoutMs) ?? [];
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
if (caller?.aborted === true) throw error;
|
|
2369
|
+
return [];
|
|
2370
|
+
}
|
|
2371
|
+
}));
|
|
2372
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2373
|
+
const models = [];
|
|
2374
|
+
for (const catalog of catalogs) for (const model of catalog) {
|
|
2375
|
+
if (seen.has(model.id)) continue;
|
|
2376
|
+
seen.add(model.id);
|
|
2377
|
+
models.push(model);
|
|
2378
|
+
}
|
|
2379
|
+
models.sort((left, right) => catalogPriority(left) - catalogPriority(right));
|
|
2380
|
+
return models;
|
|
2381
|
+
}
|
|
2382
|
+
var AccountTokenManager = class {
|
|
2383
|
+
managers = /* @__PURE__ */ new Map();
|
|
2384
|
+
io;
|
|
2385
|
+
constructor(options) {
|
|
2386
|
+
this.options = options;
|
|
2387
|
+
const provider = options.provider;
|
|
2388
|
+
this.io = options.io ?? {
|
|
2389
|
+
list: () => listAccounts(provider),
|
|
2390
|
+
get: (account) => getAccountSession(provider, account),
|
|
2391
|
+
save: (account, session) => saveAccountSession(provider, account, session),
|
|
2392
|
+
remove: (account) => deleteAccountSession(provider, account)
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
/** The provider's accounts, default first (straight from the store). */
|
|
2396
|
+
list() {
|
|
2397
|
+
return this.io.list();
|
|
2398
|
+
}
|
|
2399
|
+
/** The default account's key, or undefined when logged out. */
|
|
2400
|
+
async defaultAccount() {
|
|
2401
|
+
return (await this.list())[0]?.key;
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Resolve a usable session for one account (default when omitted),
|
|
2405
|
+
* refreshing proactively or on demand.
|
|
2406
|
+
* @param account - the account key; the default account when undefined.
|
|
2407
|
+
* @param forceRefresh - refresh regardless of expiry (used after a 401).
|
|
2408
|
+
* @returns the persisted session to send.
|
|
2409
|
+
* @throws LlmError MISSING_CREDENTIAL when the account is not logged in.
|
|
2410
|
+
*/
|
|
2411
|
+
async session(account, forceRefresh = false) {
|
|
2412
|
+
const key = account ?? await this.defaultAccount();
|
|
2413
|
+
if (key === void 0) throw this.missingCredential();
|
|
2414
|
+
return this.tokensFor(key).session(forceRefresh);
|
|
2415
|
+
}
|
|
2416
|
+
/** Read an account's stored session without any refresh side effect. */
|
|
2417
|
+
peek(account) {
|
|
2418
|
+
return this.io.get(account);
|
|
2419
|
+
}
|
|
2420
|
+
/** Whether a session is stored for the account (cheap; never refreshes). */
|
|
2421
|
+
async hasSession(account) {
|
|
2422
|
+
return await this.peek(account) !== void 0;
|
|
2423
|
+
}
|
|
2424
|
+
/** The TokenManager bound to one account (created lazily, then cached). */
|
|
2425
|
+
tokensFor(account) {
|
|
2426
|
+
let manager = this.managers.get(account);
|
|
2427
|
+
if (manager === void 0) {
|
|
2428
|
+
const io = this.io;
|
|
2429
|
+
manager = new TokenManager({
|
|
2430
|
+
displayName: this.options.displayName,
|
|
2431
|
+
...this.options.makeOptions(account),
|
|
2432
|
+
load: () => io.get(account),
|
|
2433
|
+
save: (session) => io.save(account, session),
|
|
2434
|
+
remove: () => io.remove(account),
|
|
2435
|
+
onRemoved: () => {
|
|
2436
|
+
this.options.onAccountRemoved?.(account);
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
this.managers.set(account, manager);
|
|
2440
|
+
}
|
|
2441
|
+
return manager;
|
|
2442
|
+
}
|
|
2443
|
+
/** The logged-out error, mirroring TokenManager's own message. */
|
|
2444
|
+
missingCredential() {
|
|
2445
|
+
return new LlmError(`dsh-plugin-subscriptions: not logged in to ${this.options.displayName}; log in via Settings → Subscriptions in the dsh web app`, "MISSING_CREDENTIAL");
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
|
|
1631
2449
|
//#endregion
|
|
1632
2450
|
//#region src/providers/catalog-store.ts
|
|
1633
2451
|
/**
|
|
@@ -1717,59 +2535,722 @@ function sanitizeSnapshot(value) {
|
|
|
1717
2535
|
models
|
|
1718
2536
|
};
|
|
1719
2537
|
}
|
|
1720
|
-
/** Read the whole file; missing or unparsable reads as an empty cache. */
|
|
1721
|
-
async function readCatalogFile(path) {
|
|
1722
|
-
let text;
|
|
1723
|
-
try {
|
|
1724
|
-
text = await readFile(path, "utf8");
|
|
1725
|
-
} catch {
|
|
1726
|
-
return {};
|
|
2538
|
+
/** Read the whole file; missing or unparsable reads as an empty cache. */
|
|
2539
|
+
async function readCatalogFile(path) {
|
|
2540
|
+
let text;
|
|
2541
|
+
try {
|
|
2542
|
+
text = await readFile(path, "utf8");
|
|
2543
|
+
} catch {
|
|
2544
|
+
return {};
|
|
2545
|
+
}
|
|
2546
|
+
try {
|
|
2547
|
+
const parsed = JSON.parse(text);
|
|
2548
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
2549
|
+
return parsed;
|
|
2550
|
+
} catch {
|
|
2551
|
+
return {};
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
/** Persist the whole file atomically (tmp file + rename). */
|
|
2555
|
+
async function writeCatalogFile(store, path) {
|
|
2556
|
+
await mkdir(dirname(path), { recursive: true });
|
|
2557
|
+
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
2558
|
+
try {
|
|
2559
|
+
await writeFile(tmp, JSON.stringify(store, null, 2));
|
|
2560
|
+
await rename(tmp, path);
|
|
2561
|
+
} catch (error) {
|
|
2562
|
+
await rm(tmp, { force: true });
|
|
2563
|
+
throw error;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
/**
|
|
2567
|
+
* Build the durable half of one provider's catalog cache over the shared
|
|
2568
|
+
* models.json file (concurrent writers are last-writer-wins, acceptable for
|
|
2569
|
+
* a cache).
|
|
2570
|
+
* @param provider - the provider route keying the file entry.
|
|
2571
|
+
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
2572
|
+
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
2573
|
+
*/
|
|
2574
|
+
function catalogStore(provider, path = modelsFilePath()) {
|
|
2575
|
+
return {
|
|
2576
|
+
async load() {
|
|
2577
|
+
return sanitizeSnapshot((await readCatalogFile(path))[provider]);
|
|
2578
|
+
},
|
|
2579
|
+
async save(snapshot) {
|
|
2580
|
+
const store = await readCatalogFile(path);
|
|
2581
|
+
store[provider] = snapshot;
|
|
2582
|
+
await writeCatalogFile(store, path);
|
|
2583
|
+
},
|
|
2584
|
+
async clear() {
|
|
2585
|
+
const store = await readCatalogFile(path);
|
|
2586
|
+
if (store[provider] === void 0) return;
|
|
2587
|
+
delete store[provider];
|
|
2588
|
+
await writeCatalogFile(store, path);
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
//#endregion
|
|
2594
|
+
//#region src/providers/pool-family.ts
|
|
2595
|
+
/** Map key for one provider's pool of one model (ids collide across providers). */
|
|
2596
|
+
function poolKey(provider, model) {
|
|
2597
|
+
return `${provider}/${model}`;
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Build per-provider account routes. Each model id becomes a definition of
|
|
2601
|
+
* the accounts that list it: two or more fail over; one is pinned to that
|
|
2602
|
+
* account (so a Max-only model is never sent to a Plus login). The picker
|
|
2603
|
+
* unions these catalogs; a logout that drops a model to one account keeps
|
|
2604
|
+
* the same id and pins it to whoever remains.
|
|
2605
|
+
* @param sources - per-account catalogs (providers with no accounts list
|
|
2606
|
+
* nothing and simply never join a pool).
|
|
2607
|
+
* @returns `provider/model` → pool definition (not listed as an extra entry).
|
|
2608
|
+
*/
|
|
2609
|
+
function buildAccountPools(sources) {
|
|
2610
|
+
const pools = /* @__PURE__ */ new Map();
|
|
2611
|
+
for (const [provider, source] of Object.entries(sources)) {
|
|
2612
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
2613
|
+
for (const catalog of source.catalogs) for (const model of catalog.models) {
|
|
2614
|
+
let entry = byModel.get(model.id);
|
|
2615
|
+
if (entry === void 0) {
|
|
2616
|
+
entry = {
|
|
2617
|
+
members: [],
|
|
2618
|
+
info: model
|
|
2619
|
+
};
|
|
2620
|
+
byModel.set(model.id, entry);
|
|
2621
|
+
}
|
|
2622
|
+
entry.members.push({
|
|
2623
|
+
provider,
|
|
2624
|
+
account: catalog.account,
|
|
2625
|
+
model: model.id
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
for (const [id, { members, info }] of byModel) pools.set(poolKey(provider, id), {
|
|
2629
|
+
members,
|
|
2630
|
+
...info.name === void 0 || info.name === id ? {} : { name: info.name },
|
|
2631
|
+
...info.description === void 0 ? {} : { description: info.description }
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
return pools;
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
//#endregion
|
|
2638
|
+
//#region src/providers/pool-health.ts
|
|
2639
|
+
/** Registry key for one pool member. */
|
|
2640
|
+
function memberKey(provider, account, model) {
|
|
2641
|
+
return `${provider}/${account}/${model}`;
|
|
2642
|
+
}
|
|
2643
|
+
/** Registry key parking EVERY member of one account (account-level failures). */
|
|
2644
|
+
function accountKey(provider, account) {
|
|
2645
|
+
return `${provider}/${account}/*`;
|
|
2646
|
+
}
|
|
2647
|
+
/** Default cooldown when a quota/rate failure carries no `retry-after`. */
|
|
2648
|
+
const DEFAULT_QUOTA_COOLDOWN_MS = 5 * 6e4;
|
|
2649
|
+
/** Auth failures recheck after a day; a re-login clears the record immediately. */
|
|
2650
|
+
const AUTH_COOLDOWN_MS = 1440 * 6e4;
|
|
2651
|
+
/** Transient server-side failures cool down briefly. */
|
|
2652
|
+
const TRANSIENT_COOLDOWN_MS = 6e4;
|
|
2653
|
+
/**
|
|
2654
|
+
* Providers whose quota windows are model-scoped, so a quota failure on one
|
|
2655
|
+
* model says nothing about its siblings (Claude's Opus/Sonnet lanes). Every
|
|
2656
|
+
* other provider meters the account as a whole: one member hitting the wall
|
|
2657
|
+
* means its siblings on the SAME account would too, so the cooldown parks
|
|
2658
|
+
* the account (other accounts of the provider are unaffected).
|
|
2659
|
+
*/
|
|
2660
|
+
const MODEL_SCOPED_QUOTA_PROVIDERS = new Set(["claude"]);
|
|
2661
|
+
/** The `retry-after` an adapter propagated through `httpLlmError`, when any. */
|
|
2662
|
+
function retryAfterMs(error) {
|
|
2663
|
+
return error.failure.providerRetryAfterMs;
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Classify a member failure. Quota and rate-limit failures cool down (using
|
|
2667
|
+
* the provider's own `retry-after` when sent, which is more accurate than
|
|
2668
|
+
* any fixed guess) — account-wide for account-metered providers, per-member
|
|
2669
|
+
* for model-scoped ones; auth failures park the account until re-login
|
|
2670
|
+
* (credentials are account-level); server/timeout failures get a short
|
|
2671
|
+
* per-member cooldown; transport failures switch without a record;
|
|
2672
|
+
* everything else — most importantly CONTEXT_WINDOW_EXCEEDED and ABORTED —
|
|
2673
|
+
* is the request's own fault and is rethrown untouched.
|
|
2674
|
+
* @param error - the failure thrown by a member adapter's stream.
|
|
2675
|
+
* @param provider - the failing member's provider (decides the quota scope).
|
|
2676
|
+
* @returns the action the pool should take.
|
|
2677
|
+
*/
|
|
2678
|
+
function classifyPoolFailure(error, provider) {
|
|
2679
|
+
if (!(error instanceof LlmError)) return { action: "throw" };
|
|
2680
|
+
switch (error.code) {
|
|
2681
|
+
case QUOTA_EXCEEDED_CODE:
|
|
2682
|
+
case "RATE_LIMIT": return {
|
|
2683
|
+
action: "switch",
|
|
2684
|
+
cooldownMs: retryAfterMs(error) ?? DEFAULT_QUOTA_COOLDOWN_MS,
|
|
2685
|
+
reason: error.code,
|
|
2686
|
+
scope: MODEL_SCOPED_QUOTA_PROVIDERS.has(provider) ? "member" : "account"
|
|
2687
|
+
};
|
|
2688
|
+
case "AUTH":
|
|
2689
|
+
case "INVALID_CREDENTIAL":
|
|
2690
|
+
case "MISSING_CREDENTIAL": return {
|
|
2691
|
+
action: "switch",
|
|
2692
|
+
cooldownMs: AUTH_COOLDOWN_MS,
|
|
2693
|
+
reason: error.code,
|
|
2694
|
+
scope: "account"
|
|
2695
|
+
};
|
|
2696
|
+
case "SERVER":
|
|
2697
|
+
case "TIMEOUT":
|
|
2698
|
+
case "EMPTY_RESPONSE": return {
|
|
2699
|
+
action: "switch",
|
|
2700
|
+
cooldownMs: TRANSIENT_COOLDOWN_MS,
|
|
2701
|
+
reason: error.code,
|
|
2702
|
+
scope: "member"
|
|
2703
|
+
};
|
|
2704
|
+
case "TRANSPORT": return { action: "switch" };
|
|
2705
|
+
case "HTTP_402":
|
|
2706
|
+
case "HTTP_404": return {
|
|
2707
|
+
action: "switch",
|
|
2708
|
+
cooldownMs: TRANSIENT_COOLDOWN_MS,
|
|
2709
|
+
reason: error.code,
|
|
2710
|
+
scope: "member"
|
|
2711
|
+
};
|
|
2712
|
+
case CONTEXT_WINDOW_EXCEEDED_CODE:
|
|
2713
|
+
case "ABORTED":
|
|
2714
|
+
default: return { action: "throw" };
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Cooldown registry keyed by {@link memberKey}. A member whose cooldown has
|
|
2719
|
+
* expired is simply available again — recovery is proven by the next real
|
|
2720
|
+
* request, not by a background probe.
|
|
2721
|
+
*/
|
|
2722
|
+
var PoolHealthRegistry = class {
|
|
2723
|
+
records = /* @__PURE__ */ new Map();
|
|
2724
|
+
/** Whether a member may serve: neither it nor its whole account is cooling. */
|
|
2725
|
+
isMemberAvailable(provider, account, model, now = Date.now()) {
|
|
2726
|
+
return this.isAvailable(accountKey(provider, account), now) && this.isAvailable(memberKey(provider, account, model), now);
|
|
2727
|
+
}
|
|
2728
|
+
/** Whether one registry key is clear right now. */
|
|
2729
|
+
isAvailable(key, now = Date.now()) {
|
|
2730
|
+
const record = this.records.get(key);
|
|
2731
|
+
if (record === void 0) return true;
|
|
2732
|
+
if (record.unavailableUntil <= now) {
|
|
2733
|
+
this.records.delete(key);
|
|
2734
|
+
return true;
|
|
2735
|
+
}
|
|
2736
|
+
return false;
|
|
2737
|
+
}
|
|
2738
|
+
/** Park a member for `cooldownMs`; a longer existing cooldown wins. */
|
|
2739
|
+
markUnavailable(key, cooldownMs, reason, now = Date.now()) {
|
|
2740
|
+
const until = now + cooldownMs;
|
|
2741
|
+
const existing = this.records.get(key);
|
|
2742
|
+
if (existing !== void 0 && existing.unavailableUntil > until) return;
|
|
2743
|
+
this.records.set(key, {
|
|
2744
|
+
unavailableUntil: until,
|
|
2745
|
+
reason
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Epoch ms at which the earliest cooling record among `keys` recovers;
|
|
2750
|
+
* `undefined` when none of them is cooling. The registry is shared by
|
|
2751
|
+
* every pool, so the caller passes the keys of ITS members (member and
|
|
2752
|
+
* account keys alike) — an unrelated pool's cooldown must not shape this
|
|
2753
|
+
* pool's retry hint. Feeds the pool-exhausted error's
|
|
2754
|
+
* `providerRetryAfterMs`.
|
|
2755
|
+
*/
|
|
2756
|
+
earliestRecovery(keys, now = Date.now()) {
|
|
2757
|
+
let earliest;
|
|
2758
|
+
for (const [key, record] of this.records) {
|
|
2759
|
+
if (record.unavailableUntil <= now) {
|
|
2760
|
+
this.records.delete(key);
|
|
2761
|
+
continue;
|
|
2762
|
+
}
|
|
2763
|
+
if (!keys.has(key)) continue;
|
|
2764
|
+
if (earliest === void 0 || record.unavailableUntil < earliest) earliest = record.unavailableUntil;
|
|
2765
|
+
}
|
|
2766
|
+
return earliest;
|
|
2767
|
+
}
|
|
2768
|
+
/** Drop records of one provider, or of a single account when given (auth changes). */
|
|
2769
|
+
clear(provider, account) {
|
|
2770
|
+
const prefix = account === void 0 ? `${provider}/` : `${provider}/${account}/`;
|
|
2771
|
+
for (const key of [...this.records.keys()]) if (key.startsWith(prefix)) this.records.delete(key);
|
|
2772
|
+
}
|
|
2773
|
+
};
|
|
2774
|
+
|
|
2775
|
+
//#endregion
|
|
2776
|
+
//#region src/providers/pool.ts
|
|
2777
|
+
/** Bound on sticky-session memory; oldest entries evict past it. */
|
|
2778
|
+
const STICKY_SESSION_LIMIT = 1e3;
|
|
2779
|
+
/** Display form of one member (account shown when pinned). */
|
|
2780
|
+
function memberLabel(member) {
|
|
2781
|
+
return member.account === void 0 ? `${member.provider}/${member.model}` : `${member.provider}/${member.account}/${member.model}`;
|
|
2782
|
+
}
|
|
2783
|
+
/** How long a pools snapshot is trusted (auth changes invalidate immediately). */
|
|
2784
|
+
const POOLS_CACHE_TTL_MS = 5e3;
|
|
2785
|
+
var PoolAdapter = class extends LlmAdapter {
|
|
2786
|
+
/** sessionId|poolId → member key of the last member that served a chunk. */
|
|
2787
|
+
sticky = /* @__PURE__ */ new Map();
|
|
2788
|
+
/** Messages already warned about — configuration diagnostics repeat every request otherwise. */
|
|
2789
|
+
warned = /* @__PURE__ */ new Set();
|
|
2790
|
+
/**
|
|
2791
|
+
* Short-lived pools snapshot. `owns()` runs on every resolveModel — the
|
|
2792
|
+
* model picker issues one per entry — and pool assembly touches every
|
|
2793
|
+
* provider's catalog and account store, so recompute at most this often.
|
|
2794
|
+
* Auth changes bump {@link generation} so a stale snapshot cannot land.
|
|
2795
|
+
*/
|
|
2796
|
+
poolsCache;
|
|
2797
|
+
poolsInflight;
|
|
2798
|
+
generation = 0;
|
|
2799
|
+
constructor(options) {
|
|
2800
|
+
super();
|
|
2801
|
+
this.options = options;
|
|
2802
|
+
}
|
|
2803
|
+
/** Drop the pools snapshot so the next read reflects the current accounts. */
|
|
2804
|
+
invalidate() {
|
|
2805
|
+
this.generation += 1;
|
|
2806
|
+
this.poolsCache = void 0;
|
|
2807
|
+
this.poolsInflight = void 0;
|
|
2808
|
+
}
|
|
2809
|
+
/** Warn once per distinct message (pools() runs on every request). */
|
|
2810
|
+
warnOnce(message) {
|
|
2811
|
+
if (this.warned.has(message)) return;
|
|
2812
|
+
this.warned.add(message);
|
|
2813
|
+
this.options.onWarn(message);
|
|
2814
|
+
}
|
|
2815
|
+
/** Drop members whose adapter is not registered (copy — caller state is shared). */
|
|
2816
|
+
usable(pools) {
|
|
2817
|
+
const result = new Map(pools);
|
|
2818
|
+
for (const [id, definition] of [...result]) {
|
|
2819
|
+
const kept = definition.members.filter((member) => this.options.adapters[member.provider] !== void 0);
|
|
2820
|
+
if (kept.length === 0) result.delete(id);
|
|
2821
|
+
else if (kept.length < definition.members.length) result.set(id, {
|
|
2822
|
+
...definition,
|
|
2823
|
+
members: kept
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
return result;
|
|
2827
|
+
}
|
|
2828
|
+
/** Account pools (auto-aggregated plus config overrides) with usable members. */
|
|
2829
|
+
async familyPools() {
|
|
2830
|
+
return this.usable(new Map(await this.options.families()));
|
|
2831
|
+
}
|
|
2832
|
+
/** All pools (account pools merged with extra tiers) with usable members. */
|
|
2833
|
+
async pools() {
|
|
2834
|
+
const cached = this.poolsCache;
|
|
2835
|
+
if (cached !== void 0 && Date.now() - cached.at < POOLS_CACHE_TTL_MS) return cached.pools;
|
|
2836
|
+
const gen = this.generation;
|
|
2837
|
+
this.poolsInflight ??= this.assemblePools().then((pools) => {
|
|
2838
|
+
if (this.generation === gen) this.poolsCache = {
|
|
2839
|
+
at: Date.now(),
|
|
2840
|
+
pools
|
|
2841
|
+
};
|
|
2842
|
+
return pools;
|
|
2843
|
+
}).finally(() => {
|
|
2844
|
+
this.poolsInflight = void 0;
|
|
2845
|
+
});
|
|
2846
|
+
return this.poolsInflight;
|
|
2847
|
+
}
|
|
2848
|
+
/** Recompute the pools snapshot (account pools merged with extra tiers). */
|
|
2849
|
+
async assemblePools() {
|
|
2850
|
+
const pools = await this.familyPools();
|
|
2851
|
+
for (const [id, members] of Object.entries(this.options.tiers)) {
|
|
2852
|
+
if (members.length === 0) continue;
|
|
2853
|
+
const owner = members[0].provider;
|
|
2854
|
+
const key = poolKey(owner, id);
|
|
2855
|
+
if (pools.has(key)) this.warnOnce(`tier pool "${id}" overrides the account pool of the same id under ${owner}`);
|
|
2856
|
+
pools.set(key, {
|
|
2857
|
+
members,
|
|
2858
|
+
extra: true
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
return this.usable(pools);
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Extra picker rows one provider lists (configured tiers). Account pools
|
|
2865
|
+
* reuse the catalog entry of the same wire id, so they are not listed
|
|
2866
|
+
* again — the picker stays one row per model in ChatGPT / Claude / ….
|
|
2867
|
+
*/
|
|
2868
|
+
async modelsForProvider(provider) {
|
|
2869
|
+
const pools = await this.pools();
|
|
2870
|
+
const models = [];
|
|
2871
|
+
for (const [key, definition] of pools) {
|
|
2872
|
+
if (definition.extra !== true) continue;
|
|
2873
|
+
if (!key.startsWith(`${provider}/`)) continue;
|
|
2874
|
+
const id = key.slice(provider.length + 1);
|
|
2875
|
+
models.push({
|
|
2876
|
+
provider,
|
|
2877
|
+
id,
|
|
2878
|
+
name: definition.name ?? id,
|
|
2879
|
+
...definition.description === void 0 ? {} : { description: definition.description }
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
return models;
|
|
2883
|
+
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Whether `model` on `provider`'s route is served here (several accounts
|
|
2886
|
+
* fail over, one account is pinned, or a configured tier).
|
|
2887
|
+
*/
|
|
2888
|
+
async owns(provider, model) {
|
|
2889
|
+
return (await this.pools()).has(poolKey(provider, model));
|
|
2890
|
+
}
|
|
2891
|
+
/**
|
|
2892
|
+
* Resolve every member's account (config members may omit it to mean "the
|
|
2893
|
+
* default account") and drop members with no resolvable login. Duplicates
|
|
2894
|
+
* collapse — an explicitly pinned account and the default may coincide.
|
|
2895
|
+
*/
|
|
2896
|
+
async concrete(members) {
|
|
2897
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2898
|
+
const resolved = [];
|
|
2899
|
+
for (const member of members) {
|
|
2900
|
+
const account = member.account ?? await this.options.defaultAccount(member.provider);
|
|
2901
|
+
if (account === void 0) continue;
|
|
2902
|
+
const key = memberKey(member.provider, account, member.model);
|
|
2903
|
+
if (seen.has(key)) continue;
|
|
2904
|
+
seen.add(key);
|
|
2905
|
+
resolved.push({
|
|
2906
|
+
provider: member.provider,
|
|
2907
|
+
account,
|
|
2908
|
+
model: member.model
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
return resolved;
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* Resolve a pool model to the conservative INTERSECTION of its members'
|
|
2915
|
+
* capabilities: the smallest context window and output cap, the reasoning
|
|
2916
|
+
* efforts every member supports, and the modalities all of them accept —
|
|
2917
|
+
* so a request valid for the pool stays valid after a failover. Capability
|
|
2918
|
+
* metadata is provider-level, so each provider resolves once regardless of
|
|
2919
|
+
* how many accounts it pools.
|
|
2920
|
+
*/
|
|
2921
|
+
async resolveModel(provider, model) {
|
|
2922
|
+
const definition = (await this.pools()).get(poolKey(provider, model));
|
|
2923
|
+
if (definition === void 0) throw new LlmError(`unknown pool model "${model}"`, "NO_ADAPTER");
|
|
2924
|
+
const resolved = [];
|
|
2925
|
+
let lastFailure;
|
|
2926
|
+
const seenProviders = /* @__PURE__ */ new Set();
|
|
2927
|
+
for (const member of definition.members) {
|
|
2928
|
+
if (seenProviders.has(member.provider)) continue;
|
|
2929
|
+
seenProviders.add(member.provider);
|
|
2930
|
+
const adapter = this.options.adapters[member.provider];
|
|
2931
|
+
if (adapter === void 0) continue;
|
|
2932
|
+
try {
|
|
2933
|
+
resolved.push(await adapter.resolveOwnModel(member.provider, member.model));
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
lastFailure = error;
|
|
2936
|
+
this.warnOnce(`pool "${model}": member ${memberLabel(member)} failed to resolve (${error instanceof Error ? error.message : String(error)}); excluding it`);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
if (resolved.length === 0) throw new LlmError(`pool "${model}" has no usable member`, "NO_ADAPTER", { ...lastFailure === void 0 ? {} : { cause: lastFailure } });
|
|
2940
|
+
const contextWindows = resolved.map((info) => info.context?.contextWindow).filter(isNumber);
|
|
2941
|
+
const maxTokens = resolved.map((info) => info.defaultMaxTokens).filter(isNumber);
|
|
2942
|
+
const reasoning = intersectReasoning(resolved);
|
|
2943
|
+
const modalities = intersectModalities(resolved);
|
|
2944
|
+
return {
|
|
2945
|
+
provider,
|
|
2946
|
+
id: model,
|
|
2947
|
+
name: definition.name ?? model,
|
|
2948
|
+
...definition.description === void 0 ? {} : { description: definition.description },
|
|
2949
|
+
...contextWindows.length > 0 ? { context: { contextWindow: Math.min(...contextWindows) } } : {},
|
|
2950
|
+
...maxTokens.length > 0 ? { defaultMaxTokens: Math.min(...maxTokens) } : {},
|
|
2951
|
+
...reasoning === void 0 ? {} : { reasoning },
|
|
2952
|
+
...modalities === void 0 ? {} : { inputModalities: modalities }
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
async *stream(options) {
|
|
2956
|
+
const definition = (await this.pools()).get(poolKey(options.provider, options.model));
|
|
2957
|
+
if (definition === void 0) throw new LlmError(`unknown pool model "${options.model}"`, "NO_ADAPTER");
|
|
2958
|
+
const members = await this.concrete(definition.members);
|
|
2959
|
+
const candidates = await this.select(options.model, members, options.sessionId);
|
|
2960
|
+
if (candidates.length === 0) throw this.exhausted(options.model, members);
|
|
2961
|
+
let lastError;
|
|
2962
|
+
for (const member of candidates) {
|
|
2963
|
+
const adapter = this.options.adapters[member.provider];
|
|
2964
|
+
if (adapter === void 0) continue;
|
|
2965
|
+
const iterator = adapter.streamAccount({
|
|
2966
|
+
...options,
|
|
2967
|
+
provider: member.provider,
|
|
2968
|
+
model: member.model
|
|
2969
|
+
}, member.account)[Symbol.asyncIterator]();
|
|
2970
|
+
let first;
|
|
2971
|
+
try {
|
|
2972
|
+
first = await iterator.next();
|
|
2973
|
+
if (first.done === true) throw new LlmError(`${memberLabel(member)} returned an empty stream`, EMPTY_RESPONSE_CODE);
|
|
2974
|
+
} catch (error) {
|
|
2975
|
+
const classification = classifyPoolFailure(error, member.provider);
|
|
2976
|
+
if (classification.action === "throw") throw error;
|
|
2977
|
+
if ("cooldownMs" in classification) {
|
|
2978
|
+
this.options.health.markUnavailable(classification.scope === "account" ? accountKey(member.provider, member.account) : memberKey(member.provider, member.account, member.model), classification.cooldownMs, classification.reason);
|
|
2979
|
+
if (classification.reason === QUOTA_EXCEEDED_CODE || classification.reason === "RATE_LIMIT") this.options.usage.invalidate(member.provider, member.account);
|
|
2980
|
+
}
|
|
2981
|
+
this.options.onWarn(`pool "${options.model}": ${memberLabel(member)} failed before any output (${error instanceof Error ? error.message : String(error)}); trying the next member`);
|
|
2982
|
+
lastError = error;
|
|
2983
|
+
continue;
|
|
2984
|
+
}
|
|
2985
|
+
this.remember(options.model, options.sessionId, member);
|
|
2986
|
+
try {
|
|
2987
|
+
yield first.value;
|
|
2988
|
+
for (let next = await iterator.next(); next.done !== true; next = await iterator.next()) yield next.value;
|
|
2989
|
+
} finally {
|
|
2990
|
+
try {
|
|
2991
|
+
await iterator.return?.();
|
|
2992
|
+
} catch {}
|
|
2993
|
+
}
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
throw this.exhausted(options.model, members, lastError);
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Order the candidates for one request. Health filters both strategies;
|
|
3000
|
+
* `quota_aware` then ranks by urgency (members without telemetry, e.g.
|
|
3001
|
+
* copilot, score zero and sink to the bottom of their class), while
|
|
3002
|
+
* quota-exhausted members stay as a last-resort tail in pool order. The
|
|
3003
|
+
* sticky member keeps its lead unless a challenger out-scores it by
|
|
3004
|
+
* `switchMargin`.
|
|
3005
|
+
*/
|
|
3006
|
+
async select(poolId, members, sessionId) {
|
|
3007
|
+
const usable = members.filter((member) => this.options.adapters[member.provider] !== void 0 && this.options.health.isMemberAvailable(member.provider, member.account, member.model));
|
|
3008
|
+
if (usable.length === 0) return [];
|
|
3009
|
+
const stickyMember = sessionId === void 0 ? void 0 : usable.find((member) => memberKey(member.provider, member.account, member.model) === this.sticky.get(stickyKey(poolId, sessionId)));
|
|
3010
|
+
if (this.options.strategy === "priority") return stickyMember === void 0 ? usable : [stickyMember, ...usable.filter((member) => member !== stickyMember)];
|
|
3011
|
+
const quotas = new Map(await Promise.all(usable.map(async (member) => [member, await this.options.usage.quotaFor(member)])));
|
|
3012
|
+
const scored = usable.filter((member) => quotas.get(member)?.available === true);
|
|
3013
|
+
const quotaFull = usable.filter((member) => quotas.get(member)?.available === false);
|
|
3014
|
+
scored.sort((a, b) => (quotas.get(b)?.urgency ?? 0) - (quotas.get(a)?.urgency ?? 0));
|
|
3015
|
+
if (stickyMember !== void 0 && scored.includes(stickyMember)) {
|
|
3016
|
+
const best = scored[0];
|
|
3017
|
+
const stickyUrgency = quotas.get(stickyMember)?.urgency ?? 0;
|
|
3018
|
+
const bestUrgency = quotas.get(best)?.urgency ?? 0;
|
|
3019
|
+
if (best === stickyMember || bestUrgency <= stickyUrgency * this.options.switchMargin) {
|
|
3020
|
+
scored.splice(scored.indexOf(stickyMember), 1);
|
|
3021
|
+
scored.unshift(stickyMember);
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return [...scored, ...quotaFull];
|
|
3025
|
+
}
|
|
3026
|
+
/** Pin the serving member to the session (with bounded memory). */
|
|
3027
|
+
remember(poolId, sessionId, member) {
|
|
3028
|
+
if (sessionId === void 0) return;
|
|
3029
|
+
const key = stickyKey(poolId, sessionId);
|
|
3030
|
+
this.sticky.delete(key);
|
|
3031
|
+
if (this.sticky.size >= STICKY_SESSION_LIMIT) {
|
|
3032
|
+
const oldest = this.sticky.keys().next();
|
|
3033
|
+
if (oldest.done !== true) this.sticky.delete(oldest.value);
|
|
3034
|
+
}
|
|
3035
|
+
this.sticky.set(key, memberKey(member.provider, member.account, member.model));
|
|
3036
|
+
}
|
|
3037
|
+
/**
|
|
3038
|
+
* The error for an exhausted pool, carrying the earliest recovery hint of
|
|
3039
|
+
* THIS pool's members (the health registry is shared across pools, so the
|
|
3040
|
+
* hint is scoped to the keys this pool can actually recover through).
|
|
3041
|
+
*/
|
|
3042
|
+
exhausted(model, pool, cause) {
|
|
3043
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3044
|
+
for (const member of pool) {
|
|
3045
|
+
keys.add(memberKey(member.provider, member.account, member.model));
|
|
3046
|
+
keys.add(accountKey(member.provider, member.account));
|
|
3047
|
+
}
|
|
3048
|
+
const recovery = this.options.health.earliestRecovery(keys);
|
|
3049
|
+
const retryAfterMs$1 = recovery === void 0 ? void 0 : Math.max(recovery - Date.now(), 1);
|
|
3050
|
+
return new LlmError(`pool "${model}" exhausted: every member is unavailable or failed`, "RATE_LIMIT", {
|
|
3051
|
+
...retryAfterMs$1 === void 0 ? {} : { providerRetryAfterMs: retryAfterMs$1 },
|
|
3052
|
+
...cause === void 0 ? {} : { cause }
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
3056
|
+
function stickyKey(poolId, sessionId) {
|
|
3057
|
+
return `${String(sessionId)}|${poolId}`;
|
|
3058
|
+
}
|
|
3059
|
+
function isNumber(value) {
|
|
3060
|
+
return value !== void 0;
|
|
3061
|
+
}
|
|
3062
|
+
/** Reasoning efforts every member supports (id intersection, first member's order). */
|
|
3063
|
+
function intersectReasoning(resolved) {
|
|
3064
|
+
const [first, ...rest] = resolved;
|
|
3065
|
+
if (first?.reasoning === void 0) return void 0;
|
|
3066
|
+
const efforts = first.reasoning.efforts.filter((effort) => rest.every((info) => info.reasoning?.efforts.some((other) => other.id === effort.id) === true));
|
|
3067
|
+
if (efforts.length === 0) return void 0;
|
|
3068
|
+
const defaultEffort = first.reasoning.defaultEffort !== void 0 && efforts.some((effort) => effort.id === first.reasoning?.defaultEffort) ? first.reasoning.defaultEffort : void 0;
|
|
3069
|
+
return {
|
|
3070
|
+
efforts,
|
|
3071
|
+
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
/** Modalities all members accept; undefined when any member leaves it unknown. */
|
|
3075
|
+
function intersectModalities(resolved) {
|
|
3076
|
+
const [first, ...rest] = resolved;
|
|
3077
|
+
if (first?.inputModalities === void 0) return void 0;
|
|
3078
|
+
const modalities = first.inputModalities.filter((modality) => rest.every((info) => info.inputModalities?.includes(modality) === true));
|
|
3079
|
+
return modalities.length === 0 ? void 0 : modalities;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
//#endregion
|
|
3083
|
+
//#region src/providers/pool-usage.ts
|
|
3084
|
+
/** A member is taken out of rotation once any window crosses this fill level. */
|
|
3085
|
+
const QUOTA_FULL_PERCENT = 95;
|
|
3086
|
+
/** How long a usage snapshot is trusted before a background refresh. */
|
|
3087
|
+
const USAGE_TTL_MS = 5 * 6e4;
|
|
3088
|
+
/** Assumed window length when the provider discloses no `resetsAt`. */
|
|
3089
|
+
const FALLBACK_HORIZON_MS = {
|
|
3090
|
+
session: 300 * 6e4,
|
|
3091
|
+
weekly: 10080 * 6e4,
|
|
3092
|
+
other: 720 * 60 * 6e4
|
|
3093
|
+
};
|
|
3094
|
+
/**
|
|
3095
|
+
* Per-ACCOUNT usage snapshots with in-flight dedupe and
|
|
3096
|
+
* stale-while-revalidate refresh. Providers without a usage endpoint
|
|
3097
|
+
* (copilot) resolve no fetcher and score a constant zero urgency — which
|
|
3098
|
+
* naturally ranks them behind every measured member. Fetchers are resolved
|
|
3099
|
+
* lazily per (provider, account) so accounts added after startup join
|
|
3100
|
+
* tracking on their first score.
|
|
3101
|
+
*/
|
|
3102
|
+
var PoolUsageTracker = class {
|
|
3103
|
+
entries = /* @__PURE__ */ new Map();
|
|
3104
|
+
inflight = /* @__PURE__ */ new Map();
|
|
3105
|
+
constructor(fetcherFor, ttlMs = USAGE_TTL_MS) {
|
|
3106
|
+
this.fetcherFor = fetcherFor;
|
|
3107
|
+
this.ttlMs = ttlMs;
|
|
1727
3108
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
3109
|
+
/**
|
|
3110
|
+
* The quota view of one member. A cold cache awaits the first fetch; a
|
|
3111
|
+
* stale one answers immediately while the refresh serves the NEXT call
|
|
3112
|
+
* (member selection must never block on the network mid-conversation). A
|
|
3113
|
+
* failure still cooling down degrades immediately with no network call.
|
|
3114
|
+
* @param member - the pool member to score (account resolved).
|
|
3115
|
+
* @returns availability plus the urgency score.
|
|
3116
|
+
*/
|
|
3117
|
+
async quotaFor(member) {
|
|
3118
|
+
const key = `${member.provider}/${member.account}`;
|
|
3119
|
+
const fetcher = this.fetcherFor(member.provider, member.account);
|
|
3120
|
+
if (fetcher === void 0) return {
|
|
3121
|
+
available: true,
|
|
3122
|
+
urgency: 0,
|
|
3123
|
+
fetchedAt: 0
|
|
3124
|
+
};
|
|
3125
|
+
const entry = this.entries.get(key);
|
|
3126
|
+
if (entry !== void 0) {
|
|
3127
|
+
const fresh = Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs);
|
|
3128
|
+
if (entry.snapshot !== void 0) {
|
|
3129
|
+
if (!fresh) this.refresh(key, fetcher).catch(() => void 0);
|
|
3130
|
+
return this.score(member, entry);
|
|
3131
|
+
}
|
|
3132
|
+
if (fresh) return degradedQuota(entry.error);
|
|
3133
|
+
}
|
|
3134
|
+
try {
|
|
3135
|
+
const snapshot = await this.refresh(key, fetcher);
|
|
3136
|
+
return this.score(member, {
|
|
3137
|
+
snapshot,
|
|
3138
|
+
at: Date.now()
|
|
3139
|
+
});
|
|
3140
|
+
} catch (error) {
|
|
3141
|
+
return degradedQuota(error);
|
|
3142
|
+
}
|
|
1734
3143
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
3144
|
+
/**
|
|
3145
|
+
* Same cache as {@link quotaFor}, for direct display (the Settings page):
|
|
3146
|
+
* the raw snapshot, or the original fetch error, instead of a routing
|
|
3147
|
+
* score.
|
|
3148
|
+
* @param provider - the account's provider.
|
|
3149
|
+
* @param account - the account key.
|
|
3150
|
+
* @param force - bypass a fresh cached SNAPSHOT for an honest re-check (the
|
|
3151
|
+
* manual Refresh button). A live failure cooldown is never bypassed —
|
|
3152
|
+
* retrying through it is exactly what turns a 429 into a permanent
|
|
3153
|
+
* lockout, so even a forced call still answers from the negative cache.
|
|
3154
|
+
* @returns `{ supported: false }` when the provider has no usage fetcher.
|
|
3155
|
+
*/
|
|
3156
|
+
async snapshotFor(provider, account, force = false) {
|
|
3157
|
+
const fetcher = this.fetcherFor(provider, account);
|
|
3158
|
+
if (fetcher === void 0) return { supported: false };
|
|
3159
|
+
const key = `${provider}/${account}`;
|
|
3160
|
+
const entry = this.entries.get(key);
|
|
3161
|
+
if (entry !== void 0 && Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs)) if (entry.snapshot !== void 0) {
|
|
3162
|
+
if (!force) return entry.snapshot;
|
|
3163
|
+
} else throw entry.error;
|
|
3164
|
+
return this.refresh(key, fetcher);
|
|
3165
|
+
}
|
|
3166
|
+
/** Drop cached snapshots: one account, or a whole provider when `account` is omitted. */
|
|
3167
|
+
invalidate(provider, account) {
|
|
3168
|
+
if (account !== void 0) {
|
|
3169
|
+
this.entries.delete(`${provider}/${account}`);
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
for (const key of [...this.entries.keys()]) if (key.startsWith(`${provider}/`)) this.entries.delete(key);
|
|
3173
|
+
}
|
|
3174
|
+
/**
|
|
3175
|
+
* Run (or join) the single in-flight fetch for one account key, caching
|
|
3176
|
+
* either outcome. A missing/invalid credential is deliberately NOT
|
|
3177
|
+
* negative-cached: it costs no network round trip (the session lookup
|
|
3178
|
+
* fails before the request goes out) and re-checking live means the
|
|
3179
|
+
* member rejoins routing the instant its login is fixed, rather than
|
|
3180
|
+
* waiting out a stale cooldown.
|
|
3181
|
+
*/
|
|
3182
|
+
refresh(key, fetcher) {
|
|
3183
|
+
let pending = this.inflight.get(key);
|
|
3184
|
+
if (pending === void 0) {
|
|
3185
|
+
pending = fetcher().then((snapshot) => {
|
|
3186
|
+
this.entries.set(key, {
|
|
3187
|
+
snapshot,
|
|
3188
|
+
at: Date.now()
|
|
3189
|
+
});
|
|
3190
|
+
return snapshot;
|
|
3191
|
+
}, (error) => {
|
|
3192
|
+
if (!isMissingOrInvalidCredential(error)) this.entries.set(key, {
|
|
3193
|
+
error,
|
|
3194
|
+
at: Date.now(),
|
|
3195
|
+
cooldownMs: cooldownFor(error, this.ttlMs)
|
|
3196
|
+
});
|
|
3197
|
+
throw error;
|
|
3198
|
+
}).finally(() => {
|
|
3199
|
+
this.inflight.delete(key);
|
|
3200
|
+
});
|
|
3201
|
+
this.inflight.set(key, pending);
|
|
3202
|
+
}
|
|
3203
|
+
return pending;
|
|
3204
|
+
}
|
|
3205
|
+
/** Score one member against a snapshot's windows. */
|
|
3206
|
+
score(member, entry) {
|
|
3207
|
+
const windows = (entry.snapshot.windows ?? []).filter((window) => windowApplies(window, member.model));
|
|
3208
|
+
let available = true;
|
|
3209
|
+
let urgency = 0;
|
|
3210
|
+
for (const window of windows) {
|
|
3211
|
+
if (window.usedPercent >= QUOTA_FULL_PERCENT) available = false;
|
|
3212
|
+
urgency = Math.max(urgency, windowUrgency(window));
|
|
3213
|
+
}
|
|
3214
|
+
return {
|
|
3215
|
+
available,
|
|
3216
|
+
urgency,
|
|
3217
|
+
fetchedAt: entry.at
|
|
3218
|
+
};
|
|
1746
3219
|
}
|
|
3220
|
+
};
|
|
3221
|
+
/**
|
|
3222
|
+
* The routing view of a fetch failure. Logged out: the member cannot serve
|
|
3223
|
+
* at all. Any other failure (network, endpoint rate limit) must not block
|
|
3224
|
+
* routing — the member stays available with a zero score, degrading the
|
|
3225
|
+
* strategy to plain priority order for it.
|
|
3226
|
+
*/
|
|
3227
|
+
function degradedQuota(error) {
|
|
3228
|
+
return isMissingOrInvalidCredential(error) ? {
|
|
3229
|
+
available: false,
|
|
3230
|
+
urgency: 0,
|
|
3231
|
+
fetchedAt: 0
|
|
3232
|
+
} : {
|
|
3233
|
+
available: true,
|
|
3234
|
+
urgency: 0,
|
|
3235
|
+
fetchedAt: 0
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
/** How long to hold a failure in the negative cache: the endpoint's own `retry-after`, or the default TTL. */
|
|
3239
|
+
function cooldownFor(error, defaultTtlMs) {
|
|
3240
|
+
return error instanceof OAuthEndpointError && error.retryAfterMs !== void 0 ? error.retryAfterMs : defaultTtlMs;
|
|
1747
3241
|
}
|
|
1748
3242
|
/**
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1751
|
-
*
|
|
1752
|
-
* @param provider - the provider route keying the file entry.
|
|
1753
|
-
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
1754
|
-
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
3243
|
+
* Whether a window constrains this model: unscoped windows always do; a
|
|
3244
|
+
* model-scoped window (Claude's Opus/Sonnet lanes) applies when its scope
|
|
3245
|
+
* names the model family.
|
|
1755
3246
|
*/
|
|
1756
|
-
function
|
|
1757
|
-
return
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
store[provider] = snapshot;
|
|
1764
|
-
await writeCatalogFile(store, path);
|
|
1765
|
-
},
|
|
1766
|
-
async clear() {
|
|
1767
|
-
const store = await readCatalogFile(path);
|
|
1768
|
-
if (store[provider] === void 0) return;
|
|
1769
|
-
delete store[provider];
|
|
1770
|
-
await writeCatalogFile(store, path);
|
|
1771
|
-
}
|
|
1772
|
-
};
|
|
3247
|
+
function windowApplies(window, model) {
|
|
3248
|
+
if (window.scope === void 0) return true;
|
|
3249
|
+
return model.toLowerCase().includes(window.scope.toLowerCase());
|
|
3250
|
+
}
|
|
3251
|
+
/** The required burn rate of one window (fraction per ms). */
|
|
3252
|
+
function windowUrgency(window, now = Date.now()) {
|
|
3253
|
+
return Math.max(0, 1 - window.usedPercent / 100) / (window.resetsAt !== void 0 ? Math.max(window.resetsAt - now, 1) : FALLBACK_HORIZON_MS[window.kind]);
|
|
1773
3254
|
}
|
|
1774
3255
|
|
|
1775
3256
|
//#endregion
|
|
@@ -1825,6 +3306,14 @@ async function resolveImages(messages, attachments, signal) {
|
|
|
1825
3306
|
})));
|
|
1826
3307
|
}
|
|
1827
3308
|
|
|
3309
|
+
//#endregion
|
|
3310
|
+
//#region src/compat.ts
|
|
3311
|
+
/** Brand a string as a tool-call id: alpha's `ToolCallId`, rc.2's `CallId`. */
|
|
3312
|
+
const ToolCallId = (() => {
|
|
3313
|
+
const exports = llm;
|
|
3314
|
+
return exports["ToolCallId"] ?? exports["CallId"];
|
|
3315
|
+
})();
|
|
3316
|
+
|
|
1828
3317
|
//#endregion
|
|
1829
3318
|
//#region src/translate/sse.ts
|
|
1830
3319
|
/**
|
|
@@ -2019,7 +3508,7 @@ function closeBlock$2(block) {
|
|
|
2019
3508
|
};
|
|
2020
3509
|
case "tool-call": return {
|
|
2021
3510
|
type: "tool-call",
|
|
2022
|
-
id:
|
|
3511
|
+
id: ToolCallId(block.callId),
|
|
2023
3512
|
name: block.name ?? "",
|
|
2024
3513
|
arguments: block.text
|
|
2025
3514
|
};
|
|
@@ -2109,7 +3598,7 @@ var ResponsesStreamTranslator = class {
|
|
|
2109
3598
|
chunks.push({
|
|
2110
3599
|
type: "tool-call-delta",
|
|
2111
3600
|
index: block.index,
|
|
2112
|
-
id:
|
|
3601
|
+
id: ToolCallId(callId),
|
|
2113
3602
|
...item.name === void 0 ? {} : { name: item.name },
|
|
2114
3603
|
argumentsDelta: ""
|
|
2115
3604
|
});
|
|
@@ -2151,7 +3640,7 @@ var ResponsesStreamTranslator = class {
|
|
|
2151
3640
|
chunks.push({
|
|
2152
3641
|
type: "tool-call-delta",
|
|
2153
3642
|
index: block.index,
|
|
2154
|
-
id:
|
|
3643
|
+
id: ToolCallId(block.callId),
|
|
2155
3644
|
...block.name === void 0 ? {} : { name: block.name },
|
|
2156
3645
|
argumentsDelta: event.delta ?? ""
|
|
2157
3646
|
});
|
|
@@ -2244,6 +3733,28 @@ const CODEX_CONTEXT_WINDOW = 4e5;
|
|
|
2244
3733
|
const CODEX_DEFAULT_MAX_TOKENS = 128e3;
|
|
2245
3734
|
/** Refresh when the access token has less than this much life left. */
|
|
2246
3735
|
const CODEX_PREEMPT_MS = 5 * 6e4;
|
|
3736
|
+
/**
|
|
3737
|
+
* Body fields the backend uses to name a reset. A window-exhaustion rejection
|
|
3738
|
+
* carries `usage_limit_reached` with the seconds left on the window — the case
|
|
3739
|
+
* that used to classify as a terminal quota and never be retried at all.
|
|
3740
|
+
*/
|
|
3741
|
+
const CODEX_RESET_FIELDS = [
|
|
3742
|
+
"resets_in_seconds",
|
|
3743
|
+
"reset_after_seconds",
|
|
3744
|
+
"resets_at",
|
|
3745
|
+
"reset_at"
|
|
3746
|
+
];
|
|
3747
|
+
/**
|
|
3748
|
+
* Reads the reset instant of the Codex window that rejected a request.
|
|
3749
|
+
*
|
|
3750
|
+
* Body only. The `x-codex-{primary,secondary}-reset-after-seconds` headers are
|
|
3751
|
+
* rollover snapshots the backend attaches to every response, one per window,
|
|
3752
|
+
* so they say nothing about which window refused: a burst 429 that would clear
|
|
3753
|
+
* in seconds still carries a primary rollover hours out, and reading it would
|
|
3754
|
+
* park the turn for those hours. They reach the operator through
|
|
3755
|
+
* `rateLimitDiagnostics` instead.
|
|
3756
|
+
*/
|
|
3757
|
+
const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
|
|
2247
3758
|
/** Default instruction when the request carries no system prompt. */
|
|
2248
3759
|
const DEFAULT_CODEX_INSTRUCTIONS = "You are Codex, a coding agent based on GPT-5. Help the user with their software engineering tasks.";
|
|
2249
3760
|
/** Refresh-grant rejections that mean the login is gone for good. */
|
|
@@ -2492,10 +4003,6 @@ const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
|
|
|
2492
4003
|
* the range of current codex CLI releases.
|
|
2493
4004
|
*/
|
|
2494
4005
|
const CODEX_CLIENT_VERSION = "0.147.0";
|
|
2495
|
-
/** Display name for a wire reasoning-effort value. */
|
|
2496
|
-
function effortName(effort) {
|
|
2497
|
-
return effort === "xhigh" ? "Extra High" : effort.charAt(0).toUpperCase() + effort.slice(1);
|
|
2498
|
-
}
|
|
2499
4006
|
/**
|
|
2500
4007
|
* Whether a catalog entry advertises the fast tier. Mirrors codex-rs
|
|
2501
4008
|
* `ModelPreset::supports_fast_mode`: a `service_tiers` id matching the fast
|
|
@@ -2508,16 +4015,20 @@ function supportsFastTier(entry) {
|
|
|
2508
4015
|
* Fetch the live codex model catalog with the session's auth headers.
|
|
2509
4016
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
2510
4017
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
4018
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
2511
4019
|
* @returns discovered models: hidden entries dropped, sorted by priority.
|
|
2512
4020
|
*/
|
|
2513
|
-
async function fetchCodexModels(session, fetchFn = proxiedFetch) {
|
|
2514
|
-
const response = await fetchFn(`${CODEX_MODELS_URL}?client_version=${CODEX_CLIENT_VERSION}`, {
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
4021
|
+
async function fetchCodexModels(session, fetchFn = proxiedFetch, signal) {
|
|
4022
|
+
const response = await fetchFn(`${CODEX_MODELS_URL}?client_version=${CODEX_CLIENT_VERSION}`, {
|
|
4023
|
+
headers: {
|
|
4024
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
4025
|
+
"chatgpt-account-id": session.accountId,
|
|
4026
|
+
"originator": "codex_cli_rs",
|
|
4027
|
+
"accept": "application/json",
|
|
4028
|
+
...attributionHeaders()
|
|
4029
|
+
},
|
|
4030
|
+
...signal === void 0 ? {} : { signal }
|
|
4031
|
+
});
|
|
2521
4032
|
if (!response.ok) throw await oauthEndpointError(response, "codex models");
|
|
2522
4033
|
const payload = await response.json();
|
|
2523
4034
|
if (!Array.isArray(payload.models)) throw new Error("codex models endpoint returned no models array");
|
|
@@ -2527,7 +4038,7 @@ async function fetchCodexModels(session, fetchFn = proxiedFetch) {
|
|
|
2527
4038
|
if (entry.visibility === "hide" || entry.visibility === "none") continue;
|
|
2528
4039
|
const efforts = (entry.supported_reasoning_levels ?? []).filter((level) => typeof level.effort === "string" && level.effort.length > 0).map((level) => ({
|
|
2529
4040
|
id: ReasoningEffortId(level.effort),
|
|
2530
|
-
name:
|
|
4041
|
+
name: effortDisplayName(level.effort),
|
|
2531
4042
|
...level.description === void 0 ? {} : { description: level.description }
|
|
2532
4043
|
}));
|
|
2533
4044
|
const defaultEffort = typeof entry.default_reasoning_level === "string" && entry.default_reasoning_level.length > 0 && efforts.some((effort) => effort.id === ReasoningEffortId(entry.default_reasoning_level)) ? ReasoningEffortId(entry.default_reasoning_level) : void 0;
|
|
@@ -2621,14 +4132,43 @@ function codexRequestBody(options, resolved, fast) {
|
|
|
2621
4132
|
/** Codex wire adapter: one instance serves the `codex` provider route. */
|
|
2622
4133
|
var CodexAdapter = class extends LlmAdapter {
|
|
2623
4134
|
catalog;
|
|
4135
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
4136
|
+
accountCatalogs = /* @__PURE__ */ new Map();
|
|
4137
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
4138
|
+
catalogOwner;
|
|
2624
4139
|
constructor(options) {
|
|
2625
4140
|
super();
|
|
2626
4141
|
this.options = options;
|
|
2627
4142
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
2628
4143
|
}
|
|
2629
4144
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
2630
|
-
async fetchCatalog() {
|
|
2631
|
-
return fetchCodexModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
4145
|
+
async fetchCatalog(account, signal) {
|
|
4146
|
+
return fetchCodexModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
4147
|
+
}
|
|
4148
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
4149
|
+
clearAccountCatalog(account) {
|
|
4150
|
+
if (account === void 0) this.accountCatalogs.clear();
|
|
4151
|
+
else this.accountCatalogs.delete(account);
|
|
4152
|
+
if (account === void 0 || this.catalogOwner === account || this.catalogOwner === void 0) {
|
|
4153
|
+
this.catalogOwner = void 0;
|
|
4154
|
+
this.catalog.invalidate();
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
4158
|
+
async catalogFor(account) {
|
|
4159
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
4160
|
+
const key = account ?? defaultKey;
|
|
4161
|
+
if (key === void 0 || key === defaultKey) {
|
|
4162
|
+
if (this.catalogOwner !== void 0 && this.catalogOwner !== defaultKey) this.catalog.invalidate();
|
|
4163
|
+
this.catalogOwner = defaultKey;
|
|
4164
|
+
return this.catalog;
|
|
4165
|
+
}
|
|
4166
|
+
let cache = this.accountCatalogs.get(key);
|
|
4167
|
+
if (cache === void 0) {
|
|
4168
|
+
cache = new ModelCatalogCache();
|
|
4169
|
+
this.accountCatalogs.set(key, cache);
|
|
4170
|
+
}
|
|
4171
|
+
return cache;
|
|
2632
4172
|
}
|
|
2633
4173
|
providerInfo(provider) {
|
|
2634
4174
|
return {
|
|
@@ -2636,6 +4176,9 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
2636
4176
|
name: "ChatGPT (Codex)"
|
|
2637
4177
|
};
|
|
2638
4178
|
}
|
|
4179
|
+
providerRetryPolicy(provider) {
|
|
4180
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `codex: provider "${provider}" retryPolicy`);
|
|
4181
|
+
}
|
|
2639
4182
|
staticModels(provider) {
|
|
2640
4183
|
return this.options.models.map((model) => ({
|
|
2641
4184
|
provider,
|
|
@@ -2645,17 +4188,37 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
2645
4188
|
}));
|
|
2646
4189
|
}
|
|
2647
4190
|
async listModels(provider) {
|
|
2648
|
-
|
|
4191
|
+
const own = await this.listOwnModels(provider);
|
|
4192
|
+
const pool = this.options.pool?.();
|
|
4193
|
+
if (pool === void 0) return own;
|
|
4194
|
+
const extra = await pool.modelsForProvider(provider);
|
|
4195
|
+
const seen = new Set(own.map((model) => model.id));
|
|
4196
|
+
return [...own, ...extra.filter((model) => !seen.has(model.id))];
|
|
4197
|
+
}
|
|
4198
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
4199
|
+
async listOwnModels(provider, account, signal) {
|
|
4200
|
+
if (account === void 0) {
|
|
4201
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
4202
|
+
if (accounts.length === 0) return [];
|
|
4203
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), {
|
|
4204
|
+
timeoutMs: this.options.discoveryTimeoutMs ?? DISCOVERY_TIMEOUT_MS,
|
|
4205
|
+
...signal === void 0 ? {} : { signal }
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
4208
|
+
if (!await this.options.tokens.hasSession(account)) return [];
|
|
2649
4209
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
4210
|
+
const catalog = await this.catalogFor(account);
|
|
2650
4211
|
try {
|
|
2651
|
-
return (await discoverOrRetryAuth((force) => this.options.tokens.session(force),
|
|
4212
|
+
return (await discoverOrRetryAuth((force) => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)))).map((model) => ({
|
|
2652
4213
|
provider,
|
|
2653
4214
|
id: model.id,
|
|
2654
4215
|
name: model.name,
|
|
2655
4216
|
...model.description === void 0 ? {} : { description: model.description },
|
|
2656
|
-
inputModalities: CODEX_MODALITIES
|
|
4217
|
+
inputModalities: CODEX_MODALITIES,
|
|
4218
|
+
...model.priority === void 0 ? {} : { priority: model.priority }
|
|
2657
4219
|
}));
|
|
2658
4220
|
} catch (error) {
|
|
4221
|
+
if (isDiscoveryAborted(error, signal)) throw error;
|
|
2659
4222
|
if (isMissingOrInvalidCredential(error)) return [];
|
|
2660
4223
|
this.options.onWarn?.(`codex model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
2661
4224
|
return this.staticModels(provider);
|
|
@@ -2670,7 +4233,9 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
2670
4233
|
*/
|
|
2671
4234
|
async discovered(model) {
|
|
2672
4235
|
if (!this.options.discovery) return void 0;
|
|
2673
|
-
return (await this.
|
|
4236
|
+
return discoverAcrossAccounts((await this.options.tokens.list()).map((entry) => entry.key), async (account) => {
|
|
4237
|
+
return (await (await this.catalogFor(account)).resolve(() => this.fetchCatalog(account)))?.find((entry) => entry.id === model);
|
|
4238
|
+
});
|
|
2674
4239
|
}
|
|
2675
4240
|
/** Whether the discovered catalog advertises a fast tier for this model. */
|
|
2676
4241
|
async supportsFastTier(model) {
|
|
@@ -2679,12 +4244,33 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
2679
4244
|
/** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
|
|
2680
4245
|
async fastCapableModels() {
|
|
2681
4246
|
if (!this.options.discovery) return [];
|
|
2682
|
-
|
|
2683
|
-
|
|
4247
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
4248
|
+
if (accounts.length === 0) return [];
|
|
4249
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4250
|
+
const ids = [];
|
|
4251
|
+
for (const account of accounts) try {
|
|
4252
|
+
const models = await (await this.catalogFor(account)).resolve(() => this.fetchCatalog(account));
|
|
4253
|
+
for (const model of models ?? []) {
|
|
4254
|
+
if (model.fastTier !== true || seen.has(model.id)) continue;
|
|
4255
|
+
seen.add(model.id);
|
|
4256
|
+
ids.push(model.id);
|
|
4257
|
+
}
|
|
4258
|
+
} catch {}
|
|
4259
|
+
return ids;
|
|
2684
4260
|
}
|
|
2685
4261
|
async resolveModel(provider, model) {
|
|
4262
|
+
const pool = this.options.pool?.();
|
|
4263
|
+
if (pool !== void 0 && await pool.owns(provider, model)) return pool.resolveModel(provider, model);
|
|
4264
|
+
return this.resolveOwnModel(provider, model);
|
|
4265
|
+
}
|
|
4266
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
4267
|
+
async resolveOwnModel(provider, model) {
|
|
2686
4268
|
const discovered = await this.discovered(model);
|
|
2687
4269
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
4270
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning ?? {
|
|
4271
|
+
efforts: CODEX_EFFORTS,
|
|
4272
|
+
defaultEffort: CODEX_DEFAULT_EFFORT
|
|
4273
|
+
}, { extendable: discovered?.reasoning === void 0 });
|
|
2688
4274
|
return {
|
|
2689
4275
|
provider,
|
|
2690
4276
|
id: model,
|
|
@@ -2693,22 +4279,34 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
2693
4279
|
inputModalities: configured?.inputModalities ?? CODEX_MODALITIES,
|
|
2694
4280
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? CODEX_CONTEXT_WINDOW },
|
|
2695
4281
|
defaultMaxTokens: configured?.maxTokens ?? CODEX_DEFAULT_MAX_TOKENS,
|
|
2696
|
-
reasoning:
|
|
2697
|
-
efforts: CODEX_EFFORTS,
|
|
2698
|
-
defaultEffort: CODEX_DEFAULT_EFFORT
|
|
2699
|
-
}
|
|
4282
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
2700
4283
|
};
|
|
2701
4284
|
}
|
|
2702
4285
|
async *stream(options) {
|
|
4286
|
+
const pool = this.options.pool?.();
|
|
4287
|
+
if (pool !== void 0 && await pool.owns(options.provider, options.model)) {
|
|
4288
|
+
yield* pool.stream(options);
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4291
|
+
yield* this.streamCore(options);
|
|
4292
|
+
}
|
|
4293
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
4294
|
+
streamAccount(options, account) {
|
|
4295
|
+
return this.streamCore(options, account);
|
|
4296
|
+
}
|
|
4297
|
+
async *streamCore(options, account) {
|
|
2703
4298
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
2704
4299
|
try {
|
|
2705
|
-
let session = await this.options.tokens.session();
|
|
4300
|
+
let session = await this.options.tokens.session(account);
|
|
2706
4301
|
let response = await this.request(options, session, watchdog.signal);
|
|
2707
4302
|
if (response.status === 401) {
|
|
2708
|
-
session = await this.options.tokens.session(true);
|
|
4303
|
+
session = await this.options.tokens.session(account, true);
|
|
2709
4304
|
response = await this.request(options, session, watchdog.signal);
|
|
2710
4305
|
}
|
|
2711
|
-
if (!response.ok) throw await httpLlmError(response, "codex API"
|
|
4306
|
+
if (!response.ok) throw await httpLlmError(response, "codex API", {
|
|
4307
|
+
rateLimitReset: codexRateLimitReset,
|
|
4308
|
+
...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn }
|
|
4309
|
+
});
|
|
2712
4310
|
if (response.body === null) throw new LlmError("codex API returned no response body", EMPTY_RESPONSE_CODE);
|
|
2713
4311
|
yield* streamResponses(response.body, () => {
|
|
2714
4312
|
watchdog.pulse();
|
|
@@ -2961,7 +4559,7 @@ function closeBlock$1(block) {
|
|
|
2961
4559
|
};
|
|
2962
4560
|
case "tool-call": return {
|
|
2963
4561
|
type: "tool-call",
|
|
2964
|
-
id:
|
|
4562
|
+
id: ToolCallId(block.callId),
|
|
2965
4563
|
name: block.name ?? "",
|
|
2966
4564
|
arguments: block.text
|
|
2967
4565
|
};
|
|
@@ -3064,7 +4662,7 @@ var AnthropicStreamTranslator = class {
|
|
|
3064
4662
|
chunks.push({
|
|
3065
4663
|
type: "tool-call-delta",
|
|
3066
4664
|
index: opened.index,
|
|
3067
|
-
id:
|
|
4665
|
+
id: ToolCallId(opened.callId),
|
|
3068
4666
|
...block.name === void 0 ? {} : { name: block.name },
|
|
3069
4667
|
argumentsDelta: ""
|
|
3070
4668
|
});
|
|
@@ -3101,7 +4699,7 @@ var AnthropicStreamTranslator = class {
|
|
|
3101
4699
|
chunks.push({
|
|
3102
4700
|
type: "tool-call-delta",
|
|
3103
4701
|
index: block.index,
|
|
3104
|
-
id:
|
|
4702
|
+
id: ToolCallId(block.callId),
|
|
3105
4703
|
...block.name === void 0 ? {} : { name: block.name },
|
|
3106
4704
|
argumentsDelta: delta.partial_json ?? ""
|
|
3107
4705
|
});
|
|
@@ -3205,6 +4803,34 @@ const CLAUDE_DEFAULT_MAX_TOKENS = 32e3;
|
|
|
3205
4803
|
/** Refresh when the access token has less than this much life left. */
|
|
3206
4804
|
const CLAUDE_PREEMPT_MS = 5 * 6e4;
|
|
3207
4805
|
/**
|
|
4806
|
+
* Body fields Anthropic uses to name a reset instant, read when the unified
|
|
4807
|
+
* headers are absent.
|
|
4808
|
+
*/
|
|
4809
|
+
const CLAUDE_RESET_FIELDS = [
|
|
4810
|
+
"resets_at",
|
|
4811
|
+
"resetsAt",
|
|
4812
|
+
"reset_at",
|
|
4813
|
+
"retry_after"
|
|
4814
|
+
];
|
|
4815
|
+
/**
|
|
4816
|
+
* Reads the reset instant of the Anthropic window that rejected a request.
|
|
4817
|
+
*
|
|
4818
|
+
* `anthropic-ratelimit-unified-*` is the subscription-plan family — the one
|
|
4819
|
+
* Claude Code renders as "resets 3pm" — and is the only header that names the
|
|
4820
|
+
* window which actually rejected this request. The per-bucket
|
|
4821
|
+
* `anthropic-ratelimit-{requests,tokens,input-tokens,output-tokens}-reset`
|
|
4822
|
+
* headers are deliberately not read: they are rollover snapshots attached to
|
|
4823
|
+
* every response, so on a 429 they cannot say which bucket refused, and the
|
|
4824
|
+
* earliest of them is typically the bucket that still had room — a wait that
|
|
4825
|
+
* lands straight back in the closed window. They reach the operator through
|
|
4826
|
+
* `rateLimitDiagnostics` instead.
|
|
4827
|
+
*/
|
|
4828
|
+
const claudeRateLimitReset = (response, body, now) => {
|
|
4829
|
+
const unified = earliestReset(resetInstantFromHeader(response, "anthropic-ratelimit-unified-reset", now), resetInstantFromHeader(response, "anthropic-ratelimit-unified-fallback-reset", now));
|
|
4830
|
+
if (unified !== void 0) return unified;
|
|
4831
|
+
return resetFromFields(jsonBody(body), CLAUDE_RESET_FIELDS, now);
|
|
4832
|
+
};
|
|
4833
|
+
/**
|
|
3208
4834
|
* The subscription endpoint only serves requests presenting as Claude Code,
|
|
3209
4835
|
* so these headers impersonate the CLI; the harness attribution user-agent
|
|
3210
4836
|
* cannot be sent here (one user-agent slot, and the CLI's wins).
|
|
@@ -3444,15 +5070,18 @@ function claudeReasoning(capabilities) {
|
|
|
3444
5070
|
}));
|
|
3445
5071
|
return efforts.length > 0 ? { efforts } : void 0;
|
|
3446
5072
|
}
|
|
3447
|
-
/** Fetch the live model catalog from the subscription endpoint. */
|
|
3448
|
-
async function fetchClaudeModels(session, fetchFn = proxiedFetch) {
|
|
3449
|
-
const response = await fetchFn(CLAUDE_MODELS_URL, {
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
5073
|
+
/** Fetch the live model catalog from the subscription endpoint. `signal` cancels the request. */
|
|
5074
|
+
async function fetchClaudeModels(session, fetchFn = proxiedFetch, signal) {
|
|
5075
|
+
const response = await fetchFn(CLAUDE_MODELS_URL, {
|
|
5076
|
+
headers: {
|
|
5077
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
5078
|
+
"anthropic-version": "2023-06-01",
|
|
5079
|
+
"user-agent": getClaudeCliUserAgent(),
|
|
5080
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
5081
|
+
"accept": "application/json"
|
|
5082
|
+
},
|
|
5083
|
+
...signal === void 0 ? {} : { signal }
|
|
5084
|
+
});
|
|
3456
5085
|
if (!response.ok) throw await httpLlmError(response, "claude models API");
|
|
3457
5086
|
const payload = await response.json();
|
|
3458
5087
|
if (!Array.isArray(payload.data)) throw new Error("claude models API returned an invalid catalog");
|
|
@@ -3469,14 +5098,6 @@ async function fetchClaudeModels(session, fetchFn = proxiedFetch) {
|
|
|
3469
5098
|
if (models.length === 0) throw new Error("claude models API returned an empty catalog");
|
|
3470
5099
|
return models;
|
|
3471
5100
|
}
|
|
3472
|
-
/**
|
|
3473
|
-
* Claude Code's own SDK retry shape: exponential backoff starting at 1s,
|
|
3474
|
-
* doubling per attempt, capped at 60s, plus jitter. `maxRetries` is the
|
|
3475
|
-
* count of retries after the first attempt (Claude Code defaults to 10).
|
|
3476
|
-
*/
|
|
3477
|
-
const CLAUDE_RETRY_INITIAL_DELAY_MS = 1e3;
|
|
3478
|
-
const CLAUDE_RETRY_MAX_DELAY_MS = 6e4;
|
|
3479
|
-
const CLAUDE_RETRY_JITTER_RATIO = .2;
|
|
3480
5101
|
/** The Claude 4.5 family accepts image input. */
|
|
3481
5102
|
const CLAUDE_MODALITIES = ["text", "image"];
|
|
3482
5103
|
/**
|
|
@@ -3512,17 +5133,48 @@ function claudeRequestBody(options, messages, maxTokens, thinking, effort) {
|
|
|
3512
5133
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
3513
5134
|
var ClaudeAdapter = class extends LlmAdapter {
|
|
3514
5135
|
catalog;
|
|
5136
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
5137
|
+
accountCatalogs = /* @__PURE__ */ new Map();
|
|
5138
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
5139
|
+
catalogOwner;
|
|
3515
5140
|
constructor(options) {
|
|
3516
5141
|
super();
|
|
3517
5142
|
this.options = options;
|
|
3518
5143
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
3519
5144
|
}
|
|
3520
|
-
async fetchCatalog() {
|
|
3521
|
-
return fetchClaudeModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
5145
|
+
async fetchCatalog(account, signal) {
|
|
5146
|
+
return fetchClaudeModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
5147
|
+
}
|
|
5148
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
5149
|
+
clearAccountCatalog(account) {
|
|
5150
|
+
if (account === void 0) this.accountCatalogs.clear();
|
|
5151
|
+
else this.accountCatalogs.delete(account);
|
|
5152
|
+
if (account === void 0 || this.catalogOwner === account || this.catalogOwner === void 0) {
|
|
5153
|
+
this.catalogOwner = void 0;
|
|
5154
|
+
this.catalog.invalidate();
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
5158
|
+
async catalogFor(account) {
|
|
5159
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
5160
|
+
const key = account ?? defaultKey;
|
|
5161
|
+
if (key === void 0 || key === defaultKey) {
|
|
5162
|
+
if (this.catalogOwner !== void 0 && this.catalogOwner !== defaultKey) this.catalog.invalidate();
|
|
5163
|
+
this.catalogOwner = defaultKey;
|
|
5164
|
+
return this.catalog;
|
|
5165
|
+
}
|
|
5166
|
+
let cache = this.accountCatalogs.get(key);
|
|
5167
|
+
if (cache === void 0) {
|
|
5168
|
+
cache = new ModelCatalogCache();
|
|
5169
|
+
this.accountCatalogs.set(key, cache);
|
|
5170
|
+
}
|
|
5171
|
+
return cache;
|
|
3522
5172
|
}
|
|
3523
5173
|
async discovered(model) {
|
|
3524
5174
|
if (!this.options.discovery) return void 0;
|
|
3525
|
-
return (await this.
|
|
5175
|
+
return discoverAcrossAccounts((await this.options.tokens.list()).map((entry) => entry.key), async (account) => {
|
|
5176
|
+
return (await (await this.catalogFor(account)).resolve(() => this.fetchCatalog(account)))?.find((entry) => entry.id === model);
|
|
5177
|
+
});
|
|
3526
5178
|
}
|
|
3527
5179
|
staticModels(provider) {
|
|
3528
5180
|
return this.options.models.map((model) => ({
|
|
@@ -3539,37 +5191,53 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
3539
5191
|
};
|
|
3540
5192
|
}
|
|
3541
5193
|
providerRetryPolicy(provider) {
|
|
3542
|
-
|
|
3543
|
-
return resolveRetryPolicy({
|
|
3544
|
-
mode: "normal",
|
|
3545
|
-
maxRetries: this.options.maxRetries,
|
|
3546
|
-
backoff: {
|
|
3547
|
-
initialDelayMs: CLAUDE_RETRY_INITIAL_DELAY_MS,
|
|
3548
|
-
maxDelayMs: CLAUDE_RETRY_MAX_DELAY_MS,
|
|
3549
|
-
jitterRatio: CLAUDE_RETRY_JITTER_RATIO
|
|
3550
|
-
}
|
|
3551
|
-
}, `claude: provider "${provider}" retryPolicy`);
|
|
5194
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `claude: provider "${provider}" retryPolicy`);
|
|
3552
5195
|
}
|
|
3553
5196
|
async listModels(provider) {
|
|
3554
|
-
|
|
5197
|
+
const own = await this.listOwnModels(provider);
|
|
5198
|
+
const pool = this.options.pool?.();
|
|
5199
|
+
if (pool === void 0) return own;
|
|
5200
|
+
const extra = await pool.modelsForProvider(provider);
|
|
5201
|
+
const seen = new Set(own.map((model) => model.id));
|
|
5202
|
+
return [...own, ...extra.filter((model) => !seen.has(model.id))];
|
|
5203
|
+
}
|
|
5204
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
5205
|
+
async listOwnModels(provider, account, signal) {
|
|
5206
|
+
if (account === void 0) {
|
|
5207
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
5208
|
+
if (accounts.length === 0) return [];
|
|
5209
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), {
|
|
5210
|
+
timeoutMs: DISCOVERY_TIMEOUT_MS,
|
|
5211
|
+
...signal === void 0 ? {} : { signal }
|
|
5212
|
+
});
|
|
5213
|
+
}
|
|
5214
|
+
if (!await this.options.tokens.hasSession(account)) return [];
|
|
3555
5215
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
5216
|
+
const catalog = await this.catalogFor(account);
|
|
3556
5217
|
try {
|
|
3557
|
-
return (await discoverOrRetryAuth((force) => this.options.tokens.session(force),
|
|
5218
|
+
return (await discoverOrRetryAuth((force) => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)))).map((model) => ({
|
|
3558
5219
|
provider,
|
|
3559
5220
|
id: model.id,
|
|
3560
5221
|
name: model.name,
|
|
3561
5222
|
inputModalities: CLAUDE_MODALITIES
|
|
3562
5223
|
}));
|
|
3563
5224
|
} catch (error) {
|
|
5225
|
+
if (isDiscoveryAborted(error, signal)) throw error;
|
|
3564
5226
|
if (isMissingOrInvalidCredential(error)) return [];
|
|
3565
5227
|
this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
3566
5228
|
return this.staticModels(provider);
|
|
3567
5229
|
}
|
|
3568
5230
|
}
|
|
3569
5231
|
async resolveModel(provider, model) {
|
|
5232
|
+
const pool = this.options.pool?.();
|
|
5233
|
+
if (pool !== void 0 && await pool.owns(provider, model)) return pool.resolveModel(provider, model);
|
|
5234
|
+
return this.resolveOwnModel(provider, model);
|
|
5235
|
+
}
|
|
5236
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
5237
|
+
async resolveOwnModel(provider, model) {
|
|
3570
5238
|
const disc = await this.discovered(model);
|
|
3571
5239
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
3572
|
-
const reasoning = disc?.reasoning;
|
|
5240
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), disc?.reasoning);
|
|
3573
5241
|
return {
|
|
3574
5242
|
provider,
|
|
3575
5243
|
id: model,
|
|
@@ -3581,15 +5249,30 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
3581
5249
|
};
|
|
3582
5250
|
}
|
|
3583
5251
|
async *stream(options) {
|
|
5252
|
+
const pool = this.options.pool?.();
|
|
5253
|
+
if (pool !== void 0 && await pool.owns(options.provider, options.model)) {
|
|
5254
|
+
yield* pool.stream(options);
|
|
5255
|
+
return;
|
|
5256
|
+
}
|
|
5257
|
+
yield* this.streamCore(options);
|
|
5258
|
+
}
|
|
5259
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
5260
|
+
streamAccount(options, account) {
|
|
5261
|
+
return this.streamCore(options, account);
|
|
5262
|
+
}
|
|
5263
|
+
async *streamCore(options, account) {
|
|
3584
5264
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
3585
5265
|
try {
|
|
3586
|
-
let session = await this.options.tokens.session();
|
|
5266
|
+
let session = await this.options.tokens.session(account);
|
|
3587
5267
|
let response = await this.request(options, session, watchdog.signal);
|
|
3588
5268
|
if (response.status === 401) {
|
|
3589
|
-
session = await this.options.tokens.session(true);
|
|
5269
|
+
session = await this.options.tokens.session(account, true);
|
|
3590
5270
|
response = await this.request(options, session, watchdog.signal);
|
|
3591
5271
|
}
|
|
3592
|
-
if (!response.ok) throw await httpLlmError(response, "claude API"
|
|
5272
|
+
if (!response.ok) throw await httpLlmError(response, "claude API", {
|
|
5273
|
+
rateLimitReset: claudeRateLimitReset,
|
|
5274
|
+
...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn }
|
|
5275
|
+
});
|
|
3593
5276
|
if (response.body === null) throw new LlmError("claude API returned no response body", EMPTY_RESPONSE_CODE);
|
|
3594
5277
|
yield* streamAnthropic(response.body, () => {
|
|
3595
5278
|
watchdog.pulse();
|
|
@@ -3656,6 +5339,24 @@ const GROK_CONTEXT_WINDOW = 256e3;
|
|
|
3656
5339
|
const GROK_DEFAULT_MAX_TOKENS = 32e3;
|
|
3657
5340
|
/** Refresh when the access token has less than this much life left. */
|
|
3658
5341
|
const GROK_PREEMPT_MS = 2 * 6e4;
|
|
5342
|
+
/** Body fields xAI uses to name a delay or reset. */
|
|
5343
|
+
const GROK_RESET_FIELDS = [
|
|
5344
|
+
"retry_after",
|
|
5345
|
+
"retry_after_seconds",
|
|
5346
|
+
"resets_at",
|
|
5347
|
+
"reset_at"
|
|
5348
|
+
];
|
|
5349
|
+
/**
|
|
5350
|
+
* Reads the reset instant of the xAI window that rejected a request.
|
|
5351
|
+
*
|
|
5352
|
+
* Body only. xAI serves the OpenAI-compatible `x-ratelimit-reset-*` family,
|
|
5353
|
+
* whose values are rollover durations (`6m0s`) present on every response, one
|
|
5354
|
+
* per bucket — on a 429 the earliest of them is usually a bucket with room
|
|
5355
|
+
* (`0s` for the request bucket while the token bucket is the one exhausted),
|
|
5356
|
+
* which would burn the whole retry budget in seconds. They reach the operator
|
|
5357
|
+
* through `rateLimitDiagnostics` instead.
|
|
5358
|
+
*/
|
|
5359
|
+
const grokRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), GROK_RESET_FIELDS, now);
|
|
3659
5360
|
/** A discovered URL must be https on x.ai or a subdomain; anything else is a hostile document. */
|
|
3660
5361
|
function assertXaiEndpoint(url, field) {
|
|
3661
5362
|
let parsed;
|
|
@@ -3918,15 +5619,19 @@ function grokCliReasoning(entry) {
|
|
|
3918
5619
|
* Fetch the CLI catalog and index its per-model metadata by model id.
|
|
3919
5620
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
3920
5621
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
5622
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
3921
5623
|
* @returns model id → contributed metadata.
|
|
3922
5624
|
*/
|
|
3923
|
-
async function fetchGrokCliCatalog(session, fetchFn = proxiedFetch) {
|
|
3924
|
-
const response = await fetchFn(GROK_CLI_MODELS_URL, {
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
5625
|
+
async function fetchGrokCliCatalog(session, fetchFn = proxiedFetch, signal) {
|
|
5626
|
+
const response = await fetchFn(GROK_CLI_MODELS_URL, {
|
|
5627
|
+
headers: {
|
|
5628
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
5629
|
+
"x-xai-token-auth": "xai-grok-cli",
|
|
5630
|
+
"accept": "application/json",
|
|
5631
|
+
...attributionHeaders()
|
|
5632
|
+
},
|
|
5633
|
+
...signal === void 0 ? {} : { signal }
|
|
5634
|
+
});
|
|
3930
5635
|
if (!response.ok) throw await oauthEndpointError(response, "grok CLI catalog");
|
|
3931
5636
|
const payload = await response.json();
|
|
3932
5637
|
if (!Array.isArray(payload.data)) throw new Error("grok CLI catalog returned no data array");
|
|
@@ -3979,15 +5684,20 @@ function grokPriorMeta(prior) {
|
|
|
3979
5684
|
* @param onWarn - warning sink for a failed CLI catalog fetch.
|
|
3980
5685
|
* @param previous - last-known catalog used to keep enrichment when the CLI
|
|
3981
5686
|
* catalog is down or omits a model.
|
|
5687
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
3982
5688
|
* @returns discovered chat models in endpoint order.
|
|
3983
5689
|
*/
|
|
3984
|
-
async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, previous) {
|
|
5690
|
+
async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, previous, signal) {
|
|
3985
5691
|
const previousById = previous === void 0 || previous.length === 0 ? void 0 : new Map(previous.map((model) => [model.id, model]));
|
|
3986
|
-
const [response, cliCatalog] = await Promise.all([fetchFn(GROK_MODELS_URL, {
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
5692
|
+
const [response, cliCatalog] = await Promise.all([fetchFn(GROK_MODELS_URL, {
|
|
5693
|
+
headers: {
|
|
5694
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
5695
|
+
"accept": "application/json",
|
|
5696
|
+
...attributionHeaders()
|
|
5697
|
+
},
|
|
5698
|
+
...signal === void 0 ? {} : { signal }
|
|
5699
|
+
}), fetchGrokCliCatalog(session, fetchFn, signal).catch((error) => {
|
|
5700
|
+
if (isDiscoveryAborted(error, signal)) throw error;
|
|
3991
5701
|
onWarn?.(previousById === void 0 ? `grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})` : `grok CLI catalog fetch failed; keeping last-known reasoning efforts (${errorChain(error)})`);
|
|
3992
5702
|
})]);
|
|
3993
5703
|
if (!response.ok) throw await oauthEndpointError(response, "grok models");
|
|
@@ -4012,14 +5722,44 @@ async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, previous
|
|
|
4012
5722
|
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
4013
5723
|
var GrokAdapter = class extends LlmAdapter {
|
|
4014
5724
|
catalog;
|
|
5725
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
5726
|
+
accountCatalogs = /* @__PURE__ */ new Map();
|
|
5727
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
5728
|
+
catalogOwner;
|
|
4015
5729
|
constructor(options) {
|
|
4016
5730
|
super();
|
|
4017
5731
|
this.options = options;
|
|
4018
5732
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
4019
5733
|
}
|
|
4020
5734
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
4021
|
-
async fetchCatalog() {
|
|
4022
|
-
|
|
5735
|
+
async fetchCatalog(account, signal) {
|
|
5736
|
+
const lastKnown = account === void 0 || account === await this.options.tokens.defaultAccount() ? this.catalog.lastKnown() : this.accountCatalogs.get(account)?.lastKnown();
|
|
5737
|
+
return fetchGrokModels(await this.options.tokens.session(account), this.options.fetchFn, this.options.onWarn, lastKnown, signal);
|
|
5738
|
+
}
|
|
5739
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
5740
|
+
clearAccountCatalog(account) {
|
|
5741
|
+
if (account === void 0) this.accountCatalogs.clear();
|
|
5742
|
+
else this.accountCatalogs.delete(account);
|
|
5743
|
+
if (account === void 0 || this.catalogOwner === account || this.catalogOwner === void 0) {
|
|
5744
|
+
this.catalogOwner = void 0;
|
|
5745
|
+
this.catalog.invalidate();
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
5749
|
+
async catalogFor(account) {
|
|
5750
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
5751
|
+
const key = account ?? defaultKey;
|
|
5752
|
+
if (key === void 0 || key === defaultKey) {
|
|
5753
|
+
if (this.catalogOwner !== void 0 && this.catalogOwner !== defaultKey) this.catalog.invalidate();
|
|
5754
|
+
this.catalogOwner = defaultKey;
|
|
5755
|
+
return this.catalog;
|
|
5756
|
+
}
|
|
5757
|
+
let cache = this.accountCatalogs.get(key);
|
|
5758
|
+
if (cache === void 0) {
|
|
5759
|
+
cache = new ModelCatalogCache();
|
|
5760
|
+
this.accountCatalogs.set(key, cache);
|
|
5761
|
+
}
|
|
5762
|
+
return cache;
|
|
4023
5763
|
}
|
|
4024
5764
|
listed(provider, discovered) {
|
|
4025
5765
|
return discovered.map((model) => ({
|
|
@@ -4036,6 +5776,9 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
4036
5776
|
name: "Grok (Subscription)"
|
|
4037
5777
|
};
|
|
4038
5778
|
}
|
|
5779
|
+
providerRetryPolicy(provider) {
|
|
5780
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `grok: provider "${provider}" retryPolicy`);
|
|
5781
|
+
}
|
|
4039
5782
|
staticModels(provider) {
|
|
4040
5783
|
return this.options.models.map((model) => ({
|
|
4041
5784
|
provider,
|
|
@@ -4045,11 +5788,30 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
4045
5788
|
}));
|
|
4046
5789
|
}
|
|
4047
5790
|
async listModels(provider) {
|
|
4048
|
-
|
|
5791
|
+
const own = await this.listOwnModels(provider);
|
|
5792
|
+
const pool = this.options.pool?.();
|
|
5793
|
+
if (pool === void 0) return own;
|
|
5794
|
+
const extra = await pool.modelsForProvider(provider);
|
|
5795
|
+
const seen = new Set(own.map((model) => model.id));
|
|
5796
|
+
return [...own, ...extra.filter((model) => !seen.has(model.id))];
|
|
5797
|
+
}
|
|
5798
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
5799
|
+
async listOwnModels(provider, account, signal) {
|
|
5800
|
+
if (account === void 0) {
|
|
5801
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
5802
|
+
if (accounts.length === 0) return [];
|
|
5803
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), {
|
|
5804
|
+
timeoutMs: DISCOVERY_TIMEOUT_MS,
|
|
5805
|
+
...signal === void 0 ? {} : { signal }
|
|
5806
|
+
});
|
|
5807
|
+
}
|
|
5808
|
+
if (!await this.options.tokens.hasSession(account)) return [];
|
|
4049
5809
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
5810
|
+
const catalog = await this.catalogFor(account);
|
|
4050
5811
|
try {
|
|
4051
|
-
return this.listed(provider, await discoverOrRetryAuth((force) => this.options.tokens.session(force),
|
|
5812
|
+
return this.listed(provider, await discoverOrRetryAuth((force) => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal))));
|
|
4052
5813
|
} catch (error) {
|
|
5814
|
+
if (isDiscoveryAborted(error, signal)) throw error;
|
|
4053
5815
|
if (isMissingOrInvalidCredential(error)) return [];
|
|
4054
5816
|
this.options.onWarn?.(`grok model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
4055
5817
|
return this.staticModels(provider);
|
|
@@ -4065,11 +5827,20 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
4065
5827
|
*/
|
|
4066
5828
|
async discovered(model) {
|
|
4067
5829
|
if (!this.options.discovery) return void 0;
|
|
4068
|
-
return (await this.
|
|
5830
|
+
return discoverAcrossAccounts((await this.options.tokens.list()).map((entry) => entry.key), async (account) => {
|
|
5831
|
+
return (await (await this.catalogFor(account)).resolve(() => this.fetchCatalog(account)))?.find((entry) => entry.id === model);
|
|
5832
|
+
});
|
|
4069
5833
|
}
|
|
4070
5834
|
async resolveModel(provider, model) {
|
|
5835
|
+
const pool = this.options.pool?.();
|
|
5836
|
+
if (pool !== void 0 && await pool.owns(provider, model)) return pool.resolveModel(provider, model);
|
|
5837
|
+
return this.resolveOwnModel(provider, model);
|
|
5838
|
+
}
|
|
5839
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
5840
|
+
async resolveOwnModel(provider, model) {
|
|
4071
5841
|
const discovered = await this.discovered(model);
|
|
4072
5842
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
5843
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning);
|
|
4073
5844
|
return {
|
|
4074
5845
|
provider,
|
|
4075
5846
|
id: model,
|
|
@@ -4078,19 +5849,34 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
4078
5849
|
inputModalities: configured?.inputModalities ?? grokModalities(model),
|
|
4079
5850
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
4080
5851
|
defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
|
|
4081
|
-
...
|
|
5852
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
4082
5853
|
};
|
|
4083
5854
|
}
|
|
4084
5855
|
async *stream(options) {
|
|
5856
|
+
const pool = this.options.pool?.();
|
|
5857
|
+
if (pool !== void 0 && await pool.owns(options.provider, options.model)) {
|
|
5858
|
+
yield* pool.stream(options);
|
|
5859
|
+
return;
|
|
5860
|
+
}
|
|
5861
|
+
yield* this.streamCore(options);
|
|
5862
|
+
}
|
|
5863
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
5864
|
+
streamAccount(options, account) {
|
|
5865
|
+
return this.streamCore(options, account);
|
|
5866
|
+
}
|
|
5867
|
+
async *streamCore(options, account) {
|
|
4085
5868
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
4086
5869
|
try {
|
|
4087
|
-
let session = await this.options.tokens.session();
|
|
5870
|
+
let session = await this.options.tokens.session(account);
|
|
4088
5871
|
let response = await this.request(options, session, watchdog.signal);
|
|
4089
5872
|
if (response.status === 401) {
|
|
4090
|
-
session = await this.options.tokens.session(true);
|
|
5873
|
+
session = await this.options.tokens.session(account, true);
|
|
4091
5874
|
response = await this.request(options, session, watchdog.signal);
|
|
4092
5875
|
}
|
|
4093
|
-
if (!response.ok) throw await httpLlmError(response, "grok API"
|
|
5876
|
+
if (!response.ok) throw await httpLlmError(response, "grok API", {
|
|
5877
|
+
rateLimitReset: grokRateLimitReset,
|
|
5878
|
+
...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn }
|
|
5879
|
+
});
|
|
4094
5880
|
if (response.body === null) throw new LlmError("grok API returned no response body", EMPTY_RESPONSE_CODE);
|
|
4095
5881
|
yield* streamResponses(response.body, () => {
|
|
4096
5882
|
watchdog.pulse();
|
|
@@ -4112,6 +5898,7 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
4112
5898
|
parallel_tool_calls: true,
|
|
4113
5899
|
...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
|
|
4114
5900
|
...options.reasoningEffort !== void 0 ? { reasoning: { effort: String(options.reasoningEffort) } } : {},
|
|
5901
|
+
...options.sessionId !== void 0 ? { prompt_cache_key: String(options.sessionId) } : {},
|
|
4115
5902
|
store: false,
|
|
4116
5903
|
stream: true
|
|
4117
5904
|
};
|
|
@@ -4274,7 +6061,7 @@ function closeBlock(block) {
|
|
|
4274
6061
|
};
|
|
4275
6062
|
case "tool-call": return {
|
|
4276
6063
|
type: "tool-call",
|
|
4277
|
-
id:
|
|
6064
|
+
id: ToolCallId(block.callId),
|
|
4278
6065
|
name: block.name ?? "",
|
|
4279
6066
|
arguments: block.text
|
|
4280
6067
|
};
|
|
@@ -4428,7 +6215,7 @@ var ChatCompletionsStreamTranslator = class {
|
|
|
4428
6215
|
chunks.push({
|
|
4429
6216
|
type: "tool-call-delta",
|
|
4430
6217
|
index: block.index,
|
|
4431
|
-
id:
|
|
6218
|
+
id: ToolCallId(block.callId),
|
|
4432
6219
|
...block.name === void 0 ? {} : { name: block.name },
|
|
4433
6220
|
argumentsDelta: ""
|
|
4434
6221
|
});
|
|
@@ -4438,7 +6225,7 @@ var ChatCompletionsStreamTranslator = class {
|
|
|
4438
6225
|
chunks.push({
|
|
4439
6226
|
type: "tool-call-delta",
|
|
4440
6227
|
index: block.index,
|
|
4441
|
-
id:
|
|
6228
|
+
id: ToolCallId(block.callId),
|
|
4442
6229
|
argumentsDelta: call.function.arguments
|
|
4443
6230
|
});
|
|
4444
6231
|
}
|
|
@@ -4694,14 +6481,18 @@ function copilotReasoning(entry) {
|
|
|
4694
6481
|
* reasoning efforts (the endpoint discloses no default, so none is claimed).
|
|
4695
6482
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
4696
6483
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
6484
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
4697
6485
|
* @returns discovered chat models in endpoint order.
|
|
4698
6486
|
*/
|
|
4699
|
-
async function fetchCopilotModels(session, fetchFn = proxiedFetch) {
|
|
4700
|
-
const response = await fetchFn(COPILOT_MODELS_URL, {
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
6487
|
+
async function fetchCopilotModels(session, fetchFn = proxiedFetch, signal) {
|
|
6488
|
+
const response = await fetchFn(COPILOT_MODELS_URL, {
|
|
6489
|
+
headers: {
|
|
6490
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
6491
|
+
"accept": "application/json",
|
|
6492
|
+
...copilotHeaders(false, await latestVsCodeVersion(fetchFn))
|
|
6493
|
+
},
|
|
6494
|
+
...signal === void 0 ? {} : { signal }
|
|
6495
|
+
});
|
|
4705
6496
|
if (!response.ok) throw await oauthEndpointError(response, "copilot models");
|
|
4706
6497
|
const payload = await response.json();
|
|
4707
6498
|
if (!Array.isArray(payload.data)) throw new Error("copilot models endpoint returned no data array");
|
|
@@ -4923,6 +6714,10 @@ var CopilotResponsesItemNormalizer = class {
|
|
|
4923
6714
|
/** Copilot wire adapter: one instance serves the `copilot` provider route. */
|
|
4924
6715
|
var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
4925
6716
|
catalog;
|
|
6717
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
6718
|
+
accountCatalogs = /* @__PURE__ */ new Map();
|
|
6719
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
6720
|
+
catalogOwner;
|
|
4926
6721
|
/**
|
|
4927
6722
|
* [2026-08-23]-[a reasoning model continuing a tool chain must get its
|
|
4928
6723
|
* reasoning back or it restarts from scratch every tool round trip; the
|
|
@@ -4945,8 +6740,33 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4945
6740
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
4946
6741
|
}
|
|
4947
6742
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
4948
|
-
async fetchCatalog() {
|
|
4949
|
-
return fetchCopilotModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
6743
|
+
async fetchCatalog(account, signal) {
|
|
6744
|
+
return fetchCopilotModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
6745
|
+
}
|
|
6746
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
6747
|
+
clearAccountCatalog(account) {
|
|
6748
|
+
if (account === void 0) this.accountCatalogs.clear();
|
|
6749
|
+
else this.accountCatalogs.delete(account);
|
|
6750
|
+
if (account === void 0 || this.catalogOwner === account || this.catalogOwner === void 0) {
|
|
6751
|
+
this.catalogOwner = void 0;
|
|
6752
|
+
this.catalog.invalidate();
|
|
6753
|
+
}
|
|
6754
|
+
}
|
|
6755
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
6756
|
+
async catalogFor(account) {
|
|
6757
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
6758
|
+
const key = account ?? defaultKey;
|
|
6759
|
+
if (key === void 0 || key === defaultKey) {
|
|
6760
|
+
if (this.catalogOwner !== void 0 && this.catalogOwner !== defaultKey) this.catalog.invalidate();
|
|
6761
|
+
this.catalogOwner = defaultKey;
|
|
6762
|
+
return this.catalog;
|
|
6763
|
+
}
|
|
6764
|
+
let cache = this.accountCatalogs.get(key);
|
|
6765
|
+
if (cache === void 0) {
|
|
6766
|
+
cache = new ModelCatalogCache();
|
|
6767
|
+
this.accountCatalogs.set(key, cache);
|
|
6768
|
+
}
|
|
6769
|
+
return cache;
|
|
4950
6770
|
}
|
|
4951
6771
|
providerInfo(provider) {
|
|
4952
6772
|
return {
|
|
@@ -4954,6 +6774,9 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4954
6774
|
name: "GitHub Copilot"
|
|
4955
6775
|
};
|
|
4956
6776
|
}
|
|
6777
|
+
providerRetryPolicy(provider) {
|
|
6778
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `copilot: provider "${provider}" retryPolicy`);
|
|
6779
|
+
}
|
|
4957
6780
|
staticModels(provider) {
|
|
4958
6781
|
return this.options.models.map((model) => ({
|
|
4959
6782
|
provider,
|
|
@@ -4963,10 +6786,28 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4963
6786
|
}));
|
|
4964
6787
|
}
|
|
4965
6788
|
async listModels(provider) {
|
|
4966
|
-
|
|
6789
|
+
const own = await this.listOwnModels(provider);
|
|
6790
|
+
const pool = this.options.pool?.();
|
|
6791
|
+
if (pool === void 0) return own;
|
|
6792
|
+
const extra = await pool.modelsForProvider(provider);
|
|
6793
|
+
const seen = new Set(own.map((model) => model.id));
|
|
6794
|
+
return [...own, ...extra.filter((model) => !seen.has(model.id))];
|
|
6795
|
+
}
|
|
6796
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
6797
|
+
async listOwnModels(provider, account, signal) {
|
|
6798
|
+
if (account === void 0) {
|
|
6799
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
6800
|
+
if (accounts.length === 0) return [];
|
|
6801
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), {
|
|
6802
|
+
timeoutMs: DISCOVERY_TIMEOUT_MS,
|
|
6803
|
+
...signal === void 0 ? {} : { signal }
|
|
6804
|
+
});
|
|
6805
|
+
}
|
|
6806
|
+
if (!await this.options.tokens.hasSession(account)) return [];
|
|
4967
6807
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
6808
|
+
const catalog = await this.catalogFor(account);
|
|
4968
6809
|
try {
|
|
4969
|
-
return (await discoverOrRetryAuth((force) => this.options.tokens.session(force),
|
|
6810
|
+
return (await discoverOrRetryAuth((force) => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)))).map((model) => ({
|
|
4970
6811
|
provider,
|
|
4971
6812
|
id: model.id,
|
|
4972
6813
|
name: model.name,
|
|
@@ -4974,6 +6815,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4974
6815
|
...model.inputModalities === void 0 ? {} : { inputModalities: model.inputModalities }
|
|
4975
6816
|
}));
|
|
4976
6817
|
} catch (error) {
|
|
6818
|
+
if (isDiscoveryAborted(error, signal)) throw error;
|
|
4977
6819
|
if (isMissingOrInvalidCredential(error)) return [];
|
|
4978
6820
|
this.options.onWarn?.(`copilot model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
4979
6821
|
return this.staticModels(provider);
|
|
@@ -4987,7 +6829,9 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4987
6829
|
*/
|
|
4988
6830
|
async discovered(model) {
|
|
4989
6831
|
if (!this.options.discovery) return void 0;
|
|
4990
|
-
return (await this.
|
|
6832
|
+
return discoverAcrossAccounts((await this.options.tokens.list()).map((entry) => entry.key), async (account) => {
|
|
6833
|
+
return (await (await this.catalogFor(account)).resolve(() => this.fetchCatalog(account)))?.find((entry) => entry.id === model);
|
|
6834
|
+
});
|
|
4991
6835
|
}
|
|
4992
6836
|
/**
|
|
4993
6837
|
* [2026-08-23]-[a manually configured responses-only model combined with
|
|
@@ -5087,8 +6931,15 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
5087
6931
|
this.replayByScope.clear();
|
|
5088
6932
|
}
|
|
5089
6933
|
async resolveModel(provider, model) {
|
|
6934
|
+
const pool = this.options.pool?.();
|
|
6935
|
+
if (pool !== void 0 && await pool.owns(provider, model)) return pool.resolveModel(provider, model);
|
|
6936
|
+
return this.resolveOwnModel(provider, model);
|
|
6937
|
+
}
|
|
6938
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
6939
|
+
async resolveOwnModel(provider, model) {
|
|
5090
6940
|
const discovered = await this.discovered(model);
|
|
5091
6941
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
6942
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning);
|
|
5092
6943
|
return {
|
|
5093
6944
|
provider,
|
|
5094
6945
|
id: model,
|
|
@@ -5097,22 +6948,34 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
5097
6948
|
inputModalities: discovered?.inputModalities ?? configured?.inputModalities ?? ["text"],
|
|
5098
6949
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? COPILOT_CONTEXT_WINDOW },
|
|
5099
6950
|
defaultMaxTokens: configured?.maxTokens ?? COPILOT_DEFAULT_MAX_TOKENS,
|
|
5100
|
-
...
|
|
6951
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
5101
6952
|
};
|
|
5102
6953
|
}
|
|
5103
6954
|
async *stream(options) {
|
|
6955
|
+
const pool = this.options.pool?.();
|
|
6956
|
+
if (pool !== void 0 && await pool.owns(options.provider, options.model)) {
|
|
6957
|
+
yield* pool.stream(options);
|
|
6958
|
+
return;
|
|
6959
|
+
}
|
|
6960
|
+
yield* this.streamCore(options);
|
|
6961
|
+
}
|
|
6962
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
6963
|
+
streamAccount(options, account) {
|
|
6964
|
+
return this.streamCore(options, account);
|
|
6965
|
+
}
|
|
6966
|
+
async *streamCore(options, account) {
|
|
5104
6967
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
5105
6968
|
try {
|
|
5106
6969
|
const wire = copilotRequestWire(this.configuredWireEntry(options.model) ?? await this.discovered(options.model), options);
|
|
5107
|
-
let session = await this.options.tokens.session();
|
|
6970
|
+
let session = await this.options.tokens.session(account);
|
|
5108
6971
|
const scope = this.replayScope(session.refreshToken, options);
|
|
5109
6972
|
let response = await this.request(options, session, watchdog.signal, wire, scope);
|
|
5110
6973
|
if (response.status === 401) {
|
|
5111
6974
|
await latestVsCodeVersion(this.options.fetchFn ?? proxiedFetch, true);
|
|
5112
|
-
session = await this.options.tokens.session(true);
|
|
6975
|
+
session = await this.options.tokens.session(account, true);
|
|
5113
6976
|
response = await this.request(options, session, watchdog.signal, wire, scope);
|
|
5114
6977
|
}
|
|
5115
|
-
if (!response.ok) throw await httpLlmError(response, "copilot API");
|
|
6978
|
+
if (!response.ok) throw await httpLlmError(response, "copilot API", { ...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn } });
|
|
5116
6979
|
if (response.body === null) throw new LlmError("copilot API returned no response body", EMPTY_RESPONSE_CODE);
|
|
5117
6980
|
const pulse = () => {
|
|
5118
6981
|
watchdog.pulse();
|
|
@@ -5435,13 +7298,13 @@ function truncate$1(text, max = 60) {
|
|
|
5435
7298
|
* image input; any resolution failure means "no".
|
|
5436
7299
|
*/
|
|
5437
7300
|
async function routeDeclaresImageInput(resolveLlm, exec) {
|
|
5438
|
-
const llm = resolveLlm?.();
|
|
7301
|
+
const llm$1 = resolveLlm?.();
|
|
5439
7302
|
const routed = exec.agent?.session.requestHeader()?.config;
|
|
5440
7303
|
const provider = routed?.provider ?? exec.agent?.options.provider;
|
|
5441
7304
|
const model = routed?.model ?? exec.agent?.options.model;
|
|
5442
|
-
if (llm === void 0 || provider === void 0 || model === void 0) return false;
|
|
7305
|
+
if (llm$1 === void 0 || provider === void 0 || model === void 0) return false;
|
|
5443
7306
|
try {
|
|
5444
|
-
return (await llm.resolveModelInfo(provider, model, exec.signal)).inputModalities?.includes("image") === true;
|
|
7307
|
+
return (await llm$1.resolveModelInfo(provider, model, exec.signal)).inputModalities?.includes("image") === true;
|
|
5445
7308
|
} catch {
|
|
5446
7309
|
return false;
|
|
5447
7310
|
}
|
|
@@ -5901,6 +7764,8 @@ const name = "dsh-plugin-subscriptions";
|
|
|
5901
7764
|
const inject = ["llm"];
|
|
5902
7765
|
/** Default maximum provider idle time while one stream read is outstanding. */
|
|
5903
7766
|
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
7767
|
+
/** Bound on one pool quota poll — member selection must not hang on a usage endpoint. */
|
|
7768
|
+
const POOL_USAGE_TIMEOUT_MS = DISCOVERY_TIMEOUT_MS;
|
|
5904
7769
|
const providerIdSchema = z.union([
|
|
5905
7770
|
"codex",
|
|
5906
7771
|
"claude",
|
|
@@ -5915,6 +7780,11 @@ const modelEntrySchema = z.object({
|
|
|
5915
7780
|
inputModalities: z.array(z.union(["text", "image"])),
|
|
5916
7781
|
wire: z.union(["chat-completions", "responses"])
|
|
5917
7782
|
});
|
|
7783
|
+
const poolMemberSchema = z.object({
|
|
7784
|
+
provider: providerIdSchema.required(),
|
|
7785
|
+
account: z.string(),
|
|
7786
|
+
model: z.string().required()
|
|
7787
|
+
});
|
|
5918
7788
|
const Config = z.object({
|
|
5919
7789
|
providers: z.array(providerIdSchema).default([
|
|
5920
7790
|
"codex",
|
|
@@ -5923,11 +7793,24 @@ const Config = z.object({
|
|
|
5923
7793
|
"copilot"
|
|
5924
7794
|
]),
|
|
5925
7795
|
streamIdleTimeoutMs: z.number().min(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
7796
|
+
rateLimit: z.object({
|
|
7797
|
+
wait: z.boolean().default(true),
|
|
7798
|
+
maxWaitMs: z.number().min(1).default(DEFAULT_RATE_LIMIT_MAX_WAIT_MS)
|
|
7799
|
+
}),
|
|
5926
7800
|
models: z.object({
|
|
5927
7801
|
codex: z.array(modelEntrySchema),
|
|
5928
7802
|
claude: z.array(modelEntrySchema),
|
|
5929
7803
|
grok: z.array(modelEntrySchema),
|
|
5930
7804
|
copilot: z.array(modelEntrySchema)
|
|
7805
|
+
}),
|
|
7806
|
+
pool: z.object({
|
|
7807
|
+
enabled: z.boolean().default(true),
|
|
7808
|
+
strategy: z.union(["priority", "quota_aware"]).default("quota_aware"),
|
|
7809
|
+
switchMargin: z.number().min(1).default(2),
|
|
7810
|
+
autoAccounts: z.boolean().default(true),
|
|
7811
|
+
autoFamilies: z.boolean(),
|
|
7812
|
+
families: z.dict(z.array(poolMemberSchema)),
|
|
7813
|
+
tiers: z.dict(z.array(poolMemberSchema))
|
|
5931
7814
|
})
|
|
5932
7815
|
});
|
|
5933
7816
|
/** Built-in catalogs used when the config does not override a provider's models. */
|
|
@@ -6034,6 +7917,15 @@ function accountOf(provider, session) {
|
|
|
6034
7917
|
case "copilot": return session.account;
|
|
6035
7918
|
}
|
|
6036
7919
|
}
|
|
7920
|
+
/** The plan name a stored session carries, when the provider told us. */
|
|
7921
|
+
function planOf(provider, session) {
|
|
7922
|
+
switch (provider) {
|
|
7923
|
+
case "codex": return session.planType;
|
|
7924
|
+
case "claude": return session.subscriptionType;
|
|
7925
|
+
case "grok": return;
|
|
7926
|
+
case "copilot": return;
|
|
7927
|
+
}
|
|
7928
|
+
}
|
|
6037
7929
|
/**
|
|
6038
7930
|
* Auth operations behind the `/subscriptions-auth` RPC channel: start/complete
|
|
6039
7931
|
* OAuth attempts in the background, feed pasted codes, cancel, log out, and
|
|
@@ -6067,18 +7959,20 @@ var SubscriptionsAuthController = class {
|
|
|
6067
7959
|
* cannot be read off the flow manager.
|
|
6068
7960
|
*/
|
|
6069
7961
|
claims = /* @__PURE__ */ new Map();
|
|
6070
|
-
constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials) {
|
|
7962
|
+
constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials, poolUsage = void 0) {
|
|
6071
7963
|
this.flows = flows;
|
|
6072
7964
|
this.deviceFlows = deviceFlows;
|
|
6073
7965
|
this.onAuthChanged = onAuthChanged;
|
|
6074
7966
|
this.resolveAttachments = resolveAttachments;
|
|
6075
7967
|
this.usageFetchers = usageFetchers;
|
|
6076
7968
|
this.readClaudeCreds = readClaudeCreds;
|
|
7969
|
+
this.poolUsage = poolUsage;
|
|
6077
7970
|
}
|
|
6078
|
-
usage(provider, signal) {
|
|
7971
|
+
usage(provider, account, signal, force = false) {
|
|
6079
7972
|
const fetcher = this.usageFetchers[provider];
|
|
6080
7973
|
if (fetcher === void 0) return Promise.resolve({ supported: false });
|
|
6081
|
-
return fetcher(signal);
|
|
7974
|
+
if (this.poolUsage === void 0) return fetcher(account, signal);
|
|
7975
|
+
return this.poolUsage.snapshotFor(provider, account, force);
|
|
6082
7976
|
}
|
|
6083
7977
|
async readImage(ref, signal) {
|
|
6084
7978
|
const attachments = this.resolveAttachments();
|
|
@@ -6096,28 +7990,45 @@ var SubscriptionsAuthController = class {
|
|
|
6096
7990
|
};
|
|
6097
7991
|
}
|
|
6098
7992
|
async status(provider) {
|
|
6099
|
-
const
|
|
6100
|
-
const account = accountOf(provider, session);
|
|
7993
|
+
const entries = await listAccounts(provider);
|
|
6101
7994
|
const detail = this.lastError.get(provider);
|
|
6102
7995
|
return {
|
|
6103
|
-
loggedIn: session !== void 0,
|
|
6104
7996
|
busy: this.flows.isBusy(provider) || this.deviceFlows.isBusy(provider) || this.finalizing.has(provider),
|
|
6105
|
-
|
|
6106
|
-
|
|
7997
|
+
accounts: entries.map(({ key, session }, index) => {
|
|
7998
|
+
const account = accountOf(provider, session);
|
|
7999
|
+
const plan = planOf(provider, session);
|
|
8000
|
+
return {
|
|
8001
|
+
key,
|
|
8002
|
+
isDefault: index === 0,
|
|
8003
|
+
expiresAt: session.expiresAt,
|
|
8004
|
+
...account === void 0 ? {} : { account },
|
|
8005
|
+
...plan === void 0 ? {} : { plan }
|
|
8006
|
+
};
|
|
8007
|
+
}),
|
|
6107
8008
|
...detail === void 0 ? {} : { detail }
|
|
6108
8009
|
};
|
|
6109
8010
|
}
|
|
6110
|
-
async login(provider) {
|
|
6111
|
-
if (provider === "claude") {
|
|
8011
|
+
async login(provider, method) {
|
|
8012
|
+
if (provider === "claude" && method !== "oauth") {
|
|
6112
8013
|
const imported = this.readClaudeCreds();
|
|
6113
8014
|
if (imported !== void 0) {
|
|
6114
8015
|
this.claim("claude");
|
|
6115
8016
|
this.flows.pending("claude")?.cancel();
|
|
6116
|
-
|
|
8017
|
+
const session = {
|
|
8018
|
+
...imported,
|
|
8019
|
+
keychainBound: true
|
|
8020
|
+
};
|
|
8021
|
+
await this.persist("claude", session);
|
|
6117
8022
|
this.lastError.delete("claude");
|
|
6118
|
-
this.onAuthChanged("claude");
|
|
8023
|
+
this.onAuthChanged("claude", accountKeyOf("claude", session));
|
|
6119
8024
|
return { authorizeUrl: "" };
|
|
6120
8025
|
}
|
|
8026
|
+
if (method === "keychain") throw new Error("no Claude Code credentials found; run `claude` and log in first, or choose the browser flow");
|
|
8027
|
+
const attempt$1 = await this.flows.start("claude", claudeFlow);
|
|
8028
|
+
this.completions.set("claude", this.complete("claude", attempt$1, this.claim("claude")));
|
|
8029
|
+
return { authorizeUrl: attempt$1.authorizeUrl };
|
|
8030
|
+
}
|
|
8031
|
+
if (provider === "claude") {
|
|
6121
8032
|
const attempt$1 = await this.flows.start("claude", claudeFlow);
|
|
6122
8033
|
this.completions.set("claude", this.complete("claude", attempt$1, this.claim("claude")));
|
|
6123
8034
|
return { authorizeUrl: attempt$1.authorizeUrl };
|
|
@@ -6159,7 +8070,7 @@ var SubscriptionsAuthController = class {
|
|
|
6159
8070
|
if (this.claims.get(provider) !== claim) return;
|
|
6160
8071
|
await this.persist(provider, session);
|
|
6161
8072
|
this.lastError.delete(provider);
|
|
6162
|
-
this.onAuthChanged(provider);
|
|
8073
|
+
this.onAuthChanged(provider, accountKeyOf(provider, session));
|
|
6163
8074
|
} catch (error) {
|
|
6164
8075
|
if (this.claims.get(provider) !== claim) return;
|
|
6165
8076
|
if (!(error instanceof Error && error.message === "login cancelled")) this.lastError.set(provider, errorChain(error));
|
|
@@ -6171,7 +8082,7 @@ var SubscriptionsAuthController = class {
|
|
|
6171
8082
|
const session = await completeCopilotLogin(await attempt.waitToken());
|
|
6172
8083
|
await this.persist(provider, session);
|
|
6173
8084
|
this.lastError.delete(provider);
|
|
6174
|
-
this.onAuthChanged(provider);
|
|
8085
|
+
this.onAuthChanged(provider, accountKeyOf(provider, session));
|
|
6175
8086
|
} catch (error) {
|
|
6176
8087
|
if (!(error instanceof Error && error.message === "login cancelled")) this.lastError.set(provider, errorChain(error));
|
|
6177
8088
|
} finally {
|
|
@@ -6187,12 +8098,7 @@ var SubscriptionsAuthController = class {
|
|
|
6187
8098
|
}
|
|
6188
8099
|
}
|
|
6189
8100
|
persist(provider, session) {
|
|
6190
|
-
|
|
6191
|
-
case "codex": return saveSession("codex", session);
|
|
6192
|
-
case "claude": return saveSession("claude", session);
|
|
6193
|
-
case "grok": return saveSession("grok", session);
|
|
6194
|
-
case "copilot": return saveSession("copilot", session);
|
|
6195
|
-
}
|
|
8101
|
+
return saveAccountSession(provider, accountKeyOf(provider, session), session);
|
|
6196
8102
|
}
|
|
6197
8103
|
/**
|
|
6198
8104
|
* Settle once no OAuth completion is running for a provider.
|
|
@@ -6216,19 +8122,24 @@ var SubscriptionsAuthController = class {
|
|
|
6216
8122
|
this.deviceFlows.pending(provider)?.cancel();
|
|
6217
8123
|
return Promise.resolve();
|
|
6218
8124
|
}
|
|
6219
|
-
async logout(provider) {
|
|
8125
|
+
async logout(provider, account) {
|
|
6220
8126
|
this.claim(provider);
|
|
6221
8127
|
this.flows.pending(provider)?.cancel();
|
|
6222
8128
|
this.deviceFlows.pending(provider)?.cancel();
|
|
6223
|
-
await
|
|
8129
|
+
await deleteAccountSession(provider, account);
|
|
6224
8130
|
this.lastError.delete(provider);
|
|
6225
|
-
this.onAuthChanged(provider);
|
|
8131
|
+
this.onAuthChanged(provider, account);
|
|
8132
|
+
}
|
|
8133
|
+
async setDefault(provider, account) {
|
|
8134
|
+
await setDefaultAccount(provider, account);
|
|
8135
|
+
this.onAuthChanged(provider, account);
|
|
6226
8136
|
}
|
|
6227
8137
|
};
|
|
6228
8138
|
function apply(ctx, config) {
|
|
6229
8139
|
const providers = [...new Set(config.providers ?? [...PROVIDER_IDS])];
|
|
6230
8140
|
const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
|
|
6231
8141
|
if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0) throw new Error(`${name}: streamIdleTimeoutMs must be a positive finite number`);
|
|
8142
|
+
const rateLimit = resolveRateLimitWait(config.rateLimit, `${name}: rateLimit`);
|
|
6232
8143
|
const catalog = resolveCatalog(config.models);
|
|
6233
8144
|
const overridden = new Set(PROVIDER_IDS.filter((provider) => (config.models?.[provider]?.length ?? 0) > 0));
|
|
6234
8145
|
const flows = new OAuthFlowManager();
|
|
@@ -6238,10 +8149,20 @@ function apply(ctx, config) {
|
|
|
6238
8149
|
};
|
|
6239
8150
|
const resolveAttachments = () => ctx.get("attachments");
|
|
6240
8151
|
const handles = /* @__PURE__ */ new Map();
|
|
6241
|
-
const
|
|
8152
|
+
const adapters = /* @__PURE__ */ new Map();
|
|
8153
|
+
const accountTokens = /* @__PURE__ */ new Map();
|
|
8154
|
+
let poolHealth;
|
|
8155
|
+
let poolUsage;
|
|
8156
|
+
let poolAdapter;
|
|
8157
|
+
const authChanged = (provider, account) => {
|
|
6242
8158
|
if (provider === "copilot") copilotAdapter?.clearReplayState();
|
|
6243
|
-
|
|
8159
|
+
adapters.get(provider)?.clearAccountCatalog(account);
|
|
8160
|
+
poolHealth?.clear(provider, account);
|
|
8161
|
+
poolUsage?.invalidate(provider, account);
|
|
8162
|
+
poolAdapter?.invalidate();
|
|
8163
|
+
for (const [route, handle] of handles) handle.replace([route]);
|
|
6244
8164
|
};
|
|
8165
|
+
loadModelDefaults();
|
|
6245
8166
|
let codexTokens;
|
|
6246
8167
|
let claudeTokens;
|
|
6247
8168
|
let grokTokens;
|
|
@@ -6251,115 +8172,199 @@ function apply(ctx, config) {
|
|
|
6251
8172
|
let copilotAdapter;
|
|
6252
8173
|
for (const provider of providers) switch (provider) {
|
|
6253
8174
|
case "codex": {
|
|
6254
|
-
const tokens = new
|
|
8175
|
+
const tokens = new AccountTokenManager({
|
|
8176
|
+
provider: "codex",
|
|
6255
8177
|
displayName: "ChatGPT (Codex)",
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
authChanged("codex");
|
|
8178
|
+
makeOptions: () => ({
|
|
8179
|
+
preemptMs: CODEX_PREEMPT_MS,
|
|
8180
|
+
refresh: refreshCodex,
|
|
8181
|
+
isPermanent: isCodexPermanentRefreshError
|
|
8182
|
+
}),
|
|
8183
|
+
onAccountRemoved: (account) => {
|
|
8184
|
+
authChanged("codex", account);
|
|
6264
8185
|
}
|
|
6265
8186
|
});
|
|
6266
8187
|
codexTokens = tokens;
|
|
6267
|
-
|
|
8188
|
+
accountTokens.set("codex", tokens);
|
|
8189
|
+
usageFetchers.codex = async (account, signal) => fetchCodexUsage(await tokens.session(account), proxiedFetch, signal);
|
|
6268
8190
|
let adapter;
|
|
6269
8191
|
adapter = new CodexAdapter({
|
|
6270
8192
|
models: catalog.codex,
|
|
6271
8193
|
streamIdleTimeoutMs,
|
|
8194
|
+
rateLimit,
|
|
6272
8195
|
tokens,
|
|
6273
8196
|
discovery: !overridden.has("codex"),
|
|
6274
8197
|
onWarn,
|
|
6275
8198
|
resolveAttachments,
|
|
6276
8199
|
catalogStore: catalogStore("codex"),
|
|
8200
|
+
defaultEffortOf: (model) => defaultEffortOf("codex", model),
|
|
8201
|
+
pool: () => poolAdapter,
|
|
6277
8202
|
speedFor: (sessionId, model) => sessionId !== void 0 && speedBySession.get(sessionId) === "fast" && adapter.supportsFastTier(model)
|
|
6278
8203
|
});
|
|
6279
8204
|
codexAdapter = adapter;
|
|
8205
|
+
adapters.set("codex", adapter);
|
|
6280
8206
|
handles.set("codex", ctx.llm.registerAdapter(["codex"], adapter));
|
|
6281
8207
|
break;
|
|
6282
8208
|
}
|
|
6283
8209
|
case "claude": {
|
|
6284
|
-
const tokens = new
|
|
8210
|
+
const tokens = new AccountTokenManager({
|
|
8211
|
+
provider: "claude",
|
|
6285
8212
|
displayName: "Claude (Subscription)",
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
authChanged("claude");
|
|
8213
|
+
makeOptions: () => ({
|
|
8214
|
+
preemptMs: CLAUDE_PREEMPT_MS,
|
|
8215
|
+
refresh: (session) => session.keychainBound === true ? refreshClaudeSynced(session, refreshClaude) : refreshClaude(session),
|
|
8216
|
+
isPermanent: isClaudePermanentRefreshError
|
|
8217
|
+
}),
|
|
8218
|
+
onAccountRemoved: (account) => {
|
|
8219
|
+
authChanged("claude", account);
|
|
6294
8220
|
}
|
|
6295
8221
|
});
|
|
6296
8222
|
claudeTokens = tokens;
|
|
6297
|
-
|
|
6298
|
-
|
|
8223
|
+
accountTokens.set("claude", tokens);
|
|
8224
|
+
usageFetchers.claude = async (account, signal) => fetchClaudeUsage(await tokens.session(account), proxiedFetch, signal);
|
|
8225
|
+
const adapter = new ClaudeAdapter({
|
|
6299
8226
|
models: catalog.claude,
|
|
6300
8227
|
streamIdleTimeoutMs,
|
|
8228
|
+
rateLimit,
|
|
6301
8229
|
tokens,
|
|
6302
8230
|
discovery: !overridden.has("claude"),
|
|
6303
8231
|
onWarn,
|
|
6304
|
-
maxRetries: 10,
|
|
6305
8232
|
resolveAttachments,
|
|
6306
|
-
catalogStore: catalogStore("claude")
|
|
6307
|
-
|
|
8233
|
+
catalogStore: catalogStore("claude"),
|
|
8234
|
+
defaultEffortOf: (model) => defaultEffortOf("claude", model),
|
|
8235
|
+
pool: () => poolAdapter
|
|
8236
|
+
});
|
|
8237
|
+
adapters.set("claude", adapter);
|
|
8238
|
+
handles.set("claude", ctx.llm.registerAdapter(["claude"], adapter));
|
|
6308
8239
|
break;
|
|
6309
8240
|
}
|
|
6310
8241
|
case "grok": {
|
|
6311
|
-
const tokens = new
|
|
8242
|
+
const tokens = new AccountTokenManager({
|
|
8243
|
+
provider: "grok",
|
|
6312
8244
|
displayName: "Grok (Subscription)",
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
authChanged("grok");
|
|
8245
|
+
makeOptions: () => ({
|
|
8246
|
+
preemptMs: GROK_PREEMPT_MS,
|
|
8247
|
+
refresh: refreshGrok,
|
|
8248
|
+
isPermanent: isGrokPermanentRefreshError
|
|
8249
|
+
}),
|
|
8250
|
+
onAccountRemoved: (account) => {
|
|
8251
|
+
authChanged("grok", account);
|
|
6321
8252
|
}
|
|
6322
8253
|
});
|
|
6323
8254
|
grokTokens = tokens;
|
|
6324
|
-
|
|
6325
|
-
|
|
8255
|
+
accountTokens.set("grok", tokens);
|
|
8256
|
+
usageFetchers.grok = async (account, signal) => fetchGrokUsage(await tokens.session(account), proxiedFetch, signal);
|
|
8257
|
+
const adapter = new GrokAdapter({
|
|
6326
8258
|
models: catalog.grok,
|
|
6327
8259
|
streamIdleTimeoutMs,
|
|
8260
|
+
rateLimit,
|
|
6328
8261
|
tokens,
|
|
6329
8262
|
discovery: !overridden.has("grok"),
|
|
6330
8263
|
onWarn,
|
|
6331
8264
|
resolveAttachments,
|
|
6332
|
-
catalogStore: catalogStore("grok")
|
|
6333
|
-
|
|
8265
|
+
catalogStore: catalogStore("grok"),
|
|
8266
|
+
defaultEffortOf: (model) => defaultEffortOf("grok", model),
|
|
8267
|
+
pool: () => poolAdapter
|
|
8268
|
+
});
|
|
8269
|
+
adapters.set("grok", adapter);
|
|
8270
|
+
handles.set("grok", ctx.llm.registerAdapter(["grok"], adapter));
|
|
6334
8271
|
break;
|
|
6335
8272
|
}
|
|
6336
8273
|
case "copilot": {
|
|
6337
|
-
const tokens = new
|
|
8274
|
+
const tokens = new AccountTokenManager({
|
|
8275
|
+
provider: "copilot",
|
|
6338
8276
|
displayName: "GitHub Copilot",
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
authChanged("copilot");
|
|
8277
|
+
makeOptions: () => ({
|
|
8278
|
+
preemptMs: COPILOT_PREEMPT_MS,
|
|
8279
|
+
refresh: refreshCopilot,
|
|
8280
|
+
isPermanent: isCopilotPermanentRefreshError
|
|
8281
|
+
}),
|
|
8282
|
+
onAccountRemoved: (account) => {
|
|
8283
|
+
authChanged("copilot", account);
|
|
6347
8284
|
}
|
|
6348
8285
|
});
|
|
8286
|
+
accountTokens.set("copilot", tokens);
|
|
6349
8287
|
copilotAdapter = new CopilotAdapter({
|
|
6350
8288
|
models: catalog.copilot,
|
|
6351
8289
|
streamIdleTimeoutMs,
|
|
8290
|
+
rateLimit,
|
|
6352
8291
|
tokens,
|
|
6353
8292
|
discovery: !overridden.has("copilot"),
|
|
6354
8293
|
onWarn,
|
|
6355
8294
|
resolveAttachments,
|
|
6356
|
-
catalogStore: catalogStore("copilot")
|
|
8295
|
+
catalogStore: catalogStore("copilot"),
|
|
8296
|
+
defaultEffortOf: (model) => defaultEffortOf("copilot", model),
|
|
8297
|
+
pool: () => poolAdapter
|
|
6357
8298
|
});
|
|
8299
|
+
adapters.set("copilot", copilotAdapter);
|
|
6358
8300
|
handles.set("copilot", ctx.llm.registerAdapter(["copilot"], copilotAdapter));
|
|
6359
8301
|
break;
|
|
6360
8302
|
}
|
|
6361
8303
|
}
|
|
6362
|
-
|
|
8304
|
+
const poolConfig = config.pool;
|
|
8305
|
+
const autoAccounts = poolConfig?.autoAccounts ?? poolConfig?.autoFamilies ?? true;
|
|
8306
|
+
if (poolConfig?.enabled !== false && adapters.size >= 1) {
|
|
8307
|
+
const fetcherFor = (provider, account) => {
|
|
8308
|
+
switch (provider) {
|
|
8309
|
+
case "codex": {
|
|
8310
|
+
const tokens = codexTokens;
|
|
8311
|
+
return tokens === void 0 ? void 0 : async () => fetchCodexUsage(await tokens.session(account), proxiedFetch, AbortSignal.timeout(POOL_USAGE_TIMEOUT_MS));
|
|
8312
|
+
}
|
|
8313
|
+
case "claude": {
|
|
8314
|
+
const tokens = claudeTokens;
|
|
8315
|
+
return tokens === void 0 ? void 0 : async () => fetchClaudeUsage(await tokens.session(account), proxiedFetch, AbortSignal.timeout(POOL_USAGE_TIMEOUT_MS));
|
|
8316
|
+
}
|
|
8317
|
+
case "grok": {
|
|
8318
|
+
const tokens = grokTokens;
|
|
8319
|
+
return tokens === void 0 ? void 0 : async () => fetchGrokUsage(await tokens.session(account), proxiedFetch, AbortSignal.timeout(POOL_USAGE_TIMEOUT_MS));
|
|
8320
|
+
}
|
|
8321
|
+
case "copilot": return;
|
|
8322
|
+
}
|
|
8323
|
+
};
|
|
8324
|
+
poolHealth = new PoolHealthRegistry();
|
|
8325
|
+
poolUsage = new PoolUsageTracker(fetcherFor);
|
|
8326
|
+
const families = async () => {
|
|
8327
|
+
const pools = /* @__PURE__ */ new Map();
|
|
8328
|
+
if (autoAccounts) {
|
|
8329
|
+
const sources = {};
|
|
8330
|
+
await Promise.all([...adapters].map(async ([provider, adapter]) => {
|
|
8331
|
+
try {
|
|
8332
|
+
const accounts = (await accountTokens.get(provider)?.list() ?? []).map((entry) => entry.key);
|
|
8333
|
+
if (accounts.length < 2) return;
|
|
8334
|
+
const catalogs = (await Promise.all(accounts.map(async (account) => {
|
|
8335
|
+
const models = await withTimeout((signal) => adapter.listOwnModels(provider, account, signal), POOL_USAGE_TIMEOUT_MS);
|
|
8336
|
+
return models === void 0 ? void 0 : {
|
|
8337
|
+
account,
|
|
8338
|
+
models
|
|
8339
|
+
};
|
|
8340
|
+
}))).filter((entry) => entry !== void 0);
|
|
8341
|
+
if (catalogs.length >= 2) sources[provider] = { catalogs };
|
|
8342
|
+
} catch {}
|
|
8343
|
+
}));
|
|
8344
|
+
for (const [key, definition] of buildAccountPools(sources)) pools.set(key, definition);
|
|
8345
|
+
}
|
|
8346
|
+
for (const [id, members] of Object.entries(poolConfig?.families ?? {})) {
|
|
8347
|
+
if (members.length === 0) continue;
|
|
8348
|
+
const owner = members[0].provider;
|
|
8349
|
+
const kept = members.filter((member) => member.provider === owner);
|
|
8350
|
+
if (kept.length < members.length) onWarn(`pool "${id}": cross-provider members are ignored; only ${owner} accounts are pooled`);
|
|
8351
|
+
pools.set(poolKey(owner, id), { members: kept });
|
|
8352
|
+
}
|
|
8353
|
+
return pools;
|
|
8354
|
+
};
|
|
8355
|
+
poolAdapter = new PoolAdapter({
|
|
8356
|
+
adapters: Object.fromEntries(adapters),
|
|
8357
|
+
health: poolHealth,
|
|
8358
|
+
usage: poolUsage,
|
|
8359
|
+
strategy: poolConfig?.strategy ?? "quota_aware",
|
|
8360
|
+
switchMargin: poolConfig?.switchMargin ?? 2,
|
|
8361
|
+
defaultAccount: (provider) => accountTokens.get(provider)?.defaultAccount() ?? Promise.resolve(void 0),
|
|
8362
|
+
families,
|
|
8363
|
+
tiers: poolConfig?.tiers ?? {},
|
|
8364
|
+
onWarn
|
|
8365
|
+
});
|
|
8366
|
+
}
|
|
8367
|
+
registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers, void 0, poolUsage), {
|
|
6363
8368
|
async speed(sessionId) {
|
|
6364
8369
|
return {
|
|
6365
8370
|
tier: speedBySession.get(sessionId) ?? "standard",
|
|
@@ -6374,10 +8379,73 @@ function apply(ctx, config) {
|
|
|
6374
8379
|
get: () => proxyGetConfig(),
|
|
6375
8380
|
set: (input) => proxySetConfig(input),
|
|
6376
8381
|
test: (payload) => proxyTestConnection(payload.url, payload.proxy)
|
|
8382
|
+
}, {
|
|
8383
|
+
async catalog() {
|
|
8384
|
+
const visible = new Set((await ctx.llm.listProviders()).map((provider) => provider.id));
|
|
8385
|
+
const catalog$1 = [];
|
|
8386
|
+
for (const provider of PROVIDER_IDS) {
|
|
8387
|
+
if (!visible.has(provider)) continue;
|
|
8388
|
+
let models = [];
|
|
8389
|
+
try {
|
|
8390
|
+
models = await ctx.llm.listModels(provider);
|
|
8391
|
+
} catch {
|
|
8392
|
+
continue;
|
|
8393
|
+
}
|
|
8394
|
+
let tierIds = /* @__PURE__ */ new Set();
|
|
8395
|
+
try {
|
|
8396
|
+
const tiers = await poolAdapter?.modelsForProvider(provider);
|
|
8397
|
+
if (tiers !== void 0) tierIds = new Set(tiers.map((tier) => tier.id));
|
|
8398
|
+
} catch {}
|
|
8399
|
+
const views = [];
|
|
8400
|
+
for (const model of models) {
|
|
8401
|
+
if (tierIds.has(model.id)) continue;
|
|
8402
|
+
let info;
|
|
8403
|
+
try {
|
|
8404
|
+
info = await ctx.llm.resolveModelInfo(provider, model.id);
|
|
8405
|
+
} catch {
|
|
8406
|
+
continue;
|
|
8407
|
+
}
|
|
8408
|
+
if (info === void 0) continue;
|
|
8409
|
+
const override = defaultEffortOf(provider, model.id);
|
|
8410
|
+
views.push({
|
|
8411
|
+
id: model.id,
|
|
8412
|
+
name: model.name,
|
|
8413
|
+
efforts: info.reasoning?.efforts.map((effort) => ({
|
|
8414
|
+
id: effort.id,
|
|
8415
|
+
name: effort.name
|
|
8416
|
+
})) ?? [],
|
|
8417
|
+
...override === void 0 ? {} : { configured: override }
|
|
8418
|
+
});
|
|
8419
|
+
}
|
|
8420
|
+
catalog$1.push({
|
|
8421
|
+
provider,
|
|
8422
|
+
models: views
|
|
8423
|
+
});
|
|
8424
|
+
}
|
|
8425
|
+
return catalog$1;
|
|
8426
|
+
},
|
|
8427
|
+
async set(provider, model, effort) {
|
|
8428
|
+
if (effort !== void 0) {
|
|
8429
|
+
let info;
|
|
8430
|
+
try {
|
|
8431
|
+
info = await ctx.llm.resolveModelInfo(provider, model);
|
|
8432
|
+
} catch {}
|
|
8433
|
+
const offered = info?.reasoning?.efforts ?? [];
|
|
8434
|
+
if (offered.length > 0 && !offered.some((entry) => entry.id === effort)) throw new BadRequest(`model ${model} does not advertise a "${effort}" reasoning effort`);
|
|
8435
|
+
}
|
|
8436
|
+
await setDefaultEffort(provider, model, effort);
|
|
8437
|
+
handles.get(provider)?.replace([provider]);
|
|
8438
|
+
}
|
|
6377
8439
|
});
|
|
6378
8440
|
if (claudeTokens !== void 0) {
|
|
8441
|
+
const tokens = claudeTokens;
|
|
6379
8442
|
const syncTimer = setInterval(() => {
|
|
6380
|
-
|
|
8443
|
+
tokens.list().then((accounts) => {
|
|
8444
|
+
for (const { key, session } of accounts) {
|
|
8445
|
+
if (session.keychainBound !== true) continue;
|
|
8446
|
+
tokens.session(key).catch(() => {});
|
|
8447
|
+
}
|
|
8448
|
+
}, () => void 0);
|
|
6381
8449
|
}, 5 * 6e4);
|
|
6382
8450
|
ctx.effect(() => () => {
|
|
6383
8451
|
clearInterval(syncTimer);
|
|
@@ -6398,4 +8466,4 @@ function apply(ctx, config) {
|
|
|
6398
8466
|
}
|
|
6399
8467
|
|
|
6400
8468
|
//#endregion
|
|
6401
|
-
export { Config, DEFAULT_STREAM_IDLE_TIMEOUT_MS, SubscriptionsAuthController, apply, inject, name };
|
|
8469
|
+
export { Config, DEFAULT_STREAM_IDLE_TIMEOUT_MS, POOL_USAGE_TIMEOUT_MS, SubscriptionsAuthController, apply, inject, name, withTimeout };
|