dsh-plugin-subscription-oauth 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.dev.md +1 -1
  2. package/README.md +7 -6
  3. package/README.zh.dev.md +1 -1
  4. package/README.zh.md +7 -6
  5. package/lib/auth/workbuddy-flow.d.ts +1 -1
  6. package/lib/auth/workbuddy-flow.js +14 -9
  7. package/lib/client/index.js +10 -1
  8. package/lib/client.js +6 -1
  9. package/lib/client.js.map +1 -1
  10. package/lib/index.js +21 -0
  11. package/lib/providers/antigravity.d.ts +34 -4
  12. package/lib/providers/antigravity.js +100 -28
  13. package/lib/providers/ark-provider.d.ts +10 -0
  14. package/lib/providers/ark-provider.js +6 -0
  15. package/lib/providers/ark.d.ts +96 -10
  16. package/lib/providers/ark.js +176 -48
  17. package/lib/providers/catalog.d.ts +91 -0
  18. package/lib/providers/catalog.js +190 -0
  19. package/lib/providers/common.d.ts +14 -0
  20. package/lib/providers/common.js +23 -0
  21. package/lib/providers/glm-provider.d.ts +10 -0
  22. package/lib/providers/glm-provider.js +6 -0
  23. package/lib/providers/glm.d.ts +92 -16
  24. package/lib/providers/glm.js +172 -85
  25. package/lib/providers/grok.d.ts +106 -27
  26. package/lib/providers/grok.js +312 -128
  27. package/lib/providers/kimi-provider.d.ts +10 -0
  28. package/lib/providers/kimi-provider.js +8 -3
  29. package/lib/providers/kimi.d.ts +104 -32
  30. package/lib/providers/kimi.js +198 -129
  31. package/lib/providers/mimo-provider.d.ts +14 -1
  32. package/lib/providers/mimo-provider.js +17 -6
  33. package/lib/providers/mimo.d.ts +164 -19
  34. package/lib/providers/mimo.js +318 -19
  35. package/lib/providers/model-cache.d.ts +57 -0
  36. package/lib/providers/model-cache.js +105 -0
  37. package/lib/providers/responses-stream.d.ts +8 -0
  38. package/lib/providers/responses-stream.js +21 -5
  39. package/lib/providers/workbuddy-provider.d.ts +17 -0
  40. package/lib/providers/workbuddy-provider.js +14 -6
  41. package/lib/providers/workbuddy.d.ts +140 -13
  42. package/lib/providers/workbuddy.js +520 -55
  43. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -25,6 +25,7 @@ import { createArkProvider } from './providers/ark-provider.js';
25
25
  import { createMimoProvider } from './providers/mimo-provider.js';
26
26
  import { createWorkBuddyProvider } from './providers/workbuddy-provider.js';
27
27
  import { ModelPreferences } from './settings.js';
28
+ import { ModelCatalogCache } from './providers/model-cache.js';
28
29
  /** Plugin id; also the name every effect label and log line is prefixed with. */
29
30
  export const name = 'dsh-plugin-subscription-oauth';
30
31
  /** The provider routes need the LLM runtime; everything else is optional. */
@@ -68,6 +69,10 @@ export function apply(ctx, config = {}) {
68
69
  const dataDir = dshHomePath(...KIMI_DATA_DIR);
69
70
  const credentialFile = path.join(dataDir, 'auth.json');
70
71
  const preferences = new ModelPreferences(path.join(dataDir, 'preferences.json'));
72
+ // The endpoint's own last answer per route. It is written whenever a route reads its
73
+ // live listing — at startup, after a login, and on the page's "refresh models" action
74
+ // — and read as the route's model table, so a picker query is never a request.
75
+ const catalogCache = new ModelCatalogCache(path.join(dataDir, 'model-catalog.json'));
71
76
  /** Registration handles, so a catalog change can re-announce one route. */
72
77
  const handles = new Map();
73
78
  const reannounce = (id) => { handles.get(id)?.replace([id]); };
@@ -84,6 +89,8 @@ export function apply(ctx, config = {}) {
84
89
  streamIdleTimeoutMs,
85
90
  ...config.defaultModel === undefined ? {} : { defaultModel: config.defaultModel },
86
91
  ...config.extraModels === undefined ? {} : { extraModels: config.extraModels },
92
+ cachedListing: () => catalogCache.read('kimi'),
93
+ saveListing: (rows) => catalogCache.write('kimi', rows),
87
94
  preferences,
88
95
  // Resolved per request: the attachment service may start after this plugin.
89
96
  resolveAttachments: () => ctx.get('attachments'),
@@ -98,6 +105,8 @@ export function apply(ctx, config = {}) {
98
105
  discovery: config.discovery ?? true,
99
106
  streamIdleTimeoutMs,
100
107
  preferences,
108
+ cachedListing: () => catalogCache.read('grok'),
109
+ saveListing: (payload) => catalogCache.write('grok', payload),
101
110
  resolveAttachments: () => ctx.get('attachments'),
102
111
  onCatalogChanged: () => { reannounce('grok'); },
103
112
  onWarn,
@@ -113,6 +122,8 @@ export function apply(ctx, config = {}) {
113
122
  resolveAttachments: () => ctx.get('attachments'),
114
123
  onCatalogChanged: () => { reannounce('antigravity'); },
115
124
  onWarn,
125
+ cachedListing: () => catalogCache.read('antigravity'),
126
+ saveListing: (payload) => catalogCache.write('antigravity', payload),
116
127
  fetchFn: proxiedFetch,
117
128
  }));
118
129
  break;
@@ -122,6 +133,8 @@ export function apply(ctx, config = {}) {
122
133
  discovery: config.discovery ?? true,
123
134
  streamIdleTimeoutMs,
124
135
  preferences,
136
+ cachedListing: () => catalogCache.read('glm'),
137
+ saveListing: (payload) => catalogCache.write('glm', payload),
125
138
  resolveAttachments: () => ctx.get('attachments'),
126
139
  onCatalogChanged: () => { reannounce('glm'); },
127
140
  onWarn,
@@ -134,6 +147,8 @@ export function apply(ctx, config = {}) {
134
147
  discovery: config.discovery ?? true,
135
148
  streamIdleTimeoutMs,
136
149
  preferences,
150
+ cachedListing: () => catalogCache.read('ark'),
151
+ saveListing: (payload) => catalogCache.write('ark', payload),
137
152
  resolveAttachments: () => ctx.get('attachments'),
138
153
  onCatalogChanged: () => { reannounce('ark'); },
139
154
  onWarn,
@@ -143,8 +158,11 @@ export function apply(ctx, config = {}) {
143
158
  case 'mimo':
144
159
  routes.push(createMimoProvider({
145
160
  credentialFile,
161
+ discovery: config.discovery ?? true,
146
162
  streamIdleTimeoutMs,
147
163
  preferences,
164
+ cachedListing: () => catalogCache.read('mimo'),
165
+ saveListing: (payload) => catalogCache.write('mimo', payload),
148
166
  resolveAttachments: () => ctx.get('attachments'),
149
167
  onCatalogChanged: () => { reannounce('mimo'); },
150
168
  onWarn,
@@ -154,8 +172,11 @@ export function apply(ctx, config = {}) {
154
172
  case 'workbuddy':
155
173
  routes.push(createWorkBuddyProvider({
156
174
  credentialFile,
175
+ discovery: config.discovery ?? true,
157
176
  streamIdleTimeoutMs,
158
177
  preferences,
178
+ cachedListing: () => catalogCache.read('workbuddy'),
179
+ saveListing: (payload) => catalogCache.write('workbuddy', payload),
159
180
  onCatalogChanged: () => { reannounce('workbuddy'); },
160
181
  onWarn,
161
182
  fetchFn: proxiedFetch,
@@ -195,6 +195,17 @@ export interface AntigravityAdapterOptions {
195
195
  modelOrder?: () => Promise<readonly string[]>;
196
196
  fetchFn?: FetchFn;
197
197
  onCredentialRejected?: (account: string) => void;
198
+ /**
199
+ * The endpoint's last live listing, from this plugin's cache.
200
+ *
201
+ * Read once per session, before the first catalog query, so the picker offers real
202
+ * models without asking the endpoint anything. A route that fetched once and cached
203
+ * the answer has no reason to fetch again on every refresh; the Settings "refresh
204
+ * models" action is what re-reads it.
205
+ */
206
+ cachedListing?: () => Promise<unknown>;
207
+ /** Record what a live listing just produced, replacing the cached table. */
208
+ saveListing?: (payload: unknown) => Promise<void>;
198
209
  }
199
210
  /** Antigravity wire adapter serving the `antigravity` route. */
200
211
  export declare class AntigravityAdapter extends LlmAdapter {
@@ -203,6 +214,8 @@ export declare class AntigravityAdapter extends LlmAdapter {
203
214
  private discovered;
204
215
  /** Live catalog entries, so the Settings list matches what the account has. */
205
216
  private live;
217
+ /** Whether the cached listing has been read; it is read at most once per session. */
218
+ private hydrated;
206
219
  /** Per-model quota windows from the last catalog read. */
207
220
  private quotas;
208
221
  /** Ids the user added by hand, cached so the sync catalog can name them. */
@@ -213,12 +226,20 @@ export declare class AntigravityAdapter extends LlmAdapter {
213
226
  listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
214
227
  private hidden;
215
228
  /**
216
- * The catalog the route serves: its own entries, then the ids the user added
217
- * by hand. The two are built separately so the hand-added check can look at
218
- * the shipped list without recursing.
229
+ * Read the cached listing, once, so the picker works without asking the endpoint.
230
+ *
231
+ * The cache is this route's last live catalog — the entries the endpoint reported, which
232
+ * is also what a fresh read produces — so adopting it is indistinguishable from having
233
+ * just fetched. A read failure is a cache miss, never a failure.
219
234
  */
235
+ hydrate(): Promise<void>;
236
+ /** Publish one listing as this route's catalog. */
237
+ private adopt;
238
+ /** The catalog the route serves: its own entries, then the ids the user added
239
+ * by hand. The two are built separately so the hand-added check can look at
240
+ * the shipped list without recursing. */
220
241
  private catalog;
221
- /** Live entries when discovery has run, otherwise an empty catalog. */
242
+ /** The live entries: the cached listing until a fresh one replaces it. */
222
243
  private shippedCatalog;
223
244
  resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
224
245
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
@@ -332,6 +353,15 @@ export interface AntigravityProviderOptions {
332
353
  fetchFn?: FetchFn;
333
354
  /** Activate an eligible account when `loadCodeAssist` reports no project. */
334
355
  onboard?: boolean;
356
+ /**
357
+ * The endpoint's last live catalog, from this plugin's cache.
358
+ *
359
+ * This route has no curated list — what an account may use is plan-dependent — so the
360
+ * cache is what makes a cold start (or an unreachable endpoint) still show models.
361
+ */
362
+ cachedListing?: () => Promise<unknown>;
363
+ /** Record what a live listing just produced, replacing the cached table. */
364
+ saveListing?: (payload: unknown) => Promise<void>;
335
365
  }
336
366
  /**
337
367
  * Build the `antigravity` route: its adapter and its login/lifecycle controller.
@@ -329,16 +329,6 @@ export async function fetchAntigravityModels(input) {
329
329
  if (typeof models !== 'object' || models === null) {
330
330
  throw new LlmError('Antigravity models endpoint returned no models object', 'MALFORMED_RESPONSE');
331
331
  }
332
- // TEMP diagnostics: write the raw model-id list to a file so an over-eager filter is
333
- // inspectable after a discovery run, since the node half's stdout is unreachable in
334
- // the desktop shell.
335
- try {
336
- const { writeFileSync } = require('node:fs');
337
- const { homedir } = require('node:os');
338
- const { join } = require('node:path');
339
- writeFileSync(join(homedir(), '.dsh', 'antigravity-raw-models.json'), JSON.stringify(Object.keys(models), null, 2), 'utf8');
340
- }
341
- catch { /* diagnostics never break parsing */ }
342
332
  // The response names far more than the agent may call; the filter keeps the
343
333
  // callable grouping and reports the per-model quota for the fallback path.
344
334
  const entries = parseAntigravityModels(payload);
@@ -365,6 +355,33 @@ function quotaWindow(scope, quota) {
365
355
  ...Number.isFinite(resetsAt) ? { resetsAt } : {},
366
356
  };
367
357
  }
358
+ /** Whether one cached value is a usable catalog entry. */
359
+ function isCachedEntry(value) {
360
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
361
+ return false;
362
+ const record = value;
363
+ return typeof record['id'] === 'string' && record['id'] !== ''
364
+ && typeof record['name'] === 'string'
365
+ && typeof record['contextWindow'] === 'number' && Number.isFinite(record['contextWindow']);
366
+ }
367
+ /**
368
+ * Read a cached catalog payload.
369
+ *
370
+ * The route writes the entries themselves; a whole `{ entries: [...] }` document is
371
+ * accepted too, so a payload written in either shape cannot silently empty the picker.
372
+ * Anything unusable is dropped rather than trusted.
373
+ *
374
+ * @param payload - whatever the cache returned.
375
+ * @returns the usable entries, possibly none.
376
+ */
377
+ function cachedEntries(payload) {
378
+ const rows = Array.isArray(payload)
379
+ ? payload
380
+ : typeof payload === 'object' && payload !== null && Array.isArray(payload.entries)
381
+ ? payload.entries
382
+ : [];
383
+ return rows.filter(isCachedEntry);
384
+ }
368
385
  /** Antigravity wire adapter serving the `antigravity` route. */
369
386
  export class AntigravityAdapter extends LlmAdapter {
370
387
  options;
@@ -372,6 +389,8 @@ export class AntigravityAdapter extends LlmAdapter {
372
389
  discovered;
373
390
  /** Live catalog entries, so the Settings list matches what the account has. */
374
391
  live;
392
+ /** Whether the cached listing has been read; it is read at most once per session. */
393
+ hydrated = false;
375
394
  /** Per-model quota windows from the last catalog read. */
376
395
  quotas = new Map();
377
396
  /** Ids the user added by hand, cached so the sync catalog can name them. */
@@ -392,6 +411,7 @@ export class AntigravityAdapter extends LlmAdapter {
392
411
  // must not appear in the picker, and Auto must not try it.
393
412
  if (await this.options.tokens.session() === undefined)
394
413
  return [];
414
+ await this.hydrate();
395
415
  await this.syncUserModels();
396
416
  const hidden = await this.hidden();
397
417
  return resolveCatalog(this.catalog(), [], hidden).entries.map((entry) => ({
@@ -414,18 +434,44 @@ export class AntigravityAdapter extends LlmAdapter {
414
434
  }
415
435
  }
416
436
  /**
417
- * The catalog the route serves: its own entries, then the ids the user added
418
- * by hand. The two are built separately so the hand-added check can look at
419
- * the shipped list without recursing.
437
+ * Read the cached listing, once, so the picker works without asking the endpoint.
438
+ *
439
+ * The cache is this route's last live catalog — the entries the endpoint reported, which
440
+ * is also what a fresh read produces — so adopting it is indistinguishable from having
441
+ * just fetched. A read failure is a cache miss, never a failure.
420
442
  */
443
+ async hydrate() {
444
+ if (this.hydrated)
445
+ return;
446
+ this.hydrated = true;
447
+ if (this.live !== undefined || this.options.cachedListing === undefined)
448
+ return;
449
+ try {
450
+ const entries = cachedEntries(await this.options.cachedListing());
451
+ if (entries.length > 0)
452
+ this.adopt(entries);
453
+ }
454
+ catch (error) {
455
+ this.options.onWarn?.(`cached model catalog unavailable (${errorChain(error)})`);
456
+ }
457
+ }
458
+ /** Publish one listing as this route's catalog. */
459
+ adopt(entries) {
460
+ this.live = entries;
461
+ this.discovered = entries.map((entry) => entry.id);
462
+ }
463
+ /** The catalog the route serves: its own entries, then the ids the user added
464
+ * by hand. The two are built separately so the hand-added check can look at
465
+ * the shipped list without recursing. */
421
466
  catalog() {
422
467
  return [...this.shippedCatalog(), ...this.addedEntries()];
423
468
  }
424
- /** Live entries when discovery has run, otherwise an empty catalog. */
469
+ /** The live entries: the cached listing until a fresh one replaces it. */
425
470
  shippedCatalog() {
426
471
  return this.live ?? [];
427
472
  }
428
473
  async resolveModel(_provider, model) {
474
+ await this.hydrate();
429
475
  await this.syncUserModels();
430
476
  const entries = this.catalog();
431
477
  const entry = findEntry(entries, model);
@@ -591,7 +637,13 @@ export class AntigravityAdapter extends LlmAdapter {
591
637
  if (Date.now() < session.expiresAt) {
592
638
  return { account, accessToken: session.accessToken, expiresAt: session.expiresAt };
593
639
  }
594
- return await this.options.tokens.resolve(account, true);
640
+ try {
641
+ const resolved = await this.options.tokens.resolve(account, true);
642
+ return resolved;
643
+ }
644
+ catch (error) {
645
+ throw error;
646
+ }
595
647
  }
596
648
  /** Translate an auth failure into the message a user can act on. */
597
649
  refreshHint(error) {
@@ -611,17 +663,23 @@ export class AntigravityAdapter extends LlmAdapter {
611
663
  const envelope = await toAntigravityRequest({ ...options, model }, projectId, options.system, this.imageResolver(signal), true);
612
664
  const fetchFn = this.options.fetchFn ?? proxiedFetch;
613
665
  const origin = this.options.baseUrl ?? ANTIGRAVITY_BASE_URL;
614
- return fetchFn(`${origin}/v1internal:streamGenerateContent?alt=sse`, {
615
- method: 'POST',
616
- headers: attributedHeaders({
617
- 'authorization': `Bearer ${accessToken}`,
618
- 'accept': 'text/event-stream',
619
- 'content-type': 'application/json',
620
- 'user-agent': ANTIGRAVITY_USER_AGENT,
621
- }),
622
- body: JSON.stringify(envelope),
623
- signal,
624
- });
666
+ try {
667
+ const response = await fetchFn(`${origin}/v1internal:streamGenerateContent?alt=sse`, {
668
+ method: 'POST',
669
+ headers: attributedHeaders({
670
+ 'authorization': `Bearer ${accessToken}`,
671
+ 'accept': 'text/event-stream',
672
+ 'content-type': 'application/json',
673
+ 'user-agent': ANTIGRAVITY_USER_AGENT,
674
+ }),
675
+ body: JSON.stringify(envelope),
676
+ signal,
677
+ });
678
+ return response;
679
+ }
680
+ catch (error) {
681
+ throw error;
682
+ }
625
683
  }
626
684
  /** Resolve image blocks through the attachment service, when it is present. */
627
685
  imageResolver(signal) {
@@ -664,8 +722,17 @@ export class AntigravityAdapter extends LlmAdapter {
664
722
  ...signal === undefined ? {} : { signal },
665
723
  });
666
724
  this.quotas = quotas;
667
- this.live = entries;
668
- this.discovered = entries.map((entry) => entry.id);
725
+ this.adopt(entries);
726
+ // The catalog the endpoint just reported is what the next session starts from, so
727
+ // the picker does not have to ask again. A cache failure never fails this read.
728
+ if (this.options.saveListing !== undefined) {
729
+ try {
730
+ await this.options.saveListing(entries);
731
+ }
732
+ catch (error) {
733
+ this.options.onWarn?.(`model catalog cache write failed (${errorChain(error)})`);
734
+ }
735
+ }
669
736
  return this.discovered;
670
737
  }
671
738
  catch (error) {
@@ -911,8 +978,13 @@ export function createAntigravityProvider(options) {
911
978
  hiddenModels: () => options.preferences.hiddenIds(ANTIGRAVITY_PROVIDER_ID),
912
979
  userModels: () => options.preferences.extraIds(ANTIGRAVITY_PROVIDER_ID),
913
980
  modelOrder: () => options.preferences.orderOf(ANTIGRAVITY_PROVIDER_ID),
981
+ ...options.cachedListing === undefined ? {} : { cachedListing: options.cachedListing },
982
+ ...options.saveListing === undefined ? {} : { saveListing: options.saveListing },
914
983
  fetchFn,
915
984
  });
985
+ // Read the cached table now rather than at the first picker query: the Settings page
986
+ // reads the catalog synchronously, so a request-free start must already have it.
987
+ void adapter.hydrate().catch(() => undefined);
916
988
  const spec = {
917
989
  id: ANTIGRAVITY_PROVIDER_ID,
918
990
  displayName: ANTIGRAVITY_PROVIDER_NAME,
@@ -27,6 +27,16 @@ export interface ArkProviderOptions {
27
27
  streamIdleTimeoutMs: number;
28
28
  defaultModel?: string;
29
29
  extraModels?: readonly string[];
30
+ /**
31
+ * The endpoint's last live listing, from this plugin's cache.
32
+ *
33
+ * The cached table is what the picker shows before any request: the endpoint's own
34
+ * answer from the last read, so a release does not need a plugin update and a refresh
35
+ * does not need a request.
36
+ */
37
+ cachedListing?: () => Promise<unknown>;
38
+ /** Record what a live listing just produced, replacing the cached table. */
39
+ saveListing?: (payload: unknown) => Promise<void>;
30
40
  preferences: ModelPreferences;
31
41
  resolveAttachments?: () => AttachmentStore | undefined;
32
42
  onCatalogChanged?: () => void;
@@ -113,8 +113,14 @@ export function createArkProvider(options) {
113
113
  hiddenModels: () => options.preferences.hiddenIds(ARK_PROVIDER_ID),
114
114
  userModels: () => options.preferences.extraIds(ARK_PROVIDER_ID),
115
115
  ...options.extraModels === undefined ? {} : { extraModels: options.extraModels },
116
+ ...options.defaultModel === undefined ? {} : { defaultModel: options.defaultModel },
117
+ ...options.cachedListing === undefined ? {} : { cachedListing: options.cachedListing },
118
+ ...options.saveListing === undefined ? {} : { saveListing: options.saveListing },
116
119
  fetchFn,
117
120
  });
121
+ // Read the cached table now rather than at the first picker query: the Settings page
122
+ // reads the catalog synchronously, so a request-free start must already have it.
123
+ void adapter.hydrate().catch(() => undefined);
118
124
  const spec = {
119
125
  id: ARK_PROVIDER_ID,
120
126
  displayName: ARK_PROVIDER_NAME,
@@ -11,7 +11,10 @@
11
11
  * malformed one is told so (`401 The API key format is incorrect`). So the
12
12
  * credential is a UUID key, sent as a plain bearer — no request signing.
13
13
  * - `/api/coding/v3` is documented by the official CLI as "OpenTOP + CodingPlan
14
- * API", and the catalogue below is the plan's roster.
14
+ * API", and its `/models` listing — read once and kept in this plugin's cache, see
15
+ * `hydrate` and `fetchCatalog` — is the roster this route publishes. Nothing in this
16
+ * module decides which models the plan serves, so one released, renamed or retired
17
+ * server-side reaches the picker without a plugin update.
15
18
  *
16
19
  * @module dsh-plugin-subscription-oauth/providers/ark
17
20
  */
@@ -42,14 +45,12 @@ export declare const ARK_DEFAULT_MODEL = "ark-code-latest";
42
45
  /**
43
46
  * Context capacity advertised for the plan's models.
44
47
  *
45
- * The Coding Plan is a curated, text-first roster and states no window per model;
46
- * discovery refines this where the endpoint reports one.
48
+ * The plan states no window in this build; the endpoint's own listing supplies one
49
+ * where it names a model, and discovery refines this where it does not.
47
50
  */
48
51
  export declare const ARK_CONTEXT_WINDOW = 262144;
49
52
  /** Output budget used when the caller sets no cap. */
50
53
  export declare const ARK_DEFAULT_MAX_TOKENS = 32000;
51
- /** The plan's model roster, as the reference registry records it. */
52
- export declare const ARK_MODELS: readonly CatalogEntry[];
53
54
  /** One signed-in Ark account. */
54
55
  export interface ArkSession extends SessionBase {
55
56
  /**
@@ -89,6 +90,25 @@ export interface ArkAdapterOptions {
89
90
  hiddenModels?: () => Promise<ReadonlySet<string>> | ReadonlySet<string>;
90
91
  userModels?: () => Promise<readonly string[]> | readonly string[];
91
92
  extraModels?: readonly string[];
93
+ /**
94
+ * Model this route preselects, and therefore always offers.
95
+ *
96
+ * Configuration rather than a shipped fact: the plan re-points its alias at a new
97
+ * release on its own schedule, and the picker must be able to preselect it. Defaults
98
+ * to {@link ARK_DEFAULT_MODEL}.
99
+ */
100
+ defaultModel?: string;
101
+ /**
102
+ * The endpoint's last live listing, from this plugin's cache.
103
+ *
104
+ * Read once per session, before the first catalog query, so the picker offers real
105
+ * models without asking the endpoint anything. A route that fetched once and cached
106
+ * the answer has no reason to fetch again on every refresh; the Settings "refresh
107
+ * models" action is what re-reads it.
108
+ */
109
+ cachedListing?: () => Promise<unknown>;
110
+ /** Record what a live listing just produced, replacing the cached table. */
111
+ saveListing?: (payload: unknown) => Promise<void>;
92
112
  resolveAttachments?: () => AttachmentStore | undefined;
93
113
  onWarn?: (message: string) => void;
94
114
  onCredentialRejected?: (account: string) => void;
@@ -99,10 +119,45 @@ export interface ArkAdapterOptions {
99
119
  /** The adapter behind the `ark` route. */
100
120
  export declare class ArkAdapter extends LlmAdapter {
101
121
  private readonly options;
122
+ /**
123
+ * The catalog this route serves: entries derived from the endpoint's own listing,
124
+ * either the one just read or the one the cache kept from the last read.
125
+ *
126
+ * Undefined means neither source has produced a listing yet, so only the configured
127
+ * default model is offered until one does.
128
+ */
102
129
  private live;
103
- /** Image-generation model id, when discovery reported one. */
130
+ /** Whether the cached listing has been read; it is read at most once per session. */
131
+ private hydrated;
132
+ /** Image-generation model id, when the listing reported one. */
104
133
  private imageModel;
134
+ /**
135
+ * Fast-variant wire id per picker row.
136
+ *
137
+ * Derived from the listing rather than a curated alias map: a row whose name is a
138
+ * listed model plus the provider's own fast word rides that row instead of becoming
139
+ * one of its own.
140
+ */
141
+ private readonly fastVariants;
105
142
  constructor(options: ArkAdapterOptions);
143
+ /**
144
+ * Adopt the rows of one live listing as this route's catalog.
145
+ *
146
+ * Everything the picker shows is derived here from the endpoint's own answer: the
147
+ * rows that become picker entries and the fast variant that pairs with each row.
148
+ * Nothing about which models exist, or what each one is, is decided by this build.
149
+ *
150
+ * @param rows - decoded rows of the endpoint's listing, in the endpoint's order.
151
+ * @returns the entries to publish, in picker order.
152
+ */
153
+ private adoptListing;
154
+ /**
155
+ * Read the cached listing, once, so the picker works without asking the endpoint.
156
+ *
157
+ * Called before the first catalog query. A read failure is a cache miss, never a
158
+ * failure: the route simply has no listing until one is fetched.
159
+ */
160
+ hydrate(): Promise<void>;
106
161
  providerInfo(provider: string): LlmProviderInfo;
107
162
  providerRetryPolicy(): undefined;
108
163
  listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
@@ -113,18 +168,49 @@ export declare class ArkAdapter extends LlmAdapter {
113
168
  /** Translate an auth failure into the message a user can act on. */
114
169
  private refreshHint;
115
170
  private catalog;
171
+ /**
172
+ * The catalog to present: the models the endpoint's listing named, in its own order,
173
+ * each with the metadata that listing stated, then the configured extras.
174
+ *
175
+ * There is no shipped roster. A listing is read once and cached (see {@link hydrate}
176
+ * and {@link fetchCatalog}), and that cached table is what the picker shows until the
177
+ * next read, so a model released, renamed or retired server-side is reflected without
178
+ * a plugin update. Only the configured default is always offered, because the picker
179
+ * preselects it.
180
+ */
116
181
  private shippedCatalog;
182
+ /** The model the picker preselects, from configuration. */
183
+ private defaultModel;
117
184
  private addedEntries;
118
185
  private hidden;
119
- /** Adopt the endpoint's live catalog. */
186
+ /**
187
+ * Adopt the ids a caller reports as the served set, without a listing.
188
+ *
189
+ * {@link fetchCatalog} already derived the entries from the rows it read, and the
190
+ * controller reports those same ids back here; the entries it recorded are kept. A
191
+ * caller that only has ids (a test, another discovery path) gets conservative entries
192
+ * for them.
193
+ *
194
+ * @param ids - model ids the caller reports.
195
+ */
120
196
  discover(ids: readonly string[]): void;
121
197
  /**
122
- * Read the plan's live model list.
198
+ * Read the plan's live model list and adopt it as this route's catalog.
199
+ *
200
+ * The Coding Plan answers the OpenAI models shape on its own base, so the rows are
201
+ * parsed exactly as the other routes parse theirs: what the listing names is what the
202
+ * picker offers, with the display name, window, modalities and ladder it states. The
203
+ * rows are also handed to {@link ArkAdapterOptions.saveListing}, so the next session
204
+ * starts from this table instead of asking again — the Settings "refresh models"
205
+ * action is what re-reads it.
123
206
  *
124
- * The Coding Plan answers the OpenAI models shape on its own base; an endpoint
125
- * that does not serve it leaves the shipped roster in place.
207
+ * @param apiKey - bearer for the account.
208
+ * @param signal - optional cancellation.
209
+ * @returns the model ids now served, or undefined when the listing is unavailable.
126
210
  */
127
211
  fetchCatalog(apiKey: string, signal?: AbortSignal): Promise<readonly string[] | undefined>;
212
+ /** Persist one live listing, reporting a cache failure without failing the read. */
213
+ private saveListing;
128
214
  /** Send one streaming request over the Responses wire. */
129
215
  private request;
130
216
  /** Resolve image blocks through the attachment service, when it is present. */