dsh-plugin-subscription-oauth 0.8.14 → 0.8.15

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,25 @@ 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
+ // A fast variant is not a picker row, but the host validates a GUI selection
450
+ // against the exact list `listModels` returns, so the variant must appear there
451
+ // or selecting it is refused with `session/model-unavailable`. The picker's
452
+ // menu collapses entries that share one `group` value into a single choice,
453
+ // so the variant rides with its base id as the group: the host can name and
454
+ // select it, and the picker still shows one row per model.
313
455
  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
- }));
456
+ const listed = resolveCatalog(imageless, [], hidden).entries.flatMap((entry) => {
457
+ const fast = this.fastVariants.variantOf(entry.id);
458
+ const base = {
459
+ provider: GROK_PROVIDER_ID,
460
+ id: entry.id,
461
+ name: entry.name,
462
+ ...entry.description === undefined ? {} : { description: entry.description },
463
+ ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
464
+ };
465
+ return fast === undefined ? [base] : [base, { provider: GROK_PROVIDER_ID, id: fast, name: entry.name, group: entry.id, description: 'Fast variant', ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities } }];
466
+ });
467
+ return listed;
321
468
  }
322
469
  async hidden() {
323
470
  if (this.options.hiddenModels === undefined)
@@ -332,7 +479,12 @@ export class GrokAdapter extends LlmAdapter {
332
479
  }
333
480
  async resolveModel(_provider, model) {
334
481
  await this.syncUserModels();
335
- const entry = findEntry(this.catalog(), model);
482
+ // A fast variant is not a catalog row of its own: it answers with its base model's
483
+ // metadata — the same 500k window, name and ladder the picker shows — while the id
484
+ // stays the fast wire id the request must send.
485
+ const pair = this.fastPairOf(model);
486
+ const entry = findEntry(this.catalog(), model)
487
+ ?? (pair === undefined ? undefined : findEntry(this.catalog(), pair.baseId));
336
488
  if (entry === undefined) {
337
489
  const template = findEntry(GROK_MODELS, 'grok-4.6');
338
490
  const reasoning = template === undefined ? undefined : reasoningInfo(template);
@@ -348,7 +500,7 @@ export class GrokAdapter extends LlmAdapter {
348
500
  const reasoning = reasoningInfo(entry);
349
501
  return {
350
502
  provider: GROK_PROVIDER_ID,
351
- id: entry.id,
503
+ id: model,
352
504
  name: entry.name,
353
505
  ...entry.description === undefined ? {} : { description: entry.description },
354
506
  ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
@@ -358,7 +510,9 @@ export class GrokAdapter extends LlmAdapter {
358
510
  };
359
511
  }
360
512
  stream(options) {
361
- return streamProviderResponses(options, {
513
+ // Resolve @Nk context suffixes to the largest variant the endpoint offers.
514
+ const model = this.resolveModelId(options.model);
515
+ return streamProviderResponses({ ...options, model }, {
362
516
  label: 'xAI API',
363
517
  tokens: this.options.tokens,
364
518
  request: (call, model, accessToken, signal) => this.request(call, model, accessToken, signal),
@@ -409,14 +563,17 @@ export class GrokAdapter extends LlmAdapter {
409
563
  ...meta?.defaultEffort === undefined ? {} : { defaultEffort: meta.defaultEffort },
410
564
  });
411
565
  }
412
- // The seeds also pick up live metadata (context window, reasoning ladder).
566
+ // The seeds also pick up live metadata (reasoning ladder, default effort). The
567
+ // curated context window is never narrowed by a live figure: the CLI catalog states
568
+ // 256k for `grok-4.7`, which serves 500k, and a live read may only widen what this
569
+ // build ships.
413
570
  const seeded = GROK_MODELS.map((entry) => {
414
571
  const meta = this.metadata.get(entry.id);
415
572
  if (meta === undefined)
416
573
  return entry;
417
574
  return {
418
575
  ...entry,
419
- contextWindow: meta.contextWindow ?? entry.contextWindow,
576
+ contextWindow: Math.max(entry.contextWindow, meta.contextWindow ?? 0),
420
577
  ...meta.reasoningEfforts === undefined ? {} : { reasoningEfforts: meta.reasoningEfforts },
421
578
  ...meta.defaultEffort === undefined ? {} : { defaultEffort: meta.defaultEffort },
422
579
  };
@@ -459,7 +616,11 @@ export class GrokAdapter extends LlmAdapter {
459
616
  }
460
617
  /** The fast/lightspeed variant wire id for a base model, when discovery reported one. */
461
618
  fastVariant(baseId) {
462
- return this.fastVariants.get(baseId);
619
+ return this.fastVariants.variantOf(baseId);
620
+ }
621
+ /** The fast pair one picker id belongs to, from either side. */
622
+ fastPairOf(id) {
623
+ return this.fastVariants.pairOf(id);
463
624
  }
464
625
  /** The image-generation model id, when discovery reported one. */
465
626
  imageModelId() {
@@ -474,6 +635,22 @@ export class GrokAdapter extends LlmAdapter {
474
635
  return this.shippedCatalog();
475
636
  }
476
637
  /** Build and send one request for one model. */
638
+ /** The default context variant per base model, set during discovery. */
639
+ defaultContext = new Map();
640
+ /** Resolve a model id to the wire id the endpoint accepts (largest context). */
641
+ resolveModelId(model) {
642
+ return this.defaultContext.get(model) ?? model;
643
+ }
644
+ /** Record one default-context mapping. */
645
+ noteContextVariant(base, context, wireId) {
646
+ const existing = this.defaultContext.get(base);
647
+ if (existing !== undefined) {
648
+ const existingCtx = contextOf(existing);
649
+ if (existingCtx !== undefined && existingCtx >= context)
650
+ return;
651
+ }
652
+ this.defaultContext.set(base, wireId);
653
+ }
477
654
  async request(options, model, accessToken, signal) {
478
655
  const resolved = await toResponsesInput(options.messages, options.system, this.imageResolver(signal));
479
656
  const body = {
@@ -552,24 +729,37 @@ export class GrokAdapter extends LlmAdapter {
552
729
  const rawIds = allIds.filter((id) => !NON_CHAT_ID.test(id));
553
730
  // Fast/lightspeed variants ride with their base model: they share the base's
554
731
  // visibility switch and are reached by a picker button, not a separate row.
732
+ // Context variants (@500k) also fold into the base; the largest context wins.
555
733
  this.fastVariants.clear();
734
+ this.defaultContext.clear();
556
735
  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);
736
+ this.fastVariants.noteListed(id);
562
737
  }
738
+ this.noteWireVariants(rawIds);
563
739
  const ids = rawIds
564
- .map((id) => (isFastVariant(id) ? baseModelOf(id) : id))
740
+ .map((id) => {
741
+ if (isFastVariant(id))
742
+ return this.fastVariants.pair(id) ?? baseModelOf(id);
743
+ const ctx = contextOf(id);
744
+ if (ctx !== undefined)
745
+ return stripContext(id);
746
+ return id;
747
+ })
565
748
  .filter((id, index, list) => list.indexOf(id) === index);
566
749
  if (ids.length === 0) {
567
750
  this.options.onWarn?.('xAI model discovery returned an empty catalog; using the built-in catalog');
568
751
  return undefined;
569
752
  }
570
- const metadata = await this.fetchCliMetadata(fetchFn, signal);
571
- for (const [id, meta] of metadata)
753
+ const cli = await this.readCliCatalog(fetchFn, signal, accessToken);
754
+ for (const [id, meta] of cli.metadata)
572
755
  this.metadata.set(id, meta);
756
+ // The account's own CLI catalog is the only surface that names a fast variant:
757
+ // the public `/models` list omits `grok-4.7-build-fast` entirely. Its ids fold
758
+ // into the same base rows, and are never added as picker rows of their own.
759
+ for (const id of cli.ids) {
760
+ this.fastVariants.noteListed(id);
761
+ }
762
+ this.noteWireVariants(cli.ids);
573
763
  return ids;
574
764
  }
575
765
  catch (error) {
@@ -579,21 +769,55 @@ export class GrokAdapter extends LlmAdapter {
579
769
  return undefined;
580
770
  }
581
771
  }
582
- /** Read per-model capabilities from the CLI catalog; failures are silent. */
583
- async fetchCliMetadata(fetchFn, signal) {
772
+ /**
773
+ * Fold the fast and context variants of one id list into their base models.
774
+ *
775
+ * @param ids - wire ids to classify.
776
+ */
777
+ noteWireVariants(ids) {
778
+ for (const id of ids) {
779
+ if (NON_CHAT_ID.test(id) || isImageModel(id))
780
+ continue;
781
+ if (isFastVariant(id)) {
782
+ // Pair with the longest listed prefix, not a derived string: the prefix is a
783
+ // model the account actually serves, so the button always targets a real row.
784
+ const base = this.fastVariants.pair(id);
785
+ this.fastVariants.note(id, () => base);
786
+ continue;
787
+ }
788
+ const ctx = contextOf(id);
789
+ if (ctx !== undefined)
790
+ this.noteContextVariant(stripContext(id), ctx, id);
791
+ }
792
+ }
793
+ /**
794
+ * Read per-model capabilities from the CLI catalog; failures are silent.
795
+ *
796
+ * The catalog answers only for the signed-in account's own bearer — an anonymous
797
+ * request is refused — and it, unlike the public `/models` list, names the account's
798
+ * fast variant.
799
+ *
800
+ * @param fetchFn - fetch implementation.
801
+ * @param signal - caller cancellation.
802
+ * @param accessToken - the account's bearer.
803
+ * @returns the capability metadata and every id the catalog named.
804
+ */
805
+ async readCliCatalog(fetchFn, signal, accessToken) {
584
806
  const result = new Map();
807
+ const ids = [];
585
808
  try {
586
809
  const response = await fetchFn(GROK_CLI_MODELS_URL, {
587
- headers: attributedHeaders({ 'authorization': 'Bearer anonymous' }),
810
+ headers: attributedHeaders({ 'authorization': `Bearer ${accessToken}` }),
588
811
  ...signal === undefined ? {} : { signal },
589
812
  });
590
813
  if (!response.ok)
591
- return result;
814
+ return { metadata: result, ids };
592
815
  const payload = await response.json();
593
816
  for (const entry of payload.data ?? []) {
594
817
  const id = typeof entry.id === 'string' && entry.id.length > 0 ? entry.id : undefined;
595
818
  if (id === undefined)
596
819
  continue;
820
+ ids.push(id);
597
821
  const contextWindow = typeof entry.context_window === 'number' && Number.isFinite(entry.context_window)
598
822
  ? entry.context_window
599
823
  : undefined;
@@ -617,7 +841,7 @@ export class GrokAdapter extends LlmAdapter {
617
841
  catch {
618
842
  // Capability metadata is a bonus; the id list is what matters.
619
843
  }
620
- return result;
844
+ return { metadata: result, ids };
621
845
  }
622
846
  }
623
847
  /**
@@ -759,10 +983,10 @@ export function createGrokProvider(options) {
759
983
  usage: (force) => controller.usage(force),
760
984
  reload: () => controller.reload(),
761
985
  modelVariants: (id) => {
762
- const fast = adapter.fastVariant(id);
986
+ const pair = adapter.fastPairOf(id);
763
987
  const image = adapter.imageModelId();
764
988
  return {
765
- ...fast === undefined ? {} : { fast },
989
+ ...pair === undefined ? {} : { fast: pair.wireId, fastBase: pair.baseId },
766
990
  ...image === undefined ? {} : { image },
767
991
  };
768
992
  },
@@ -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,33 @@ 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
+ // A fast variant is not a picker row, but the host validates a GUI selection
210
+ // against the exact list `listModels` returns, so the variant must appear there
211
+ // or selecting it is refused with `session/model-unavailable`. The picker's
212
+ // menu collapses entries that share one `group` value into a single choice,
213
+ // so the variant rides with its base id as the group: the host can name and
214
+ // select it, and the picker still shows one row per model.
182
215
  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
- }));
216
+ const listed = resolveCatalog(imageless, [], hidden).entries.flatMap((entry) => {
217
+ const fast = this.fastVariants.variantOf(entry.id);
218
+ const base = {
219
+ provider: KIMI_PROVIDER_ID,
220
+ id: entry.id,
221
+ name: entry.name,
222
+ ...entry.description === undefined ? {} : { description: entry.description },
223
+ ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
224
+ };
225
+ return fast === undefined ? [base] : [base, { provider: KIMI_PROVIDER_ID, id: fast, name: entry.name, group: entry.id, description: 'Fast variant', ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities } }];
226
+ });
227
+ return listed;
190
228
  }
191
229
  /** The image-generation model id, when discovery reported one. */
192
230
  imageModelId() {
193
231
  return this.imageModel;
194
232
  }
195
- /** The fast/lightspeed variant wire id for a base model, when discovery reported one. */
233
+ /** The fast/lightspeed variant wire id for a picker row. */
196
234
  fastVariant(baseId) {
197
- return this.fastVariants.get(baseId);
235
+ return this.fastVariants.variantOf(baseId);
198
236
  }
199
237
  /** The ids the picker must currently omit. */
200
238
  async hidden() {
@@ -228,9 +266,13 @@ export class KimiAdapter extends LlmAdapter {
228
266
  };
229
267
  }
230
268
  const reasoning = reasoningInfo(entry);
269
+ // A fast variant is a wire id of its own — the button switches to it and back, so the
270
+ // selection must keep that id — while an ordinary alias is just another spelling of
271
+ // the row and resolves onto it.
272
+ const pair = this.fastVariants.pairOf(model);
231
273
  return {
232
274
  provider: KIMI_PROVIDER_ID,
233
- id: entry.id,
275
+ id: pair !== undefined && pair.wireId === model ? model : entry.id,
234
276
  name: entry.name,
235
277
  ...entry.description === undefined ? {} : { description: entry.description },
236
278
  ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
@@ -436,17 +478,25 @@ export class KimiAdapter extends LlmAdapter {
436
478
  .filter((id) => typeof id === 'string' && id.length > 0);
437
479
  // Image models are never picker rows: they are agent-called, always on.
438
480
  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.
481
+ // Fast/lightspeed variants ride with their base row: they share the row's
482
+ // visibility switch and are reached by the composer's quick-mode button, not a
483
+ // separate row. The live list is the authority on what this account serves, so
484
+ // it is recorded first, and each variant pairs with the longest listed prefix
485
+ // of its own name — a provider that names its variants after the base model
486
+ // pairs them with no curated map at all.
441
487
  this.fastVariants.clear();
442
488
  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);
489
+ this.fastVariants.noteListed(id);
490
+ }
491
+ for (const id of allIds) {
492
+ const base = this.fastVariants.pair(id);
493
+ this.fastVariants.note(id, () => base ?? this.entryFor(baseModelOf(id))?.id ?? baseModelOf(id));
448
494
  }
449
- const ids = allIds.filter((id) => !NON_CHAT_ID.test(id));
495
+ this.seedCuratedFastVariants();
496
+ const ids = allIds
497
+ .filter((id) => !NON_CHAT_ID.test(id))
498
+ .map((id) => (isFastVariant(id) ? this.fastVariants.pair(id) ?? baseModelOf(id) : id))
499
+ .filter((id, index, list) => list.indexOf(id) === index);
450
500
  if (ids.length === 0) {
451
501
  this.options.onWarn?.('Kimi model discovery returned an empty catalog; using the built-in catalog');
452
502
  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.15",
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",