dsh-plugin-subscriptions 0.5.1 → 0.5.3
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 +42 -1
- package/README.zh.md +42 -1
- package/lib/auth/device-flow.d.ts +0 -9
- package/lib/auth/device-flow.js +2 -1
- package/lib/auth/rpc.d.ts +44 -13
- package/lib/auth/rpc.js +127 -9
- package/lib/auth/store.d.ts +75 -17
- package/lib/auth/store.js +148 -27
- package/lib/client/SubscriptionsSection.d.ts +26 -3
- package/lib/client/SubscriptionsSection.js +263 -67
- package/lib/client/index.js +11 -0
- package/lib/client/locales.d.ts +82 -10
- package/lib/client/locales.js +82 -10
- package/lib/client.js +837 -223
- package/lib/client.js.map +1 -1
- package/lib/http.d.ts +114 -0
- package/lib/http.js +402 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.js +1938 -208
- package/lib/providers/accounts.d.ts +102 -0
- package/lib/providers/accounts.js +123 -0
- package/lib/providers/antigravity.d.ts +90 -0
- package/lib/providers/antigravity.js +392 -0
- package/lib/providers/claude.d.ts +22 -4
- package/lib/providers/claude.js +97 -16
- package/lib/providers/codex.d.ts +24 -3
- package/lib/providers/codex.js +121 -21
- package/lib/providers/common.d.ts +17 -0
- package/lib/providers/common.js +67 -3
- package/lib/providers/copilot.d.ts +23 -4
- package/lib/providers/copilot.js +99 -19
- package/lib/providers/grok.d.ts +24 -4
- package/lib/providers/grok.js +106 -19
- 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 +57 -0
- package/lib/providers/pool-usage.js +130 -0
- package/lib/providers/pool.d.ts +107 -0
- package/lib/providers/pool.js +371 -0
- package/lib/tools/image-generate.d.ts +3 -3
- package/lib/tools/image-generate.js +4 -2
- package/lib/tools/video-generate.d.ts +2 -2
- package/lib/tools/video-generate.js +4 -2
- package/lib/tools/x-search.d.ts +2 -2
- package/lib/tools/x-search.js +4 -2
- package/lib/translate/antigravity.d.ts +110 -0
- package/lib/translate/antigravity.js +303 -0
- package/package.json +14 -9
package/lib/index.js
CHANGED
|
@@ -2,13 +2,14 @@ import z from "@deepseek-ai/schemastery";
|
|
|
2
2
|
import { CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, errorChain, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
|
+
import { ProxyAgent, fetch as fetch$1 } from "undici";
|
|
5
6
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
7
|
import { basename, dirname, join } from "node:path";
|
|
8
|
+
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
7
9
|
import { execFileSync } from "node:child_process";
|
|
8
10
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
11
|
import { homedir } from "node:os";
|
|
10
12
|
import { AttachmentId } from "@deepseek-ai/dsh-attachment";
|
|
11
|
-
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
12
13
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
13
14
|
|
|
14
15
|
//#region src/auth/pkce.ts
|
|
@@ -251,16 +252,355 @@ var OAuthFlowManager = class {
|
|
|
251
252
|
};
|
|
252
253
|
|
|
253
254
|
//#endregion
|
|
254
|
-
//#region src/
|
|
255
|
+
//#region src/http.ts
|
|
256
|
+
/**
|
|
257
|
+
* undici's own fetch, typed to the DOM fetch signature: its bundled types are
|
|
258
|
+
* stricter (Request requires `duplex`, `RequestInit.body` is non-null) and
|
|
259
|
+
* incompatible with the DOM shapes the provider code passes. The runtime
|
|
260
|
+
* object is the same Web-fetch implementation Node uses.
|
|
261
|
+
*/
|
|
262
|
+
const dispatchFetch = fetch$1;
|
|
263
|
+
/** Destination the `proxyTest` endpoint probes when none is given. */
|
|
264
|
+
const DEFAULT_PROXY_TEST_URL = "https://api.x.ai/v1/models";
|
|
265
|
+
/** Probe deadline; a hung proxy must not pin the Settings dialog forever. */
|
|
266
|
+
const DEFAULT_PROXY_TEST_TIMEOUT_MS = 15e3;
|
|
267
|
+
/** Disabled configuration: the module state before the first load. */
|
|
268
|
+
const DISABLED = {
|
|
269
|
+
enabled: false,
|
|
270
|
+
url: "",
|
|
271
|
+
bypass: []
|
|
272
|
+
};
|
|
273
|
+
/** Current config; updated by every load/apply/save. */
|
|
274
|
+
let current = DISABLED;
|
|
275
|
+
/** The live dispatcher, or undefined when proxies are off/errored. */
|
|
276
|
+
let agent;
|
|
277
|
+
/** Last load/apply failure, surfaced by the config view. */
|
|
278
|
+
let configError;
|
|
279
|
+
/** One lazy load of the on-disk config (module-import cheap; file read once). */
|
|
280
|
+
let ready;
|
|
281
|
+
/** Absolute path of the proxy config file. */
|
|
282
|
+
function proxyFilePath() {
|
|
283
|
+
return dshHomePath("plugins", "subscriptions", "proxy.json");
|
|
284
|
+
}
|
|
285
|
+
function errorMessage(error) {
|
|
286
|
+
return error instanceof Error ? error.message : String(error);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Flatten a fetch failure into a readable message: undici wraps the true
|
|
290
|
+
* cause (`connect ECONNREFUSED ...`) behind a bare "fetch failed", so walk
|
|
291
|
+
* the cause chain and append each distinct layer (up to four, cycle-safe).
|
|
292
|
+
* A hostname resolving to several addresses (e.g. `localhost` → ::1 and
|
|
293
|
+
* 127.0.0.1) fails as an `AggregateError` with an empty message, so its
|
|
294
|
+
* per-address `errors` entries are folded in too.
|
|
295
|
+
*/
|
|
296
|
+
function describeFetchError(error) {
|
|
297
|
+
const parts = [];
|
|
298
|
+
let node = error;
|
|
299
|
+
for (let depth = 0; depth < 4 && node !== void 0 && node !== null; depth += 1) {
|
|
300
|
+
const layer = node;
|
|
301
|
+
if (Array.isArray(layer.errors)) for (const child of layer.errors) {
|
|
302
|
+
const childText = child instanceof Error && child.message !== "" ? child.message : String(child);
|
|
303
|
+
if (childText !== "" && !parts.includes(childText)) parts.push(childText);
|
|
304
|
+
}
|
|
305
|
+
let text = layer instanceof Error ? layer.message : String(node);
|
|
306
|
+
const code = layer.code;
|
|
307
|
+
if (typeof code === "string" && code !== "") {
|
|
308
|
+
if (text === "") text = code;
|
|
309
|
+
else if (!text.includes(code)) text = `${text} (${code})`;
|
|
310
|
+
}
|
|
311
|
+
if (text !== "" && !parts.includes(text)) parts.push(text);
|
|
312
|
+
const next = layer.cause;
|
|
313
|
+
if (next === void 0 || next === null || next === node) break;
|
|
314
|
+
node = next;
|
|
315
|
+
}
|
|
316
|
+
return parts.join(" → ");
|
|
317
|
+
}
|
|
318
|
+
function withError(error) {
|
|
319
|
+
configError = errorMessage(error);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Parse and validate a proxy URL. Only HTTP(S) proxies are supported because
|
|
323
|
+
* the undici dispatcher speaks CONNECT over HTTP; socks5 is not supported.
|
|
324
|
+
* @param raw - the URL the user configured.
|
|
325
|
+
* @returns the parsed URL (credentials attached by the caller).
|
|
326
|
+
*/
|
|
327
|
+
function parseProxyUrl(raw) {
|
|
328
|
+
let url;
|
|
329
|
+
try {
|
|
330
|
+
url = new URL(raw);
|
|
331
|
+
} catch {
|
|
332
|
+
throw new Error(`proxy URL "${raw}" is not a valid URL`);
|
|
333
|
+
}
|
|
334
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`proxy URL must use the http:// or https:// scheme (got "${raw}")`);
|
|
335
|
+
if (url.hostname === "") throw new Error("proxy URL must include a host");
|
|
336
|
+
return url;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Whether a request hostname bypasses the proxy.
|
|
340
|
+
* @param hostname - the request's hostname.
|
|
341
|
+
* @param entries - configured bypass entries: exact host, plain suffix
|
|
342
|
+
* (`example.com` also matches `api.example.com`), or `*.example.com`.
|
|
343
|
+
*/
|
|
344
|
+
function matchesBypass(hostname, entries) {
|
|
345
|
+
const host = hostname.toLowerCase();
|
|
346
|
+
for (const raw of entries) {
|
|
347
|
+
let entry = raw.trim().toLowerCase();
|
|
348
|
+
if (entry === "") continue;
|
|
349
|
+
if (entry.includes("://")) try {
|
|
350
|
+
entry = new URL(entry).hostname;
|
|
351
|
+
} catch {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
entry = entry.replace(/:\d+$/, "");
|
|
355
|
+
if (entry === "" || entry === "*") continue;
|
|
356
|
+
if (entry.startsWith("*.")) {
|
|
357
|
+
if (host.endsWith(entry.slice(1))) return true;
|
|
358
|
+
} else if (host === entry || host.endsWith(`.${entry}`)) return true;
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
/** Validate and normalize one config (throws with a user-facing message). */
|
|
363
|
+
function normalizeConfig(input) {
|
|
364
|
+
const url = input.url.trim();
|
|
365
|
+
if (input.enabled && url === "") throw new Error("a proxy URL is required when the proxy is enabled");
|
|
366
|
+
if (url !== "") parseProxyUrl(url);
|
|
367
|
+
const bypass = Array.from(new Set((input.bypass ?? []).map((entry) => entry.trim()).filter((entry) => entry !== "")));
|
|
368
|
+
return {
|
|
369
|
+
enabled: input.enabled,
|
|
370
|
+
url,
|
|
371
|
+
...input.username !== void 0 && input.username !== "" ? { username: input.username.trim() } : {},
|
|
372
|
+
...input.password !== void 0 && input.password !== "" && input.password !== null ? { password: input.password } : {},
|
|
373
|
+
bypass
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/** Build the undici agent for a config (throws on an unusable URL). */
|
|
377
|
+
function buildAgent(cfg) {
|
|
378
|
+
if (!cfg.enabled || cfg.url === "") return void 0;
|
|
379
|
+
const url = parseProxyUrl(cfg.url);
|
|
380
|
+
if (cfg.username !== void 0) url.username = cfg.username;
|
|
381
|
+
if (cfg.password !== void 0) url.password = cfg.password;
|
|
382
|
+
return new ProxyAgent(url.toString());
|
|
383
|
+
}
|
|
384
|
+
/** Swap in a config and its agent; a failed agent keeps the requests direct. */
|
|
385
|
+
async function applyConfig(cfg) {
|
|
386
|
+
let next;
|
|
387
|
+
if (cfg !== void 0) {
|
|
388
|
+
configError = void 0;
|
|
389
|
+
try {
|
|
390
|
+
next = buildAgent(cfg);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
withError(error);
|
|
393
|
+
next = void 0;
|
|
394
|
+
}
|
|
395
|
+
current = cfg;
|
|
396
|
+
}
|
|
397
|
+
const previous = agent;
|
|
398
|
+
agent = next;
|
|
399
|
+
if (previous !== void 0) previous.close().catch(() => void 0);
|
|
400
|
+
}
|
|
401
|
+
/** Read the on-disk config. A missing file is the disabled default. */
|
|
402
|
+
async function loadConfigFile(path) {
|
|
403
|
+
let text;
|
|
404
|
+
try {
|
|
405
|
+
text = await readFile(path, "utf8");
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (error.code === "ENOENT") return {
|
|
408
|
+
...DISABLED,
|
|
409
|
+
bypass: []
|
|
410
|
+
};
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
let parsed;
|
|
414
|
+
try {
|
|
415
|
+
parsed = JSON.parse(text);
|
|
416
|
+
} catch {
|
|
417
|
+
throw new Error(`subscriptions proxy config at ${path} is not valid JSON; fix or delete the file`);
|
|
418
|
+
}
|
|
419
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("subscriptions proxy config must be a JSON object");
|
|
420
|
+
const record = parsed;
|
|
421
|
+
const enabled = record.enabled === true;
|
|
422
|
+
const url = typeof record.url === "string" ? record.url : "";
|
|
423
|
+
const username = typeof record.username === "string" ? record.username : void 0;
|
|
424
|
+
const password = typeof record.password === "string" ? record.password : void 0;
|
|
425
|
+
const bypass = Array.isArray(record.bypass) ? record.bypass.filter((entry) => typeof entry === "string") : [];
|
|
426
|
+
return normalizeConfig({
|
|
427
|
+
enabled,
|
|
428
|
+
url,
|
|
429
|
+
...username === void 0 ? {} : { username },
|
|
430
|
+
...password === void 0 ? {} : { password },
|
|
431
|
+
bypass
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
/** Resolve the module state once from disk; failures disable the proxy. */
|
|
435
|
+
async function ensureReady() {
|
|
436
|
+
ready ??= loadConfigFile(proxyFilePath()).then(async (cfg) => {
|
|
437
|
+
await applyConfig(cfg);
|
|
438
|
+
return current;
|
|
439
|
+
}, async (error) => {
|
|
440
|
+
withError(error);
|
|
441
|
+
await applyConfig(void 0);
|
|
442
|
+
return current;
|
|
443
|
+
});
|
|
444
|
+
return ready;
|
|
445
|
+
}
|
|
446
|
+
/** Persist a config atomically with owner-only permissions, then apply it. */
|
|
447
|
+
async function persistConfig(cfg, path) {
|
|
448
|
+
await mkdir(dirname(path), { recursive: true });
|
|
449
|
+
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
450
|
+
try {
|
|
451
|
+
await writeFile(tmp, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
452
|
+
await chmod(tmp, 384);
|
|
453
|
+
await rename(tmp, path);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
await rm(tmp, { force: true });
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Current proxy config as served to the client (secrets omitted).
|
|
461
|
+
* @returns the view; {@link ProxyConfigView.error} carries the last
|
|
462
|
+
* load/apply failure when the stored config is unusable.
|
|
463
|
+
*/
|
|
464
|
+
async function proxyGetConfig() {
|
|
465
|
+
await ensureReady();
|
|
466
|
+
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],
|
|
472
|
+
...configError === void 0 ? {} : { error: configError }
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Validate, persist, and apply one proxy config. A `password` of `undefined`
|
|
477
|
+
* keeps the stored value; `null` or `''` clears it.
|
|
478
|
+
* @param input - the client's payload.
|
|
479
|
+
* @returns the resulting view (secrets omitted).
|
|
480
|
+
*/
|
|
481
|
+
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;
|
|
484
|
+
const next = normalizeConfig({
|
|
485
|
+
enabled: input.enabled,
|
|
486
|
+
url: input.url,
|
|
487
|
+
...input.username === void 0 ? {} : { username: input.username },
|
|
488
|
+
...password === void 0 ? {} : { password },
|
|
489
|
+
bypass: input.bypass ?? current.bypass
|
|
490
|
+
});
|
|
491
|
+
await persistConfig(next, proxyFilePath());
|
|
492
|
+
await applyConfig(next);
|
|
493
|
+
return proxyGetConfig();
|
|
494
|
+
}
|
|
255
495
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
496
|
+
* The fetch caller all subscription code uses: routes through the configured
|
|
497
|
+
* proxy unless the host bypasses it. Identity-passthrough otherwise.
|
|
498
|
+
*
|
|
499
|
+
* Proxied requests run on undici's own fetch (not the global one) so the
|
|
500
|
+
* ProxyAgent dispatcher always comes from the same undici build the request
|
|
501
|
+
* is issued with — a mismatched dispatcher can be silently ignored by the
|
|
502
|
+
* host's global fetch.
|
|
263
503
|
*/
|
|
504
|
+
async function proxiedFetch(input, init = {}) {
|
|
505
|
+
await ensureReady();
|
|
506
|
+
let dispatcher;
|
|
507
|
+
if (current.enabled && agent !== void 0) {
|
|
508
|
+
let hostname = "";
|
|
509
|
+
try {
|
|
510
|
+
hostname = (typeof input === "string" ? new URL(input) : input instanceof URL ? input : new URL(input.url)).hostname;
|
|
511
|
+
} catch {
|
|
512
|
+
hostname = "";
|
|
513
|
+
}
|
|
514
|
+
if (!matchesBypass(hostname, current.bypass)) dispatcher = agent;
|
|
515
|
+
}
|
|
516
|
+
if (dispatcher === void 0) return fetch(input, init);
|
|
517
|
+
return dispatchFetch(input, {
|
|
518
|
+
...init,
|
|
519
|
+
dispatcher
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Probe a destination through a proxy, answering with the HTTP status or a
|
|
524
|
+
* flattened transport error. The probe uses `draft` when given (the dialog's
|
|
525
|
+
* current inputs, without saving) and the stored config otherwise.
|
|
526
|
+
* @param target - `http(s)` URL to fetch; defaults to {@link DEFAULT_PROXY_TEST_URL}.
|
|
527
|
+
* @param draft - unsaved proxy inputs to test; absent means the stored config.
|
|
528
|
+
* @returns the result; any HTTP status counts as a successful connection,
|
|
529
|
+
* only a transport failure is an error.
|
|
530
|
+
*/
|
|
531
|
+
async function proxyTestConnection(target = DEFAULT_PROXY_TEST_URL, draft) {
|
|
532
|
+
let parsed;
|
|
533
|
+
try {
|
|
534
|
+
parsed = new URL(target);
|
|
535
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return {
|
|
536
|
+
ok: false,
|
|
537
|
+
viaProxy: false,
|
|
538
|
+
error: `test destination must be http or https (got "${parsed.protocol}//")`
|
|
539
|
+
};
|
|
540
|
+
} catch (error) {
|
|
541
|
+
return {
|
|
542
|
+
ok: false,
|
|
543
|
+
viaProxy: false,
|
|
544
|
+
error: errorMessage(error)
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
await ensureReady();
|
|
548
|
+
let probeAgent;
|
|
549
|
+
let viaProxy;
|
|
550
|
+
let closeProbe = false;
|
|
551
|
+
if (draft !== void 0) try {
|
|
552
|
+
probeAgent = buildAgent(normalizeConfig({
|
|
553
|
+
enabled: true,
|
|
554
|
+
url: draft.url,
|
|
555
|
+
...draft.username === void 0 || draft.username === "" ? {} : { username: draft.username },
|
|
556
|
+
...draft.password === void 0 || draft.password === "" ? {} : { password: draft.password },
|
|
557
|
+
bypass: []
|
|
558
|
+
}));
|
|
559
|
+
viaProxy = probeAgent !== void 0;
|
|
560
|
+
closeProbe = true;
|
|
561
|
+
} catch (error) {
|
|
562
|
+
return {
|
|
563
|
+
ok: false,
|
|
564
|
+
viaProxy: false,
|
|
565
|
+
error: errorMessage(error)
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
viaProxy = current.enabled && agent !== void 0 && !matchesBypass(parsed.hostname, current.bypass);
|
|
570
|
+
probeAgent = viaProxy ? agent : void 0;
|
|
571
|
+
}
|
|
572
|
+
const started = Date.now();
|
|
573
|
+
try {
|
|
574
|
+
const init = probeAgent !== void 0 ? {
|
|
575
|
+
method: "GET",
|
|
576
|
+
dispatcher: probeAgent,
|
|
577
|
+
signal: AbortSignal.timeout(DEFAULT_PROXY_TEST_TIMEOUT_MS)
|
|
578
|
+
} : {
|
|
579
|
+
method: "GET",
|
|
580
|
+
signal: AbortSignal.timeout(DEFAULT_PROXY_TEST_TIMEOUT_MS)
|
|
581
|
+
};
|
|
582
|
+
const response = probeAgent !== void 0 ? await dispatchFetch(parsed.toString(), init) : await fetch(parsed.toString(), init);
|
|
583
|
+
response.arrayBuffer().catch(() => void 0);
|
|
584
|
+
return {
|
|
585
|
+
ok: true,
|
|
586
|
+
viaProxy,
|
|
587
|
+
status: response.status,
|
|
588
|
+
latencyMs: Date.now() - started
|
|
589
|
+
};
|
|
590
|
+
} catch (error) {
|
|
591
|
+
return {
|
|
592
|
+
ok: false,
|
|
593
|
+
viaProxy,
|
|
594
|
+
latencyMs: Date.now() - started,
|
|
595
|
+
error: describeFetchError(error)
|
|
596
|
+
};
|
|
597
|
+
} finally {
|
|
598
|
+
if (closeProbe && probeAgent !== void 0) await probeAgent.close().catch(() => void 0);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/auth/device-flow.ts
|
|
264
604
|
/** Default poll interval when the device-code response omits one. */
|
|
265
605
|
const DEFAULT_INTERVAL_SEC = 5;
|
|
266
606
|
/** Default device-code lifetime when the response omits one (GitHub: 15 minutes). */
|
|
@@ -316,7 +656,7 @@ var DeviceFlowManager = class {
|
|
|
316
656
|
*/
|
|
317
657
|
async start(provider, spec) {
|
|
318
658
|
if (this.attempts.has(provider)) throw new Error(`a ${provider} login attempt is already in progress`);
|
|
319
|
-
const fetchFn = spec.fetchFn ??
|
|
659
|
+
const fetchFn = spec.fetchFn ?? proxiedFetch;
|
|
320
660
|
const response = await fetchFn(spec.deviceCodeUrl, {
|
|
321
661
|
method: "POST",
|
|
322
662
|
headers: {
|
|
@@ -599,6 +939,31 @@ const PROVIDER_IDS = [
|
|
|
599
939
|
"copilot"
|
|
600
940
|
];
|
|
601
941
|
/**
|
|
942
|
+
* The stable identity of one session's account: codex keys on the always
|
|
943
|
+
* present `accountId` claim, the others on their display identity, falling
|
|
944
|
+
* back to a refresh-token hash for sessions stored before identity fields
|
|
945
|
+
* existed. Logging the same account in again lands on the same key, so a
|
|
946
|
+
* re-login updates in place instead of duplicating. (The hash fallback can
|
|
947
|
+
* miss that dedup once for a legacy session re-logged with a now-known
|
|
948
|
+
* identity — the duplicate is visible on the Settings page and can simply
|
|
949
|
+
* be logged out.)
|
|
950
|
+
* @param provider - the provider route.
|
|
951
|
+
* @param session - the session to key.
|
|
952
|
+
* @returns the account map key.
|
|
953
|
+
*/
|
|
954
|
+
function accountKeyOf(provider, session) {
|
|
955
|
+
switch (provider) {
|
|
956
|
+
case "codex": return session.accountId;
|
|
957
|
+
case "claude": return session.emailAddress ?? tokenHash(session.refreshToken);
|
|
958
|
+
case "grok": return session.account ?? tokenHash(session.refreshToken);
|
|
959
|
+
case "copilot": return session.account ?? tokenHash(session.refreshToken);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/** Short stable hash for sessions without an identity field. */
|
|
963
|
+
function tokenHash(refreshToken) {
|
|
964
|
+
return `token-${createHash("sha256").update(refreshToken).digest("hex").slice(0, 16)}`;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
602
967
|
* Absolute path of the auth store file.
|
|
603
968
|
* @returns `dshHomePath('plugins', 'subscriptions', 'auth.json')`.
|
|
604
969
|
*/
|
|
@@ -609,16 +974,17 @@ function authFilePath() {
|
|
|
609
974
|
function legacyAuthFilePath() {
|
|
610
975
|
return dshHomePath("plugins", "router", "auth.json");
|
|
611
976
|
}
|
|
612
|
-
/** Check that one durable
|
|
613
|
-
function assertSessionShape(provider, value) {
|
|
614
|
-
if (typeof value !== "object" || value === null) throw new Error(`subscriptions auth store: entry "${provider}" is not an object; fix or delete the store file`);
|
|
977
|
+
/** Check that one durable session carries the fields every session needs. */
|
|
978
|
+
function assertSessionShape(provider, account, value) {
|
|
979
|
+
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`);
|
|
615
980
|
const entry = value;
|
|
616
|
-
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`);
|
|
981
|
+
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`);
|
|
617
982
|
}
|
|
618
983
|
/**
|
|
619
984
|
* Read the whole store. A missing file is an empty store; malformed JSON or a
|
|
620
985
|
* malformed entry throws, because silently discarding tokens would strand the
|
|
621
|
-
* user without a diagnosis.
|
|
986
|
+
* user without a diagnosis. Single-account entries are migrated in memory;
|
|
987
|
+
* the next write persists the new shape.
|
|
622
988
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
623
989
|
* @returns the parsed session map.
|
|
624
990
|
*/
|
|
@@ -642,7 +1008,7 @@ async function loadStore(path = authFilePath()) {
|
|
|
642
1008
|
}
|
|
643
1009
|
return parseStore(text, path);
|
|
644
1010
|
}
|
|
645
|
-
/** Parse and
|
|
1011
|
+
/** Parse, validate, and migrate store JSON read from `path`. */
|
|
646
1012
|
function parseStore(text, path) {
|
|
647
1013
|
let parsed;
|
|
648
1014
|
try {
|
|
@@ -651,10 +1017,28 @@ function parseStore(text, path) {
|
|
|
651
1017
|
throw new Error(`subscriptions auth store at ${path} is not valid JSON; fix or delete the file`);
|
|
652
1018
|
}
|
|
653
1019
|
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`);
|
|
654
|
-
const
|
|
1020
|
+
const raw = parsed;
|
|
1021
|
+
const store = {};
|
|
655
1022
|
for (const provider of PROVIDER_IDS) {
|
|
656
|
-
const entry =
|
|
657
|
-
if (entry
|
|
1023
|
+
const entry = raw[provider];
|
|
1024
|
+
if (entry === void 0) continue;
|
|
1025
|
+
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`);
|
|
1026
|
+
const record = entry;
|
|
1027
|
+
if (typeof record.accessToken === "string") {
|
|
1028
|
+
assertSessionShape(provider, "(legacy)", record);
|
|
1029
|
+
const session = record;
|
|
1030
|
+
const key = accountKeyOf(provider, session);
|
|
1031
|
+
store[provider] = {
|
|
1032
|
+
default: key,
|
|
1033
|
+
accounts: { [key]: session }
|
|
1034
|
+
};
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
const accounts = record.accounts;
|
|
1038
|
+
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`);
|
|
1039
|
+
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`);
|
|
1040
|
+
for (const [account, session] of Object.entries(accounts)) assertSessionShape(provider, account, session);
|
|
1041
|
+
store[provider] = record;
|
|
658
1042
|
}
|
|
659
1043
|
return store;
|
|
660
1044
|
}
|
|
@@ -674,8 +1058,8 @@ async function writeStore(store, path) {
|
|
|
674
1058
|
/**
|
|
675
1059
|
* One write chain per store path. Every mutation is a read-modify-write of a
|
|
676
1060
|
* single JSON file, and the plugin has several independent writers — a login,
|
|
677
|
-
* a logout, and one token refresh per provider
|
|
678
|
-
* schedule. Overlapping them unserialized costs whichever
|
|
1061
|
+
* a logout, and one token refresh per provider account, each on its own
|
|
1062
|
+
* schedule. Overlapping them unserialized costs whichever account read the
|
|
679
1063
|
* store first its entry.
|
|
680
1064
|
*
|
|
681
1065
|
* A chain is dropped once nothing is queued behind it, so the map holds an
|
|
@@ -700,37 +1084,94 @@ async function serialize(path, action) {
|
|
|
700
1084
|
}
|
|
701
1085
|
}
|
|
702
1086
|
/**
|
|
703
|
-
*
|
|
1087
|
+
* List one provider's accounts, default first.
|
|
704
1088
|
* @param provider - the provider route.
|
|
705
1089
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
706
|
-
* @returns the
|
|
1090
|
+
* @returns the account entries in stable order (empty when logged out).
|
|
707
1091
|
*/
|
|
708
|
-
async function
|
|
709
|
-
|
|
1092
|
+
async function listAccounts(provider, path = authFilePath()) {
|
|
1093
|
+
const entry = (await loadStore(path))[provider];
|
|
1094
|
+
if (entry === void 0) return [];
|
|
1095
|
+
const accounts = Object.entries(entry.accounts).map(([key, session]) => ({
|
|
1096
|
+
key,
|
|
1097
|
+
session
|
|
1098
|
+
}));
|
|
1099
|
+
accounts.sort((a, b) => Number(b.key === entry.default) - Number(a.key === entry.default));
|
|
1100
|
+
return accounts;
|
|
710
1101
|
}
|
|
711
1102
|
/**
|
|
712
|
-
*
|
|
1103
|
+
* Read one account's session.
|
|
713
1104
|
* @param provider - the provider route.
|
|
1105
|
+
* @param account - the account key; defaults to the provider's default account.
|
|
1106
|
+
* @param path - store file path; defaults to {@link authFilePath}.
|
|
1107
|
+
* @returns the stored session, or `undefined` when absent.
|
|
1108
|
+
*/
|
|
1109
|
+
async function getAccountSession(provider, account, path = authFilePath()) {
|
|
1110
|
+
const entry = (await loadStore(path))[provider];
|
|
1111
|
+
if (entry === void 0) return void 0;
|
|
1112
|
+
const key = account ?? entry.default;
|
|
1113
|
+
if (key === void 0) return void 0;
|
|
1114
|
+
return entry.accounts[key];
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Write one account's session, preserving the others. The first account of a
|
|
1118
|
+
* provider becomes its default.
|
|
1119
|
+
* @param provider - the provider route.
|
|
1120
|
+
* @param account - the account key (see {@link accountKeyOf}).
|
|
714
1121
|
* @param session - the fresh session from a login or refresh.
|
|
715
1122
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
716
1123
|
*/
|
|
717
|
-
async function
|
|
1124
|
+
async function saveAccountSession(provider, account, session, path = authFilePath()) {
|
|
718
1125
|
return serialize(path, async () => {
|
|
719
1126
|
const store = await loadStore(path);
|
|
720
|
-
store[provider]
|
|
1127
|
+
const entry = store[provider];
|
|
1128
|
+
store[provider] = {
|
|
1129
|
+
default: entry?.default ?? account,
|
|
1130
|
+
accounts: {
|
|
1131
|
+
...entry?.accounts,
|
|
1132
|
+
[account]: session
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
721
1135
|
await writeStore(store, path);
|
|
722
1136
|
});
|
|
723
1137
|
}
|
|
724
1138
|
/**
|
|
725
|
-
* Delete one
|
|
1139
|
+
* Delete one account's session (logout). Deleting the default moves the badge
|
|
1140
|
+
* to the next remaining account.
|
|
726
1141
|
* @param provider - the provider route.
|
|
1142
|
+
* @param account - the account key.
|
|
727
1143
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
728
1144
|
*/
|
|
729
|
-
async function
|
|
1145
|
+
async function deleteAccountSession(provider, account, path = authFilePath()) {
|
|
730
1146
|
return serialize(path, async () => {
|
|
731
1147
|
const store = await loadStore(path);
|
|
732
|
-
|
|
733
|
-
|
|
1148
|
+
const entry = store[provider];
|
|
1149
|
+
if (entry === void 0 || !(account in entry.accounts)) return;
|
|
1150
|
+
const accounts = { ...entry.accounts };
|
|
1151
|
+
delete accounts[account];
|
|
1152
|
+
if (Object.keys(accounts).length === 0) delete store[provider];
|
|
1153
|
+
else store[provider] = {
|
|
1154
|
+
...entry.default === account ? { default: Object.keys(accounts)[0] } : { default: entry.default },
|
|
1155
|
+
accounts
|
|
1156
|
+
};
|
|
1157
|
+
await writeStore(store, path);
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Pin the account direct (non-pool) routes serve.
|
|
1162
|
+
* @param provider - the provider route.
|
|
1163
|
+
* @param account - the account key; must exist.
|
|
1164
|
+
* @param path - store file path; defaults to {@link authFilePath}.
|
|
1165
|
+
*/
|
|
1166
|
+
async function setDefaultAccount(provider, account, path = authFilePath()) {
|
|
1167
|
+
return serialize(path, async () => {
|
|
1168
|
+
const store = await loadStore(path);
|
|
1169
|
+
const entry = store[provider];
|
|
1170
|
+
if (entry === void 0 || !(account in entry.accounts)) throw new Error(`no ${provider} account "${account}" is logged in`);
|
|
1171
|
+
store[provider] = {
|
|
1172
|
+
...entry,
|
|
1173
|
+
default: account
|
|
1174
|
+
};
|
|
734
1175
|
await writeStore(store, path);
|
|
735
1176
|
});
|
|
736
1177
|
}
|
|
@@ -786,6 +1227,14 @@ function readString(payload, field) {
|
|
|
786
1227
|
if (typeof value !== "string" || value.length === 0) throw new BadRequest(`payload.${field} must be a non-empty string`);
|
|
787
1228
|
return value;
|
|
788
1229
|
}
|
|
1230
|
+
/** Validate the optional Claude login method. */
|
|
1231
|
+
function readLoginMethod(payload, provider) {
|
|
1232
|
+
const method = payload.method;
|
|
1233
|
+
if (method === void 0) return void 0;
|
|
1234
|
+
if (provider !== "claude") throw new BadRequest("payload.method is only valid for claude");
|
|
1235
|
+
if (method !== "oauth" && method !== "keychain") throw new BadRequest("payload.method must be \"oauth\" or \"keychain\"");
|
|
1236
|
+
return method;
|
|
1237
|
+
}
|
|
789
1238
|
/** Validate the `setSpeed` endpoint's tier. */
|
|
790
1239
|
function readSpeedTier(payload) {
|
|
791
1240
|
const tier = payload.tier;
|
|
@@ -835,13 +1284,78 @@ function readSessionId(payload) {
|
|
|
835
1284
|
if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
|
|
836
1285
|
return readString(payload, "sessionId");
|
|
837
1286
|
}
|
|
838
|
-
|
|
1287
|
+
/** Validate a `proxySet` payload into a shape `ProxyInput` accepts. */
|
|
1288
|
+
function readProxyInput(payload) {
|
|
1289
|
+
if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
|
|
1290
|
+
const record = payload;
|
|
1291
|
+
if (typeof record.enabled !== "boolean") throw new BadRequest("payload.enabled must be a boolean");
|
|
1292
|
+
if (typeof record.url !== "string") throw new BadRequest("payload.url must be a string");
|
|
1293
|
+
let username;
|
|
1294
|
+
if (record.username !== void 0) {
|
|
1295
|
+
if (typeof record.username !== "string") throw new BadRequest("payload.username must be a string when present");
|
|
1296
|
+
username = record.username;
|
|
1297
|
+
}
|
|
1298
|
+
let password;
|
|
1299
|
+
if (record.password !== void 0) {
|
|
1300
|
+
if (record.password !== null && typeof record.password !== "string") throw new BadRequest("payload.password must be a string or null when present");
|
|
1301
|
+
password = record.password;
|
|
1302
|
+
}
|
|
1303
|
+
let bypass;
|
|
1304
|
+
if (record.bypass !== void 0) {
|
|
1305
|
+
if (!Array.isArray(record.bypass) || record.bypass.some((entry) => typeof entry !== "string")) throw new BadRequest("payload.bypass must be an array of strings when present");
|
|
1306
|
+
bypass = record.bypass;
|
|
1307
|
+
}
|
|
1308
|
+
return {
|
|
1309
|
+
enabled: record.enabled,
|
|
1310
|
+
url: record.url,
|
|
1311
|
+
...username === void 0 ? {} : { username },
|
|
1312
|
+
...password === void 0 ? {} : { password },
|
|
1313
|
+
...bypass === void 0 ? {} : { bypass }
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
/** Validate a `proxyTest` payload (the destination URL and an optional draft). */
|
|
1317
|
+
function readProxyTestPayload(payload) {
|
|
1318
|
+
if (typeof payload !== "object" || payload === null) return {};
|
|
1319
|
+
const record = payload;
|
|
1320
|
+
const url = record.url;
|
|
1321
|
+
if (url === void 0 && record.proxy === void 0) return {};
|
|
1322
|
+
if (url !== void 0 && (typeof url !== "string" || url.length === 0)) throw new BadRequest("payload.url must be a non-empty string when present");
|
|
1323
|
+
let proxy;
|
|
1324
|
+
if (record.proxy !== void 0) {
|
|
1325
|
+
if (typeof record.proxy !== "object" || record.proxy === null) throw new BadRequest("payload.proxy must be an object when present");
|
|
1326
|
+
const draftRecord = record.proxy;
|
|
1327
|
+
if (typeof draftRecord.url !== "string" || draftRecord.url.length === 0) throw new BadRequest("payload.proxy.url must be a non-empty string");
|
|
1328
|
+
let username;
|
|
1329
|
+
if (draftRecord.username !== void 0) {
|
|
1330
|
+
if (typeof draftRecord.username !== "string") throw new BadRequest("payload.proxy.username must be a string when present");
|
|
1331
|
+
username = draftRecord.username;
|
|
1332
|
+
}
|
|
1333
|
+
let password;
|
|
1334
|
+
if (draftRecord.password !== void 0) {
|
|
1335
|
+
if (typeof draftRecord.password !== "string") throw new BadRequest("payload.proxy.password must be a string when present");
|
|
1336
|
+
password = draftRecord.password;
|
|
1337
|
+
}
|
|
1338
|
+
proxy = {
|
|
1339
|
+
url: draftRecord.url,
|
|
1340
|
+
...username === void 0 ? {} : { username },
|
|
1341
|
+
...password === void 0 ? {} : { password }
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
...url === void 0 ? {} : { url },
|
|
1346
|
+
...proxy === void 0 ? {} : { proxy }
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
839
1350
|
switch (endpoint) {
|
|
840
1351
|
case "status": {
|
|
841
1352
|
const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider)]));
|
|
842
1353
|
return ok({ providers: Object.fromEntries(entries) });
|
|
843
1354
|
}
|
|
844
|
-
case "login":
|
|
1355
|
+
case "login": {
|
|
1356
|
+
const provider = readProvider(payload);
|
|
1357
|
+
return ok(await controller.login(provider, readLoginMethod(payload, provider)));
|
|
1358
|
+
}
|
|
845
1359
|
case "manual": {
|
|
846
1360
|
const provider = readProvider(payload);
|
|
847
1361
|
await controller.manual(provider, readString(payload, "input"));
|
|
@@ -850,16 +1364,35 @@ async function dispatch(controller, speed, endpoint, payload, signal) {
|
|
|
850
1364
|
case "cancel":
|
|
851
1365
|
await controller.cancel(readProvider(payload));
|
|
852
1366
|
return ok({ ok: true });
|
|
853
|
-
case "logout":
|
|
854
|
-
|
|
1367
|
+
case "logout": {
|
|
1368
|
+
const provider = readProvider(payload);
|
|
1369
|
+
await controller.logout(provider, readString(payload, "account"));
|
|
1370
|
+
return ok({ ok: true });
|
|
1371
|
+
}
|
|
1372
|
+
case "setDefault": {
|
|
1373
|
+
const provider = readProvider(payload);
|
|
1374
|
+
await controller.setDefault(provider, readString(payload, "account"));
|
|
855
1375
|
return ok({ ok: true });
|
|
856
|
-
|
|
1376
|
+
}
|
|
1377
|
+
case "usage": {
|
|
1378
|
+
const provider = readProvider(payload);
|
|
1379
|
+
return ok(await controller.usage(provider, readString(payload, "account"), signal));
|
|
1380
|
+
}
|
|
857
1381
|
case "image": return ok(await controller.readImage(readImageRef(payload), signal));
|
|
858
1382
|
case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
|
|
859
1383
|
case "speed": return ok(await speed.speed(readSessionId(payload)));
|
|
860
1384
|
case "setSpeed":
|
|
861
1385
|
await speed.setSpeed(readSessionId(payload), readSpeedTier(payload));
|
|
862
1386
|
return ok({ ok: true });
|
|
1387
|
+
case "proxyGet":
|
|
1388
|
+
if (proxy === void 0) throw new BadRequest("proxy configuration is unavailable");
|
|
1389
|
+
return ok(await proxy.get());
|
|
1390
|
+
case "proxySet":
|
|
1391
|
+
if (proxy === void 0) throw new BadRequest("proxy configuration is unavailable");
|
|
1392
|
+
return ok(await proxy.set(readProxyInput(payload)));
|
|
1393
|
+
case "proxyTest":
|
|
1394
|
+
if (proxy === void 0) throw new BadRequest("proxy configuration is unavailable");
|
|
1395
|
+
return ok(await proxy.test(readProxyTestPayload(payload)));
|
|
863
1396
|
default: throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
|
|
864
1397
|
}
|
|
865
1398
|
}
|
|
@@ -868,13 +1401,14 @@ async function dispatch(controller, speed, endpoint, payload, signal) {
|
|
|
868
1401
|
* @param ctx - the plugin context (headless profiles have no `connection`).
|
|
869
1402
|
* @param controller - the auth operations backing the endpoints.
|
|
870
1403
|
* @param speed - the per-session speed-tier state backing the Speed toggle.
|
|
1404
|
+
* @param proxy - optional proxy-config controller backing `proxyGet`/`proxySet`/`proxyTest`.
|
|
871
1405
|
*/
|
|
872
|
-
function registerAuthRpc(ctx, controller, speed) {
|
|
1406
|
+
function registerAuthRpc(ctx, controller, speed, proxy = void 0) {
|
|
873
1407
|
ctx.inject(["connection"], (ctx$1) => {
|
|
874
1408
|
const connection = ctx$1.get("connection");
|
|
875
1409
|
ctx$1.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload, signal) => {
|
|
876
1410
|
try {
|
|
877
|
-
return await dispatch(controller, speed, endpoint, payload, signal);
|
|
1411
|
+
return await dispatch(controller, speed, proxy, endpoint, payload, signal);
|
|
878
1412
|
} catch (error) {
|
|
879
1413
|
return failure(error);
|
|
880
1414
|
}
|
|
@@ -1075,13 +1609,41 @@ var TokenManager = class {
|
|
|
1075
1609
|
}
|
|
1076
1610
|
}
|
|
1077
1611
|
async doRefresh(session) {
|
|
1078
|
-
const current = await this.options.load();
|
|
1079
|
-
if (current !== void 0 && current.accessToken !== session.accessToken && current.expiresAt - Date.now() > this.options.preemptMs) return current;
|
|
1080
|
-
const next = await this.options.refresh(current ?? session);
|
|
1612
|
+
const current$1 = await this.options.load();
|
|
1613
|
+
if (current$1 !== void 0 && current$1.accessToken !== session.accessToken && current$1.expiresAt - Date.now() > this.options.preemptMs) return current$1;
|
|
1614
|
+
const next = await this.options.refresh(current$1 ?? session);
|
|
1081
1615
|
await this.options.save(next);
|
|
1082
1616
|
return next;
|
|
1083
1617
|
}
|
|
1084
1618
|
};
|
|
1619
|
+
/** Bound on one account catalog fetch or usage poll — a hang must not block the picker. */
|
|
1620
|
+
const DISCOVERY_TIMEOUT_MS = 1e4;
|
|
1621
|
+
/**
|
|
1622
|
+
* Run `work` with an aborting signal. Resolves undefined when the timeout
|
|
1623
|
+
* fires (the fetch is aborted); other failures propagate.
|
|
1624
|
+
*/
|
|
1625
|
+
function withTimeout(work, timeoutMs) {
|
|
1626
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
1627
|
+
const aborted = new Promise((resolve) => {
|
|
1628
|
+
if (signal.aborted) resolve(void 0);
|
|
1629
|
+
else signal.addEventListener("abort", () => resolve(void 0), { once: true });
|
|
1630
|
+
});
|
|
1631
|
+
return Promise.race([work(signal).then((value) => signal.aborted ? void 0 : value, (error) => {
|
|
1632
|
+
if (signal.aborted) return void 0;
|
|
1633
|
+
throw error;
|
|
1634
|
+
}), aborted]);
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* First account catalog that lists `model` (callers pass default-first).
|
|
1638
|
+
* One failing lookup sits that account out so a sibling's metadata still
|
|
1639
|
+
* resolves — the same isolation as the picker catalog union.
|
|
1640
|
+
*/
|
|
1641
|
+
async function discoverAcrossAccounts(accounts, lookup) {
|
|
1642
|
+
for (const account of accounts) try {
|
|
1643
|
+
const found = await lookup(account);
|
|
1644
|
+
if (found !== void 0) return found;
|
|
1645
|
+
} catch {}
|
|
1646
|
+
}
|
|
1085
1647
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
1086
1648
|
const DISCOVERY_TTL_MS = 5 * 6e4;
|
|
1087
1649
|
/**
|
|
@@ -1103,6 +1665,8 @@ var ModelCatalogCache = class {
|
|
|
1103
1665
|
seeded;
|
|
1104
1666
|
/** Set by {@link invalidate} so an in-flight disk read cannot resurrect dropped state. */
|
|
1105
1667
|
seedDisabled = false;
|
|
1668
|
+
/** Bumped by {@link invalidate} so a loser in-flight fetch cannot write back. */
|
|
1669
|
+
generation = 0;
|
|
1106
1670
|
constructor(persistence, ttlMs = DISCOVERY_TTL_MS) {
|
|
1107
1671
|
this.persistence = persistence;
|
|
1108
1672
|
this.ttlMs = ttlMs;
|
|
@@ -1133,7 +1697,10 @@ var ModelCatalogCache = class {
|
|
|
1133
1697
|
}
|
|
1134
1698
|
/** Run (or join) the single in-flight fetch, updating memory and disk on success. */
|
|
1135
1699
|
refresh(fetcher) {
|
|
1136
|
-
this.inflight
|
|
1700
|
+
if (this.inflight !== void 0) return this.inflight;
|
|
1701
|
+
const gen = this.generation;
|
|
1702
|
+
const pending = fetcher().then((models) => {
|
|
1703
|
+
if (this.generation !== gen) return models;
|
|
1137
1704
|
const snapshot = {
|
|
1138
1705
|
at: Date.now(),
|
|
1139
1706
|
models
|
|
@@ -1142,9 +1709,10 @@ var ModelCatalogCache = class {
|
|
|
1142
1709
|
this.persistence?.save(snapshot).catch(() => void 0);
|
|
1143
1710
|
return models;
|
|
1144
1711
|
}).finally(() => {
|
|
1145
|
-
this.inflight = void 0;
|
|
1712
|
+
if (this.generation === gen) this.inflight = void 0;
|
|
1146
1713
|
});
|
|
1147
|
-
|
|
1714
|
+
this.inflight = pending;
|
|
1715
|
+
return pending;
|
|
1148
1716
|
}
|
|
1149
1717
|
/**
|
|
1150
1718
|
* Return the cached catalog when fresh, otherwise fetch and cache it.
|
|
@@ -1182,7 +1750,9 @@ var ModelCatalogCache = class {
|
|
|
1182
1750
|
}
|
|
1183
1751
|
/** Drop the cached catalog (e.g. after a 401 proved the credential changed). */
|
|
1184
1752
|
invalidate() {
|
|
1753
|
+
this.generation += 1;
|
|
1185
1754
|
this.entry = void 0;
|
|
1755
|
+
this.inflight = void 0;
|
|
1186
1756
|
this.seedDisabled = true;
|
|
1187
1757
|
this.persistence?.clear().catch(() => void 0);
|
|
1188
1758
|
}
|
|
@@ -1191,6 +1761,11 @@ var ModelCatalogCache = class {
|
|
|
1191
1761
|
function isMissingOrInvalidCredential(error) {
|
|
1192
1762
|
return error instanceof LlmError && (error.code === "MISSING_CREDENTIAL" || error.code === "INVALID_CREDENTIAL");
|
|
1193
1763
|
}
|
|
1764
|
+
/** Whether discovery stopped because the caller cancelled or the timeout fired. */
|
|
1765
|
+
function isDiscoveryAborted(error, signal) {
|
|
1766
|
+
if (signal?.aborted === true) return true;
|
|
1767
|
+
return signal !== void 0 && error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
1768
|
+
}
|
|
1194
1769
|
/** Whether discovery failed because the access token was rejected. */
|
|
1195
1770
|
function isDiscoveryAuthFailure(error) {
|
|
1196
1771
|
return error instanceof OAuthEndpointError && error.status === 401 || error instanceof LlmError && error.code === "AUTH";
|
|
@@ -1216,6 +1791,109 @@ async function discoverOrRetryAuth(session, catalog, run) {
|
|
|
1216
1791
|
}
|
|
1217
1792
|
}
|
|
1218
1793
|
|
|
1794
|
+
//#endregion
|
|
1795
|
+
//#region src/providers/accounts.ts
|
|
1796
|
+
/** Catalog sort hint when the provider advertised one (Codex `priority`). */
|
|
1797
|
+
function catalogPriority(model) {
|
|
1798
|
+
const ranked = model;
|
|
1799
|
+
return typeof ranked.priority === "number" ? ranked.priority : Number.MAX_SAFE_INTEGER;
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Merge per-account catalogs, keeping the first occurrence of each model id.
|
|
1803
|
+
* Rows that carry a numeric `priority` (Codex discovery) are then ordered by
|
|
1804
|
+
* it so a model only the second account lists — e.g. `gpt-5.6-sol` — still
|
|
1805
|
+
* sits with its generation instead of being appended after the default
|
|
1806
|
+
* account's older ids.
|
|
1807
|
+
*/
|
|
1808
|
+
async function unionAccountCatalogs(accounts, listOne, options) {
|
|
1809
|
+
const timeoutMs = options?.timeoutMs;
|
|
1810
|
+
const caller = options?.signal;
|
|
1811
|
+
const catalogs = await Promise.all(accounts.map(async (account) => {
|
|
1812
|
+
try {
|
|
1813
|
+
if (timeoutMs === void 0) return await listOne(account, caller);
|
|
1814
|
+
return await withTimeout((timeoutSignal) => listOne(account, caller === void 0 ? timeoutSignal : AbortSignal.any([timeoutSignal, caller])), timeoutMs) ?? [];
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
if (caller?.aborted === true) throw error;
|
|
1817
|
+
return [];
|
|
1818
|
+
}
|
|
1819
|
+
}));
|
|
1820
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1821
|
+
const models = [];
|
|
1822
|
+
for (const catalog of catalogs) for (const model of catalog) {
|
|
1823
|
+
if (seen.has(model.id)) continue;
|
|
1824
|
+
seen.add(model.id);
|
|
1825
|
+
models.push(model);
|
|
1826
|
+
}
|
|
1827
|
+
models.sort((left, right) => catalogPriority(left) - catalogPriority(right));
|
|
1828
|
+
return models;
|
|
1829
|
+
}
|
|
1830
|
+
var AccountTokenManager = class {
|
|
1831
|
+
managers = /* @__PURE__ */ new Map();
|
|
1832
|
+
io;
|
|
1833
|
+
constructor(options) {
|
|
1834
|
+
this.options = options;
|
|
1835
|
+
const provider = options.provider;
|
|
1836
|
+
this.io = options.io ?? {
|
|
1837
|
+
list: () => listAccounts(provider),
|
|
1838
|
+
get: (account) => getAccountSession(provider, account),
|
|
1839
|
+
save: (account, session) => saveAccountSession(provider, account, session),
|
|
1840
|
+
remove: (account) => deleteAccountSession(provider, account)
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
/** The provider's accounts, default first (straight from the store). */
|
|
1844
|
+
list() {
|
|
1845
|
+
return this.io.list();
|
|
1846
|
+
}
|
|
1847
|
+
/** The default account's key, or undefined when logged out. */
|
|
1848
|
+
async defaultAccount() {
|
|
1849
|
+
return (await this.list())[0]?.key;
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Resolve a usable session for one account (default when omitted),
|
|
1853
|
+
* refreshing proactively or on demand.
|
|
1854
|
+
* @param account - the account key; the default account when undefined.
|
|
1855
|
+
* @param forceRefresh - refresh regardless of expiry (used after a 401).
|
|
1856
|
+
* @returns the persisted session to send.
|
|
1857
|
+
* @throws LlmError MISSING_CREDENTIAL when the account is not logged in.
|
|
1858
|
+
*/
|
|
1859
|
+
async session(account, forceRefresh = false) {
|
|
1860
|
+
const key = account ?? await this.defaultAccount();
|
|
1861
|
+
if (key === void 0) throw this.missingCredential();
|
|
1862
|
+
return this.tokensFor(key).session(forceRefresh);
|
|
1863
|
+
}
|
|
1864
|
+
/** Read an account's stored session without any refresh side effect. */
|
|
1865
|
+
peek(account) {
|
|
1866
|
+
return this.io.get(account);
|
|
1867
|
+
}
|
|
1868
|
+
/** Whether a session is stored for the account (cheap; never refreshes). */
|
|
1869
|
+
async hasSession(account) {
|
|
1870
|
+
return await this.peek(account) !== void 0;
|
|
1871
|
+
}
|
|
1872
|
+
/** The TokenManager bound to one account (created lazily, then cached). */
|
|
1873
|
+
tokensFor(account) {
|
|
1874
|
+
let manager = this.managers.get(account);
|
|
1875
|
+
if (manager === void 0) {
|
|
1876
|
+
const io = this.io;
|
|
1877
|
+
manager = new TokenManager({
|
|
1878
|
+
displayName: this.options.displayName,
|
|
1879
|
+
...this.options.makeOptions(account),
|
|
1880
|
+
load: () => io.get(account),
|
|
1881
|
+
save: (session) => io.save(account, session),
|
|
1882
|
+
remove: () => io.remove(account),
|
|
1883
|
+
onRemoved: () => {
|
|
1884
|
+
this.options.onAccountRemoved?.(account);
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
this.managers.set(account, manager);
|
|
1888
|
+
}
|
|
1889
|
+
return manager;
|
|
1890
|
+
}
|
|
1891
|
+
/** The logged-out error, mirroring TokenManager's own message. */
|
|
1892
|
+
missingCredential() {
|
|
1893
|
+
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");
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1219
1897
|
//#endregion
|
|
1220
1898
|
//#region src/providers/catalog-store.ts
|
|
1221
1899
|
/**
|
|
@@ -1360,6 +2038,616 @@ function catalogStore(provider, path = modelsFilePath()) {
|
|
|
1360
2038
|
};
|
|
1361
2039
|
}
|
|
1362
2040
|
|
|
2041
|
+
//#endregion
|
|
2042
|
+
//#region src/providers/pool-family.ts
|
|
2043
|
+
/** Map key for one provider's pool of one model (ids collide across providers). */
|
|
2044
|
+
function poolKey(provider, model) {
|
|
2045
|
+
return `${provider}/${model}`;
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Build per-provider account routes. Each model id becomes a definition of
|
|
2049
|
+
* the accounts that list it: two or more fail over; one is pinned to that
|
|
2050
|
+
* account (so a Max-only model is never sent to a Plus login). The picker
|
|
2051
|
+
* unions these catalogs; a logout that drops a model to one account keeps
|
|
2052
|
+
* the same id and pins it to whoever remains.
|
|
2053
|
+
* @param sources - per-account catalogs (providers with no accounts list
|
|
2054
|
+
* nothing and simply never join a pool).
|
|
2055
|
+
* @returns `provider/model` → pool definition (not listed as an extra entry).
|
|
2056
|
+
*/
|
|
2057
|
+
function buildAccountPools(sources) {
|
|
2058
|
+
const pools = /* @__PURE__ */ new Map();
|
|
2059
|
+
for (const [provider, source] of Object.entries(sources)) {
|
|
2060
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
2061
|
+
for (const catalog of source.catalogs) for (const model of catalog.models) {
|
|
2062
|
+
let entry = byModel.get(model.id);
|
|
2063
|
+
if (entry === void 0) {
|
|
2064
|
+
entry = {
|
|
2065
|
+
members: [],
|
|
2066
|
+
info: model
|
|
2067
|
+
};
|
|
2068
|
+
byModel.set(model.id, entry);
|
|
2069
|
+
}
|
|
2070
|
+
entry.members.push({
|
|
2071
|
+
provider,
|
|
2072
|
+
account: catalog.account,
|
|
2073
|
+
model: model.id
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
for (const [id, { members, info }] of byModel) pools.set(poolKey(provider, id), {
|
|
2077
|
+
members,
|
|
2078
|
+
...info.name === void 0 || info.name === id ? {} : { name: info.name },
|
|
2079
|
+
...info.description === void 0 ? {} : { description: info.description }
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
return pools;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
//#endregion
|
|
2086
|
+
//#region src/providers/pool-health.ts
|
|
2087
|
+
/** Registry key for one pool member. */
|
|
2088
|
+
function memberKey(provider, account, model) {
|
|
2089
|
+
return `${provider}/${account}/${model}`;
|
|
2090
|
+
}
|
|
2091
|
+
/** Registry key parking EVERY member of one account (account-level failures). */
|
|
2092
|
+
function accountKey(provider, account) {
|
|
2093
|
+
return `${provider}/${account}/*`;
|
|
2094
|
+
}
|
|
2095
|
+
/** Default cooldown when a quota/rate failure carries no `retry-after`. */
|
|
2096
|
+
const DEFAULT_QUOTA_COOLDOWN_MS = 5 * 6e4;
|
|
2097
|
+
/** Auth failures recheck after a day; a re-login clears the record immediately. */
|
|
2098
|
+
const AUTH_COOLDOWN_MS = 1440 * 6e4;
|
|
2099
|
+
/** Transient server-side failures cool down briefly. */
|
|
2100
|
+
const TRANSIENT_COOLDOWN_MS = 6e4;
|
|
2101
|
+
/**
|
|
2102
|
+
* Providers whose quota windows are model-scoped, so a quota failure on one
|
|
2103
|
+
* model says nothing about its siblings (Claude's Opus/Sonnet lanes). Every
|
|
2104
|
+
* other provider meters the account as a whole: one member hitting the wall
|
|
2105
|
+
* means its siblings on the SAME account would too, so the cooldown parks
|
|
2106
|
+
* the account (other accounts of the provider are unaffected).
|
|
2107
|
+
*/
|
|
2108
|
+
const MODEL_SCOPED_QUOTA_PROVIDERS = new Set(["claude"]);
|
|
2109
|
+
/** The `retry-after` an adapter propagated through `httpLlmError`, when any. */
|
|
2110
|
+
function retryAfterMs(error) {
|
|
2111
|
+
return error.failure.providerRetryAfterMs;
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Classify a member failure. Quota and rate-limit failures cool down (using
|
|
2115
|
+
* the provider's own `retry-after` when sent, which is more accurate than
|
|
2116
|
+
* any fixed guess) — account-wide for account-metered providers, per-member
|
|
2117
|
+
* for model-scoped ones; auth failures park the account until re-login
|
|
2118
|
+
* (credentials are account-level); server/timeout failures get a short
|
|
2119
|
+
* per-member cooldown; transport failures switch without a record;
|
|
2120
|
+
* everything else — most importantly CONTEXT_WINDOW_EXCEEDED and ABORTED —
|
|
2121
|
+
* is the request's own fault and is rethrown untouched.
|
|
2122
|
+
* @param error - the failure thrown by a member adapter's stream.
|
|
2123
|
+
* @param provider - the failing member's provider (decides the quota scope).
|
|
2124
|
+
* @returns the action the pool should take.
|
|
2125
|
+
*/
|
|
2126
|
+
function classifyPoolFailure(error, provider) {
|
|
2127
|
+
if (!(error instanceof LlmError)) return { action: "throw" };
|
|
2128
|
+
switch (error.code) {
|
|
2129
|
+
case QUOTA_EXCEEDED_CODE:
|
|
2130
|
+
case "RATE_LIMIT": return {
|
|
2131
|
+
action: "switch",
|
|
2132
|
+
cooldownMs: retryAfterMs(error) ?? DEFAULT_QUOTA_COOLDOWN_MS,
|
|
2133
|
+
reason: error.code,
|
|
2134
|
+
scope: MODEL_SCOPED_QUOTA_PROVIDERS.has(provider) ? "member" : "account"
|
|
2135
|
+
};
|
|
2136
|
+
case "AUTH":
|
|
2137
|
+
case "INVALID_CREDENTIAL":
|
|
2138
|
+
case "MISSING_CREDENTIAL": return {
|
|
2139
|
+
action: "switch",
|
|
2140
|
+
cooldownMs: AUTH_COOLDOWN_MS,
|
|
2141
|
+
reason: error.code,
|
|
2142
|
+
scope: "account"
|
|
2143
|
+
};
|
|
2144
|
+
case "SERVER":
|
|
2145
|
+
case "TIMEOUT":
|
|
2146
|
+
case "EMPTY_RESPONSE": return {
|
|
2147
|
+
action: "switch",
|
|
2148
|
+
cooldownMs: TRANSIENT_COOLDOWN_MS,
|
|
2149
|
+
reason: error.code,
|
|
2150
|
+
scope: "member"
|
|
2151
|
+
};
|
|
2152
|
+
case "TRANSPORT": return { action: "switch" };
|
|
2153
|
+
case "HTTP_402":
|
|
2154
|
+
case "HTTP_404": return {
|
|
2155
|
+
action: "switch",
|
|
2156
|
+
cooldownMs: TRANSIENT_COOLDOWN_MS,
|
|
2157
|
+
reason: error.code,
|
|
2158
|
+
scope: "member"
|
|
2159
|
+
};
|
|
2160
|
+
case CONTEXT_WINDOW_EXCEEDED_CODE:
|
|
2161
|
+
case "ABORTED":
|
|
2162
|
+
default: return { action: "throw" };
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Cooldown registry keyed by {@link memberKey}. A member whose cooldown has
|
|
2167
|
+
* expired is simply available again — recovery is proven by the next real
|
|
2168
|
+
* request, not by a background probe.
|
|
2169
|
+
*/
|
|
2170
|
+
var PoolHealthRegistry = class {
|
|
2171
|
+
records = /* @__PURE__ */ new Map();
|
|
2172
|
+
/** Whether a member may serve: neither it nor its whole account is cooling. */
|
|
2173
|
+
isMemberAvailable(provider, account, model, now = Date.now()) {
|
|
2174
|
+
return this.isAvailable(accountKey(provider, account), now) && this.isAvailable(memberKey(provider, account, model), now);
|
|
2175
|
+
}
|
|
2176
|
+
/** Whether one registry key is clear right now. */
|
|
2177
|
+
isAvailable(key, now = Date.now()) {
|
|
2178
|
+
const record = this.records.get(key);
|
|
2179
|
+
if (record === void 0) return true;
|
|
2180
|
+
if (record.unavailableUntil <= now) {
|
|
2181
|
+
this.records.delete(key);
|
|
2182
|
+
return true;
|
|
2183
|
+
}
|
|
2184
|
+
return false;
|
|
2185
|
+
}
|
|
2186
|
+
/** Park a member for `cooldownMs`; a longer existing cooldown wins. */
|
|
2187
|
+
markUnavailable(key, cooldownMs, reason, now = Date.now()) {
|
|
2188
|
+
const until = now + cooldownMs;
|
|
2189
|
+
const existing = this.records.get(key);
|
|
2190
|
+
if (existing !== void 0 && existing.unavailableUntil > until) return;
|
|
2191
|
+
this.records.set(key, {
|
|
2192
|
+
unavailableUntil: until,
|
|
2193
|
+
reason
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Epoch ms at which the earliest cooling record among `keys` recovers;
|
|
2198
|
+
* `undefined` when none of them is cooling. The registry is shared by
|
|
2199
|
+
* every pool, so the caller passes the keys of ITS members (member and
|
|
2200
|
+
* account keys alike) — an unrelated pool's cooldown must not shape this
|
|
2201
|
+
* pool's retry hint. Feeds the pool-exhausted error's
|
|
2202
|
+
* `providerRetryAfterMs`.
|
|
2203
|
+
*/
|
|
2204
|
+
earliestRecovery(keys, now = Date.now()) {
|
|
2205
|
+
let earliest;
|
|
2206
|
+
for (const [key, record] of this.records) {
|
|
2207
|
+
if (record.unavailableUntil <= now) {
|
|
2208
|
+
this.records.delete(key);
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
if (!keys.has(key)) continue;
|
|
2212
|
+
if (earliest === void 0 || record.unavailableUntil < earliest) earliest = record.unavailableUntil;
|
|
2213
|
+
}
|
|
2214
|
+
return earliest;
|
|
2215
|
+
}
|
|
2216
|
+
/** Drop records of one provider, or of a single account when given (auth changes). */
|
|
2217
|
+
clear(provider, account) {
|
|
2218
|
+
const prefix = account === void 0 ? `${provider}/` : `${provider}/${account}/`;
|
|
2219
|
+
for (const key of [...this.records.keys()]) if (key.startsWith(prefix)) this.records.delete(key);
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
|
|
2223
|
+
//#endregion
|
|
2224
|
+
//#region src/providers/pool.ts
|
|
2225
|
+
/** Bound on sticky-session memory; oldest entries evict past it. */
|
|
2226
|
+
const STICKY_SESSION_LIMIT = 1e3;
|
|
2227
|
+
/** Display form of one member (account shown when pinned). */
|
|
2228
|
+
function memberLabel(member) {
|
|
2229
|
+
return member.account === void 0 ? `${member.provider}/${member.model}` : `${member.provider}/${member.account}/${member.model}`;
|
|
2230
|
+
}
|
|
2231
|
+
/** How long a pools snapshot is trusted (auth changes invalidate immediately). */
|
|
2232
|
+
const POOLS_CACHE_TTL_MS = 5e3;
|
|
2233
|
+
var PoolAdapter = class extends LlmAdapter {
|
|
2234
|
+
/** sessionId|poolId → member key of the last member that served a chunk. */
|
|
2235
|
+
sticky = /* @__PURE__ */ new Map();
|
|
2236
|
+
/** Messages already warned about — configuration diagnostics repeat every request otherwise. */
|
|
2237
|
+
warned = /* @__PURE__ */ new Set();
|
|
2238
|
+
/**
|
|
2239
|
+
* Short-lived pools snapshot. `owns()` runs on every resolveModel — the
|
|
2240
|
+
* model picker issues one per entry — and pool assembly touches every
|
|
2241
|
+
* provider's catalog and account store, so recompute at most this often.
|
|
2242
|
+
* Auth changes bump {@link generation} so a stale snapshot cannot land.
|
|
2243
|
+
*/
|
|
2244
|
+
poolsCache;
|
|
2245
|
+
poolsInflight;
|
|
2246
|
+
generation = 0;
|
|
2247
|
+
constructor(options) {
|
|
2248
|
+
super();
|
|
2249
|
+
this.options = options;
|
|
2250
|
+
}
|
|
2251
|
+
/** Drop the pools snapshot so the next read reflects the current accounts. */
|
|
2252
|
+
invalidate() {
|
|
2253
|
+
this.generation += 1;
|
|
2254
|
+
this.poolsCache = void 0;
|
|
2255
|
+
this.poolsInflight = void 0;
|
|
2256
|
+
}
|
|
2257
|
+
/** Warn once per distinct message (pools() runs on every request). */
|
|
2258
|
+
warnOnce(message) {
|
|
2259
|
+
if (this.warned.has(message)) return;
|
|
2260
|
+
this.warned.add(message);
|
|
2261
|
+
this.options.onWarn(message);
|
|
2262
|
+
}
|
|
2263
|
+
/** Drop members whose adapter is not registered (copy — caller state is shared). */
|
|
2264
|
+
usable(pools) {
|
|
2265
|
+
const result = new Map(pools);
|
|
2266
|
+
for (const [id, definition] of [...result]) {
|
|
2267
|
+
const kept = definition.members.filter((member) => this.options.adapters[member.provider] !== void 0);
|
|
2268
|
+
if (kept.length === 0) result.delete(id);
|
|
2269
|
+
else if (kept.length < definition.members.length) result.set(id, {
|
|
2270
|
+
...definition,
|
|
2271
|
+
members: kept
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
return result;
|
|
2275
|
+
}
|
|
2276
|
+
/** Account pools (auto-aggregated plus config overrides) with usable members. */
|
|
2277
|
+
async familyPools() {
|
|
2278
|
+
return this.usable(new Map(await this.options.families()));
|
|
2279
|
+
}
|
|
2280
|
+
/** All pools (account pools merged with extra tiers) with usable members. */
|
|
2281
|
+
async pools() {
|
|
2282
|
+
const cached = this.poolsCache;
|
|
2283
|
+
if (cached !== void 0 && Date.now() - cached.at < POOLS_CACHE_TTL_MS) return cached.pools;
|
|
2284
|
+
const gen = this.generation;
|
|
2285
|
+
this.poolsInflight ??= this.assemblePools().then((pools) => {
|
|
2286
|
+
if (this.generation === gen) this.poolsCache = {
|
|
2287
|
+
at: Date.now(),
|
|
2288
|
+
pools
|
|
2289
|
+
};
|
|
2290
|
+
return pools;
|
|
2291
|
+
}).finally(() => {
|
|
2292
|
+
this.poolsInflight = void 0;
|
|
2293
|
+
});
|
|
2294
|
+
return this.poolsInflight;
|
|
2295
|
+
}
|
|
2296
|
+
/** Recompute the pools snapshot (account pools merged with extra tiers). */
|
|
2297
|
+
async assemblePools() {
|
|
2298
|
+
const pools = await this.familyPools();
|
|
2299
|
+
for (const [id, members] of Object.entries(this.options.tiers)) {
|
|
2300
|
+
if (members.length === 0) continue;
|
|
2301
|
+
const owner = members[0].provider;
|
|
2302
|
+
const key = poolKey(owner, id);
|
|
2303
|
+
if (pools.has(key)) this.warnOnce(`tier pool "${id}" overrides the account pool of the same id under ${owner}`);
|
|
2304
|
+
pools.set(key, {
|
|
2305
|
+
members,
|
|
2306
|
+
extra: true
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
return this.usable(pools);
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Extra picker rows one provider lists (configured tiers). Account pools
|
|
2313
|
+
* reuse the catalog entry of the same wire id, so they are not listed
|
|
2314
|
+
* again — the picker stays one row per model in ChatGPT / Claude / ….
|
|
2315
|
+
*/
|
|
2316
|
+
async modelsForProvider(provider) {
|
|
2317
|
+
const pools = await this.pools();
|
|
2318
|
+
const models = [];
|
|
2319
|
+
for (const [key, definition] of pools) {
|
|
2320
|
+
if (definition.extra !== true) continue;
|
|
2321
|
+
if (!key.startsWith(`${provider}/`)) continue;
|
|
2322
|
+
const id = key.slice(provider.length + 1);
|
|
2323
|
+
models.push({
|
|
2324
|
+
provider,
|
|
2325
|
+
id,
|
|
2326
|
+
name: definition.name ?? id,
|
|
2327
|
+
...definition.description === void 0 ? {} : { description: definition.description }
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
return models;
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Whether `model` on `provider`'s route is served here (several accounts
|
|
2334
|
+
* fail over, one account is pinned, or a configured tier).
|
|
2335
|
+
*/
|
|
2336
|
+
async owns(provider, model) {
|
|
2337
|
+
return (await this.pools()).has(poolKey(provider, model));
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Resolve every member's account (config members may omit it to mean "the
|
|
2341
|
+
* default account") and drop members with no resolvable login. Duplicates
|
|
2342
|
+
* collapse — an explicitly pinned account and the default may coincide.
|
|
2343
|
+
*/
|
|
2344
|
+
async concrete(members) {
|
|
2345
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2346
|
+
const resolved = [];
|
|
2347
|
+
for (const member of members) {
|
|
2348
|
+
const account = member.account ?? await this.options.defaultAccount(member.provider);
|
|
2349
|
+
if (account === void 0) continue;
|
|
2350
|
+
const key = memberKey(member.provider, account, member.model);
|
|
2351
|
+
if (seen.has(key)) continue;
|
|
2352
|
+
seen.add(key);
|
|
2353
|
+
resolved.push({
|
|
2354
|
+
provider: member.provider,
|
|
2355
|
+
account,
|
|
2356
|
+
model: member.model
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
return resolved;
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Resolve a pool model to the conservative INTERSECTION of its members'
|
|
2363
|
+
* capabilities: the smallest context window and output cap, the reasoning
|
|
2364
|
+
* efforts every member supports, and the modalities all of them accept —
|
|
2365
|
+
* so a request valid for the pool stays valid after a failover. Capability
|
|
2366
|
+
* metadata is provider-level, so each provider resolves once regardless of
|
|
2367
|
+
* how many accounts it pools.
|
|
2368
|
+
*/
|
|
2369
|
+
async resolveModel(provider, model) {
|
|
2370
|
+
const definition = (await this.pools()).get(poolKey(provider, model));
|
|
2371
|
+
if (definition === void 0) throw new LlmError(`unknown pool model "${model}"`, "NO_ADAPTER");
|
|
2372
|
+
const resolved = [];
|
|
2373
|
+
let lastFailure;
|
|
2374
|
+
const seenProviders = /* @__PURE__ */ new Set();
|
|
2375
|
+
for (const member of definition.members) {
|
|
2376
|
+
if (seenProviders.has(member.provider)) continue;
|
|
2377
|
+
seenProviders.add(member.provider);
|
|
2378
|
+
const adapter = this.options.adapters[member.provider];
|
|
2379
|
+
if (adapter === void 0) continue;
|
|
2380
|
+
try {
|
|
2381
|
+
resolved.push(await adapter.resolveOwnModel(member.provider, member.model));
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
lastFailure = error;
|
|
2384
|
+
this.warnOnce(`pool "${model}": member ${memberLabel(member)} failed to resolve (${error instanceof Error ? error.message : String(error)}); excluding it`);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
if (resolved.length === 0) throw new LlmError(`pool "${model}" has no usable member`, "NO_ADAPTER", { ...lastFailure === void 0 ? {} : { cause: lastFailure } });
|
|
2388
|
+
const contextWindows = resolved.map((info) => info.context?.contextWindow).filter(isNumber);
|
|
2389
|
+
const maxTokens = resolved.map((info) => info.defaultMaxTokens).filter(isNumber);
|
|
2390
|
+
const reasoning = intersectReasoning(resolved);
|
|
2391
|
+
const modalities = intersectModalities(resolved);
|
|
2392
|
+
return {
|
|
2393
|
+
provider,
|
|
2394
|
+
id: model,
|
|
2395
|
+
name: definition.name ?? model,
|
|
2396
|
+
...definition.description === void 0 ? {} : { description: definition.description },
|
|
2397
|
+
...contextWindows.length > 0 ? { context: { contextWindow: Math.min(...contextWindows) } } : {},
|
|
2398
|
+
...maxTokens.length > 0 ? { defaultMaxTokens: Math.min(...maxTokens) } : {},
|
|
2399
|
+
...reasoning === void 0 ? {} : { reasoning },
|
|
2400
|
+
...modalities === void 0 ? {} : { inputModalities: modalities }
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
async *stream(options) {
|
|
2404
|
+
const definition = (await this.pools()).get(poolKey(options.provider, options.model));
|
|
2405
|
+
if (definition === void 0) throw new LlmError(`unknown pool model "${options.model}"`, "NO_ADAPTER");
|
|
2406
|
+
const members = await this.concrete(definition.members);
|
|
2407
|
+
const candidates = await this.select(options.model, members, options.sessionId);
|
|
2408
|
+
if (candidates.length === 0) throw this.exhausted(options.model, members);
|
|
2409
|
+
let lastError;
|
|
2410
|
+
for (const member of candidates) {
|
|
2411
|
+
const adapter = this.options.adapters[member.provider];
|
|
2412
|
+
if (adapter === void 0) continue;
|
|
2413
|
+
const iterator = adapter.streamAccount({
|
|
2414
|
+
...options,
|
|
2415
|
+
provider: member.provider,
|
|
2416
|
+
model: member.model
|
|
2417
|
+
}, member.account)[Symbol.asyncIterator]();
|
|
2418
|
+
let first;
|
|
2419
|
+
try {
|
|
2420
|
+
first = await iterator.next();
|
|
2421
|
+
if (first.done === true) throw new LlmError(`${memberLabel(member)} returned an empty stream`, EMPTY_RESPONSE_CODE);
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
const classification = classifyPoolFailure(error, member.provider);
|
|
2424
|
+
if (classification.action === "throw") throw error;
|
|
2425
|
+
if ("cooldownMs" in classification) {
|
|
2426
|
+
this.options.health.markUnavailable(classification.scope === "account" ? accountKey(member.provider, member.account) : memberKey(member.provider, member.account, member.model), classification.cooldownMs, classification.reason);
|
|
2427
|
+
if (classification.reason === QUOTA_EXCEEDED_CODE || classification.reason === "RATE_LIMIT") this.options.usage.invalidate(member.provider, member.account);
|
|
2428
|
+
}
|
|
2429
|
+
this.options.onWarn(`pool "${options.model}": ${memberLabel(member)} failed before any output (${error instanceof Error ? error.message : String(error)}); trying the next member`);
|
|
2430
|
+
lastError = error;
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
this.remember(options.model, options.sessionId, member);
|
|
2434
|
+
try {
|
|
2435
|
+
yield first.value;
|
|
2436
|
+
for (let next = await iterator.next(); next.done !== true; next = await iterator.next()) yield next.value;
|
|
2437
|
+
} finally {
|
|
2438
|
+
try {
|
|
2439
|
+
await iterator.return?.();
|
|
2440
|
+
} catch {}
|
|
2441
|
+
}
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
throw this.exhausted(options.model, members, lastError);
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Order the candidates for one request. Health filters both strategies;
|
|
2448
|
+
* `quota_aware` then ranks by urgency (members without telemetry, e.g.
|
|
2449
|
+
* copilot, score zero and sink to the bottom of their class), while
|
|
2450
|
+
* quota-exhausted members stay as a last-resort tail in pool order. The
|
|
2451
|
+
* sticky member keeps its lead unless a challenger out-scores it by
|
|
2452
|
+
* `switchMargin`.
|
|
2453
|
+
*/
|
|
2454
|
+
async select(poolId, members, sessionId) {
|
|
2455
|
+
const usable = members.filter((member) => this.options.adapters[member.provider] !== void 0 && this.options.health.isMemberAvailable(member.provider, member.account, member.model));
|
|
2456
|
+
if (usable.length === 0) return [];
|
|
2457
|
+
const stickyMember = sessionId === void 0 ? void 0 : usable.find((member) => memberKey(member.provider, member.account, member.model) === this.sticky.get(stickyKey(poolId, sessionId)));
|
|
2458
|
+
if (this.options.strategy === "priority") return stickyMember === void 0 ? usable : [stickyMember, ...usable.filter((member) => member !== stickyMember)];
|
|
2459
|
+
const quotas = new Map(await Promise.all(usable.map(async (member) => [member, await this.options.usage.quotaFor(member)])));
|
|
2460
|
+
const scored = usable.filter((member) => quotas.get(member)?.available === true);
|
|
2461
|
+
const quotaFull = usable.filter((member) => quotas.get(member)?.available === false);
|
|
2462
|
+
scored.sort((a, b) => (quotas.get(b)?.urgency ?? 0) - (quotas.get(a)?.urgency ?? 0));
|
|
2463
|
+
if (stickyMember !== void 0 && scored.includes(stickyMember)) {
|
|
2464
|
+
const best = scored[0];
|
|
2465
|
+
const stickyUrgency = quotas.get(stickyMember)?.urgency ?? 0;
|
|
2466
|
+
const bestUrgency = quotas.get(best)?.urgency ?? 0;
|
|
2467
|
+
if (best === stickyMember || bestUrgency <= stickyUrgency * this.options.switchMargin) {
|
|
2468
|
+
scored.splice(scored.indexOf(stickyMember), 1);
|
|
2469
|
+
scored.unshift(stickyMember);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
return [...scored, ...quotaFull];
|
|
2473
|
+
}
|
|
2474
|
+
/** Pin the serving member to the session (with bounded memory). */
|
|
2475
|
+
remember(poolId, sessionId, member) {
|
|
2476
|
+
if (sessionId === void 0) return;
|
|
2477
|
+
const key = stickyKey(poolId, sessionId);
|
|
2478
|
+
this.sticky.delete(key);
|
|
2479
|
+
if (this.sticky.size >= STICKY_SESSION_LIMIT) {
|
|
2480
|
+
const oldest = this.sticky.keys().next();
|
|
2481
|
+
if (oldest.done !== true) this.sticky.delete(oldest.value);
|
|
2482
|
+
}
|
|
2483
|
+
this.sticky.set(key, memberKey(member.provider, member.account, member.model));
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* The error for an exhausted pool, carrying the earliest recovery hint of
|
|
2487
|
+
* THIS pool's members (the health registry is shared across pools, so the
|
|
2488
|
+
* hint is scoped to the keys this pool can actually recover through).
|
|
2489
|
+
*/
|
|
2490
|
+
exhausted(model, pool, cause) {
|
|
2491
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2492
|
+
for (const member of pool) {
|
|
2493
|
+
keys.add(memberKey(member.provider, member.account, member.model));
|
|
2494
|
+
keys.add(accountKey(member.provider, member.account));
|
|
2495
|
+
}
|
|
2496
|
+
const recovery = this.options.health.earliestRecovery(keys);
|
|
2497
|
+
const retryAfterMs$1 = recovery === void 0 ? void 0 : Math.max(recovery - Date.now(), 1);
|
|
2498
|
+
return new LlmError(`pool "${model}" exhausted: every member is unavailable or failed`, "RATE_LIMIT", {
|
|
2499
|
+
...retryAfterMs$1 === void 0 ? {} : { providerRetryAfterMs: retryAfterMs$1 },
|
|
2500
|
+
...cause === void 0 ? {} : { cause }
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
function stickyKey(poolId, sessionId) {
|
|
2505
|
+
return `${String(sessionId)}|${poolId}`;
|
|
2506
|
+
}
|
|
2507
|
+
function isNumber(value) {
|
|
2508
|
+
return value !== void 0;
|
|
2509
|
+
}
|
|
2510
|
+
/** Reasoning efforts every member supports (id intersection, first member's order). */
|
|
2511
|
+
function intersectReasoning(resolved) {
|
|
2512
|
+
const [first, ...rest] = resolved;
|
|
2513
|
+
if (first?.reasoning === void 0) return void 0;
|
|
2514
|
+
const efforts = first.reasoning.efforts.filter((effort) => rest.every((info) => info.reasoning?.efforts.some((other) => other.id === effort.id) === true));
|
|
2515
|
+
if (efforts.length === 0) return void 0;
|
|
2516
|
+
const defaultEffort = first.reasoning.defaultEffort !== void 0 && efforts.some((effort) => effort.id === first.reasoning?.defaultEffort) ? first.reasoning.defaultEffort : void 0;
|
|
2517
|
+
return {
|
|
2518
|
+
efforts,
|
|
2519
|
+
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
/** Modalities all members accept; undefined when any member leaves it unknown. */
|
|
2523
|
+
function intersectModalities(resolved) {
|
|
2524
|
+
const [first, ...rest] = resolved;
|
|
2525
|
+
if (first?.inputModalities === void 0) return void 0;
|
|
2526
|
+
const modalities = first.inputModalities.filter((modality) => rest.every((info) => info.inputModalities?.includes(modality) === true));
|
|
2527
|
+
return modalities.length === 0 ? void 0 : modalities;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
//#endregion
|
|
2531
|
+
//#region src/providers/pool-usage.ts
|
|
2532
|
+
/** A member is taken out of rotation once any window crosses this fill level. */
|
|
2533
|
+
const QUOTA_FULL_PERCENT = 95;
|
|
2534
|
+
/** How long a usage snapshot is trusted before a background refresh. */
|
|
2535
|
+
const USAGE_TTL_MS = 5 * 6e4;
|
|
2536
|
+
/** Assumed window length when the provider discloses no `resetsAt`. */
|
|
2537
|
+
const FALLBACK_HORIZON_MS = {
|
|
2538
|
+
session: 300 * 6e4,
|
|
2539
|
+
weekly: 10080 * 6e4,
|
|
2540
|
+
other: 720 * 60 * 6e4
|
|
2541
|
+
};
|
|
2542
|
+
/**
|
|
2543
|
+
* Per-ACCOUNT usage snapshots with in-flight dedupe and
|
|
2544
|
+
* stale-while-revalidate refresh. Providers without a usage endpoint
|
|
2545
|
+
* (copilot) resolve no fetcher and score a constant zero urgency — which
|
|
2546
|
+
* naturally ranks them behind every measured member. Fetchers are resolved
|
|
2547
|
+
* lazily per (provider, account) so accounts added after startup join
|
|
2548
|
+
* tracking on their first score.
|
|
2549
|
+
*/
|
|
2550
|
+
var PoolUsageTracker = class {
|
|
2551
|
+
entries = /* @__PURE__ */ new Map();
|
|
2552
|
+
inflight = /* @__PURE__ */ new Map();
|
|
2553
|
+
constructor(fetcherFor, ttlMs = USAGE_TTL_MS) {
|
|
2554
|
+
this.fetcherFor = fetcherFor;
|
|
2555
|
+
this.ttlMs = ttlMs;
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* The quota view of one member. A cold cache awaits the first fetch; a
|
|
2559
|
+
* stale one answers immediately while the refresh serves the NEXT call
|
|
2560
|
+
* (member selection must never block on the network mid-conversation).
|
|
2561
|
+
* @param member - the pool member to score (account resolved).
|
|
2562
|
+
* @returns availability plus the urgency score.
|
|
2563
|
+
*/
|
|
2564
|
+
async quotaFor(member) {
|
|
2565
|
+
const key = `${member.provider}/${member.account}`;
|
|
2566
|
+
const fetcher = this.fetcherFor(member.provider, member.account);
|
|
2567
|
+
if (fetcher === void 0) return {
|
|
2568
|
+
available: true,
|
|
2569
|
+
urgency: 0,
|
|
2570
|
+
fetchedAt: 0
|
|
2571
|
+
};
|
|
2572
|
+
const entry = this.entries.get(key);
|
|
2573
|
+
if (entry !== void 0 && Date.now() - entry.at < this.ttlMs) return this.score(member, entry);
|
|
2574
|
+
if (entry !== void 0) {
|
|
2575
|
+
this.refresh(key, fetcher).catch(() => void 0);
|
|
2576
|
+
return this.score(member, entry);
|
|
2577
|
+
}
|
|
2578
|
+
try {
|
|
2579
|
+
const snapshot = await this.refresh(key, fetcher);
|
|
2580
|
+
return this.score(member, {
|
|
2581
|
+
snapshot,
|
|
2582
|
+
at: Date.now()
|
|
2583
|
+
});
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
return isMissingOrInvalidCredential(error) ? {
|
|
2586
|
+
available: false,
|
|
2587
|
+
urgency: 0,
|
|
2588
|
+
fetchedAt: 0
|
|
2589
|
+
} : {
|
|
2590
|
+
available: true,
|
|
2591
|
+
urgency: 0,
|
|
2592
|
+
fetchedAt: 0
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
/** Drop cached snapshots: one account, or a whole provider when `account` is omitted. */
|
|
2597
|
+
invalidate(provider, account) {
|
|
2598
|
+
if (account !== void 0) {
|
|
2599
|
+
this.entries.delete(`${provider}/${account}`);
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
for (const key of [...this.entries.keys()]) if (key.startsWith(`${provider}/`)) this.entries.delete(key);
|
|
2603
|
+
}
|
|
2604
|
+
/** Run (or join) the single in-flight fetch for one account key. */
|
|
2605
|
+
refresh(key, fetcher) {
|
|
2606
|
+
let pending = this.inflight.get(key);
|
|
2607
|
+
if (pending === void 0) {
|
|
2608
|
+
pending = fetcher().then((snapshot) => {
|
|
2609
|
+
this.entries.set(key, {
|
|
2610
|
+
snapshot,
|
|
2611
|
+
at: Date.now()
|
|
2612
|
+
});
|
|
2613
|
+
return snapshot;
|
|
2614
|
+
}).finally(() => {
|
|
2615
|
+
this.inflight.delete(key);
|
|
2616
|
+
});
|
|
2617
|
+
this.inflight.set(key, pending);
|
|
2618
|
+
}
|
|
2619
|
+
return pending;
|
|
2620
|
+
}
|
|
2621
|
+
/** Score one member against a snapshot's windows. */
|
|
2622
|
+
score(member, entry) {
|
|
2623
|
+
const windows = (entry.snapshot.windows ?? []).filter((window) => windowApplies(window, member.model));
|
|
2624
|
+
let available = true;
|
|
2625
|
+
let urgency = 0;
|
|
2626
|
+
for (const window of windows) {
|
|
2627
|
+
if (window.usedPercent >= QUOTA_FULL_PERCENT) available = false;
|
|
2628
|
+
urgency = Math.max(urgency, windowUrgency(window));
|
|
2629
|
+
}
|
|
2630
|
+
return {
|
|
2631
|
+
available,
|
|
2632
|
+
urgency,
|
|
2633
|
+
fetchedAt: entry.at
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
/**
|
|
2638
|
+
* Whether a window constrains this model: unscoped windows always do; a
|
|
2639
|
+
* model-scoped window (Claude's Opus/Sonnet lanes) applies when its scope
|
|
2640
|
+
* names the model family.
|
|
2641
|
+
*/
|
|
2642
|
+
function windowApplies(window, model) {
|
|
2643
|
+
if (window.scope === void 0) return true;
|
|
2644
|
+
return model.toLowerCase().includes(window.scope.toLowerCase());
|
|
2645
|
+
}
|
|
2646
|
+
/** The required burn rate of one window (fraction per ms). */
|
|
2647
|
+
function windowUrgency(window, now = Date.now()) {
|
|
2648
|
+
return Math.max(0, 1 - window.usedPercent / 100) / (window.resetsAt !== void 0 ? Math.max(window.resetsAt - now, 1) : FALLBACK_HORIZON_MS[window.kind]);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
1363
2651
|
//#endregion
|
|
1364
2652
|
//#region src/auth/jwt.ts
|
|
1365
2653
|
/** Minimal JWT payload decoding for claims extraction (no signature verification). */
|
|
@@ -1960,7 +3248,7 @@ function codexSession(tokens, fallback) {
|
|
|
1960
3248
|
* @returns the session to store.
|
|
1961
3249
|
*/
|
|
1962
3250
|
async function exchangeCodexCode(code, verifier, redirectUri) {
|
|
1963
|
-
const response = await
|
|
3251
|
+
const response = await proxiedFetch(CODEX_TOKEN_URL, {
|
|
1964
3252
|
method: "POST",
|
|
1965
3253
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1966
3254
|
body: new URLSearchParams({
|
|
@@ -1980,7 +3268,7 @@ async function exchangeCodexCode(code, verifier, redirectUri) {
|
|
|
1980
3268
|
* @returns the fresh session to store.
|
|
1981
3269
|
*/
|
|
1982
3270
|
async function refreshCodex(session) {
|
|
1983
|
-
const response = await
|
|
3271
|
+
const response = await proxiedFetch(CODEX_TOKEN_URL, {
|
|
1984
3272
|
method: "POST",
|
|
1985
3273
|
headers: { "content-type": "application/json" },
|
|
1986
3274
|
body: JSON.stringify({
|
|
@@ -2048,7 +3336,7 @@ function codexUsageWindow(value, fallbackKind) {
|
|
|
2048
3336
|
* @param signal - caller cancellation from the RPC transport.
|
|
2049
3337
|
* @returns the mapped usage snapshot.
|
|
2050
3338
|
*/
|
|
2051
|
-
async function fetchCodexUsage(session, fetchFn =
|
|
3339
|
+
async function fetchCodexUsage(session, fetchFn = proxiedFetch, signal) {
|
|
2052
3340
|
const response = await fetchFn(CODEX_USAGE_URL, {
|
|
2053
3341
|
headers: {
|
|
2054
3342
|
"authorization": `Bearer ${session.accessToken}`,
|
|
@@ -2096,16 +3384,20 @@ function supportsFastTier(entry) {
|
|
|
2096
3384
|
* Fetch the live codex model catalog with the session's auth headers.
|
|
2097
3385
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
2098
3386
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
3387
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
2099
3388
|
* @returns discovered models: hidden entries dropped, sorted by priority.
|
|
2100
3389
|
*/
|
|
2101
|
-
async function fetchCodexModels(session, fetchFn =
|
|
2102
|
-
const response = await fetchFn(`${CODEX_MODELS_URL}?client_version=${CODEX_CLIENT_VERSION}`, {
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
3390
|
+
async function fetchCodexModels(session, fetchFn = proxiedFetch, signal) {
|
|
3391
|
+
const response = await fetchFn(`${CODEX_MODELS_URL}?client_version=${CODEX_CLIENT_VERSION}`, {
|
|
3392
|
+
headers: {
|
|
3393
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
3394
|
+
"chatgpt-account-id": session.accountId,
|
|
3395
|
+
"originator": "codex_cli_rs",
|
|
3396
|
+
"accept": "application/json",
|
|
3397
|
+
...attributionHeaders()
|
|
3398
|
+
},
|
|
3399
|
+
...signal === void 0 ? {} : { signal }
|
|
3400
|
+
});
|
|
2109
3401
|
if (!response.ok) throw await oauthEndpointError(response, "codex models");
|
|
2110
3402
|
const payload = await response.json();
|
|
2111
3403
|
if (!Array.isArray(payload.models)) throw new Error("codex models endpoint returned no models array");
|
|
@@ -2209,14 +3501,43 @@ function codexRequestBody(options, resolved, fast) {
|
|
|
2209
3501
|
/** Codex wire adapter: one instance serves the `codex` provider route. */
|
|
2210
3502
|
var CodexAdapter = class extends LlmAdapter {
|
|
2211
3503
|
catalog;
|
|
3504
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
3505
|
+
accountCatalogs = /* @__PURE__ */ new Map();
|
|
3506
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
3507
|
+
catalogOwner;
|
|
2212
3508
|
constructor(options) {
|
|
2213
3509
|
super();
|
|
2214
3510
|
this.options = options;
|
|
2215
3511
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
2216
3512
|
}
|
|
2217
3513
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
2218
|
-
async fetchCatalog() {
|
|
2219
|
-
return fetchCodexModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
3514
|
+
async fetchCatalog(account, signal) {
|
|
3515
|
+
return fetchCodexModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
3516
|
+
}
|
|
3517
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
3518
|
+
clearAccountCatalog(account) {
|
|
3519
|
+
if (account === void 0) this.accountCatalogs.clear();
|
|
3520
|
+
else this.accountCatalogs.delete(account);
|
|
3521
|
+
if (account === void 0 || this.catalogOwner === account || this.catalogOwner === void 0) {
|
|
3522
|
+
this.catalogOwner = void 0;
|
|
3523
|
+
this.catalog.invalidate();
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
3527
|
+
async catalogFor(account) {
|
|
3528
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
3529
|
+
const key = account ?? defaultKey;
|
|
3530
|
+
if (key === void 0 || key === defaultKey) {
|
|
3531
|
+
if (this.catalogOwner !== void 0 && this.catalogOwner !== defaultKey) this.catalog.invalidate();
|
|
3532
|
+
this.catalogOwner = defaultKey;
|
|
3533
|
+
return this.catalog;
|
|
3534
|
+
}
|
|
3535
|
+
let cache = this.accountCatalogs.get(key);
|
|
3536
|
+
if (cache === void 0) {
|
|
3537
|
+
cache = new ModelCatalogCache();
|
|
3538
|
+
this.accountCatalogs.set(key, cache);
|
|
3539
|
+
}
|
|
3540
|
+
return cache;
|
|
2220
3541
|
}
|
|
2221
3542
|
providerInfo(provider) {
|
|
2222
3543
|
return {
|
|
@@ -2233,17 +3554,37 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
2233
3554
|
}));
|
|
2234
3555
|
}
|
|
2235
3556
|
async listModels(provider) {
|
|
2236
|
-
|
|
3557
|
+
const own = await this.listOwnModels(provider);
|
|
3558
|
+
const pool = this.options.pool?.();
|
|
3559
|
+
if (pool === void 0) return own;
|
|
3560
|
+
const extra = await pool.modelsForProvider(provider);
|
|
3561
|
+
const seen = new Set(own.map((model) => model.id));
|
|
3562
|
+
return [...own, ...extra.filter((model) => !seen.has(model.id))];
|
|
3563
|
+
}
|
|
3564
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
3565
|
+
async listOwnModels(provider, account, signal) {
|
|
3566
|
+
if (account === void 0) {
|
|
3567
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
3568
|
+
if (accounts.length === 0) return [];
|
|
3569
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), {
|
|
3570
|
+
timeoutMs: this.options.discoveryTimeoutMs ?? DISCOVERY_TIMEOUT_MS,
|
|
3571
|
+
...signal === void 0 ? {} : { signal }
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
if (!await this.options.tokens.hasSession(account)) return [];
|
|
2237
3575
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
3576
|
+
const catalog = await this.catalogFor(account);
|
|
2238
3577
|
try {
|
|
2239
|
-
return (await discoverOrRetryAuth((force) => this.options.tokens.session(force),
|
|
3578
|
+
return (await discoverOrRetryAuth((force) => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)))).map((model) => ({
|
|
2240
3579
|
provider,
|
|
2241
3580
|
id: model.id,
|
|
2242
3581
|
name: model.name,
|
|
2243
3582
|
...model.description === void 0 ? {} : { description: model.description },
|
|
2244
|
-
inputModalities: CODEX_MODALITIES
|
|
3583
|
+
inputModalities: CODEX_MODALITIES,
|
|
3584
|
+
...model.priority === void 0 ? {} : { priority: model.priority }
|
|
2245
3585
|
}));
|
|
2246
3586
|
} catch (error) {
|
|
3587
|
+
if (isDiscoveryAborted(error, signal)) throw error;
|
|
2247
3588
|
if (isMissingOrInvalidCredential(error)) return [];
|
|
2248
3589
|
this.options.onWarn?.(`codex model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
2249
3590
|
return this.staticModels(provider);
|
|
@@ -2258,7 +3599,9 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
2258
3599
|
*/
|
|
2259
3600
|
async discovered(model) {
|
|
2260
3601
|
if (!this.options.discovery) return void 0;
|
|
2261
|
-
return (await this.
|
|
3602
|
+
return discoverAcrossAccounts((await this.options.tokens.list()).map((entry) => entry.key), async (account) => {
|
|
3603
|
+
return (await (await this.catalogFor(account)).resolve(() => this.fetchCatalog(account)))?.find((entry) => entry.id === model);
|
|
3604
|
+
});
|
|
2262
3605
|
}
|
|
2263
3606
|
/** Whether the discovered catalog advertises a fast tier for this model. */
|
|
2264
3607
|
async supportsFastTier(model) {
|
|
@@ -2267,10 +3610,27 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
2267
3610
|
/** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
|
|
2268
3611
|
async fastCapableModels() {
|
|
2269
3612
|
if (!this.options.discovery) return [];
|
|
2270
|
-
|
|
2271
|
-
|
|
3613
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
3614
|
+
if (accounts.length === 0) return [];
|
|
3615
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3616
|
+
const ids = [];
|
|
3617
|
+
for (const account of accounts) try {
|
|
3618
|
+
const models = await (await this.catalogFor(account)).resolve(() => this.fetchCatalog(account));
|
|
3619
|
+
for (const model of models ?? []) {
|
|
3620
|
+
if (model.fastTier !== true || seen.has(model.id)) continue;
|
|
3621
|
+
seen.add(model.id);
|
|
3622
|
+
ids.push(model.id);
|
|
3623
|
+
}
|
|
3624
|
+
} catch {}
|
|
3625
|
+
return ids;
|
|
2272
3626
|
}
|
|
2273
3627
|
async resolveModel(provider, model) {
|
|
3628
|
+
const pool = this.options.pool?.();
|
|
3629
|
+
if (pool !== void 0 && await pool.owns(provider, model)) return pool.resolveModel(provider, model);
|
|
3630
|
+
return this.resolveOwnModel(provider, model);
|
|
3631
|
+
}
|
|
3632
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
3633
|
+
async resolveOwnModel(provider, model) {
|
|
2274
3634
|
const discovered = await this.discovered(model);
|
|
2275
3635
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
2276
3636
|
return {
|
|
@@ -2288,12 +3648,24 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
2288
3648
|
};
|
|
2289
3649
|
}
|
|
2290
3650
|
async *stream(options) {
|
|
3651
|
+
const pool = this.options.pool?.();
|
|
3652
|
+
if (pool !== void 0 && await pool.owns(options.provider, options.model)) {
|
|
3653
|
+
yield* pool.stream(options);
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
yield* this.streamCore(options);
|
|
3657
|
+
}
|
|
3658
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
3659
|
+
streamAccount(options, account) {
|
|
3660
|
+
return this.streamCore(options, account);
|
|
3661
|
+
}
|
|
3662
|
+
async *streamCore(options, account) {
|
|
2291
3663
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
2292
3664
|
try {
|
|
2293
|
-
let session = await this.options.tokens.session();
|
|
3665
|
+
let session = await this.options.tokens.session(account);
|
|
2294
3666
|
let response = await this.request(options, session, watchdog.signal);
|
|
2295
3667
|
if (response.status === 401) {
|
|
2296
|
-
session = await this.options.tokens.session(true);
|
|
3668
|
+
session = await this.options.tokens.session(account, true);
|
|
2297
3669
|
response = await this.request(options, session, watchdog.signal);
|
|
2298
3670
|
}
|
|
2299
3671
|
if (!response.ok) throw await httpLlmError(response, "codex API");
|
|
@@ -2311,7 +3683,7 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
2311
3683
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
|
|
2312
3684
|
const fast = this.options.speedFor !== void 0 && await this.options.speedFor(options.sessionId, options.model);
|
|
2313
3685
|
const body = codexRequestBody(options, toResponsesInput(messages, options.system), fast);
|
|
2314
|
-
return
|
|
3686
|
+
return proxiedFetch(CODEX_API_URL, {
|
|
2315
3687
|
method: "POST",
|
|
2316
3688
|
headers: {
|
|
2317
3689
|
"authorization": `Bearer ${session.accessToken}`,
|
|
@@ -2846,7 +4218,7 @@ const claudeFlow = {
|
|
|
2846
4218
|
/** Best-effort account profile; login must not fail when this does. */
|
|
2847
4219
|
async function fetchClaudeProfile(accessToken) {
|
|
2848
4220
|
try {
|
|
2849
|
-
const response = await
|
|
4221
|
+
const response = await proxiedFetch(CLAUDE_PROFILE_URL, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
2850
4222
|
if (!response.ok) return {};
|
|
2851
4223
|
const profile = await response.json();
|
|
2852
4224
|
const account = typeof profile.account === "object" && profile.account !== null ? profile.account : {};
|
|
@@ -2884,7 +4256,7 @@ async function claudeSession(tokens, fallbackRefreshToken, withProfile) {
|
|
|
2884
4256
|
* @returns the session to store.
|
|
2885
4257
|
*/
|
|
2886
4258
|
async function exchangeClaudeCode(code, verifier, redirectUri, state) {
|
|
2887
|
-
const response = await
|
|
4259
|
+
const response = await proxiedFetch(CLAUDE_TOKEN_URL, {
|
|
2888
4260
|
method: "POST",
|
|
2889
4261
|
headers: { "content-type": "application/json" },
|
|
2890
4262
|
body: JSON.stringify({
|
|
@@ -2905,7 +4277,7 @@ async function exchangeClaudeCode(code, verifier, redirectUri, state) {
|
|
|
2905
4277
|
* @returns the fresh session to store.
|
|
2906
4278
|
*/
|
|
2907
4279
|
async function refreshClaude(session) {
|
|
2908
|
-
const response = await
|
|
4280
|
+
const response = await proxiedFetch(CLAUDE_TOKEN_URL, {
|
|
2909
4281
|
method: "POST",
|
|
2910
4282
|
headers: { "content-type": "application/json" },
|
|
2911
4283
|
body: JSON.stringify({
|
|
@@ -2980,7 +4352,7 @@ function claudeLimitsWindows(value) {
|
|
|
2980
4352
|
* @param signal - caller cancellation from the RPC transport.
|
|
2981
4353
|
* @returns the mapped usage snapshot.
|
|
2982
4354
|
*/
|
|
2983
|
-
async function fetchClaudeUsage(session, fetchFn =
|
|
4355
|
+
async function fetchClaudeUsage(session, fetchFn = proxiedFetch, signal) {
|
|
2984
4356
|
const response = await fetchFn(CLAUDE_USAGE_URL, {
|
|
2985
4357
|
headers: {
|
|
2986
4358
|
"authorization": `Bearer ${session.accessToken}`,
|
|
@@ -3032,15 +4404,18 @@ function claudeReasoning(capabilities) {
|
|
|
3032
4404
|
}));
|
|
3033
4405
|
return efforts.length > 0 ? { efforts } : void 0;
|
|
3034
4406
|
}
|
|
3035
|
-
/** Fetch the live model catalog from the subscription endpoint. */
|
|
3036
|
-
async function fetchClaudeModels(session, fetchFn =
|
|
3037
|
-
const response = await fetchFn(CLAUDE_MODELS_URL, {
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
4407
|
+
/** Fetch the live model catalog from the subscription endpoint. `signal` cancels the request. */
|
|
4408
|
+
async function fetchClaudeModels(session, fetchFn = proxiedFetch, signal) {
|
|
4409
|
+
const response = await fetchFn(CLAUDE_MODELS_URL, {
|
|
4410
|
+
headers: {
|
|
4411
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
4412
|
+
"anthropic-version": "2023-06-01",
|
|
4413
|
+
"user-agent": getClaudeCliUserAgent(),
|
|
4414
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
4415
|
+
"accept": "application/json"
|
|
4416
|
+
},
|
|
4417
|
+
...signal === void 0 ? {} : { signal }
|
|
4418
|
+
});
|
|
3044
4419
|
if (!response.ok) throw await httpLlmError(response, "claude models API");
|
|
3045
4420
|
const payload = await response.json();
|
|
3046
4421
|
if (!Array.isArray(payload.data)) throw new Error("claude models API returned an invalid catalog");
|
|
@@ -3100,17 +4475,48 @@ function claudeRequestBody(options, messages, maxTokens, thinking, effort) {
|
|
|
3100
4475
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
3101
4476
|
var ClaudeAdapter = class extends LlmAdapter {
|
|
3102
4477
|
catalog;
|
|
4478
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
4479
|
+
accountCatalogs = /* @__PURE__ */ new Map();
|
|
4480
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
4481
|
+
catalogOwner;
|
|
3103
4482
|
constructor(options) {
|
|
3104
4483
|
super();
|
|
3105
4484
|
this.options = options;
|
|
3106
4485
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
3107
4486
|
}
|
|
3108
|
-
async fetchCatalog() {
|
|
3109
|
-
return fetchClaudeModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
4487
|
+
async fetchCatalog(account, signal) {
|
|
4488
|
+
return fetchClaudeModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
4489
|
+
}
|
|
4490
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
4491
|
+
clearAccountCatalog(account) {
|
|
4492
|
+
if (account === void 0) this.accountCatalogs.clear();
|
|
4493
|
+
else this.accountCatalogs.delete(account);
|
|
4494
|
+
if (account === void 0 || this.catalogOwner === account || this.catalogOwner === void 0) {
|
|
4495
|
+
this.catalogOwner = void 0;
|
|
4496
|
+
this.catalog.invalidate();
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
4500
|
+
async catalogFor(account) {
|
|
4501
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
4502
|
+
const key = account ?? defaultKey;
|
|
4503
|
+
if (key === void 0 || key === defaultKey) {
|
|
4504
|
+
if (this.catalogOwner !== void 0 && this.catalogOwner !== defaultKey) this.catalog.invalidate();
|
|
4505
|
+
this.catalogOwner = defaultKey;
|
|
4506
|
+
return this.catalog;
|
|
4507
|
+
}
|
|
4508
|
+
let cache = this.accountCatalogs.get(key);
|
|
4509
|
+
if (cache === void 0) {
|
|
4510
|
+
cache = new ModelCatalogCache();
|
|
4511
|
+
this.accountCatalogs.set(key, cache);
|
|
4512
|
+
}
|
|
4513
|
+
return cache;
|
|
3110
4514
|
}
|
|
3111
4515
|
async discovered(model) {
|
|
3112
4516
|
if (!this.options.discovery) return void 0;
|
|
3113
|
-
return (await this.
|
|
4517
|
+
return discoverAcrossAccounts((await this.options.tokens.list()).map((entry) => entry.key), async (account) => {
|
|
4518
|
+
return (await (await this.catalogFor(account)).resolve(() => this.fetchCatalog(account)))?.find((entry) => entry.id === model);
|
|
4519
|
+
});
|
|
3114
4520
|
}
|
|
3115
4521
|
staticModels(provider) {
|
|
3116
4522
|
return this.options.models.map((model) => ({
|
|
@@ -3139,22 +4545,47 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
3139
4545
|
}, `claude: provider "${provider}" retryPolicy`);
|
|
3140
4546
|
}
|
|
3141
4547
|
async listModels(provider) {
|
|
3142
|
-
|
|
4548
|
+
const own = await this.listOwnModels(provider);
|
|
4549
|
+
const pool = this.options.pool?.();
|
|
4550
|
+
if (pool === void 0) return own;
|
|
4551
|
+
const extra = await pool.modelsForProvider(provider);
|
|
4552
|
+
const seen = new Set(own.map((model) => model.id));
|
|
4553
|
+
return [...own, ...extra.filter((model) => !seen.has(model.id))];
|
|
4554
|
+
}
|
|
4555
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
4556
|
+
async listOwnModels(provider, account, signal) {
|
|
4557
|
+
if (account === void 0) {
|
|
4558
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
4559
|
+
if (accounts.length === 0) return [];
|
|
4560
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), {
|
|
4561
|
+
timeoutMs: DISCOVERY_TIMEOUT_MS,
|
|
4562
|
+
...signal === void 0 ? {} : { signal }
|
|
4563
|
+
});
|
|
4564
|
+
}
|
|
4565
|
+
if (!await this.options.tokens.hasSession(account)) return [];
|
|
3143
4566
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
4567
|
+
const catalog = await this.catalogFor(account);
|
|
3144
4568
|
try {
|
|
3145
|
-
return (await discoverOrRetryAuth((force) => this.options.tokens.session(force),
|
|
4569
|
+
return (await discoverOrRetryAuth((force) => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)))).map((model) => ({
|
|
3146
4570
|
provider,
|
|
3147
4571
|
id: model.id,
|
|
3148
4572
|
name: model.name,
|
|
3149
4573
|
inputModalities: CLAUDE_MODALITIES
|
|
3150
4574
|
}));
|
|
3151
4575
|
} catch (error) {
|
|
4576
|
+
if (isDiscoveryAborted(error, signal)) throw error;
|
|
3152
4577
|
if (isMissingOrInvalidCredential(error)) return [];
|
|
3153
4578
|
this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
3154
4579
|
return this.staticModels(provider);
|
|
3155
4580
|
}
|
|
3156
4581
|
}
|
|
3157
4582
|
async resolveModel(provider, model) {
|
|
4583
|
+
const pool = this.options.pool?.();
|
|
4584
|
+
if (pool !== void 0 && await pool.owns(provider, model)) return pool.resolveModel(provider, model);
|
|
4585
|
+
return this.resolveOwnModel(provider, model);
|
|
4586
|
+
}
|
|
4587
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
4588
|
+
async resolveOwnModel(provider, model) {
|
|
3158
4589
|
const disc = await this.discovered(model);
|
|
3159
4590
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
3160
4591
|
const reasoning = disc?.reasoning;
|
|
@@ -3169,12 +4600,24 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
3169
4600
|
};
|
|
3170
4601
|
}
|
|
3171
4602
|
async *stream(options) {
|
|
4603
|
+
const pool = this.options.pool?.();
|
|
4604
|
+
if (pool !== void 0 && await pool.owns(options.provider, options.model)) {
|
|
4605
|
+
yield* pool.stream(options);
|
|
4606
|
+
return;
|
|
4607
|
+
}
|
|
4608
|
+
yield* this.streamCore(options);
|
|
4609
|
+
}
|
|
4610
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
4611
|
+
streamAccount(options, account) {
|
|
4612
|
+
return this.streamCore(options, account);
|
|
4613
|
+
}
|
|
4614
|
+
async *streamCore(options, account) {
|
|
3172
4615
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
3173
4616
|
try {
|
|
3174
|
-
let session = await this.options.tokens.session();
|
|
4617
|
+
let session = await this.options.tokens.session(account);
|
|
3175
4618
|
let response = await this.request(options, session, watchdog.signal);
|
|
3176
4619
|
if (response.status === 401) {
|
|
3177
|
-
session = await this.options.tokens.session(true);
|
|
4620
|
+
session = await this.options.tokens.session(account, true);
|
|
3178
4621
|
response = await this.request(options, session, watchdog.signal);
|
|
3179
4622
|
}
|
|
3180
4623
|
if (!response.ok) throw await httpLlmError(response, "claude API");
|
|
@@ -3215,7 +4658,7 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
3215
4658
|
const maxTokens = options.maxTokens ?? this.options.models.find((entry) => entry.id === options.model)?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS;
|
|
3216
4659
|
const disc = await this.discovered(options.model);
|
|
3217
4660
|
const body = claudeRequestBody(options, messages, maxTokens, this.thinkingParam(disc?.thinkingType, maxTokens), options.reasoningEffort !== void 0 && disc?.reasoning !== void 0 ? String(options.reasoningEffort) : void 0);
|
|
3218
|
-
return
|
|
4661
|
+
return proxiedFetch(CLAUDE_API_URL, {
|
|
3219
4662
|
method: "POST",
|
|
3220
4663
|
headers: {
|
|
3221
4664
|
"authorization": `Bearer ${session.accessToken}`,
|
|
@@ -3262,7 +4705,7 @@ let discoveryCache;
|
|
|
3262
4705
|
*/
|
|
3263
4706
|
async function grokDiscovery() {
|
|
3264
4707
|
if (discoveryCache !== void 0) return discoveryCache;
|
|
3265
|
-
const response = await
|
|
4708
|
+
const response = await proxiedFetch(GROK_DISCOVERY_URL);
|
|
3266
4709
|
if (!response.ok) throw await oauthEndpointError(response, "grok OIDC discovery");
|
|
3267
4710
|
const document = await response.json();
|
|
3268
4711
|
if (typeof document.authorization_endpoint !== "string" || typeof document.token_endpoint !== "string") throw new Error("grok OIDC discovery document is missing endpoints");
|
|
@@ -3363,7 +4806,7 @@ function grokSession(tokens, tokenEndpoint, fallbackRefreshToken) {
|
|
|
3363
4806
|
*/
|
|
3364
4807
|
async function exchangeGrokCode(code, verifier, redirectUri, challenge) {
|
|
3365
4808
|
const discovery = await grokDiscovery();
|
|
3366
|
-
const response = await
|
|
4809
|
+
const response = await proxiedFetch(discovery.tokenEndpoint, {
|
|
3367
4810
|
method: "POST",
|
|
3368
4811
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
3369
4812
|
body: new URLSearchParams({
|
|
@@ -3386,7 +4829,7 @@ async function exchangeGrokCode(code, verifier, redirectUri, challenge) {
|
|
|
3386
4829
|
* @returns the fresh session to store.
|
|
3387
4830
|
*/
|
|
3388
4831
|
async function refreshGrok(session) {
|
|
3389
|
-
const response = await
|
|
4832
|
+
const response = await proxiedFetch(session.tokenEndpoint, {
|
|
3390
4833
|
method: "POST",
|
|
3391
4834
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
3392
4835
|
body: new URLSearchParams({
|
|
@@ -3433,7 +4876,7 @@ function grokResetsAt(value) {
|
|
|
3433
4876
|
* @param signal - caller cancellation from the RPC transport.
|
|
3434
4877
|
* @returns the mapped usage snapshot.
|
|
3435
4878
|
*/
|
|
3436
|
-
async function fetchGrokUsage(session, fetchFn =
|
|
4879
|
+
async function fetchGrokUsage(session, fetchFn = proxiedFetch, signal) {
|
|
3437
4880
|
const response = await fetchFn(GROK_BILLING_URL, {
|
|
3438
4881
|
headers: {
|
|
3439
4882
|
"authorization": `Bearer ${session.accessToken}`,
|
|
@@ -3506,15 +4949,19 @@ function grokCliReasoning(entry) {
|
|
|
3506
4949
|
* Fetch the CLI catalog and index its per-model metadata by model id.
|
|
3507
4950
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
3508
4951
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
4952
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
3509
4953
|
* @returns model id → contributed metadata.
|
|
3510
4954
|
*/
|
|
3511
|
-
async function fetchGrokCliCatalog(session, fetchFn =
|
|
3512
|
-
const response = await fetchFn(GROK_CLI_MODELS_URL, {
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
4955
|
+
async function fetchGrokCliCatalog(session, fetchFn = proxiedFetch, signal) {
|
|
4956
|
+
const response = await fetchFn(GROK_CLI_MODELS_URL, {
|
|
4957
|
+
headers: {
|
|
4958
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
4959
|
+
"x-xai-token-auth": "xai-grok-cli",
|
|
4960
|
+
"accept": "application/json",
|
|
4961
|
+
...attributionHeaders()
|
|
4962
|
+
},
|
|
4963
|
+
...signal === void 0 ? {} : { signal }
|
|
4964
|
+
});
|
|
3518
4965
|
if (!response.ok) throw await oauthEndpointError(response, "grok CLI catalog");
|
|
3519
4966
|
const payload = await response.json();
|
|
3520
4967
|
if (!Array.isArray(payload.data)) throw new Error("grok CLI catalog returned no data array");
|
|
@@ -3567,15 +5014,20 @@ function grokPriorMeta(prior) {
|
|
|
3567
5014
|
* @param onWarn - warning sink for a failed CLI catalog fetch.
|
|
3568
5015
|
* @param previous - last-known catalog used to keep enrichment when the CLI
|
|
3569
5016
|
* catalog is down or omits a model.
|
|
5017
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
3570
5018
|
* @returns discovered chat models in endpoint order.
|
|
3571
5019
|
*/
|
|
3572
|
-
async function fetchGrokModels(session, fetchFn =
|
|
5020
|
+
async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, previous, signal) {
|
|
3573
5021
|
const previousById = previous === void 0 || previous.length === 0 ? void 0 : new Map(previous.map((model) => [model.id, model]));
|
|
3574
|
-
const [response, cliCatalog] = await Promise.all([fetchFn(GROK_MODELS_URL, {
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
5022
|
+
const [response, cliCatalog] = await Promise.all([fetchFn(GROK_MODELS_URL, {
|
|
5023
|
+
headers: {
|
|
5024
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
5025
|
+
"accept": "application/json",
|
|
5026
|
+
...attributionHeaders()
|
|
5027
|
+
},
|
|
5028
|
+
...signal === void 0 ? {} : { signal }
|
|
5029
|
+
}), fetchGrokCliCatalog(session, fetchFn, signal).catch((error) => {
|
|
5030
|
+
if (isDiscoveryAborted(error, signal)) throw error;
|
|
3579
5031
|
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)})`);
|
|
3580
5032
|
})]);
|
|
3581
5033
|
if (!response.ok) throw await oauthEndpointError(response, "grok models");
|
|
@@ -3600,14 +5052,44 @@ async function fetchGrokModels(session, fetchFn = fetch, onWarn, previous) {
|
|
|
3600
5052
|
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
3601
5053
|
var GrokAdapter = class extends LlmAdapter {
|
|
3602
5054
|
catalog;
|
|
5055
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
5056
|
+
accountCatalogs = /* @__PURE__ */ new Map();
|
|
5057
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
5058
|
+
catalogOwner;
|
|
3603
5059
|
constructor(options) {
|
|
3604
5060
|
super();
|
|
3605
5061
|
this.options = options;
|
|
3606
5062
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
3607
5063
|
}
|
|
3608
5064
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
3609
|
-
async fetchCatalog() {
|
|
3610
|
-
|
|
5065
|
+
async fetchCatalog(account, signal) {
|
|
5066
|
+
const lastKnown = account === void 0 || account === await this.options.tokens.defaultAccount() ? this.catalog.lastKnown() : this.accountCatalogs.get(account)?.lastKnown();
|
|
5067
|
+
return fetchGrokModels(await this.options.tokens.session(account), this.options.fetchFn, this.options.onWarn, lastKnown, signal);
|
|
5068
|
+
}
|
|
5069
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
5070
|
+
clearAccountCatalog(account) {
|
|
5071
|
+
if (account === void 0) this.accountCatalogs.clear();
|
|
5072
|
+
else this.accountCatalogs.delete(account);
|
|
5073
|
+
if (account === void 0 || this.catalogOwner === account || this.catalogOwner === void 0) {
|
|
5074
|
+
this.catalogOwner = void 0;
|
|
5075
|
+
this.catalog.invalidate();
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
5079
|
+
async catalogFor(account) {
|
|
5080
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
5081
|
+
const key = account ?? defaultKey;
|
|
5082
|
+
if (key === void 0 || key === defaultKey) {
|
|
5083
|
+
if (this.catalogOwner !== void 0 && this.catalogOwner !== defaultKey) this.catalog.invalidate();
|
|
5084
|
+
this.catalogOwner = defaultKey;
|
|
5085
|
+
return this.catalog;
|
|
5086
|
+
}
|
|
5087
|
+
let cache = this.accountCatalogs.get(key);
|
|
5088
|
+
if (cache === void 0) {
|
|
5089
|
+
cache = new ModelCatalogCache();
|
|
5090
|
+
this.accountCatalogs.set(key, cache);
|
|
5091
|
+
}
|
|
5092
|
+
return cache;
|
|
3611
5093
|
}
|
|
3612
5094
|
listed(provider, discovered) {
|
|
3613
5095
|
return discovered.map((model) => ({
|
|
@@ -3633,11 +5115,30 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
3633
5115
|
}));
|
|
3634
5116
|
}
|
|
3635
5117
|
async listModels(provider) {
|
|
3636
|
-
|
|
5118
|
+
const own = await this.listOwnModels(provider);
|
|
5119
|
+
const pool = this.options.pool?.();
|
|
5120
|
+
if (pool === void 0) return own;
|
|
5121
|
+
const extra = await pool.modelsForProvider(provider);
|
|
5122
|
+
const seen = new Set(own.map((model) => model.id));
|
|
5123
|
+
return [...own, ...extra.filter((model) => !seen.has(model.id))];
|
|
5124
|
+
}
|
|
5125
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
5126
|
+
async listOwnModels(provider, account, signal) {
|
|
5127
|
+
if (account === void 0) {
|
|
5128
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
5129
|
+
if (accounts.length === 0) return [];
|
|
5130
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), {
|
|
5131
|
+
timeoutMs: DISCOVERY_TIMEOUT_MS,
|
|
5132
|
+
...signal === void 0 ? {} : { signal }
|
|
5133
|
+
});
|
|
5134
|
+
}
|
|
5135
|
+
if (!await this.options.tokens.hasSession(account)) return [];
|
|
3637
5136
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
5137
|
+
const catalog = await this.catalogFor(account);
|
|
3638
5138
|
try {
|
|
3639
|
-
return this.listed(provider, await discoverOrRetryAuth((force) => this.options.tokens.session(force),
|
|
5139
|
+
return this.listed(provider, await discoverOrRetryAuth((force) => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal))));
|
|
3640
5140
|
} catch (error) {
|
|
5141
|
+
if (isDiscoveryAborted(error, signal)) throw error;
|
|
3641
5142
|
if (isMissingOrInvalidCredential(error)) return [];
|
|
3642
5143
|
this.options.onWarn?.(`grok model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
3643
5144
|
return this.staticModels(provider);
|
|
@@ -3653,9 +5154,17 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
3653
5154
|
*/
|
|
3654
5155
|
async discovered(model) {
|
|
3655
5156
|
if (!this.options.discovery) return void 0;
|
|
3656
|
-
return (await this.
|
|
5157
|
+
return discoverAcrossAccounts((await this.options.tokens.list()).map((entry) => entry.key), async (account) => {
|
|
5158
|
+
return (await (await this.catalogFor(account)).resolve(() => this.fetchCatalog(account)))?.find((entry) => entry.id === model);
|
|
5159
|
+
});
|
|
3657
5160
|
}
|
|
3658
5161
|
async resolveModel(provider, model) {
|
|
5162
|
+
const pool = this.options.pool?.();
|
|
5163
|
+
if (pool !== void 0 && await pool.owns(provider, model)) return pool.resolveModel(provider, model);
|
|
5164
|
+
return this.resolveOwnModel(provider, model);
|
|
5165
|
+
}
|
|
5166
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
5167
|
+
async resolveOwnModel(provider, model) {
|
|
3659
5168
|
const discovered = await this.discovered(model);
|
|
3660
5169
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
3661
5170
|
return {
|
|
@@ -3670,12 +5179,24 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
3670
5179
|
};
|
|
3671
5180
|
}
|
|
3672
5181
|
async *stream(options) {
|
|
5182
|
+
const pool = this.options.pool?.();
|
|
5183
|
+
if (pool !== void 0 && await pool.owns(options.provider, options.model)) {
|
|
5184
|
+
yield* pool.stream(options);
|
|
5185
|
+
return;
|
|
5186
|
+
}
|
|
5187
|
+
yield* this.streamCore(options);
|
|
5188
|
+
}
|
|
5189
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
5190
|
+
streamAccount(options, account) {
|
|
5191
|
+
return this.streamCore(options, account);
|
|
5192
|
+
}
|
|
5193
|
+
async *streamCore(options, account) {
|
|
3673
5194
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
3674
5195
|
try {
|
|
3675
|
-
let session = await this.options.tokens.session();
|
|
5196
|
+
let session = await this.options.tokens.session(account);
|
|
3676
5197
|
let response = await this.request(options, session, watchdog.signal);
|
|
3677
5198
|
if (response.status === 401) {
|
|
3678
|
-
session = await this.options.tokens.session(true);
|
|
5199
|
+
session = await this.options.tokens.session(account, true);
|
|
3679
5200
|
response = await this.request(options, session, watchdog.signal);
|
|
3680
5201
|
}
|
|
3681
5202
|
if (!response.ok) throw await httpLlmError(response, "grok API");
|
|
@@ -3703,7 +5224,7 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
3703
5224
|
store: false,
|
|
3704
5225
|
stream: true
|
|
3705
5226
|
};
|
|
3706
|
-
return
|
|
5227
|
+
return proxiedFetch(GROK_API_URL, {
|
|
3707
5228
|
method: "POST",
|
|
3708
5229
|
headers: {
|
|
3709
5230
|
"authorization": `Bearer ${session.accessToken}`,
|
|
@@ -4117,7 +5638,7 @@ let vscodeVersionInflight;
|
|
|
4117
5638
|
* @param forceRefresh - bypass the cache (a 401 `IDE token expired` retry).
|
|
4118
5639
|
* @returns a `major.minor.patch` version string.
|
|
4119
5640
|
*/
|
|
4120
|
-
async function latestVsCodeVersion(fetchFn =
|
|
5641
|
+
async function latestVsCodeVersion(fetchFn = proxiedFetch, forceRefresh = false) {
|
|
4121
5642
|
if (!forceRefresh && vscodeVersionCache !== void 0 && Date.now() - vscodeVersionCache.at < VSCODE_VERSION_TTL_MS) return vscodeVersionCache.version;
|
|
4122
5643
|
vscodeVersionInflight ??= (async () => {
|
|
4123
5644
|
try {
|
|
@@ -4177,7 +5698,7 @@ function copilotHeaders(hasVision = false, vscodeVersion = FALLBACK_VSCODE_VERSI
|
|
|
4177
5698
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
4178
5699
|
* @returns the Copilot API token and its expiry.
|
|
4179
5700
|
*/
|
|
4180
|
-
async function exchangeCopilotToken(githubToken, fetchFn =
|
|
5701
|
+
async function exchangeCopilotToken(githubToken, fetchFn = proxiedFetch) {
|
|
4181
5702
|
const response = await fetchFn(COPILOT_TOKEN_URL, { headers: {
|
|
4182
5703
|
"authorization": `Bearer ${githubToken}`,
|
|
4183
5704
|
"accept": "application/json",
|
|
@@ -4198,7 +5719,7 @@ async function exchangeCopilotToken(githubToken, fetchFn = fetch) {
|
|
|
4198
5719
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
4199
5720
|
* @returns the session to store.
|
|
4200
5721
|
*/
|
|
4201
|
-
async function completeCopilotLogin(githubToken, fetchFn =
|
|
5722
|
+
async function completeCopilotLogin(githubToken, fetchFn = proxiedFetch) {
|
|
4202
5723
|
const pair = await exchangeCopilotToken(githubToken, fetchFn);
|
|
4203
5724
|
let account;
|
|
4204
5725
|
try {
|
|
@@ -4226,7 +5747,7 @@ async function completeCopilotLogin(githubToken, fetchFn = fetch) {
|
|
|
4226
5747
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
4227
5748
|
* @returns the fresh session to store.
|
|
4228
5749
|
*/
|
|
4229
|
-
async function refreshCopilot(session, fetchFn =
|
|
5750
|
+
async function refreshCopilot(session, fetchFn = proxiedFetch) {
|
|
4230
5751
|
const pair = await exchangeCopilotToken(session.refreshToken, fetchFn);
|
|
4231
5752
|
return {
|
|
4232
5753
|
accessToken: pair.accessToken,
|
|
@@ -4282,14 +5803,18 @@ function copilotReasoning(entry) {
|
|
|
4282
5803
|
* reasoning efforts (the endpoint discloses no default, so none is claimed).
|
|
4283
5804
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
4284
5805
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
5806
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
4285
5807
|
* @returns discovered chat models in endpoint order.
|
|
4286
5808
|
*/
|
|
4287
|
-
async function fetchCopilotModels(session, fetchFn =
|
|
4288
|
-
const response = await fetchFn(COPILOT_MODELS_URL, {
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
5809
|
+
async function fetchCopilotModels(session, fetchFn = proxiedFetch, signal) {
|
|
5810
|
+
const response = await fetchFn(COPILOT_MODELS_URL, {
|
|
5811
|
+
headers: {
|
|
5812
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
5813
|
+
"accept": "application/json",
|
|
5814
|
+
...copilotHeaders(false, await latestVsCodeVersion(fetchFn))
|
|
5815
|
+
},
|
|
5816
|
+
...signal === void 0 ? {} : { signal }
|
|
5817
|
+
});
|
|
4293
5818
|
if (!response.ok) throw await oauthEndpointError(response, "copilot models");
|
|
4294
5819
|
const payload = await response.json();
|
|
4295
5820
|
if (!Array.isArray(payload.data)) throw new Error("copilot models endpoint returned no data array");
|
|
@@ -4511,6 +6036,10 @@ var CopilotResponsesItemNormalizer = class {
|
|
|
4511
6036
|
/** Copilot wire adapter: one instance serves the `copilot` provider route. */
|
|
4512
6037
|
var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
4513
6038
|
catalog;
|
|
6039
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
6040
|
+
accountCatalogs = /* @__PURE__ */ new Map();
|
|
6041
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
6042
|
+
catalogOwner;
|
|
4514
6043
|
/**
|
|
4515
6044
|
* [2026-08-23]-[a reasoning model continuing a tool chain must get its
|
|
4516
6045
|
* reasoning back or it restarts from scratch every tool round trip; the
|
|
@@ -4533,8 +6062,33 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4533
6062
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
4534
6063
|
}
|
|
4535
6064
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
4536
|
-
async fetchCatalog() {
|
|
4537
|
-
return fetchCopilotModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
6065
|
+
async fetchCatalog(account, signal) {
|
|
6066
|
+
return fetchCopilotModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
6067
|
+
}
|
|
6068
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
6069
|
+
clearAccountCatalog(account) {
|
|
6070
|
+
if (account === void 0) this.accountCatalogs.clear();
|
|
6071
|
+
else this.accountCatalogs.delete(account);
|
|
6072
|
+
if (account === void 0 || this.catalogOwner === account || this.catalogOwner === void 0) {
|
|
6073
|
+
this.catalogOwner = void 0;
|
|
6074
|
+
this.catalog.invalidate();
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
6078
|
+
async catalogFor(account) {
|
|
6079
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
6080
|
+
const key = account ?? defaultKey;
|
|
6081
|
+
if (key === void 0 || key === defaultKey) {
|
|
6082
|
+
if (this.catalogOwner !== void 0 && this.catalogOwner !== defaultKey) this.catalog.invalidate();
|
|
6083
|
+
this.catalogOwner = defaultKey;
|
|
6084
|
+
return this.catalog;
|
|
6085
|
+
}
|
|
6086
|
+
let cache = this.accountCatalogs.get(key);
|
|
6087
|
+
if (cache === void 0) {
|
|
6088
|
+
cache = new ModelCatalogCache();
|
|
6089
|
+
this.accountCatalogs.set(key, cache);
|
|
6090
|
+
}
|
|
6091
|
+
return cache;
|
|
4538
6092
|
}
|
|
4539
6093
|
providerInfo(provider) {
|
|
4540
6094
|
return {
|
|
@@ -4551,10 +6105,28 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4551
6105
|
}));
|
|
4552
6106
|
}
|
|
4553
6107
|
async listModels(provider) {
|
|
4554
|
-
|
|
6108
|
+
const own = await this.listOwnModels(provider);
|
|
6109
|
+
const pool = this.options.pool?.();
|
|
6110
|
+
if (pool === void 0) return own;
|
|
6111
|
+
const extra = await pool.modelsForProvider(provider);
|
|
6112
|
+
const seen = new Set(own.map((model) => model.id));
|
|
6113
|
+
return [...own, ...extra.filter((model) => !seen.has(model.id))];
|
|
6114
|
+
}
|
|
6115
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
6116
|
+
async listOwnModels(provider, account, signal) {
|
|
6117
|
+
if (account === void 0) {
|
|
6118
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
6119
|
+
if (accounts.length === 0) return [];
|
|
6120
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), {
|
|
6121
|
+
timeoutMs: DISCOVERY_TIMEOUT_MS,
|
|
6122
|
+
...signal === void 0 ? {} : { signal }
|
|
6123
|
+
});
|
|
6124
|
+
}
|
|
6125
|
+
if (!await this.options.tokens.hasSession(account)) return [];
|
|
4555
6126
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
6127
|
+
const catalog = await this.catalogFor(account);
|
|
4556
6128
|
try {
|
|
4557
|
-
return (await discoverOrRetryAuth((force) => this.options.tokens.session(force),
|
|
6129
|
+
return (await discoverOrRetryAuth((force) => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)))).map((model) => ({
|
|
4558
6130
|
provider,
|
|
4559
6131
|
id: model.id,
|
|
4560
6132
|
name: model.name,
|
|
@@ -4562,6 +6134,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4562
6134
|
...model.inputModalities === void 0 ? {} : { inputModalities: model.inputModalities }
|
|
4563
6135
|
}));
|
|
4564
6136
|
} catch (error) {
|
|
6137
|
+
if (isDiscoveryAborted(error, signal)) throw error;
|
|
4565
6138
|
if (isMissingOrInvalidCredential(error)) return [];
|
|
4566
6139
|
this.options.onWarn?.(`copilot model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
4567
6140
|
return this.staticModels(provider);
|
|
@@ -4575,7 +6148,9 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4575
6148
|
*/
|
|
4576
6149
|
async discovered(model) {
|
|
4577
6150
|
if (!this.options.discovery) return void 0;
|
|
4578
|
-
return (await this.
|
|
6151
|
+
return discoverAcrossAccounts((await this.options.tokens.list()).map((entry) => entry.key), async (account) => {
|
|
6152
|
+
return (await (await this.catalogFor(account)).resolve(() => this.fetchCatalog(account)))?.find((entry) => entry.id === model);
|
|
6153
|
+
});
|
|
4579
6154
|
}
|
|
4580
6155
|
/**
|
|
4581
6156
|
* [2026-08-23]-[a manually configured responses-only model combined with
|
|
@@ -4675,6 +6250,12 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4675
6250
|
this.replayByScope.clear();
|
|
4676
6251
|
}
|
|
4677
6252
|
async resolveModel(provider, model) {
|
|
6253
|
+
const pool = this.options.pool?.();
|
|
6254
|
+
if (pool !== void 0 && await pool.owns(provider, model)) return pool.resolveModel(provider, model);
|
|
6255
|
+
return this.resolveOwnModel(provider, model);
|
|
6256
|
+
}
|
|
6257
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
6258
|
+
async resolveOwnModel(provider, model) {
|
|
4678
6259
|
const discovered = await this.discovered(model);
|
|
4679
6260
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
4680
6261
|
return {
|
|
@@ -4689,15 +6270,27 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4689
6270
|
};
|
|
4690
6271
|
}
|
|
4691
6272
|
async *stream(options) {
|
|
6273
|
+
const pool = this.options.pool?.();
|
|
6274
|
+
if (pool !== void 0 && await pool.owns(options.provider, options.model)) {
|
|
6275
|
+
yield* pool.stream(options);
|
|
6276
|
+
return;
|
|
6277
|
+
}
|
|
6278
|
+
yield* this.streamCore(options);
|
|
6279
|
+
}
|
|
6280
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
6281
|
+
streamAccount(options, account) {
|
|
6282
|
+
return this.streamCore(options, account);
|
|
6283
|
+
}
|
|
6284
|
+
async *streamCore(options, account) {
|
|
4692
6285
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
4693
6286
|
try {
|
|
4694
6287
|
const wire = copilotRequestWire(this.configuredWireEntry(options.model) ?? await this.discovered(options.model), options);
|
|
4695
|
-
let session = await this.options.tokens.session();
|
|
6288
|
+
let session = await this.options.tokens.session(account);
|
|
4696
6289
|
const scope = this.replayScope(session.refreshToken, options);
|
|
4697
6290
|
let response = await this.request(options, session, watchdog.signal, wire, scope);
|
|
4698
6291
|
if (response.status === 401) {
|
|
4699
|
-
await latestVsCodeVersion(this.options.fetchFn ??
|
|
4700
|
-
session = await this.options.tokens.session(true);
|
|
6292
|
+
await latestVsCodeVersion(this.options.fetchFn ?? proxiedFetch, true);
|
|
6293
|
+
session = await this.options.tokens.session(account, true);
|
|
4701
6294
|
response = await this.request(options, session, watchdog.signal, wire, scope);
|
|
4702
6295
|
}
|
|
4703
6296
|
if (!response.ok) throw await httpLlmError(response, "copilot API");
|
|
@@ -4721,13 +6314,13 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
4721
6314
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
|
|
4722
6315
|
const hasVision = messages.some((message) => message.content.some((block) => block.type === "image"));
|
|
4723
6316
|
const body = wire === "responses" ? copilotResponsesRequestBody(options, toResponsesInput(messages, options.system, (callId) => this.replayFor(replayScopeKey, callId))) : copilotChatRequestBody(options, toChatMessages(messages, options.system));
|
|
4724
|
-
return
|
|
6317
|
+
return proxiedFetch(wire === "responses" ? COPILOT_RESPONSES_URL : COPILOT_API_URL, {
|
|
4725
6318
|
method: "POST",
|
|
4726
6319
|
headers: {
|
|
4727
6320
|
"authorization": `Bearer ${session.accessToken}`,
|
|
4728
6321
|
"accept": "text/event-stream",
|
|
4729
6322
|
"content-type": "application/json",
|
|
4730
|
-
...copilotHeaders(hasVision, await latestVsCodeVersion(this.options.fetchFn ??
|
|
6323
|
+
...copilotHeaders(hasVision, await latestVsCodeVersion(this.options.fetchFn ?? proxiedFetch))
|
|
4731
6324
|
},
|
|
4732
6325
|
body: JSON.stringify(body),
|
|
4733
6326
|
signal
|
|
@@ -4895,7 +6488,7 @@ function createXSearchTool(options) {
|
|
|
4895
6488
|
async execute(args, exec) {
|
|
4896
6489
|
const request = buildXSearchRequest(args);
|
|
4897
6490
|
const session = await options.tokens.session();
|
|
4898
|
-
const response = await (options.fetchFn ??
|
|
6491
|
+
const response = await (options.fetchFn ?? proxiedFetch)(X_SEARCH_URL, {
|
|
4899
6492
|
method: "POST",
|
|
4900
6493
|
headers: {
|
|
4901
6494
|
"authorization": `Bearer ${session.accessToken}`,
|
|
@@ -5160,7 +6753,7 @@ function createImageGenerateTool(options) {
|
|
|
5160
6753
|
content: result.content.filter((block) => block.type === "text")
|
|
5161
6754
|
}),
|
|
5162
6755
|
async execute(args, exec) {
|
|
5163
|
-
const fetchFn = options.fetchFn ??
|
|
6756
|
+
const fetchFn = options.fetchFn ?? proxiedFetch;
|
|
5164
6757
|
const preferGrok = args.provider === "grok";
|
|
5165
6758
|
const codexReady = options.codexTokens !== void 0 && await options.codexTokens.hasSession();
|
|
5166
6759
|
const grokReady = options.grokTokens !== void 0 && await options.grokTokens.hasSession();
|
|
@@ -5430,7 +7023,7 @@ function createVideoGenerateTool(options) {
|
|
|
5430
7023
|
async execute(args, exec) {
|
|
5431
7024
|
const body = buildVideoGenerateBody(args);
|
|
5432
7025
|
const session = await options.tokens.session();
|
|
5433
|
-
const fetchFn = options.fetchFn ??
|
|
7026
|
+
const fetchFn = options.fetchFn ?? proxiedFetch;
|
|
5434
7027
|
const headers = {
|
|
5435
7028
|
"authorization": `Bearer ${session.accessToken}`,
|
|
5436
7029
|
"accept": "application/json"
|
|
@@ -5489,6 +7082,8 @@ const name = "dsh-plugin-subscriptions";
|
|
|
5489
7082
|
const inject = ["llm"];
|
|
5490
7083
|
/** Default maximum provider idle time while one stream read is outstanding. */
|
|
5491
7084
|
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
7085
|
+
/** Bound on one pool quota poll — member selection must not hang on a usage endpoint. */
|
|
7086
|
+
const POOL_USAGE_TIMEOUT_MS = DISCOVERY_TIMEOUT_MS;
|
|
5492
7087
|
const providerIdSchema = z.union([
|
|
5493
7088
|
"codex",
|
|
5494
7089
|
"claude",
|
|
@@ -5503,6 +7098,11 @@ const modelEntrySchema = z.object({
|
|
|
5503
7098
|
inputModalities: z.array(z.union(["text", "image"])),
|
|
5504
7099
|
wire: z.union(["chat-completions", "responses"])
|
|
5505
7100
|
});
|
|
7101
|
+
const poolMemberSchema = z.object({
|
|
7102
|
+
provider: providerIdSchema.required(),
|
|
7103
|
+
account: z.string(),
|
|
7104
|
+
model: z.string().required()
|
|
7105
|
+
});
|
|
5506
7106
|
const Config = z.object({
|
|
5507
7107
|
providers: z.array(providerIdSchema).default([
|
|
5508
7108
|
"codex",
|
|
@@ -5516,6 +7116,15 @@ const Config = z.object({
|
|
|
5516
7116
|
claude: z.array(modelEntrySchema),
|
|
5517
7117
|
grok: z.array(modelEntrySchema),
|
|
5518
7118
|
copilot: z.array(modelEntrySchema)
|
|
7119
|
+
}),
|
|
7120
|
+
pool: z.object({
|
|
7121
|
+
enabled: z.boolean().default(true),
|
|
7122
|
+
strategy: z.union(["priority", "quota_aware"]).default("quota_aware"),
|
|
7123
|
+
switchMargin: z.number().min(1).default(2),
|
|
7124
|
+
autoAccounts: z.boolean().default(true),
|
|
7125
|
+
autoFamilies: z.boolean(),
|
|
7126
|
+
families: z.dict(z.array(poolMemberSchema)),
|
|
7127
|
+
tiers: z.dict(z.array(poolMemberSchema))
|
|
5519
7128
|
})
|
|
5520
7129
|
});
|
|
5521
7130
|
/** Built-in catalogs used when the config does not override a provider's models. */
|
|
@@ -5622,6 +7231,15 @@ function accountOf(provider, session) {
|
|
|
5622
7231
|
case "copilot": return session.account;
|
|
5623
7232
|
}
|
|
5624
7233
|
}
|
|
7234
|
+
/** The plan name a stored session carries, when the provider told us. */
|
|
7235
|
+
function planOf(provider, session) {
|
|
7236
|
+
switch (provider) {
|
|
7237
|
+
case "codex": return session.planType;
|
|
7238
|
+
case "claude": return session.subscriptionType;
|
|
7239
|
+
case "grok": return;
|
|
7240
|
+
case "copilot": return;
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
5625
7243
|
/**
|
|
5626
7244
|
* Auth operations behind the `/subscriptions-auth` RPC channel: start/complete
|
|
5627
7245
|
* OAuth attempts in the background, feed pasted codes, cancel, log out, and
|
|
@@ -5663,10 +7281,10 @@ var SubscriptionsAuthController = class {
|
|
|
5663
7281
|
this.usageFetchers = usageFetchers;
|
|
5664
7282
|
this.readClaudeCreds = readClaudeCreds;
|
|
5665
7283
|
}
|
|
5666
|
-
usage(provider, signal) {
|
|
7284
|
+
usage(provider, account, signal) {
|
|
5667
7285
|
const fetcher = this.usageFetchers[provider];
|
|
5668
7286
|
if (fetcher === void 0) return Promise.resolve({ supported: false });
|
|
5669
|
-
return fetcher(signal);
|
|
7287
|
+
return fetcher(account, signal);
|
|
5670
7288
|
}
|
|
5671
7289
|
async readImage(ref, signal) {
|
|
5672
7290
|
const attachments = this.resolveAttachments();
|
|
@@ -5684,28 +7302,45 @@ var SubscriptionsAuthController = class {
|
|
|
5684
7302
|
};
|
|
5685
7303
|
}
|
|
5686
7304
|
async status(provider) {
|
|
5687
|
-
const
|
|
5688
|
-
const account = accountOf(provider, session);
|
|
7305
|
+
const entries = await listAccounts(provider);
|
|
5689
7306
|
const detail = this.lastError.get(provider);
|
|
5690
7307
|
return {
|
|
5691
|
-
loggedIn: session !== void 0,
|
|
5692
7308
|
busy: this.flows.isBusy(provider) || this.deviceFlows.isBusy(provider) || this.finalizing.has(provider),
|
|
5693
|
-
|
|
5694
|
-
|
|
7309
|
+
accounts: entries.map(({ key, session }, index) => {
|
|
7310
|
+
const account = accountOf(provider, session);
|
|
7311
|
+
const plan = planOf(provider, session);
|
|
7312
|
+
return {
|
|
7313
|
+
key,
|
|
7314
|
+
isDefault: index === 0,
|
|
7315
|
+
expiresAt: session.expiresAt,
|
|
7316
|
+
...account === void 0 ? {} : { account },
|
|
7317
|
+
...plan === void 0 ? {} : { plan }
|
|
7318
|
+
};
|
|
7319
|
+
}),
|
|
5695
7320
|
...detail === void 0 ? {} : { detail }
|
|
5696
7321
|
};
|
|
5697
7322
|
}
|
|
5698
|
-
async login(provider) {
|
|
5699
|
-
if (provider === "claude") {
|
|
7323
|
+
async login(provider, method) {
|
|
7324
|
+
if (provider === "claude" && method !== "oauth") {
|
|
5700
7325
|
const imported = this.readClaudeCreds();
|
|
5701
7326
|
if (imported !== void 0) {
|
|
5702
7327
|
this.claim("claude");
|
|
5703
7328
|
this.flows.pending("claude")?.cancel();
|
|
5704
|
-
|
|
7329
|
+
const session = {
|
|
7330
|
+
...imported,
|
|
7331
|
+
keychainBound: true
|
|
7332
|
+
};
|
|
7333
|
+
await this.persist("claude", session);
|
|
5705
7334
|
this.lastError.delete("claude");
|
|
5706
|
-
this.onAuthChanged("claude");
|
|
7335
|
+
this.onAuthChanged("claude", accountKeyOf("claude", session));
|
|
5707
7336
|
return { authorizeUrl: "" };
|
|
5708
7337
|
}
|
|
7338
|
+
if (method === "keychain") throw new Error("no Claude Code credentials found; run `claude` and log in first, or choose the browser flow");
|
|
7339
|
+
const attempt$1 = await this.flows.start("claude", claudeFlow);
|
|
7340
|
+
this.completions.set("claude", this.complete("claude", attempt$1, this.claim("claude")));
|
|
7341
|
+
return { authorizeUrl: attempt$1.authorizeUrl };
|
|
7342
|
+
}
|
|
7343
|
+
if (provider === "claude") {
|
|
5709
7344
|
const attempt$1 = await this.flows.start("claude", claudeFlow);
|
|
5710
7345
|
this.completions.set("claude", this.complete("claude", attempt$1, this.claim("claude")));
|
|
5711
7346
|
return { authorizeUrl: attempt$1.authorizeUrl };
|
|
@@ -5747,7 +7382,7 @@ var SubscriptionsAuthController = class {
|
|
|
5747
7382
|
if (this.claims.get(provider) !== claim) return;
|
|
5748
7383
|
await this.persist(provider, session);
|
|
5749
7384
|
this.lastError.delete(provider);
|
|
5750
|
-
this.onAuthChanged(provider);
|
|
7385
|
+
this.onAuthChanged(provider, accountKeyOf(provider, session));
|
|
5751
7386
|
} catch (error) {
|
|
5752
7387
|
if (this.claims.get(provider) !== claim) return;
|
|
5753
7388
|
if (!(error instanceof Error && error.message === "login cancelled")) this.lastError.set(provider, errorChain(error));
|
|
@@ -5759,7 +7394,7 @@ var SubscriptionsAuthController = class {
|
|
|
5759
7394
|
const session = await completeCopilotLogin(await attempt.waitToken());
|
|
5760
7395
|
await this.persist(provider, session);
|
|
5761
7396
|
this.lastError.delete(provider);
|
|
5762
|
-
this.onAuthChanged(provider);
|
|
7397
|
+
this.onAuthChanged(provider, accountKeyOf(provider, session));
|
|
5763
7398
|
} catch (error) {
|
|
5764
7399
|
if (!(error instanceof Error && error.message === "login cancelled")) this.lastError.set(provider, errorChain(error));
|
|
5765
7400
|
} finally {
|
|
@@ -5775,12 +7410,7 @@ var SubscriptionsAuthController = class {
|
|
|
5775
7410
|
}
|
|
5776
7411
|
}
|
|
5777
7412
|
persist(provider, session) {
|
|
5778
|
-
|
|
5779
|
-
case "codex": return saveSession("codex", session);
|
|
5780
|
-
case "claude": return saveSession("claude", session);
|
|
5781
|
-
case "grok": return saveSession("grok", session);
|
|
5782
|
-
case "copilot": return saveSession("copilot", session);
|
|
5783
|
-
}
|
|
7413
|
+
return saveAccountSession(provider, accountKeyOf(provider, session), session);
|
|
5784
7414
|
}
|
|
5785
7415
|
/**
|
|
5786
7416
|
* Settle once no OAuth completion is running for a provider.
|
|
@@ -5804,13 +7434,17 @@ var SubscriptionsAuthController = class {
|
|
|
5804
7434
|
this.deviceFlows.pending(provider)?.cancel();
|
|
5805
7435
|
return Promise.resolve();
|
|
5806
7436
|
}
|
|
5807
|
-
async logout(provider) {
|
|
7437
|
+
async logout(provider, account) {
|
|
5808
7438
|
this.claim(provider);
|
|
5809
7439
|
this.flows.pending(provider)?.cancel();
|
|
5810
7440
|
this.deviceFlows.pending(provider)?.cancel();
|
|
5811
|
-
await
|
|
7441
|
+
await deleteAccountSession(provider, account);
|
|
5812
7442
|
this.lastError.delete(provider);
|
|
5813
|
-
this.onAuthChanged(provider);
|
|
7443
|
+
this.onAuthChanged(provider, account);
|
|
7444
|
+
}
|
|
7445
|
+
async setDefault(provider, account) {
|
|
7446
|
+
await setDefaultAccount(provider, account);
|
|
7447
|
+
this.onAuthChanged(provider, account);
|
|
5814
7448
|
}
|
|
5815
7449
|
};
|
|
5816
7450
|
function apply(ctx, config) {
|
|
@@ -5826,9 +7460,18 @@ function apply(ctx, config) {
|
|
|
5826
7460
|
};
|
|
5827
7461
|
const resolveAttachments = () => ctx.get("attachments");
|
|
5828
7462
|
const handles = /* @__PURE__ */ new Map();
|
|
5829
|
-
const
|
|
7463
|
+
const adapters = /* @__PURE__ */ new Map();
|
|
7464
|
+
const accountTokens = /* @__PURE__ */ new Map();
|
|
7465
|
+
let poolHealth;
|
|
7466
|
+
let poolUsage;
|
|
7467
|
+
let poolAdapter;
|
|
7468
|
+
const authChanged = (provider, account) => {
|
|
5830
7469
|
if (provider === "copilot") copilotAdapter?.clearReplayState();
|
|
5831
|
-
|
|
7470
|
+
adapters.get(provider)?.clearAccountCatalog(account);
|
|
7471
|
+
poolHealth?.clear(provider, account);
|
|
7472
|
+
poolUsage?.invalidate(provider, account);
|
|
7473
|
+
poolAdapter?.invalidate();
|
|
7474
|
+
for (const [route, handle] of handles) handle.replace([route]);
|
|
5832
7475
|
};
|
|
5833
7476
|
let codexTokens;
|
|
5834
7477
|
let claudeTokens;
|
|
@@ -5839,20 +7482,21 @@ function apply(ctx, config) {
|
|
|
5839
7482
|
let copilotAdapter;
|
|
5840
7483
|
for (const provider of providers) switch (provider) {
|
|
5841
7484
|
case "codex": {
|
|
5842
|
-
const tokens = new
|
|
7485
|
+
const tokens = new AccountTokenManager({
|
|
7486
|
+
provider: "codex",
|
|
5843
7487
|
displayName: "ChatGPT (Codex)",
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
authChanged("codex");
|
|
7488
|
+
makeOptions: () => ({
|
|
7489
|
+
preemptMs: CODEX_PREEMPT_MS,
|
|
7490
|
+
refresh: refreshCodex,
|
|
7491
|
+
isPermanent: isCodexPermanentRefreshError
|
|
7492
|
+
}),
|
|
7493
|
+
onAccountRemoved: (account) => {
|
|
7494
|
+
authChanged("codex", account);
|
|
5852
7495
|
}
|
|
5853
7496
|
});
|
|
5854
7497
|
codexTokens = tokens;
|
|
5855
|
-
|
|
7498
|
+
accountTokens.set("codex", tokens);
|
|
7499
|
+
usageFetchers.codex = async (account, signal) => fetchCodexUsage(await tokens.session(account), proxiedFetch, signal);
|
|
5856
7500
|
let adapter;
|
|
5857
7501
|
adapter = new CodexAdapter({
|
|
5858
7502
|
models: catalog.codex,
|
|
@@ -5862,28 +7506,31 @@ function apply(ctx, config) {
|
|
|
5862
7506
|
onWarn,
|
|
5863
7507
|
resolveAttachments,
|
|
5864
7508
|
catalogStore: catalogStore("codex"),
|
|
7509
|
+
pool: () => poolAdapter,
|
|
5865
7510
|
speedFor: (sessionId, model) => sessionId !== void 0 && speedBySession.get(sessionId) === "fast" && adapter.supportsFastTier(model)
|
|
5866
7511
|
});
|
|
5867
7512
|
codexAdapter = adapter;
|
|
7513
|
+
adapters.set("codex", adapter);
|
|
5868
7514
|
handles.set("codex", ctx.llm.registerAdapter(["codex"], adapter));
|
|
5869
7515
|
break;
|
|
5870
7516
|
}
|
|
5871
7517
|
case "claude": {
|
|
5872
|
-
const tokens = new
|
|
7518
|
+
const tokens = new AccountTokenManager({
|
|
7519
|
+
provider: "claude",
|
|
5873
7520
|
displayName: "Claude (Subscription)",
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
authChanged("claude");
|
|
7521
|
+
makeOptions: () => ({
|
|
7522
|
+
preemptMs: CLAUDE_PREEMPT_MS,
|
|
7523
|
+
refresh: (session) => session.keychainBound === true ? refreshClaudeSynced(session, refreshClaude) : refreshClaude(session),
|
|
7524
|
+
isPermanent: isClaudePermanentRefreshError
|
|
7525
|
+
}),
|
|
7526
|
+
onAccountRemoved: (account) => {
|
|
7527
|
+
authChanged("claude", account);
|
|
5882
7528
|
}
|
|
5883
7529
|
});
|
|
5884
7530
|
claudeTokens = tokens;
|
|
5885
|
-
|
|
5886
|
-
|
|
7531
|
+
accountTokens.set("claude", tokens);
|
|
7532
|
+
usageFetchers.claude = async (account, signal) => fetchClaudeUsage(await tokens.session(account), proxiedFetch, signal);
|
|
7533
|
+
const adapter = new ClaudeAdapter({
|
|
5887
7534
|
models: catalog.claude,
|
|
5888
7535
|
streamIdleTimeoutMs,
|
|
5889
7536
|
tokens,
|
|
@@ -5891,49 +7538,57 @@ function apply(ctx, config) {
|
|
|
5891
7538
|
onWarn,
|
|
5892
7539
|
maxRetries: 10,
|
|
5893
7540
|
resolveAttachments,
|
|
5894
|
-
catalogStore: catalogStore("claude")
|
|
5895
|
-
|
|
7541
|
+
catalogStore: catalogStore("claude"),
|
|
7542
|
+
pool: () => poolAdapter
|
|
7543
|
+
});
|
|
7544
|
+
adapters.set("claude", adapter);
|
|
7545
|
+
handles.set("claude", ctx.llm.registerAdapter(["claude"], adapter));
|
|
5896
7546
|
break;
|
|
5897
7547
|
}
|
|
5898
7548
|
case "grok": {
|
|
5899
|
-
const tokens = new
|
|
7549
|
+
const tokens = new AccountTokenManager({
|
|
7550
|
+
provider: "grok",
|
|
5900
7551
|
displayName: "Grok (Subscription)",
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
authChanged("grok");
|
|
7552
|
+
makeOptions: () => ({
|
|
7553
|
+
preemptMs: GROK_PREEMPT_MS,
|
|
7554
|
+
refresh: refreshGrok,
|
|
7555
|
+
isPermanent: isGrokPermanentRefreshError
|
|
7556
|
+
}),
|
|
7557
|
+
onAccountRemoved: (account) => {
|
|
7558
|
+
authChanged("grok", account);
|
|
5909
7559
|
}
|
|
5910
7560
|
});
|
|
5911
7561
|
grokTokens = tokens;
|
|
5912
|
-
|
|
5913
|
-
|
|
7562
|
+
accountTokens.set("grok", tokens);
|
|
7563
|
+
usageFetchers.grok = async (account, signal) => fetchGrokUsage(await tokens.session(account), proxiedFetch, signal);
|
|
7564
|
+
const adapter = new GrokAdapter({
|
|
5914
7565
|
models: catalog.grok,
|
|
5915
7566
|
streamIdleTimeoutMs,
|
|
5916
7567
|
tokens,
|
|
5917
7568
|
discovery: !overridden.has("grok"),
|
|
5918
7569
|
onWarn,
|
|
5919
7570
|
resolveAttachments,
|
|
5920
|
-
catalogStore: catalogStore("grok")
|
|
5921
|
-
|
|
7571
|
+
catalogStore: catalogStore("grok"),
|
|
7572
|
+
pool: () => poolAdapter
|
|
7573
|
+
});
|
|
7574
|
+
adapters.set("grok", adapter);
|
|
7575
|
+
handles.set("grok", ctx.llm.registerAdapter(["grok"], adapter));
|
|
5922
7576
|
break;
|
|
5923
7577
|
}
|
|
5924
7578
|
case "copilot": {
|
|
5925
|
-
const tokens = new
|
|
7579
|
+
const tokens = new AccountTokenManager({
|
|
7580
|
+
provider: "copilot",
|
|
5926
7581
|
displayName: "GitHub Copilot",
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
authChanged("copilot");
|
|
7582
|
+
makeOptions: () => ({
|
|
7583
|
+
preemptMs: COPILOT_PREEMPT_MS,
|
|
7584
|
+
refresh: refreshCopilot,
|
|
7585
|
+
isPermanent: isCopilotPermanentRefreshError
|
|
7586
|
+
}),
|
|
7587
|
+
onAccountRemoved: (account) => {
|
|
7588
|
+
authChanged("copilot", account);
|
|
5935
7589
|
}
|
|
5936
7590
|
});
|
|
7591
|
+
accountTokens.set("copilot", tokens);
|
|
5937
7592
|
copilotAdapter = new CopilotAdapter({
|
|
5938
7593
|
models: catalog.copilot,
|
|
5939
7594
|
streamIdleTimeoutMs,
|
|
@@ -5941,12 +7596,77 @@ function apply(ctx, config) {
|
|
|
5941
7596
|
discovery: !overridden.has("copilot"),
|
|
5942
7597
|
onWarn,
|
|
5943
7598
|
resolveAttachments,
|
|
5944
|
-
catalogStore: catalogStore("copilot")
|
|
7599
|
+
catalogStore: catalogStore("copilot"),
|
|
7600
|
+
pool: () => poolAdapter
|
|
5945
7601
|
});
|
|
7602
|
+
adapters.set("copilot", copilotAdapter);
|
|
5946
7603
|
handles.set("copilot", ctx.llm.registerAdapter(["copilot"], copilotAdapter));
|
|
5947
7604
|
break;
|
|
5948
7605
|
}
|
|
5949
7606
|
}
|
|
7607
|
+
const poolConfig = config.pool;
|
|
7608
|
+
const autoAccounts = poolConfig?.autoAccounts ?? poolConfig?.autoFamilies ?? true;
|
|
7609
|
+
if (poolConfig?.enabled !== false && adapters.size >= 1) {
|
|
7610
|
+
const fetcherFor = (provider, account) => {
|
|
7611
|
+
switch (provider) {
|
|
7612
|
+
case "codex": {
|
|
7613
|
+
const tokens = codexTokens;
|
|
7614
|
+
return tokens === void 0 ? void 0 : async () => fetchCodexUsage(await tokens.session(account), proxiedFetch, AbortSignal.timeout(POOL_USAGE_TIMEOUT_MS));
|
|
7615
|
+
}
|
|
7616
|
+
case "claude": {
|
|
7617
|
+
const tokens = claudeTokens;
|
|
7618
|
+
return tokens === void 0 ? void 0 : async () => fetchClaudeUsage(await tokens.session(account), proxiedFetch, AbortSignal.timeout(POOL_USAGE_TIMEOUT_MS));
|
|
7619
|
+
}
|
|
7620
|
+
case "grok": {
|
|
7621
|
+
const tokens = grokTokens;
|
|
7622
|
+
return tokens === void 0 ? void 0 : async () => fetchGrokUsage(await tokens.session(account), proxiedFetch, AbortSignal.timeout(POOL_USAGE_TIMEOUT_MS));
|
|
7623
|
+
}
|
|
7624
|
+
case "copilot": return;
|
|
7625
|
+
}
|
|
7626
|
+
};
|
|
7627
|
+
poolHealth = new PoolHealthRegistry();
|
|
7628
|
+
poolUsage = new PoolUsageTracker(fetcherFor);
|
|
7629
|
+
const families = async () => {
|
|
7630
|
+
const pools = /* @__PURE__ */ new Map();
|
|
7631
|
+
if (autoAccounts) {
|
|
7632
|
+
const sources = {};
|
|
7633
|
+
await Promise.all([...adapters].map(async ([provider, adapter]) => {
|
|
7634
|
+
try {
|
|
7635
|
+
const accounts = (await accountTokens.get(provider)?.list() ?? []).map((entry) => entry.key);
|
|
7636
|
+
if (accounts.length < 2) return;
|
|
7637
|
+
const catalogs = (await Promise.all(accounts.map(async (account) => {
|
|
7638
|
+
const models = await withTimeout((signal) => adapter.listOwnModels(provider, account, signal), POOL_USAGE_TIMEOUT_MS);
|
|
7639
|
+
return models === void 0 ? void 0 : {
|
|
7640
|
+
account,
|
|
7641
|
+
models
|
|
7642
|
+
};
|
|
7643
|
+
}))).filter((entry) => entry !== void 0);
|
|
7644
|
+
if (catalogs.length >= 2) sources[provider] = { catalogs };
|
|
7645
|
+
} catch {}
|
|
7646
|
+
}));
|
|
7647
|
+
for (const [key, definition] of buildAccountPools(sources)) pools.set(key, definition);
|
|
7648
|
+
}
|
|
7649
|
+
for (const [id, members] of Object.entries(poolConfig?.families ?? {})) {
|
|
7650
|
+
if (members.length === 0) continue;
|
|
7651
|
+
const owner = members[0].provider;
|
|
7652
|
+
const kept = members.filter((member) => member.provider === owner);
|
|
7653
|
+
if (kept.length < members.length) onWarn(`pool "${id}": cross-provider members are ignored; only ${owner} accounts are pooled`);
|
|
7654
|
+
pools.set(poolKey(owner, id), { members: kept });
|
|
7655
|
+
}
|
|
7656
|
+
return pools;
|
|
7657
|
+
};
|
|
7658
|
+
poolAdapter = new PoolAdapter({
|
|
7659
|
+
adapters: Object.fromEntries(adapters),
|
|
7660
|
+
health: poolHealth,
|
|
7661
|
+
usage: poolUsage,
|
|
7662
|
+
strategy: poolConfig?.strategy ?? "quota_aware",
|
|
7663
|
+
switchMargin: poolConfig?.switchMargin ?? 2,
|
|
7664
|
+
defaultAccount: (provider) => accountTokens.get(provider)?.defaultAccount() ?? Promise.resolve(void 0),
|
|
7665
|
+
families,
|
|
7666
|
+
tiers: poolConfig?.tiers ?? {},
|
|
7667
|
+
onWarn
|
|
7668
|
+
});
|
|
7669
|
+
}
|
|
5950
7670
|
registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers), {
|
|
5951
7671
|
async speed(sessionId) {
|
|
5952
7672
|
return {
|
|
@@ -5958,10 +7678,20 @@ function apply(ctx, config) {
|
|
|
5958
7678
|
if (tier === "standard") speedBySession.delete(sessionId);
|
|
5959
7679
|
else speedBySession.set(sessionId, tier);
|
|
5960
7680
|
}
|
|
7681
|
+
}, {
|
|
7682
|
+
get: () => proxyGetConfig(),
|
|
7683
|
+
set: (input) => proxySetConfig(input),
|
|
7684
|
+
test: (payload) => proxyTestConnection(payload.url, payload.proxy)
|
|
5961
7685
|
});
|
|
5962
7686
|
if (claudeTokens !== void 0) {
|
|
7687
|
+
const tokens = claudeTokens;
|
|
5963
7688
|
const syncTimer = setInterval(() => {
|
|
5964
|
-
|
|
7689
|
+
tokens.list().then((accounts) => {
|
|
7690
|
+
for (const { key, session } of accounts) {
|
|
7691
|
+
if (session.keychainBound !== true) continue;
|
|
7692
|
+
tokens.session(key).catch(() => {});
|
|
7693
|
+
}
|
|
7694
|
+
}, () => void 0);
|
|
5965
7695
|
}, 5 * 6e4);
|
|
5966
7696
|
ctx.effect(() => () => {
|
|
5967
7697
|
clearInterval(syncTimer);
|
|
@@ -5982,4 +7712,4 @@ function apply(ctx, config) {
|
|
|
5982
7712
|
}
|
|
5983
7713
|
|
|
5984
7714
|
//#endregion
|
|
5985
|
-
export { Config, DEFAULT_STREAM_IDLE_TIMEOUT_MS, SubscriptionsAuthController, apply, inject, name };
|
|
7715
|
+
export { Config, DEFAULT_STREAM_IDLE_TIMEOUT_MS, POOL_USAGE_TIMEOUT_MS, SubscriptionsAuthController, apply, inject, name, withTimeout };
|