dsh-plugin-subscription-oauth 0.8.14 → 0.8.16

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.
@@ -268,15 +268,152 @@ const IMAGE_MODEL = /(?:^|[-_/])(?:image|imagine|draw|pic|photo)(?:[-_/]|$)/i;
268
268
  export function isImageModel(id) {
269
269
  return IMAGE_MODEL.test(id);
270
270
  }
271
- /** Model ids that are the fast/lightspeed variant of a base model, matched per base. */
272
- const FAST_VARIANT = /(?:^|[-_])(?:fast|lightspeed|turbo|quick)$/i;
271
+ /**
272
+ * Model ids that are the fast/lightspeed variant of a base model, matched per base.
273
+ *
274
+ * The vocabulary is the providers' own: xAI says `fast` (`grok-4.7-build-fast`), the
275
+ * Claude/Codex world says `lightspeed` or `turbo`, and Kimi says `highspeed`
276
+ * (`kimi-for-coding-highspeed`). A word has to stand on its own, so `highspeed` never
277
+ * matches inside another word.
278
+ */
279
+ const FAST_VARIANT = /(?:^|[-_@])(?:fast|lightspeed|turbo|quick|highspeed)(?=[-_@]|$)/i;
273
280
  /** True when a model id names a fast/lightspeed variant of a base model. */
274
281
  export function isFastVariant(id) {
275
282
  return FAST_VARIANT.test(id);
276
283
  }
277
- /** The base model id a fast variant belongs to (`grok-4.7-fast` → `grok-4.7`). */
284
+ /**
285
+ * Every prefix of one model id, longest first, down to its first token.
286
+ *
287
+ * `grok-4.7-build-fast` → [`grok-4.7-build`, `grok-4.7`, `grok`]. A fast variant pairs
288
+ * with the longest prefix that is itself a listed model id, which is how
289
+ * `grok-4.7-build-fast` lands on `grok-4.7` (its own prefix `grok-4.7-build` is not a
290
+ * model) instead of inventing a base that no catalog row serves.
291
+ *
292
+ * @param id - model id.
293
+ * @returns the candidate bases, longest first.
294
+ */
295
+ export function baseCandidatesOf(id) {
296
+ const out = [];
297
+ let at = id.length;
298
+ while (at > 0) {
299
+ at = id.lastIndexOf('-', at - 1);
300
+ if (at <= 0)
301
+ break;
302
+ out.push(id.slice(0, at));
303
+ }
304
+ return out;
305
+ }
306
+ /**
307
+ * The base model id a fast variant belongs to, derived only from its own name.
308
+ *
309
+ * The fast token is removed, then any trailing generation qualifier (`build`, `beta`,
310
+ * `rc`, …) left behind by that removal, then any `@Nk` context suffix. What remains is
311
+ * the base (`grok-4.7-build-fast@500k` → `grok-4.7`, `kimi-for-coding-highspeed` →
312
+ * `kimi-for-coding`). Whether the base is a real catalog row is the index's job, not
313
+ * this function's.
314
+ */
278
315
  export function baseModelOf(id) {
279
- return id.replace(FAST_VARIANT, '');
316
+ return id
317
+ .replace(FAST_VARIANT, '')
318
+ .replace(/@\d+k$/i, '')
319
+ .replace(/[-_]$/, '')
320
+ .replace(/-(?:build|beta|alpha|rc|stable)$/i, '')
321
+ .replace(/[-_]$/, '');
322
+ }
323
+ /**
324
+ * The fast variant of one picker row, from either side of the pair.
325
+ *
326
+ * Shared by every route that discovers fast ids, so the composer's quick-mode button
327
+ * behaves the same wherever a fast model is identifiable: it finds the pair when asked
328
+ * about the base row, and when asked about the fast id itself — which is what the
329
+ * selection becomes once the mode is on — so it can switch back off again.
330
+ */
331
+ export class FastVariantIndex {
332
+ byBase = new Map();
333
+ rowOfVariant = new Map();
334
+ knownIds = new Set();
335
+ /** Forget every pair and every known id, before re-reading a catalog. */
336
+ clear() {
337
+ this.byBase.clear();
338
+ this.rowOfVariant.clear();
339
+ this.knownIds.clear();
340
+ }
341
+ /**
342
+ * Record one id the catalog listed.
343
+ *
344
+ * Listed ids are what a variant may pair with: the pair is the longest listed prefix
345
+ * of the variant's own name, so nothing is invented that no row actually serves.
346
+ *
347
+ * @param id - a wire id from a catalog.
348
+ */
349
+ noteListed(id) {
350
+ this.knownIds.add(id);
351
+ }
352
+ /**
353
+ * Pair one fast id with its base, purely from its own name.
354
+ *
355
+ * Every listed prefix of the id is a candidate base, longest first; the first one
356
+ * that is itself a listed model wins. No curated alias or hardcoded map is needed:
357
+ * a provider that names its variants after the base model pairs them automatically.
358
+ *
359
+ * @param id - a wire id the catalog named.
360
+ * @returns the base id, or undefined when no listed prefix names one.
361
+ */
362
+ pair(id) {
363
+ if (!isFastVariant(id))
364
+ return undefined;
365
+ for (const base of baseCandidatesOf(id)) {
366
+ if (this.knownIds.has(base))
367
+ return base;
368
+ }
369
+ return undefined;
370
+ }
371
+ /**
372
+ * Record one id, if it names a fast variant.
373
+ *
374
+ * The first variant named for a row wins: a catalog that lists two spellings of the
375
+ * same variant (`kimi-for-coding-highspeed` and `kimi-k2.7-code-highspeed`) offers
376
+ * one button, and the earlier entry is the more specific one.
377
+ *
378
+ * @param id - a wire id from a catalog.
379
+ * @param rowOf - maps a base id to the picker row that serves it, when the catalog
380
+ * spells the base differently from the variant's own prefix.
381
+ */
382
+ note(id, rowOf) {
383
+ if (!isFastVariant(id))
384
+ return;
385
+ const base = baseModelOf(id);
386
+ if (base.length === 0)
387
+ return;
388
+ const row = rowOf?.(base) ?? base;
389
+ if (!this.byBase.has(row))
390
+ this.byBase.set(row, id);
391
+ if (!this.rowOfVariant.has(id))
392
+ this.rowOfVariant.set(id, row);
393
+ }
394
+ /** The fast variant's wire id for one picker row, when the catalog named one. */
395
+ variantOf(baseId) {
396
+ return this.byBase.get(baseId);
397
+ }
398
+ /** The pair one picker id belongs to, from either side. */
399
+ pairOf(id) {
400
+ const direct = this.byBase.get(id);
401
+ if (direct !== undefined)
402
+ return { baseId: id, wireId: direct };
403
+ const row = this.rowOfVariant.get(id);
404
+ return row === undefined ? undefined : { baseId: row, wireId: id };
405
+ }
406
+ }
407
+ /** A context-size suffix like `@500k` on a model id. */
408
+ const CONTEXT_SUFFIX = /@(\d+)k$/i;
409
+ /** Extract the context size from a model id's `@Nk` suffix, or undefined. */
410
+ export function contextOf(id) {
411
+ const match = CONTEXT_SUFFIX.exec(id);
412
+ return match ? parseInt(match[1], 10) : undefined;
413
+ }
414
+ /** Strip the `@Nk` context suffix from a model id. */
415
+ export function stripContext(id) {
416
+ return id.replace(CONTEXT_SUFFIX, '');
280
417
  }
281
418
  /** Grok wire adapter: one instance serves the `grok` provider route. */
282
419
  export class GrokAdapter extends LlmAdapter {
@@ -286,7 +423,7 @@ export class GrokAdapter extends LlmAdapter {
286
423
  /** Capability metadata from the CLI catalog, keyed by model id. */
287
424
  metadata = new Map();
288
425
  /** Fast-variant wire id per base model, for the picker's quick-mode button. */
289
- fastVariants = new Map();
426
+ fastVariants = new FastVariantIndex();
290
427
  /** Image-generation model id, when discovery reported one. */
291
428
  imageModel;
292
429
  /** Ids the user added by hand, cached so the sync catalog can name them. */
@@ -309,15 +446,24 @@ export class GrokAdapter extends LlmAdapter {
309
446
  return [];
310
447
  await this.syncUserModels();
311
448
  const hidden = await this.hidden();
312
- // Image models are never picker rows: they are agent-called, always on.
449
+ // The host validates a GUI selection against the exact ids `listModels` returns
450
+ // and refuses an unknown one with `session/model-unavailable`, and its picker
451
+ // lists every returned id as its own row. So a fast variant is published under
452
+ // its base id: selecting it names the variant the request must send, and the
453
+ // base's own id is withheld so the variant does not appear twice.
313
454
  const imageless = this.catalog().filter((entry) => !isImageModel(entry.id));
314
- return resolveCatalog(imageless, [], hidden).entries.map((entry) => ({
315
- provider: GROK_PROVIDER_ID,
316
- id: entry.id,
317
- name: entry.name,
318
- ...entry.description === undefined ? {} : { description: entry.description },
319
- ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
320
- }));
455
+ const listed = resolveCatalog(imageless, [], hidden).entries.map((entry) => {
456
+ const fast = this.fastVariants.variantOf(entry.id);
457
+ const id = fast ?? entry.id;
458
+ return {
459
+ provider: GROK_PROVIDER_ID,
460
+ id,
461
+ name: entry.name,
462
+ ...entry.description === undefined ? {} : { description: entry.description },
463
+ ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
464
+ };
465
+ });
466
+ return listed;
321
467
  }
322
468
  async hidden() {
323
469
  if (this.options.hiddenModels === undefined)
@@ -332,7 +478,12 @@ export class GrokAdapter extends LlmAdapter {
332
478
  }
333
479
  async resolveModel(_provider, model) {
334
480
  await this.syncUserModels();
335
- const entry = findEntry(this.catalog(), model);
481
+ // A fast variant is not a catalog row of its own: it answers with its base model's
482
+ // metadata — the same 500k window, name and ladder the picker shows — while the id
483
+ // stays the fast wire id the request must send.
484
+ const pair = this.fastPairOf(model);
485
+ const entry = findEntry(this.catalog(), model)
486
+ ?? (pair === undefined ? undefined : findEntry(this.catalog(), pair.baseId));
336
487
  if (entry === undefined) {
337
488
  const template = findEntry(GROK_MODELS, 'grok-4.6');
338
489
  const reasoning = template === undefined ? undefined : reasoningInfo(template);
@@ -346,10 +497,13 @@ export class GrokAdapter extends LlmAdapter {
346
497
  };
347
498
  }
348
499
  const reasoning = reasoningInfo(entry);
500
+ // The fast selection names itself "Fast" so the picker trigger reads the mode, not
501
+ // the base model it replaces; the id stays the fast wire id the request must send.
502
+ const fast = pair !== undefined && pair.wireId === model;
349
503
  return {
350
504
  provider: GROK_PROVIDER_ID,
351
- id: entry.id,
352
- name: entry.name,
505
+ id: model,
506
+ name: fast ? `${entry.name} Fast` : entry.name,
353
507
  ...entry.description === undefined ? {} : { description: entry.description },
354
508
  ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
355
509
  context: { contextWindow: entry.contextWindow },
@@ -358,7 +512,9 @@ export class GrokAdapter extends LlmAdapter {
358
512
  };
359
513
  }
360
514
  stream(options) {
361
- return streamProviderResponses(options, {
515
+ // Resolve @Nk context suffixes to the largest variant the endpoint offers.
516
+ const model = this.resolveModelId(options.model);
517
+ return streamProviderResponses({ ...options, model }, {
362
518
  label: 'xAI API',
363
519
  tokens: this.options.tokens,
364
520
  request: (call, model, accessToken, signal) => this.request(call, model, accessToken, signal),
@@ -409,14 +565,17 @@ export class GrokAdapter extends LlmAdapter {
409
565
  ...meta?.defaultEffort === undefined ? {} : { defaultEffort: meta.defaultEffort },
410
566
  });
411
567
  }
412
- // The seeds also pick up live metadata (context window, reasoning ladder).
568
+ // The seeds also pick up live metadata (reasoning ladder, default effort). The
569
+ // curated context window is never narrowed by a live figure: the CLI catalog states
570
+ // 256k for `grok-4.7`, which serves 500k, and a live read may only widen what this
571
+ // build ships.
413
572
  const seeded = GROK_MODELS.map((entry) => {
414
573
  const meta = this.metadata.get(entry.id);
415
574
  if (meta === undefined)
416
575
  return entry;
417
576
  return {
418
577
  ...entry,
419
- contextWindow: meta.contextWindow ?? entry.contextWindow,
578
+ contextWindow: Math.max(entry.contextWindow, meta.contextWindow ?? 0),
420
579
  ...meta.reasoningEfforts === undefined ? {} : { reasoningEfforts: meta.reasoningEfforts },
421
580
  ...meta.defaultEffort === undefined ? {} : { defaultEffort: meta.defaultEffort },
422
581
  };
@@ -459,7 +618,11 @@ export class GrokAdapter extends LlmAdapter {
459
618
  }
460
619
  /** The fast/lightspeed variant wire id for a base model, when discovery reported one. */
461
620
  fastVariant(baseId) {
462
- return this.fastVariants.get(baseId);
621
+ return this.fastVariants.variantOf(baseId);
622
+ }
623
+ /** The fast pair one picker id belongs to, from either side. */
624
+ fastPairOf(id) {
625
+ return this.fastVariants.pairOf(id);
463
626
  }
464
627
  /** The image-generation model id, when discovery reported one. */
465
628
  imageModelId() {
@@ -474,6 +637,22 @@ export class GrokAdapter extends LlmAdapter {
474
637
  return this.shippedCatalog();
475
638
  }
476
639
  /** Build and send one request for one model. */
640
+ /** The default context variant per base model, set during discovery. */
641
+ defaultContext = new Map();
642
+ /** Resolve a model id to the wire id the endpoint accepts (largest context). */
643
+ resolveModelId(model) {
644
+ return this.defaultContext.get(model) ?? model;
645
+ }
646
+ /** Record one default-context mapping. */
647
+ noteContextVariant(base, context, wireId) {
648
+ const existing = this.defaultContext.get(base);
649
+ if (existing !== undefined) {
650
+ const existingCtx = contextOf(existing);
651
+ if (existingCtx !== undefined && existingCtx >= context)
652
+ return;
653
+ }
654
+ this.defaultContext.set(base, wireId);
655
+ }
477
656
  async request(options, model, accessToken, signal) {
478
657
  const resolved = await toResponsesInput(options.messages, options.system, this.imageResolver(signal));
479
658
  const body = {
@@ -552,24 +731,37 @@ export class GrokAdapter extends LlmAdapter {
552
731
  const rawIds = allIds.filter((id) => !NON_CHAT_ID.test(id));
553
732
  // Fast/lightspeed variants ride with their base model: they share the base's
554
733
  // visibility switch and are reached by a picker button, not a separate row.
734
+ // Context variants (@500k) also fold into the base; the largest context wins.
555
735
  this.fastVariants.clear();
736
+ this.defaultContext.clear();
556
737
  for (const id of rawIds) {
557
- if (!isFastVariant(id))
558
- continue;
559
- const base = baseModelOf(id);
560
- if (!this.fastVariants.has(base))
561
- this.fastVariants.set(base, id);
738
+ this.fastVariants.noteListed(id);
562
739
  }
740
+ this.noteWireVariants(rawIds);
563
741
  const ids = rawIds
564
- .map((id) => (isFastVariant(id) ? baseModelOf(id) : id))
742
+ .map((id) => {
743
+ if (isFastVariant(id))
744
+ return this.fastVariants.pair(id) ?? baseModelOf(id);
745
+ const ctx = contextOf(id);
746
+ if (ctx !== undefined)
747
+ return stripContext(id);
748
+ return id;
749
+ })
565
750
  .filter((id, index, list) => list.indexOf(id) === index);
566
751
  if (ids.length === 0) {
567
752
  this.options.onWarn?.('xAI model discovery returned an empty catalog; using the built-in catalog');
568
753
  return undefined;
569
754
  }
570
- const metadata = await this.fetchCliMetadata(fetchFn, signal);
571
- for (const [id, meta] of metadata)
755
+ const cli = await this.readCliCatalog(fetchFn, signal, accessToken);
756
+ for (const [id, meta] of cli.metadata)
572
757
  this.metadata.set(id, meta);
758
+ // The account's own CLI catalog is the only surface that names a fast variant:
759
+ // the public `/models` list omits `grok-4.7-build-fast` entirely. Its ids fold
760
+ // into the same base rows, and are never added as picker rows of their own.
761
+ for (const id of cli.ids) {
762
+ this.fastVariants.noteListed(id);
763
+ }
764
+ this.noteWireVariants(cli.ids);
573
765
  return ids;
574
766
  }
575
767
  catch (error) {
@@ -579,21 +771,55 @@ export class GrokAdapter extends LlmAdapter {
579
771
  return undefined;
580
772
  }
581
773
  }
582
- /** Read per-model capabilities from the CLI catalog; failures are silent. */
583
- async fetchCliMetadata(fetchFn, signal) {
774
+ /**
775
+ * Fold the fast and context variants of one id list into their base models.
776
+ *
777
+ * @param ids - wire ids to classify.
778
+ */
779
+ noteWireVariants(ids) {
780
+ for (const id of ids) {
781
+ if (NON_CHAT_ID.test(id) || isImageModel(id))
782
+ continue;
783
+ if (isFastVariant(id)) {
784
+ // Pair with the longest listed prefix, not a derived string: the prefix is a
785
+ // model the account actually serves, so the button always targets a real row.
786
+ const base = this.fastVariants.pair(id);
787
+ this.fastVariants.note(id, () => base);
788
+ continue;
789
+ }
790
+ const ctx = contextOf(id);
791
+ if (ctx !== undefined)
792
+ this.noteContextVariant(stripContext(id), ctx, id);
793
+ }
794
+ }
795
+ /**
796
+ * Read per-model capabilities from the CLI catalog; failures are silent.
797
+ *
798
+ * The catalog answers only for the signed-in account's own bearer — an anonymous
799
+ * request is refused — and it, unlike the public `/models` list, names the account's
800
+ * fast variant.
801
+ *
802
+ * @param fetchFn - fetch implementation.
803
+ * @param signal - caller cancellation.
804
+ * @param accessToken - the account's bearer.
805
+ * @returns the capability metadata and every id the catalog named.
806
+ */
807
+ async readCliCatalog(fetchFn, signal, accessToken) {
584
808
  const result = new Map();
809
+ const ids = [];
585
810
  try {
586
811
  const response = await fetchFn(GROK_CLI_MODELS_URL, {
587
- headers: attributedHeaders({ 'authorization': 'Bearer anonymous' }),
812
+ headers: attributedHeaders({ 'authorization': `Bearer ${accessToken}` }),
588
813
  ...signal === undefined ? {} : { signal },
589
814
  });
590
815
  if (!response.ok)
591
- return result;
816
+ return { metadata: result, ids };
592
817
  const payload = await response.json();
593
818
  for (const entry of payload.data ?? []) {
594
819
  const id = typeof entry.id === 'string' && entry.id.length > 0 ? entry.id : undefined;
595
820
  if (id === undefined)
596
821
  continue;
822
+ ids.push(id);
597
823
  const contextWindow = typeof entry.context_window === 'number' && Number.isFinite(entry.context_window)
598
824
  ? entry.context_window
599
825
  : undefined;
@@ -617,7 +843,7 @@ export class GrokAdapter extends LlmAdapter {
617
843
  catch {
618
844
  // Capability metadata is a bonus; the id list is what matters.
619
845
  }
620
- return result;
846
+ return { metadata: result, ids };
621
847
  }
622
848
  }
623
849
  /**
@@ -759,10 +985,10 @@ export function createGrokProvider(options) {
759
985
  usage: (force) => controller.usage(force),
760
986
  reload: () => controller.reload(),
761
987
  modelVariants: (id) => {
762
- const fast = adapter.fastVariant(id);
988
+ const pair = adapter.fastPairOf(id);
763
989
  const image = adapter.imageModelId();
764
990
  return {
765
- ...fast === undefined ? {} : { fast },
991
+ ...pair === undefined ? {} : { fast: pair.wireId, fastBase: pair.baseId },
766
992
  ...image === undefined ? {} : { image },
767
993
  };
768
994
  },
@@ -151,10 +151,10 @@ export function createKimiProvider(options) {
151
151
  usage: (force) => controller.usage(force),
152
152
  reload: () => controller.reload(),
153
153
  modelVariants: (id) => {
154
- const fast = adapter.fastVariant(id);
154
+ const pair = adapter.fastPairOf(id);
155
155
  const image = adapter.imageModelId();
156
156
  return {
157
- ...fast === undefined ? {} : { fast },
157
+ ...pair === undefined ? {} : { fast: pair.wireId, fastBase: pair.baseId },
158
158
  ...image === undefined ? {} : { image },
159
159
  };
160
160
  },
@@ -104,13 +104,33 @@ export declare class KimiAdapter extends LlmAdapter {
104
104
  private discovered;
105
105
  /** Image-generation model id, when discovery reported one. */
106
106
  private imageModel;
107
- /** Fast-variant wire id per base model, for the picker's quick-mode button. */
107
+ /**
108
+ * Fast-variant wire id per picker row, for the composer's quick-mode button.
109
+ *
110
+ * Seeded from the curated aliases — `kimi-for-coding-highspeed` is one — so the button
111
+ * exists before any discovery run, and then updated from the live catalog, which is
112
+ * the authority on what this account actually serves.
113
+ */
108
114
  private readonly fastVariants;
109
115
  /** Ids the user added by hand, cached so the sync catalog can name them. */
110
116
  private userAddedIds;
111
117
  /** Tools already reported as flattened, so each is stated once. */
112
118
  private readonly flattenedTools;
113
119
  constructor(options: KimiAdapterOptions);
120
+ /**
121
+ * Record the fast variants the curated catalog already names as aliases.
122
+ *
123
+ * The alias is the recognition: `kimi-for-coding-highspeed` is stated as an accepted
124
+ * id of `kimi-for-coding`, so the route can offer the quick-mode button without
125
+ * waiting for a live list that may never name it. A row a live catalog already paired
126
+ * keeps that pairing, so this only fills the gaps.
127
+ */
128
+ private seedCuratedFastVariants;
129
+ /** The fast pair one picker id belongs to, from either side. */
130
+ fastPairOf(id: string): {
131
+ baseId: string;
132
+ wireId: string;
133
+ } | undefined;
114
134
  /**
115
135
  * Tool schemas in the shape Moonshot accepts: no `$ref`, and under its size
116
136
  * ceiling. A declaration too large to send keeps its arguments, and that is
@@ -122,7 +142,7 @@ export declare class KimiAdapter extends LlmAdapter {
122
142
  listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
123
143
  /** The image-generation model id, when discovery reported one. */
124
144
  imageModelId(): string | undefined;
125
- /** The fast/lightspeed variant wire id for a base model, when discovery reported one. */
145
+ /** The fast/lightspeed variant wire id for a picker row. */
126
146
  fastVariant(baseId: string): string | undefined;
127
147
  /** The ids the picker must currently omit. */
128
148
  private hidden;
@@ -15,7 +15,7 @@ import { streamProviderResponses, accountPoolHooks } from './responses-stream.js
15
15
  import { fallbackEntry, findEntry, resolveCatalog } from './catalog.js';
16
16
  import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
17
17
  import { prepareMoonshotTool } from '../translate/moonshot-schema.js';
18
- import { isImageModel, isFastVariant, baseModelOf } from './grok.js';
18
+ import { isImageModel, isFastVariant, baseModelOf, FastVariantIndex } from './grok.js';
19
19
  /** Model ids that are clearly not chat models. */
20
20
  const NON_CHAT_ID = /(?:^|[-_/])(?:image|video|imagine|embed|embedding|whisper|tts|vision-only)/i;
21
21
  /**
@@ -134,8 +134,14 @@ export class KimiAdapter extends LlmAdapter {
134
134
  discovered;
135
135
  /** Image-generation model id, when discovery reported one. */
136
136
  imageModel;
137
- /** Fast-variant wire id per base model, for the picker's quick-mode button. */
138
- fastVariants = new Map();
137
+ /**
138
+ * Fast-variant wire id per picker row, for the composer's quick-mode button.
139
+ *
140
+ * Seeded from the curated aliases — `kimi-for-coding-highspeed` is one — so the button
141
+ * exists before any discovery run, and then updated from the live catalog, which is
142
+ * the authority on what this account actually serves.
143
+ */
144
+ fastVariants = new FastVariantIndex();
139
145
  /** Ids the user added by hand, cached so the sync catalog can name them. */
140
146
  userAddedIds = [];
141
147
  /** Tools already reported as flattened, so each is stated once. */
@@ -143,6 +149,28 @@ export class KimiAdapter extends LlmAdapter {
143
149
  constructor(options) {
144
150
  super();
145
151
  this.options = options;
152
+ this.seedCuratedFastVariants();
153
+ }
154
+ /**
155
+ * Record the fast variants the curated catalog already names as aliases.
156
+ *
157
+ * The alias is the recognition: `kimi-for-coding-highspeed` is stated as an accepted
158
+ * id of `kimi-for-coding`, so the route can offer the quick-mode button without
159
+ * waiting for a live list that may never name it. A row a live catalog already paired
160
+ * keeps that pairing, so this only fills the gaps.
161
+ */
162
+ seedCuratedFastVariants() {
163
+ for (const entry of KIMI_MODELS) {
164
+ for (const alias of entry.aliases ?? []) {
165
+ // Every alias of one row maps back to that row, so a variant whose own prefix
166
+ // names another alias (`kimi-k2.7-code-highspeed`) still pairs with the row.
167
+ this.fastVariants.note(alias, (base) => this.entryFor(base)?.id ?? base);
168
+ }
169
+ }
170
+ }
171
+ /** The fast pair one picker id belongs to, from either side. */
172
+ fastPairOf(id) {
173
+ return this.fastVariants.pairOf(id);
146
174
  }
147
175
  /**
148
176
  * Tool schemas in the shape Moonshot accepts: no `$ref`, and under its size
@@ -178,23 +206,32 @@ export class KimiAdapter extends LlmAdapter {
178
206
  return [];
179
207
  await this.syncUserModels();
180
208
  const hidden = await this.hidden();
181
- // Image models are never picker rows: they are agent-called, always on.
209
+ // The host validates a GUI selection against the exact ids `listModels` returns
210
+ // and refuses an unknown one with `session/model-unavailable`, and its picker
211
+ // lists every returned id as its own row. So a fast variant is published under
212
+ // its base id: selecting it names the variant the request must send, and the
213
+ // base's own id is withheld so the variant does not appear twice.
182
214
  const imageless = this.catalog().filter((entry) => !isImageModel(entry.id));
183
- return resolveCatalog(imageless, [], hidden).entries.map((entry) => ({
184
- provider: KIMI_PROVIDER_ID,
185
- id: entry.id,
186
- name: entry.name,
187
- ...entry.description === undefined ? {} : { description: entry.description },
188
- ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
189
- }));
215
+ const listed = resolveCatalog(imageless, [], hidden).entries.map((entry) => {
216
+ const fast = this.fastVariants.variantOf(entry.id);
217
+ const id = fast ?? entry.id;
218
+ return {
219
+ provider: KIMI_PROVIDER_ID,
220
+ id,
221
+ name: entry.name,
222
+ ...entry.description === undefined ? {} : { description: entry.description },
223
+ ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
224
+ };
225
+ });
226
+ return listed;
190
227
  }
191
228
  /** The image-generation model id, when discovery reported one. */
192
229
  imageModelId() {
193
230
  return this.imageModel;
194
231
  }
195
- /** The fast/lightspeed variant wire id for a base model, when discovery reported one. */
232
+ /** The fast/lightspeed variant wire id for a picker row. */
196
233
  fastVariant(baseId) {
197
- return this.fastVariants.get(baseId);
234
+ return this.fastVariants.variantOf(baseId);
198
235
  }
199
236
  /** The ids the picker must currently omit. */
200
237
  async hidden() {
@@ -228,10 +265,16 @@ export class KimiAdapter extends LlmAdapter {
228
265
  };
229
266
  }
230
267
  const reasoning = reasoningInfo(entry);
268
+ // A fast variant is a wire id of its own — the button switches to it and back, so the
269
+ // selection must keep that id — while an ordinary alias is just another spelling of
270
+ // the row and resolves onto it. The fast selection names itself "Fast" so the picker
271
+ // trigger reads the mode, not the base model it replaces.
272
+ const pair = this.fastVariants.pairOf(model);
273
+ const fast = pair !== undefined && pair.wireId === model;
231
274
  return {
232
275
  provider: KIMI_PROVIDER_ID,
233
- id: entry.id,
234
- name: entry.name,
276
+ id: fast ? model : entry.id,
277
+ name: fast ? `${entry.name} Fast` : entry.name,
235
278
  ...entry.description === undefined ? {} : { description: entry.description },
236
279
  ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
237
280
  context: { contextWindow: entry.contextWindow },
@@ -436,17 +479,25 @@ export class KimiAdapter extends LlmAdapter {
436
479
  .filter((id) => typeof id === 'string' && id.length > 0);
437
480
  // Image models are never picker rows: they are agent-called, always on.
438
481
  this.imageModel = allIds.find((id) => isImageModel(id));
439
- // Fast/lightspeed variants ride with their base model: they share the base's
440
- // visibility switch and are reached by a picker button, not a separate row.
482
+ // Fast/lightspeed variants ride with their base row: they share the row's
483
+ // visibility switch and are reached by the composer's quick-mode button, not a
484
+ // separate row. The live list is the authority on what this account serves, so
485
+ // it is recorded first, and each variant pairs with the longest listed prefix
486
+ // of its own name — a provider that names its variants after the base model
487
+ // pairs them with no curated map at all.
441
488
  this.fastVariants.clear();
442
489
  for (const id of allIds) {
443
- if (!isFastVariant(id))
444
- continue;
445
- const base = baseModelOf(id);
446
- if (!this.fastVariants.has(base))
447
- this.fastVariants.set(base, id);
490
+ this.fastVariants.noteListed(id);
491
+ }
492
+ for (const id of allIds) {
493
+ const base = this.fastVariants.pair(id);
494
+ this.fastVariants.note(id, () => base ?? this.entryFor(baseModelOf(id))?.id ?? baseModelOf(id));
448
495
  }
449
- const ids = allIds.filter((id) => !NON_CHAT_ID.test(id));
496
+ this.seedCuratedFastVariants();
497
+ const ids = allIds
498
+ .filter((id) => !NON_CHAT_ID.test(id))
499
+ .map((id) => (isFastVariant(id) ? this.fastVariants.pair(id) ?? baseModelOf(id) : id))
500
+ .filter((id, index, list) => list.indexOf(id) === index);
450
501
  if (ids.length === 0) {
451
502
  this.options.onWarn?.('Kimi model discovery returned an empty catalog; using the built-in catalog');
452
503
  return undefined;
@@ -25,7 +25,7 @@ import { parseSse } from './sse.js';
25
25
  export function antigravityReasoning(model) {
26
26
  const ids = model.startsWith('claude-') ? ['high']
27
27
  : model.startsWith('gpt-oss-') ? ['medium']
28
- : model.startsWith('gemini-3.1-pro') || model === 'gemini-pro-agent' ? ['low', 'high']
28
+ : model.startsWith('gemini-3.1-pro') || model === 'gemini-pro' || model === 'gemini-pro-agent' ? ['low', 'high']
29
29
  : /^gemini-(?:2\.5|3)/.test(model) ? ['low', 'medium', 'high'] : [];
30
30
  const fixed = /^gemini-.*-(low|medium|high)$/.exec(model)?.[1];
31
31
  const efforts = fixed !== undefined && ids.includes(fixed) ? [fixed] : ids;
@@ -59,10 +59,10 @@ export function antigravityThinking(model, effort, maxTokens) {
59
59
  budget = 1024;
60
60
  else if (model.startsWith('gpt-oss-'))
61
61
  budget = 8192;
62
- else if (model.startsWith('gemini-3.1-pro') || model === 'gemini-pro-agent') {
62
+ else if (model.startsWith('gemini-3.1-pro') || model === 'gemini-pro' || model === 'gemini-pro-agent') {
63
63
  budget = effort === 'high' ? 10001 : 1001;
64
64
  }
65
- else if (model.startsWith('gemini-3.5-flash') || model === 'gemini-3-flash-agent') {
65
+ else if (model.startsWith('gemini-3.5-flash') || model === 'gemini-3-flash' || model === 'gemini-3-flash-agent') {
66
66
  budget = effort === 'high' ? 10000 : effort === 'medium' ? 4000 : 1000;
67
67
  }
68
68
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subscription-oauth",
3
- "version": "0.8.14",
3
+ "version": "0.8.16",
4
4
  "description": "Use subscription accounts as DeepSeek Harness LLM providers — Kimi Code (OAuth device flow), Grok / X Premium (OIDC + PKCE), Google Antigravity (Google OAuth + Cloud Code Assist) and GLM / BigModel (ZCode's browser sign-in) — with per-model visibility, drag-to-reorder Auto fallback and plan quotas from the web Settings page",
5
5
  "license": "MIT",
6
6
  "author": "EricFL1998",