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.
Files changed (50) hide show
  1. package/README.md +42 -1
  2. package/README.zh.md +42 -1
  3. package/lib/auth/device-flow.d.ts +0 -9
  4. package/lib/auth/device-flow.js +2 -1
  5. package/lib/auth/rpc.d.ts +44 -13
  6. package/lib/auth/rpc.js +127 -9
  7. package/lib/auth/store.d.ts +75 -17
  8. package/lib/auth/store.js +148 -27
  9. package/lib/client/SubscriptionsSection.d.ts +26 -3
  10. package/lib/client/SubscriptionsSection.js +263 -67
  11. package/lib/client/index.js +11 -0
  12. package/lib/client/locales.d.ts +82 -10
  13. package/lib/client/locales.js +82 -10
  14. package/lib/client.js +837 -223
  15. package/lib/client.js.map +1 -1
  16. package/lib/http.d.ts +114 -0
  17. package/lib/http.js +402 -0
  18. package/lib/index.d.ts +21 -0
  19. package/lib/index.js +1938 -208
  20. package/lib/providers/accounts.d.ts +102 -0
  21. package/lib/providers/accounts.js +123 -0
  22. package/lib/providers/antigravity.d.ts +90 -0
  23. package/lib/providers/antigravity.js +392 -0
  24. package/lib/providers/claude.d.ts +22 -4
  25. package/lib/providers/claude.js +97 -16
  26. package/lib/providers/codex.d.ts +24 -3
  27. package/lib/providers/codex.js +121 -21
  28. package/lib/providers/common.d.ts +17 -0
  29. package/lib/providers/common.js +67 -3
  30. package/lib/providers/copilot.d.ts +23 -4
  31. package/lib/providers/copilot.js +99 -19
  32. package/lib/providers/grok.d.ts +24 -4
  33. package/lib/providers/grok.js +106 -19
  34. package/lib/providers/pool-family.d.ts +56 -0
  35. package/lib/providers/pool-family.js +45 -0
  36. package/lib/providers/pool-health.d.ts +74 -0
  37. package/lib/providers/pool-health.js +148 -0
  38. package/lib/providers/pool-usage.d.ts +57 -0
  39. package/lib/providers/pool-usage.js +130 -0
  40. package/lib/providers/pool.d.ts +107 -0
  41. package/lib/providers/pool.js +371 -0
  42. package/lib/tools/image-generate.d.ts +3 -3
  43. package/lib/tools/image-generate.js +4 -2
  44. package/lib/tools/video-generate.d.ts +2 -2
  45. package/lib/tools/video-generate.js +4 -2
  46. package/lib/tools/x-search.d.ts +2 -2
  47. package/lib/tools/x-search.js +4 -2
  48. package/lib/translate/antigravity.d.ts +110 -0
  49. package/lib/translate/antigravity.js +303 -0
  50. package/package.json +14 -9
@@ -7,9 +7,10 @@ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
7
7
  import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
8
8
  import type { FlowSpec } from '../auth/oauth-flow.js';
9
9
  import type { CodexSession } from '../auth/store.js';
10
+ import type { PoolAdapter } from './pool.js';
10
11
  import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
11
12
  import type { ResponsesRequestInput } from '../translate/responses.js';
12
- import { TokenManager } from './common.js';
13
+ import { AccountTokenManager } from './accounts.js';
13
14
  import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
14
15
  export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
15
16
  export declare const CODEX_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
@@ -90,14 +91,17 @@ export declare const CODEX_CLIENT_VERSION = "0.147.0";
90
91
  * Fetch the live codex model catalog with the session's auth headers.
91
92
  * @param session - the stored session (used as-is; never refreshed here).
92
93
  * @param fetchFn - fetch implementation (injectable for tests).
94
+ * @param signal - caller cancellation (pool-assembly timeout).
93
95
  * @returns discovered models: hidden entries dropped, sorted by priority.
94
96
  */
95
- export declare function fetchCodexModels(session: CodexSession, fetchFn?: FetchFn): Promise<DiscoveredModel[]>;
97
+ export declare function fetchCodexModels(session: CodexSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<DiscoveredModel[]>;
96
98
  /** Constructor dependencies for {@link CodexAdapter}. */
97
99
  export interface CodexAdapterOptions {
98
100
  models: readonly ModelEntry[];
99
101
  streamIdleTimeoutMs: number;
100
- tokens: TokenManager<CodexSession>;
102
+ tokens: AccountTokenManager<CodexSession>;
103
+ /** Late-bound pool facade (wired after adapter construction); pools list under their first member's provider. */
104
+ pool?: () => PoolAdapter | undefined;
101
105
  /** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
102
106
  discovery: boolean;
103
107
  /** Warning sink for discovery failures that fall back to the static catalog. */
@@ -108,6 +112,8 @@ export interface CodexAdapterOptions {
108
112
  resolveAttachments?: () => AttachmentStore | undefined;
109
113
  /** Durable catalog store seeding capability metadata across restarts. */
110
114
  catalogStore?: CatalogPersistence;
115
+ /** Per-account catalog bound for the picker union (defaults to {@link DISCOVERY_TIMEOUT_MS}). */
116
+ discoveryTimeoutMs?: number;
111
117
  /**
112
118
  * Per-request speed lookup (the composer Speed toggle's host half). Returns
113
119
  * whether this session's current choice sends the model on the fast tier;
@@ -126,12 +132,22 @@ export declare function codexRequestBody(options: GenerateOptions, resolved: Res
126
132
  export declare class CodexAdapter extends LlmAdapter {
127
133
  private readonly options;
128
134
  private readonly catalog;
135
+ /** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
136
+ private readonly accountCatalogs;
137
+ /** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
138
+ private catalogOwner;
129
139
  constructor(options: CodexAdapterOptions);
130
140
  /** Discovery fetcher: resolves the session through the refresh-aware path. */
131
141
  private fetchCatalog;
142
+ /** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
143
+ clearAccountCatalog(account?: string): void;
144
+ /** Persisted cache for the default account; a throwaway cache for any other. */
145
+ private catalogFor;
132
146
  providerInfo(provider: string): LlmProviderInfo;
133
147
  private staticModels;
134
148
  listModels(provider: string): Promise<readonly LlmModelInfo[]>;
149
+ /** The provider's own catalog: union of every account, or one account when named. */
150
+ listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
135
151
  /**
136
152
  * The discovered entry for one model. Resolved through the cache's
137
153
  * stale-while-revalidate path so capability metadata stays stable across a
@@ -145,6 +161,11 @@ export declare class CodexAdapter extends LlmAdapter {
145
161
  /** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
146
162
  fastCapableModels(): Promise<string[]>;
147
163
  resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
164
+ /** Capability resolution of the provider's own models (the pool resolves members here). */
165
+ resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
148
166
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
167
+ /** Pool seam: stream through one specific account instead of the default. */
168
+ streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
169
+ private streamCore;
149
170
  private request;
150
171
  }
@@ -8,7 +8,9 @@ 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, TokenManager, } from './common.js';
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';
13
+ import { proxiedFetch } from '../http.js';
12
14
  export const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
13
15
  export const CODEX_AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
14
16
  export const CODEX_TOKEN_URL = 'https://auth.openai.com/oauth/token';
@@ -152,7 +154,7 @@ function codexSession(tokens, fallback) {
152
154
  * @returns the session to store.
153
155
  */
154
156
  export async function exchangeCodexCode(code, verifier, redirectUri) {
155
- const response = await fetch(CODEX_TOKEN_URL, {
157
+ const response = await proxiedFetch(CODEX_TOKEN_URL, {
156
158
  method: 'POST',
157
159
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
158
160
  body: new URLSearchParams({
@@ -173,7 +175,7 @@ export async function exchangeCodexCode(code, verifier, redirectUri) {
173
175
  * @returns the fresh session to store.
174
176
  */
175
177
  export async function refreshCodex(session) {
176
- const response = await fetch(CODEX_TOKEN_URL, {
178
+ const response = await proxiedFetch(CODEX_TOKEN_URL, {
177
179
  method: 'POST',
178
180
  headers: { 'content-type': 'application/json' },
179
181
  body: JSON.stringify({
@@ -253,7 +255,7 @@ function codexUsageWindow(value, fallbackKind) {
253
255
  * @param signal - caller cancellation from the RPC transport.
254
256
  * @returns the mapped usage snapshot.
255
257
  */
256
- export async function fetchCodexUsage(session, fetchFn = fetch, signal) {
258
+ export async function fetchCodexUsage(session, fetchFn = proxiedFetch, signal) {
257
259
  const response = await fetchFn(CODEX_USAGE_URL, {
258
260
  headers: {
259
261
  'authorization': `Bearer ${session.accessToken}`,
@@ -307,9 +309,10 @@ function supportsFastTier(entry) {
307
309
  * Fetch the live codex model catalog with the session's auth headers.
308
310
  * @param session - the stored session (used as-is; never refreshed here).
309
311
  * @param fetchFn - fetch implementation (injectable for tests).
312
+ * @param signal - caller cancellation (pool-assembly timeout).
310
313
  * @returns discovered models: hidden entries dropped, sorted by priority.
311
314
  */
312
- export async function fetchCodexModels(session, fetchFn = fetch) {
315
+ export async function fetchCodexModels(session, fetchFn = proxiedFetch, signal) {
313
316
  const url = `${CODEX_MODELS_URL}?client_version=${CODEX_CLIENT_VERSION}`;
314
317
  const response = await fetchFn(url, {
315
318
  headers: {
@@ -319,6 +322,7 @@ export async function fetchCodexModels(session, fetchFn = fetch) {
319
322
  'accept': 'application/json',
320
323
  ...attributionHeaders(),
321
324
  },
325
+ ...signal === undefined ? {} : { signal },
322
326
  });
323
327
  if (!response.ok)
324
328
  throw await oauthEndpointError(response, 'codex models');
@@ -450,14 +454,47 @@ export function codexRequestBody(options, resolved, fast) {
450
454
  export class CodexAdapter extends LlmAdapter {
451
455
  options;
452
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;
453
461
  constructor(options) {
454
462
  super();
455
463
  this.options = options;
456
464
  this.catalog = new ModelCatalogCache(options.catalogStore);
457
465
  }
458
466
  /** Discovery fetcher: resolves the session through the refresh-aware path. */
459
- async fetchCatalog() {
460
- 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;
461
498
  }
462
499
  providerInfo(provider) {
463
500
  return { id: provider, name: 'ChatGPT (Codex)' };
@@ -471,26 +508,48 @@ export class CodexAdapter extends LlmAdapter {
471
508
  }));
472
509
  }
473
510
  async listModels(provider) {
474
- // Not logged in → empty catalog, so the web picker drops the provider.
475
- const session = await this.options.tokens.peek();
476
- if (session === undefined)
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)) {
477
529
  return [];
530
+ }
478
531
  if (!this.options.discovery)
479
532
  return this.staticModels(provider);
533
+ const catalog = await this.catalogFor(account);
480
534
  try {
481
535
  // The fetcher runs only on a cache miss, and resolves the session
482
536
  // through the refresh-aware path so an expired access token renews here
483
537
  // instead of failing discovery into the static fallback.
484
- const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(force), this.catalog, () => this.catalog.get(() => this.fetchCatalog()));
538
+ const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)));
485
539
  return discovered.map(model => ({
486
540
  provider,
487
541
  id: model.id,
488
542
  name: model.name,
489
543
  ...model.description === undefined ? {} : { description: model.description },
490
544
  inputModalities: CODEX_MODALITIES,
545
+ ...model.priority === undefined ? {} : { priority: model.priority },
491
546
  }));
492
547
  }
493
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;
494
553
  // A permanent refresh failure deletes the stored session: the provider
495
554
  // is logged out, so hide it instead of showing a stale static catalog.
496
555
  if (isMissingOrInvalidCredential(error))
@@ -509,8 +568,12 @@ export class CodexAdapter extends LlmAdapter {
509
568
  async discovered(model) {
510
569
  if (!this.options.discovery)
511
570
  return undefined;
512
- const models = await this.catalog.resolve(() => this.fetchCatalog());
513
- return models?.find(entry => entry.id === model);
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
+ });
514
577
  }
515
578
  /** Whether the discovered catalog advertises a fast tier for this model. */
516
579
  async supportsFastTier(model) {
@@ -521,14 +584,39 @@ export class CodexAdapter extends LlmAdapter {
521
584
  if (!this.options.discovery)
522
585
  return [];
523
586
  // Not logged in → no fast models, so the Speed toggle hides after logout
524
- // (mirrors the listModels guard above).
525
- const session = await this.options.tokens.peek();
526
- if (session === undefined)
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)
527
591
  return [];
528
- const models = await this.catalog.resolve(() => this.fetchCatalog());
529
- return (models ?? []).filter(model => model.fastTier === true).map(model => model.id);
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;
530
610
  }
531
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) {
532
620
  // Discovered metadata (when discovery is on) wins over the static entry;
533
621
  // the static entry wins over the built-in defaults.
534
622
  const discovered = await this.discovered(model);
@@ -545,13 +633,25 @@ export class CodexAdapter extends LlmAdapter {
545
633
  };
546
634
  }
547
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) {
548
648
  const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
549
649
  try {
550
- let session = await this.options.tokens.session();
650
+ let session = await this.options.tokens.session(account);
551
651
  let response = await this.request(options, session, watchdog.signal);
552
652
  if (response.status === 401) {
553
653
  // One forced refresh + retry on an unexpired-but-rejected token.
554
- session = await this.options.tokens.session(true);
654
+ session = await this.options.tokens.session(account, true);
555
655
  response = await this.request(options, session, watchdog.signal);
556
656
  }
557
657
  if (!response.ok)
@@ -573,7 +673,7 @@ export class CodexAdapter extends LlmAdapter {
573
673
  const fast = this.options.speedFor !== undefined
574
674
  && await this.options.speedFor(options.sessionId, options.model);
575
675
  const body = codexRequestBody(options, toResponsesInput(messages, options.system), fast);
576
- return fetch(CODEX_API_URL, {
676
+ return proxiedFetch(CODEX_API_URL, {
577
677
  method: 'POST',
578
678
  headers: {
579
679
  'authorization': `Bearer ${session.accessToken}`,
@@ -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
@@ -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 ??= fetcher()
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(() => { this.inflight = undefined; });
332
- return this.inflight;
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 { TokenManager } from './common.js';
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,12 +217,14 @@ export declare class CopilotResponsesItemNormalizer {
215
217
  export interface CopilotAdapterOptions {
216
218
  models: readonly ModelEntry[];
217
219
  streamIdleTimeoutMs: number;
218
- tokens: TokenManager<CopilotSession>;
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. */
222
226
  onWarn?: (message: string) => void;
223
- /** Fetch implementation for discovery (defaults to global fetch). */
227
+ /** Fetch implementation for discovery (defaults to the proxy-aware fetch). */
224
228
  fetchFn?: FetchFn;
225
229
  /** Resolve the attachment service per request; absent means image requests fail loudly. */
226
230
  resolveAttachments?: () => AttachmentStore | undefined;
@@ -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 {};