dsh-plugin-subscriptions 0.5.2 → 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 +36 -1
- package/README.zh.md +36 -1
- package/lib/auth/rpc.d.ts +29 -12
- package/lib/auth/rpc.js +29 -6
- package/lib/auth/store.d.ts +75 -17
- package/lib/auth/store.js +148 -27
- package/lib/client/SubscriptionsSection.d.ts +9 -3
- package/lib/client/SubscriptionsSection.js +93 -65
- package/lib/client/locales.d.ts +18 -10
- package/lib/client/locales.js +18 -10
- package/lib/client.js +250 -127
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +21 -0
- package/lib/index.js +1482 -168
- package/lib/providers/accounts.d.ts +102 -0
- package/lib/providers/accounts.js +123 -0
- package/lib/providers/claude.d.ts +22 -4
- package/lib/providers/claude.js +91 -11
- package/lib/providers/codex.d.ts +24 -3
- package/lib/providers/codex.js +116 -17
- package/lib/providers/common.d.ts +17 -0
- package/lib/providers/common.js +67 -3
- package/lib/providers/copilot.d.ts +22 -3
- package/lib/providers/copilot.js +91 -12
- package/lib/providers/grok.d.ts +24 -4
- package/lib/providers/grok.js +100 -14
- 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 +2 -1
- package/lib/tools/video-generate.d.ts +2 -2
- package/lib/tools/video-generate.js +2 -1
- package/lib/tools/x-search.d.ts +2 -2
- package/lib/tools/x-search.js +2 -1
- package/package.json +1 -1
package/lib/providers/codex.js
CHANGED
|
@@ -8,7 +8,8 @@ import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmErr
|
|
|
8
8
|
import { decodeJwtPayload } from '../auth/jwt.js';
|
|
9
9
|
import { resolveImages } from '../translate/resolved.js';
|
|
10
10
|
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
11
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError,
|
|
11
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
12
|
+
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
12
13
|
import { proxiedFetch } from '../http.js';
|
|
13
14
|
export const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
14
15
|
export const CODEX_AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
|
|
@@ -308,9 +309,10 @@ function supportsFastTier(entry) {
|
|
|
308
309
|
* Fetch the live codex model catalog with the session's auth headers.
|
|
309
310
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
310
311
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
312
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
311
313
|
* @returns discovered models: hidden entries dropped, sorted by priority.
|
|
312
314
|
*/
|
|
313
|
-
export async function fetchCodexModels(session, fetchFn = proxiedFetch) {
|
|
315
|
+
export async function fetchCodexModels(session, fetchFn = proxiedFetch, signal) {
|
|
314
316
|
const url = `${CODEX_MODELS_URL}?client_version=${CODEX_CLIENT_VERSION}`;
|
|
315
317
|
const response = await fetchFn(url, {
|
|
316
318
|
headers: {
|
|
@@ -320,6 +322,7 @@ export async function fetchCodexModels(session, fetchFn = proxiedFetch) {
|
|
|
320
322
|
'accept': 'application/json',
|
|
321
323
|
...attributionHeaders(),
|
|
322
324
|
},
|
|
325
|
+
...signal === undefined ? {} : { signal },
|
|
323
326
|
});
|
|
324
327
|
if (!response.ok)
|
|
325
328
|
throw await oauthEndpointError(response, 'codex models');
|
|
@@ -451,14 +454,47 @@ export function codexRequestBody(options, resolved, fast) {
|
|
|
451
454
|
export class CodexAdapter extends LlmAdapter {
|
|
452
455
|
options;
|
|
453
456
|
catalog;
|
|
457
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
458
|
+
accountCatalogs = new Map();
|
|
459
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
460
|
+
catalogOwner;
|
|
454
461
|
constructor(options) {
|
|
455
462
|
super();
|
|
456
463
|
this.options = options;
|
|
457
464
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
458
465
|
}
|
|
459
466
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
460
|
-
async fetchCatalog() {
|
|
461
|
-
return fetchCodexModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
467
|
+
async fetchCatalog(account, signal) {
|
|
468
|
+
return fetchCodexModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
469
|
+
}
|
|
470
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
471
|
+
clearAccountCatalog(account) {
|
|
472
|
+
if (account === undefined)
|
|
473
|
+
this.accountCatalogs.clear();
|
|
474
|
+
else
|
|
475
|
+
this.accountCatalogs.delete(account);
|
|
476
|
+
if (account === undefined || this.catalogOwner === account || this.catalogOwner === undefined) {
|
|
477
|
+
this.catalogOwner = undefined;
|
|
478
|
+
this.catalog.invalidate();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
482
|
+
async catalogFor(account) {
|
|
483
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
484
|
+
const key = account ?? defaultKey;
|
|
485
|
+
if (key === undefined || key === defaultKey) {
|
|
486
|
+
if (this.catalogOwner !== undefined && this.catalogOwner !== defaultKey) {
|
|
487
|
+
this.catalog.invalidate();
|
|
488
|
+
}
|
|
489
|
+
this.catalogOwner = defaultKey;
|
|
490
|
+
return this.catalog;
|
|
491
|
+
}
|
|
492
|
+
let cache = this.accountCatalogs.get(key);
|
|
493
|
+
if (cache === undefined) {
|
|
494
|
+
cache = new ModelCatalogCache();
|
|
495
|
+
this.accountCatalogs.set(key, cache);
|
|
496
|
+
}
|
|
497
|
+
return cache;
|
|
462
498
|
}
|
|
463
499
|
providerInfo(provider) {
|
|
464
500
|
return { id: provider, name: 'ChatGPT (Codex)' };
|
|
@@ -472,26 +508,48 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
472
508
|
}));
|
|
473
509
|
}
|
|
474
510
|
async listModels(provider) {
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
if (
|
|
511
|
+
const own = await this.listOwnModels(provider);
|
|
512
|
+
const pool = this.options.pool?.();
|
|
513
|
+
if (pool === undefined)
|
|
514
|
+
return own;
|
|
515
|
+
const extra = await pool.modelsForProvider(provider);
|
|
516
|
+
const seen = new Set(own.map(model => model.id));
|
|
517
|
+
// Account pools reuse the catalog row; only configured tiers are extra.
|
|
518
|
+
return [...own, ...extra.filter(model => !seen.has(model.id))];
|
|
519
|
+
}
|
|
520
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
521
|
+
async listOwnModels(provider, account, signal) {
|
|
522
|
+
if (account === undefined) {
|
|
523
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
524
|
+
if (accounts.length === 0)
|
|
525
|
+
return [];
|
|
526
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), { timeoutMs: this.options.discoveryTimeoutMs ?? DISCOVERY_TIMEOUT_MS, ...signal === undefined ? {} : { signal } });
|
|
527
|
+
}
|
|
528
|
+
if (!await this.options.tokens.hasSession(account)) {
|
|
478
529
|
return [];
|
|
530
|
+
}
|
|
479
531
|
if (!this.options.discovery)
|
|
480
532
|
return this.staticModels(provider);
|
|
533
|
+
const catalog = await this.catalogFor(account);
|
|
481
534
|
try {
|
|
482
535
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
483
536
|
// through the refresh-aware path so an expired access token renews here
|
|
484
537
|
// instead of failing discovery into the static fallback.
|
|
485
|
-
const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(force),
|
|
538
|
+
const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)));
|
|
486
539
|
return discovered.map(model => ({
|
|
487
540
|
provider,
|
|
488
541
|
id: model.id,
|
|
489
542
|
name: model.name,
|
|
490
543
|
...model.description === undefined ? {} : { description: model.description },
|
|
491
544
|
inputModalities: CODEX_MODALITIES,
|
|
545
|
+
...model.priority === undefined ? {} : { priority: model.priority },
|
|
492
546
|
}));
|
|
493
547
|
}
|
|
494
548
|
catch (error) {
|
|
549
|
+
// A cancelled discovery must not fall back to the static catalog — the
|
|
550
|
+
// caller (pool assembly) treats abort as "this account sits out".
|
|
551
|
+
if (isDiscoveryAborted(error, signal))
|
|
552
|
+
throw error;
|
|
495
553
|
// A permanent refresh failure deletes the stored session: the provider
|
|
496
554
|
// is logged out, so hide it instead of showing a stale static catalog.
|
|
497
555
|
if (isMissingOrInvalidCredential(error))
|
|
@@ -510,8 +568,12 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
510
568
|
async discovered(model) {
|
|
511
569
|
if (!this.options.discovery)
|
|
512
570
|
return undefined;
|
|
513
|
-
const
|
|
514
|
-
return
|
|
571
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
572
|
+
return discoverAcrossAccounts(accounts, async (account) => {
|
|
573
|
+
const catalog = await this.catalogFor(account);
|
|
574
|
+
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
575
|
+
return models?.find(entry => entry.id === model);
|
|
576
|
+
});
|
|
515
577
|
}
|
|
516
578
|
/** Whether the discovered catalog advertises a fast tier for this model. */
|
|
517
579
|
async supportsFastTier(model) {
|
|
@@ -522,14 +584,39 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
522
584
|
if (!this.options.discovery)
|
|
523
585
|
return [];
|
|
524
586
|
// Not logged in → no fast models, so the Speed toggle hides after logout
|
|
525
|
-
// (mirrors the listModels guard above).
|
|
526
|
-
|
|
527
|
-
|
|
587
|
+
// (mirrors the listModels guard above). Union every account: a fast-capable
|
|
588
|
+
// model only the non-default lists (e.g. gpt-5.6-sol) must still show Speed.
|
|
589
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
590
|
+
if (accounts.length === 0)
|
|
528
591
|
return [];
|
|
529
|
-
const
|
|
530
|
-
|
|
592
|
+
const seen = new Set();
|
|
593
|
+
const ids = [];
|
|
594
|
+
for (const account of accounts) {
|
|
595
|
+
try {
|
|
596
|
+
const catalog = await this.catalogFor(account);
|
|
597
|
+
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
598
|
+
for (const model of models ?? []) {
|
|
599
|
+
if (model.fastTier !== true || seen.has(model.id))
|
|
600
|
+
continue;
|
|
601
|
+
seen.add(model.id);
|
|
602
|
+
ids.push(model.id);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// sit out
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return ids;
|
|
531
610
|
}
|
|
532
611
|
async resolveModel(provider, model) {
|
|
612
|
+
const pool = this.options.pool?.();
|
|
613
|
+
if (pool !== undefined && await pool.owns(provider, model)) {
|
|
614
|
+
return pool.resolveModel(provider, model);
|
|
615
|
+
}
|
|
616
|
+
return this.resolveOwnModel(provider, model);
|
|
617
|
+
}
|
|
618
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
619
|
+
async resolveOwnModel(provider, model) {
|
|
533
620
|
// Discovered metadata (when discovery is on) wins over the static entry;
|
|
534
621
|
// the static entry wins over the built-in defaults.
|
|
535
622
|
const discovered = await this.discovered(model);
|
|
@@ -546,13 +633,25 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
546
633
|
};
|
|
547
634
|
}
|
|
548
635
|
async *stream(options) {
|
|
636
|
+
const pool = this.options.pool?.();
|
|
637
|
+
if (pool !== undefined && await pool.owns(options.provider, options.model)) {
|
|
638
|
+
yield* pool.stream(options);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
yield* this.streamCore(options);
|
|
642
|
+
}
|
|
643
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
644
|
+
streamAccount(options, account) {
|
|
645
|
+
return this.streamCore(options, account);
|
|
646
|
+
}
|
|
647
|
+
async *streamCore(options, account) {
|
|
549
648
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
550
649
|
try {
|
|
551
|
-
let session = await this.options.tokens.session();
|
|
650
|
+
let session = await this.options.tokens.session(account);
|
|
552
651
|
let response = await this.request(options, session, watchdog.signal);
|
|
553
652
|
if (response.status === 401) {
|
|
554
653
|
// One forced refresh + retry on an unexpired-but-rejected token.
|
|
555
|
-
session = await this.options.tokens.session(true);
|
|
654
|
+
session = await this.options.tokens.session(account, true);
|
|
556
655
|
response = await this.request(options, session, watchdog.signal);
|
|
557
656
|
}
|
|
558
657
|
if (!response.ok)
|
|
@@ -141,6 +141,13 @@ export declare class TokenManager<S extends TimedSession> {
|
|
|
141
141
|
}
|
|
142
142
|
/** Fetch signature adapters accept for discovery calls (injectable for tests). */
|
|
143
143
|
export type FetchFn = typeof fetch;
|
|
144
|
+
/** Bound on one account catalog fetch or usage poll — a hang must not block the picker. */
|
|
145
|
+
export declare const DISCOVERY_TIMEOUT_MS = 10000;
|
|
146
|
+
/**
|
|
147
|
+
* Run `work` with an aborting signal. Resolves undefined when the timeout
|
|
148
|
+
* fires (the fetch is aborted); other failures propagate.
|
|
149
|
+
*/
|
|
150
|
+
export declare function withTimeout<T>(work: (signal: AbortSignal) => Promise<T>, timeoutMs: number): Promise<T | undefined>;
|
|
144
151
|
/** One rate-limit window reported by a provider's usage endpoint. */
|
|
145
152
|
export interface UsageWindow {
|
|
146
153
|
/** Window kind: `session` for the short rolling window, `weekly` for the 7-day one. */
|
|
@@ -196,6 +203,12 @@ export interface DiscoveredModel {
|
|
|
196
203
|
*/
|
|
197
204
|
copilotResponses?: boolean;
|
|
198
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* First account catalog that lists `model` (callers pass default-first).
|
|
208
|
+
* One failing lookup sits that account out so a sibling's metadata still
|
|
209
|
+
* resolves — the same isolation as the picker catalog union.
|
|
210
|
+
*/
|
|
211
|
+
export declare function discoverAcrossAccounts(accounts: readonly string[], lookup: (account: string) => Promise<DiscoveredModel | undefined>): Promise<DiscoveredModel | undefined>;
|
|
199
212
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
200
213
|
export declare const DISCOVERY_TTL_MS: number;
|
|
201
214
|
/** A durable snapshot of one provider's discovered catalog. */
|
|
@@ -234,6 +247,8 @@ export declare class ModelCatalogCache {
|
|
|
234
247
|
private seeded;
|
|
235
248
|
/** Set by {@link invalidate} so an in-flight disk read cannot resurrect dropped state. */
|
|
236
249
|
private seedDisabled;
|
|
250
|
+
/** Bumped by {@link invalidate} so a loser in-flight fetch cannot write back. */
|
|
251
|
+
private generation;
|
|
237
252
|
constructor(persistence?: CatalogPersistence | undefined, ttlMs?: number);
|
|
238
253
|
/**
|
|
239
254
|
* The cached catalog when fresh, without fetching.
|
|
@@ -272,6 +287,8 @@ export declare class ModelCatalogCache {
|
|
|
272
287
|
}
|
|
273
288
|
/** Whether discovery failed because the stored login is gone. */
|
|
274
289
|
export declare function isMissingOrInvalidCredential(error: unknown): boolean;
|
|
290
|
+
/** Whether discovery stopped because the caller cancelled or the timeout fired. */
|
|
291
|
+
export declare function isDiscoveryAborted(error: unknown, signal?: AbortSignal): boolean;
|
|
275
292
|
/**
|
|
276
293
|
* Run a catalog fetch, retrying once after a forced token refresh when the
|
|
277
294
|
* first attempt is a 401/AUTH. Only {@link ModelCatalogCache.invalidate}s
|
package/lib/providers/common.js
CHANGED
|
@@ -263,6 +263,47 @@ export class TokenManager {
|
|
|
263
263
|
return next;
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
+
/** Bound on one account catalog fetch or usage poll — a hang must not block the picker. */
|
|
267
|
+
export const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
268
|
+
/**
|
|
269
|
+
* Run `work` with an aborting signal. Resolves undefined when the timeout
|
|
270
|
+
* fires (the fetch is aborted); other failures propagate.
|
|
271
|
+
*/
|
|
272
|
+
export function withTimeout(work, timeoutMs) {
|
|
273
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
274
|
+
const aborted = new Promise(resolve => {
|
|
275
|
+
if (signal.aborted)
|
|
276
|
+
resolve(undefined);
|
|
277
|
+
else
|
|
278
|
+
signal.addEventListener('abort', () => resolve(undefined), { once: true });
|
|
279
|
+
});
|
|
280
|
+
return Promise.race([
|
|
281
|
+
work(signal).then(value => (signal.aborted ? undefined : value), (error) => {
|
|
282
|
+
if (signal.aborted)
|
|
283
|
+
return undefined;
|
|
284
|
+
throw error;
|
|
285
|
+
}),
|
|
286
|
+
aborted,
|
|
287
|
+
]);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* First account catalog that lists `model` (callers pass default-first).
|
|
291
|
+
* One failing lookup sits that account out so a sibling's metadata still
|
|
292
|
+
* resolves — the same isolation as the picker catalog union.
|
|
293
|
+
*/
|
|
294
|
+
export async function discoverAcrossAccounts(accounts, lookup) {
|
|
295
|
+
for (const account of accounts) {
|
|
296
|
+
try {
|
|
297
|
+
const found = await lookup(account);
|
|
298
|
+
if (found !== undefined)
|
|
299
|
+
return found;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// sit out
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
266
307
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
267
308
|
export const DISCOVERY_TTL_MS = 5 * 60_000;
|
|
268
309
|
/**
|
|
@@ -286,6 +327,8 @@ export class ModelCatalogCache {
|
|
|
286
327
|
seeded;
|
|
287
328
|
/** Set by {@link invalidate} so an in-flight disk read cannot resurrect dropped state. */
|
|
288
329
|
seedDisabled = false;
|
|
330
|
+
/** Bumped by {@link invalidate} so a loser in-flight fetch cannot write back. */
|
|
331
|
+
generation = 0;
|
|
289
332
|
constructor(persistence, ttlMs = DISCOVERY_TTL_MS) {
|
|
290
333
|
this.persistence = persistence;
|
|
291
334
|
this.ttlMs = ttlMs;
|
|
@@ -320,16 +363,25 @@ export class ModelCatalogCache {
|
|
|
320
363
|
}
|
|
321
364
|
/** Run (or join) the single in-flight fetch, updating memory and disk on success. */
|
|
322
365
|
refresh(fetcher) {
|
|
323
|
-
this.inflight
|
|
366
|
+
if (this.inflight !== undefined)
|
|
367
|
+
return this.inflight;
|
|
368
|
+
const gen = this.generation;
|
|
369
|
+
const pending = fetcher()
|
|
324
370
|
.then((models) => {
|
|
371
|
+
if (this.generation !== gen)
|
|
372
|
+
return models;
|
|
325
373
|
const snapshot = { at: Date.now(), models };
|
|
326
374
|
this.entry = snapshot;
|
|
327
375
|
// Write-through is fire-and-forget: a failed save only costs durability.
|
|
328
376
|
void this.persistence?.save(snapshot).catch(() => undefined);
|
|
329
377
|
return models;
|
|
330
378
|
})
|
|
331
|
-
.finally(() => {
|
|
332
|
-
|
|
379
|
+
.finally(() => {
|
|
380
|
+
if (this.generation === gen)
|
|
381
|
+
this.inflight = undefined;
|
|
382
|
+
});
|
|
383
|
+
this.inflight = pending;
|
|
384
|
+
return pending;
|
|
333
385
|
}
|
|
334
386
|
/**
|
|
335
387
|
* Return the cached catalog when fresh, otherwise fetch and cache it.
|
|
@@ -370,7 +422,9 @@ export class ModelCatalogCache {
|
|
|
370
422
|
}
|
|
371
423
|
/** Drop the cached catalog (e.g. after a 401 proved the credential changed). */
|
|
372
424
|
invalidate() {
|
|
425
|
+
this.generation += 1;
|
|
373
426
|
this.entry = undefined;
|
|
427
|
+
this.inflight = undefined;
|
|
374
428
|
this.seedDisabled = true;
|
|
375
429
|
void this.persistence?.clear().catch(() => undefined);
|
|
376
430
|
}
|
|
@@ -380,6 +434,16 @@ export function isMissingOrInvalidCredential(error) {
|
|
|
380
434
|
return error instanceof LlmError
|
|
381
435
|
&& (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL');
|
|
382
436
|
}
|
|
437
|
+
/** Whether discovery stopped because the caller cancelled or the timeout fired. */
|
|
438
|
+
export function isDiscoveryAborted(error, signal) {
|
|
439
|
+
if (signal?.aborted === true)
|
|
440
|
+
return true;
|
|
441
|
+
// Only treat abort-shaped errors as cancellation when this call had a signal;
|
|
442
|
+
// a refresh TimeoutError must not fail the whole picker union.
|
|
443
|
+
return signal !== undefined
|
|
444
|
+
&& error instanceof Error
|
|
445
|
+
&& (error.name === 'AbortError' || error.name === 'TimeoutError');
|
|
446
|
+
}
|
|
383
447
|
/** Whether discovery failed because the access token was rejected. */
|
|
384
448
|
function isDiscoveryAuthFailure(error) {
|
|
385
449
|
return (error instanceof OAuthEndpointError && error.status === 401)
|
|
@@ -17,9 +17,10 @@ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
|
17
17
|
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
18
18
|
import type { DeviceFlowSpec } from '../auth/device-flow.js';
|
|
19
19
|
import type { CopilotSession } from '../auth/store.js';
|
|
20
|
+
import type { PoolAdapter } from './pool.js';
|
|
20
21
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
21
22
|
import type { ReasoningReplayItem, ResponsesRequestInput, ResponsesStreamEvent } from '../translate/responses.js';
|
|
22
|
-
import {
|
|
23
|
+
import { AccountTokenManager } from './accounts.js';
|
|
23
24
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry } from './common.js';
|
|
24
25
|
/**
|
|
25
26
|
* Client id of the VS Code Copilot Chat GitHub App (pi-mono and
|
|
@@ -118,9 +119,10 @@ export declare function isCopilotPermanentRefreshError(error: unknown): boolean;
|
|
|
118
119
|
* reasoning efforts (the endpoint discloses no default, so none is claimed).
|
|
119
120
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
120
121
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
122
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
121
123
|
* @returns discovered chat models in endpoint order.
|
|
122
124
|
*/
|
|
123
|
-
export declare function fetchCopilotModels(session: CopilotSession, fetchFn?: FetchFn): Promise<DiscoveredModel[]>;
|
|
125
|
+
export declare function fetchCopilotModels(session: CopilotSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<DiscoveredModel[]>;
|
|
124
126
|
/** Which upstream protocol one Copilot model speaks. */
|
|
125
127
|
export type CopilotWire = 'chat-completions' | 'responses';
|
|
126
128
|
/**
|
|
@@ -215,7 +217,9 @@ export declare class CopilotResponsesItemNormalizer {
|
|
|
215
217
|
export interface CopilotAdapterOptions {
|
|
216
218
|
models: readonly ModelEntry[];
|
|
217
219
|
streamIdleTimeoutMs: number;
|
|
218
|
-
tokens:
|
|
220
|
+
tokens: AccountTokenManager<CopilotSession>;
|
|
221
|
+
/** Late-bound pool facade (wired after adapter construction); pools list under their first member's provider. */
|
|
222
|
+
pool?: () => PoolAdapter | undefined;
|
|
219
223
|
/** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
|
|
220
224
|
discovery: boolean;
|
|
221
225
|
/** Warning sink for discovery failures that fall back to the static catalog. */
|
|
@@ -231,6 +235,10 @@ export interface CopilotAdapterOptions {
|
|
|
231
235
|
export declare class CopilotAdapter extends LlmAdapter {
|
|
232
236
|
private readonly options;
|
|
233
237
|
private readonly catalog;
|
|
238
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
239
|
+
private readonly accountCatalogs;
|
|
240
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
241
|
+
private catalogOwner;
|
|
234
242
|
/**
|
|
235
243
|
* [2026-08-23]-[a reasoning model continuing a tool chain must get its
|
|
236
244
|
* reasoning back or it restarts from scratch every tool round trip; the
|
|
@@ -250,9 +258,15 @@ export declare class CopilotAdapter extends LlmAdapter {
|
|
|
250
258
|
constructor(options: CopilotAdapterOptions);
|
|
251
259
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
252
260
|
private fetchCatalog;
|
|
261
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
262
|
+
clearAccountCatalog(account?: string): void;
|
|
263
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
264
|
+
private catalogFor;
|
|
253
265
|
providerInfo(provider: string): LlmProviderInfo;
|
|
254
266
|
private staticModels;
|
|
255
267
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
268
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
269
|
+
listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
|
|
256
270
|
/**
|
|
257
271
|
* The discovered entry for one model. Resolved through the cache's
|
|
258
272
|
* stale-while-revalidate path: capability metadata must stay stable across
|
|
@@ -309,7 +323,12 @@ export declare class CopilotAdapter extends LlmAdapter {
|
|
|
309
323
|
*/
|
|
310
324
|
clearReplayState(): void;
|
|
311
325
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
326
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
327
|
+
resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
312
328
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
329
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
330
|
+
streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
|
|
331
|
+
private streamCore;
|
|
313
332
|
private request;
|
|
314
333
|
}
|
|
315
334
|
export {};
|
package/lib/providers/copilot.js
CHANGED
|
@@ -17,7 +17,8 @@ import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortI
|
|
|
17
17
|
import { resolveImages } from '../translate/resolved.js';
|
|
18
18
|
import { streamChatCompletions, toChatMessages, toChatTools, } from '../translate/chat-completions.js';
|
|
19
19
|
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
20
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError,
|
|
20
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
21
|
+
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
21
22
|
import { proxiedFetch } from '../http.js';
|
|
22
23
|
/**
|
|
23
24
|
* Client id of the VS Code Copilot Chat GitHub App (pi-mono and
|
|
@@ -243,15 +244,17 @@ function copilotReasoning(entry) {
|
|
|
243
244
|
* reasoning efforts (the endpoint discloses no default, so none is claimed).
|
|
244
245
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
245
246
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
247
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
246
248
|
* @returns discovered chat models in endpoint order.
|
|
247
249
|
*/
|
|
248
|
-
export async function fetchCopilotModels(session, fetchFn = proxiedFetch) {
|
|
250
|
+
export async function fetchCopilotModels(session, fetchFn = proxiedFetch, signal) {
|
|
249
251
|
const response = await fetchFn(COPILOT_MODELS_URL, {
|
|
250
252
|
headers: {
|
|
251
253
|
'authorization': `Bearer ${session.accessToken}`,
|
|
252
254
|
'accept': 'application/json',
|
|
253
255
|
...copilotHeaders(false, await latestVsCodeVersion(fetchFn)),
|
|
254
256
|
},
|
|
257
|
+
...signal === undefined ? {} : { signal },
|
|
255
258
|
});
|
|
256
259
|
if (!response.ok)
|
|
257
260
|
throw await oauthEndpointError(response, 'copilot models');
|
|
@@ -510,6 +513,10 @@ export class CopilotResponsesItemNormalizer {
|
|
|
510
513
|
export class CopilotAdapter extends LlmAdapter {
|
|
511
514
|
options;
|
|
512
515
|
catalog;
|
|
516
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
517
|
+
accountCatalogs = new Map();
|
|
518
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
519
|
+
catalogOwner;
|
|
513
520
|
/**
|
|
514
521
|
* [2026-08-23]-[a reasoning model continuing a tool chain must get its
|
|
515
522
|
* reasoning back or it restarts from scratch every tool round trip; the
|
|
@@ -532,8 +539,37 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
532
539
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
533
540
|
}
|
|
534
541
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
535
|
-
async fetchCatalog() {
|
|
536
|
-
return fetchCopilotModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
542
|
+
async fetchCatalog(account, signal) {
|
|
543
|
+
return fetchCopilotModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
544
|
+
}
|
|
545
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
546
|
+
clearAccountCatalog(account) {
|
|
547
|
+
if (account === undefined)
|
|
548
|
+
this.accountCatalogs.clear();
|
|
549
|
+
else
|
|
550
|
+
this.accountCatalogs.delete(account);
|
|
551
|
+
if (account === undefined || this.catalogOwner === account || this.catalogOwner === undefined) {
|
|
552
|
+
this.catalogOwner = undefined;
|
|
553
|
+
this.catalog.invalidate();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
557
|
+
async catalogFor(account) {
|
|
558
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
559
|
+
const key = account ?? defaultKey;
|
|
560
|
+
if (key === undefined || key === defaultKey) {
|
|
561
|
+
if (this.catalogOwner !== undefined && this.catalogOwner !== defaultKey) {
|
|
562
|
+
this.catalog.invalidate();
|
|
563
|
+
}
|
|
564
|
+
this.catalogOwner = defaultKey;
|
|
565
|
+
return this.catalog;
|
|
566
|
+
}
|
|
567
|
+
let cache = this.accountCatalogs.get(key);
|
|
568
|
+
if (cache === undefined) {
|
|
569
|
+
cache = new ModelCatalogCache();
|
|
570
|
+
this.accountCatalogs.set(key, cache);
|
|
571
|
+
}
|
|
572
|
+
return cache;
|
|
537
573
|
}
|
|
538
574
|
providerInfo(provider) {
|
|
539
575
|
return { id: provider, name: 'GitHub Copilot' };
|
|
@@ -547,17 +583,34 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
547
583
|
}));
|
|
548
584
|
}
|
|
549
585
|
async listModels(provider) {
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
if (
|
|
586
|
+
const own = await this.listOwnModels(provider);
|
|
587
|
+
const pool = this.options.pool?.();
|
|
588
|
+
if (pool === undefined)
|
|
589
|
+
return own;
|
|
590
|
+
const extra = await pool.modelsForProvider(provider);
|
|
591
|
+
const seen = new Set(own.map(model => model.id));
|
|
592
|
+
// Account pools reuse the catalog row; only configured tiers are extra.
|
|
593
|
+
return [...own, ...extra.filter(model => !seen.has(model.id))];
|
|
594
|
+
}
|
|
595
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
596
|
+
async listOwnModels(provider, account, signal) {
|
|
597
|
+
if (account === undefined) {
|
|
598
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
599
|
+
if (accounts.length === 0)
|
|
600
|
+
return [];
|
|
601
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), { timeoutMs: DISCOVERY_TIMEOUT_MS, ...signal === undefined ? {} : { signal } });
|
|
602
|
+
}
|
|
603
|
+
if (!await this.options.tokens.hasSession(account)) {
|
|
553
604
|
return [];
|
|
605
|
+
}
|
|
554
606
|
if (!this.options.discovery)
|
|
555
607
|
return this.staticModels(provider);
|
|
608
|
+
const catalog = await this.catalogFor(account);
|
|
556
609
|
try {
|
|
557
610
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
558
611
|
// through the refresh-aware path so an expired access token renews here
|
|
559
612
|
// instead of failing discovery into the static fallback.
|
|
560
|
-
const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(force),
|
|
613
|
+
const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)));
|
|
561
614
|
return discovered.map(model => ({
|
|
562
615
|
provider,
|
|
563
616
|
id: model.id,
|
|
@@ -567,6 +620,8 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
567
620
|
}));
|
|
568
621
|
}
|
|
569
622
|
catch (error) {
|
|
623
|
+
if (isDiscoveryAborted(error, signal))
|
|
624
|
+
throw error;
|
|
570
625
|
// A permanent refresh failure deletes the stored session: the provider
|
|
571
626
|
// is logged out, so hide it instead of showing a stale static catalog.
|
|
572
627
|
if (isMissingOrInvalidCredential(error))
|
|
@@ -584,8 +639,12 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
584
639
|
async discovered(model) {
|
|
585
640
|
if (!this.options.discovery)
|
|
586
641
|
return undefined;
|
|
587
|
-
const
|
|
588
|
-
return
|
|
642
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
643
|
+
return discoverAcrossAccounts(accounts, async (account) => {
|
|
644
|
+
const catalog = await this.catalogFor(account);
|
|
645
|
+
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
646
|
+
return models?.find(entry => entry.id === model);
|
|
647
|
+
});
|
|
589
648
|
}
|
|
590
649
|
/**
|
|
591
650
|
* [2026-08-23]-[a manually configured responses-only model combined with
|
|
@@ -699,6 +758,14 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
699
758
|
this.replayByScope.clear();
|
|
700
759
|
}
|
|
701
760
|
async resolveModel(provider, model) {
|
|
761
|
+
const pool = this.options.pool?.();
|
|
762
|
+
if (pool !== undefined && await pool.owns(provider, model)) {
|
|
763
|
+
return pool.resolveModel(provider, model);
|
|
764
|
+
}
|
|
765
|
+
return this.resolveOwnModel(provider, model);
|
|
766
|
+
}
|
|
767
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
768
|
+
async resolveOwnModel(provider, model) {
|
|
702
769
|
const discovered = await this.discovered(model);
|
|
703
770
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
704
771
|
return {
|
|
@@ -717,6 +784,18 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
717
784
|
};
|
|
718
785
|
}
|
|
719
786
|
async *stream(options) {
|
|
787
|
+
const pool = this.options.pool?.();
|
|
788
|
+
if (pool !== undefined && await pool.owns(options.provider, options.model)) {
|
|
789
|
+
yield* pool.stream(options);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
yield* this.streamCore(options);
|
|
793
|
+
}
|
|
794
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
795
|
+
streamAccount(options, account) {
|
|
796
|
+
return this.streamCore(options, account);
|
|
797
|
+
}
|
|
798
|
+
async *streamCore(options, account) {
|
|
720
799
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
721
800
|
try {
|
|
722
801
|
// The discovered catalog decides the protocol: `/responses`-only model
|
|
@@ -725,7 +804,7 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
725
804
|
// tools with a reasoning effort (gpt-5.4 400s on the chat wire then).
|
|
726
805
|
// A configured `wire` outranks the catalog (see configuredWireEntry).
|
|
727
806
|
const wire = copilotRequestWire(this.configuredWireEntry(options.model) ?? await this.discovered(options.model), options);
|
|
728
|
-
let session = await this.options.tokens.session();
|
|
807
|
+
let session = await this.options.tokens.session(account);
|
|
729
808
|
// Replay scope: account identity × conversation × model (see
|
|
730
809
|
// replayScope); a Copilot-token refresh preserves the GitHub token, so
|
|
731
810
|
// the 401 retry below reuses it too.
|
|
@@ -737,7 +816,7 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
737
816
|
// means GitHub raised its minimum VS Code version, and only a fresh
|
|
738
817
|
// Editor-Version header fixes that (a new token does not).
|
|
739
818
|
await latestVsCodeVersion(this.options.fetchFn ?? proxiedFetch, true);
|
|
740
|
-
session = await this.options.tokens.session(true);
|
|
819
|
+
session = await this.options.tokens.session(account, true);
|
|
741
820
|
response = await this.request(options, session, watchdog.signal, wire, scope);
|
|
742
821
|
}
|
|
743
822
|
if (!response.ok)
|