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.
package/lib/index.js CHANGED
@@ -243,6 +243,8 @@ export function apply(ctx, config = {}) {
243
243
  };
244
244
  registerSubscriptionRpc(ctx, registry, balanceContext);
245
245
  // Warm each route's live catalog for an install that is already signed in.
246
+ // Discovery uses the stored token as-is (no resolve, no refresh), so a dead grant
247
+ // is skipped silently rather than opening the browser.
246
248
  for (const id of registry.ids()) {
247
249
  void registry.discover(id, false).catch((error) => {
248
250
  onWarn(`${id} startup discovery failed: ${error instanceof Error ? error.message : String(error)}`);
package/lib/protocol.d.ts CHANGED
@@ -324,7 +324,7 @@ export interface RpcMap {
324
324
  value: CcSwitchBalanceResult;
325
325
  };
326
326
  'model-variants': {
327
- payload: DiscoverRequest;
327
+ payload: ModelVariantsRequest;
328
328
  value: ModelVariantsResult;
329
329
  };
330
330
  }
@@ -357,11 +357,23 @@ export type BalanceResult =
357
357
  export interface ModelVariants {
358
358
  /** Fast/lightspeed variant wire id, when the provider reported one. */
359
359
  fast?: string;
360
+ /**
361
+ * The base model the fast variant belongs to.
362
+ *
363
+ * Sent with `fast` so the picker button can switch back off when the selection is
364
+ * already the fast variant, which is not itself a listed row.
365
+ */
366
+ fastBase?: string;
360
367
  /** Image-generation model wire id, when the provider reported one. */
361
368
  image?: string;
362
369
  }
363
370
  /** Result of `model-variants`: the variants for one model id. */
364
371
  export type ModelVariantsResult = ModelVariants | undefined;
372
+ /** Payload of `model-variants`. */
373
+ export interface ModelVariantsRequest {
374
+ provider: string;
375
+ id: string;
376
+ }
365
377
  /** Payload of `deepseek-balance`. */
366
378
  export interface DeepSeekBalanceRequest {
367
379
  /** Client metadata the host's account service expects. */
@@ -14,20 +14,40 @@
14
14
  */
15
15
  import type { UsageWindow } from '../protocol.js';
16
16
  import type { CatalogEntry } from './catalog.js';
17
- /** True when a model id names a specific tier of a family, not a model with a ladder. */
18
- export declare function isTierMarked(id: string): boolean;
19
- /** The base family a tier-marked id belongs to (`gemini-3-flash-agent` → `gemini-3-flash`). */
20
- export declare function baseFamilyOf(id: string): string;
17
+ /** True when a model id names an image-generation model. */
18
+ export declare function isImageModel(id: string): boolean;
19
+ /**
20
+ * The wire id one gemini flash family and effort level map to when no live listing
21
+ * has been parsed yet.
22
+ *
23
+ * The live listing is the authority (see `wireIdFor`); this is the fallback that
24
+ * keeps a cold start — a persisted selection used before discovery ran — from
25
+ * sending a bare family id the endpoint does not serve. The service names tiers
26
+ * inconsistently: the High tier of 3.5 Flash is `gemini-3.5-flash-high` in some
27
+ * groups and `gemini-3.5-flash-agent` in others; 3.8 Flash is
28
+ * `gemini-3.8-flash-tiered`.
29
+ */
30
+ export declare function geminiTierId(base: string, effort: string): string;
21
31
  /** Antigravity's own reasoning ladder, per model family. */
22
32
  export declare function antigravityEffortsFor(model: string): readonly string[];
33
+ /**
34
+ * The wire id one picker row and effort level must dispatch.
35
+ *
36
+ * @param rowId - the row the picker offered.
37
+ * @param effort - the chosen level, when the caller picked one.
38
+ * @returns the wire id, or undefined when no live listing covers this row.
39
+ */
40
+ export declare function wireIdFor(rowId: string, effort?: string): string | undefined;
41
+ /** Collapse families into one row each, as magpie does. */
42
+ export declare function collapseFamilies(raw: CatalogEntry[]): CatalogEntry[];
23
43
  /**
24
44
  * Read the agent-callable catalog from a `fetchAvailableModels` response.
25
45
  *
26
- * Every listed id is a wire id the response itself named, so each row stays
27
- * dispatchable; the filter removes the surfaces the agent envelope cannot call
28
- * (tabs, commit messages, transcription, standalone image generation) and keeps the
29
- * response's own grouping order. A tier-suffixed id advertises no effort ladder,
30
- * because its suffix already fixes the tier.
46
+ * The response's own grouping decides which ids the agent envelope may call; this
47
+ * removes the surfaces it cannot (tabs, commit messages, transcription, standalone
48
+ * image generation) and keeps the response's own order. Tiers of one family then
49
+ * collapse into the single picker row that serves them, with a wire-id map so the
50
+ * request still names an id the account listed (see `wireIdFor`).
31
51
  *
32
52
  * @param payload - the decoded response.
33
53
  * @returns the catalog entries, in the response's order.
@@ -12,6 +12,7 @@
12
12
  *
13
13
  * @module dsh-plugin-subscription-oauth/providers/antigravity-catalog
14
14
  */
15
+ import { baseCandidatesOf } from './grok.js';
15
16
  /** Effort rungs that fold into one picker row. */
16
17
  const EFFORTS = ['low', 'medium', 'high'];
17
18
  /** True for a plain object usable as a JSON record. */
@@ -48,19 +49,33 @@ function modelInfo(raw) {
48
49
  ...raw.supportsImages === true ? { supportsImages: true } : {},
49
50
  };
50
51
  }
51
- /** A model id whose suffix or infix names a fixed reasoning tier, folded into its base family. */
52
- const TIER_MARKED = /(?:^|[-_])(?:extra-low|low|medium|high|max)(?:$|[-_])/i;
53
- /** True when a model id names a specific tier of a family, not a model with a ladder. */
54
- export function isTierMarked(id) {
55
- return TIER_MARKED.test(id);
52
+ /** Model ids that are image-generation models, never picker rows. */
53
+ const IMAGE_MODEL = /(?:^|[-_])(?:image|imagine|draw|pic|photo)(?:[-_]|$)/i;
54
+ /** True when a model id names an image-generation model. */
55
+ export function isImageModel(id) {
56
+ return IMAGE_MODEL.test(id);
56
57
  }
57
- /** The base family a tier-marked id belongs to (`gemini-3-flash-agent` → `gemini-3-flash`). */
58
- export function baseFamilyOf(id) {
59
- // Remove the tier token wherever it sits (suffix or infix), then tidy any trailing
60
- // separator the removal left behind.
61
- return id
62
- .replace(/[-_](?:extra-low|low|medium|high|max)(?=[-_]|$)/i, '')
63
- .replace(/[-_]$/, '');
58
+ /**
59
+ * The wire id one gemini flash family and effort level map to when no live listing
60
+ * has been parsed yet.
61
+ *
62
+ * The live listing is the authority (see `wireIdFor`); this is the fallback that
63
+ * keeps a cold start — a persisted selection used before discovery ran — from
64
+ * sending a bare family id the endpoint does not serve. The service names tiers
65
+ * inconsistently: the High tier of 3.5 Flash is `gemini-3.5-flash-high` in some
66
+ * groups and `gemini-3.5-flash-agent` in others; 3.8 Flash is
67
+ * `gemini-3.8-flash-tiered`.
68
+ */
69
+ export function geminiTierId(base, effort) {
70
+ if (effort === 'high') {
71
+ if (base === 'gemini-3.5-flash')
72
+ return 'gemini-3.5-flash-agent';
73
+ if (base === 'gemini-3.8-flash')
74
+ return 'gemini-3.8-flash-tiered';
75
+ if (base === 'gemini-3-flash')
76
+ return 'gemini-3-flash-agent';
77
+ }
78
+ return `${base}-${effort}`;
64
79
  }
65
80
  /** Antigravity's own reasoning ladder, per model family. */
66
81
  export function antigravityEffortsFor(model) {
@@ -72,6 +87,180 @@ export function antigravityEffortsFor(model) {
72
87
  return ['low', 'medium', 'high'];
73
88
  return [];
74
89
  }
90
+ // ── magpie-style family collapse ─────────────────────────────────────────
91
+ /** One effort word of Antigravity's ids and the level it means. */
92
+ const EFFORT_WORDS = [
93
+ { word: 'extra-high', level: 'xhigh' },
94
+ { word: 'extra-low', level: 'minimal' },
95
+ { word: 'xhigh', level: 'xhigh' },
96
+ { word: 'minimal', level: 'minimal' },
97
+ // A flash family's High tier is spelled `-high`, `-tiered` or `-agent` depending on
98
+ // the generation, so all three mean the same rung of the ladder.
99
+ { word: 'tiered', level: 'high' },
100
+ { word: 'agent', level: 'high' },
101
+ { word: 'low', level: 'low' },
102
+ { word: 'medium', level: 'medium' },
103
+ { word: 'high', level: 'high' },
104
+ { word: 'max', level: 'max' },
105
+ ];
106
+ /** A ladder's canonical order. */
107
+ const LEVEL_ORDER = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
108
+ /** An id's family and the level its effort word says, '' when it has none. */
109
+ function splitEffortId(id) {
110
+ for (const { word, level } of EFFORT_WORDS) {
111
+ if (id.endsWith(`-${word}`)) {
112
+ const base = id.slice(0, id.length - word.length - 1);
113
+ if (base.length > 0)
114
+ return { family: base, effort: level };
115
+ }
116
+ }
117
+ return { family: id, effort: '' };
118
+ }
119
+ /** The level a display name's bracketed suffix says ("Gemini 3.7 Flash (High)" → high). */
120
+ function nameEffort(name) {
121
+ const open = name.lastIndexOf(' (');
122
+ if (open < 0 || !name.endsWith(')'))
123
+ return { base: name, effort: '' };
124
+ const inner = name.slice(open + 2, -1).toLowerCase();
125
+ let effort = '';
126
+ if (inner === 'minimal' || inner === 'extra low')
127
+ effort = 'minimal';
128
+ else if (inner === 'low')
129
+ effort = 'low';
130
+ else if (inner === 'medium')
131
+ effort = 'medium';
132
+ else if (inner === 'high')
133
+ effort = 'high';
134
+ else if (inner === 'extra high' || inner === 'xhigh')
135
+ effort = 'xhigh';
136
+ if (effort === '')
137
+ return { base: name, effort: '' };
138
+ return { base: name.slice(0, open).trim(), effort };
139
+ }
140
+ /**
141
+ * Group raw entries into the families the service names.
142
+ *
143
+ * Grouping follows the display name, not the id, because the service names the family
144
+ * and lets the wire id drift: the High tier of Gemini 3.5 Flash is
145
+ * `gemini-3-flash-agent` — an id that names a different generation than the name it
146
+ * carries. A level is stated by the name's own bracket when it has one, and by the
147
+ * id's effort word otherwise; the family a variant belongs to is the longest listed
148
+ * prefix of its own id, so `gemini-3.5-flash-low` folds into whichever listed row its
149
+ * prefix names with no hand-written family map.
150
+ *
151
+ * @param raw - one entry per wire id the listing exposed.
152
+ * @returns the families, in the order their first member appeared.
153
+ */
154
+ function familiesOf(raw) {
155
+ const byName = new Map();
156
+ const order = [];
157
+ const listed = new Set(raw.map((entry) => entry.id));
158
+ const familyOf = (id, fallback) => {
159
+ for (const base of baseCandidatesOf(id)) {
160
+ if (listed.has(base))
161
+ return base;
162
+ }
163
+ return fallback;
164
+ };
165
+ for (const entry of raw) {
166
+ const { base: name, effort: named } = nameEffort(entry.name);
167
+ const { family: idBase, effort: idLevel } = splitEffortId(entry.id);
168
+ // The family's key: the name's bracket-free base, when the name states a level, so
169
+ // "Gemini 3.5 Flash (Medium)" is always that family even if no listed id starts
170
+ // with `gemini-3.5-flash`; otherwise the longest listed prefix of the id itself.
171
+ const familyBase = named !== '' ? name : (idBase !== entry.id ? familyOf(entry.id, name) : name);
172
+ let f = byName.get(familyBase);
173
+ if (f === undefined) {
174
+ f = { name: familyBase, variants: [], base: '' };
175
+ byName.set(familyBase, f);
176
+ order.push(f);
177
+ }
178
+ if (f.base === '' && idBase !== entry.id)
179
+ f.base = idBase;
180
+ f.variants.push({
181
+ id: entry.id,
182
+ contextWindow: entry.contextWindow,
183
+ inputModalities: entry.inputModalities ?? ['text'],
184
+ level: named !== '' ? named : idLevel,
185
+ });
186
+ }
187
+ return order;
188
+ }
189
+ /** The rungs a family defaults to, best first, when no variant is the bare model. */
190
+ const DEFAULT_ORDER = ['xhigh', 'high', 'medium', 'low', 'minimal', 'max'];
191
+ /** The variant whose id the family's row carries: the bare model, else its top rung. */
192
+ function defaultVariant(f) {
193
+ const bare = f.variants.find((v) => v.level === '');
194
+ if (bare !== undefined)
195
+ return bare;
196
+ for (const level of DEFAULT_ORDER) {
197
+ const found = f.variants.find((v) => v.level === level);
198
+ if (found !== undefined)
199
+ return found;
200
+ }
201
+ return f.variants[0];
202
+ }
203
+ /**
204
+ * The wire id each picker row must send per effort level, as the last parsed family
205
+ * listing reported it.
206
+ *
207
+ * Collapsing gives the picker one row per family, but the endpoint only serves the
208
+ * suffixed ids the listing named: the High tier of Gemini 3.5 Flash is
209
+ * `gemini-3-flash-agent`, and `gpt-oss-120b` only exists as `gpt-oss-120b-medium`.
210
+ * Nothing here is invented — a level the listing did not name falls back to the
211
+ * family's own default id, and only a tiered gemini family may synthesise the tier
212
+ * spelling the endpoint accepts (see `geminiTierId`).
213
+ */
214
+ const wireIds = new Map();
215
+ /**
216
+ * The wire id one picker row and effort level must dispatch.
217
+ *
218
+ * @param rowId - the row the picker offered.
219
+ * @param effort - the chosen level, when the caller picked one.
220
+ * @returns the wire id, or undefined when no live listing covers this row.
221
+ */
222
+ export function wireIdFor(rowId, effort) {
223
+ const row = wireIds.get(rowId);
224
+ if (row === undefined)
225
+ return undefined;
226
+ const chosen = effort === undefined ? undefined : row.levels.get(effort);
227
+ if (chosen !== undefined)
228
+ return chosen;
229
+ // A tiered gemini family the listing exposed only one way still takes its own tier
230
+ // spelling for the three levels this build knows (`gemini-3.8-flash` + low →
231
+ // `gemini-3.8-flash-low`). Nothing else is synthesised.
232
+ const tiered = effort === 'low' || effort === 'medium' || effort === 'high';
233
+ if (tiered && row.base !== '' && row.base !== rowId)
234
+ return geminiTierId(row.base, effort);
235
+ return row.levels.get('') ?? rowId;
236
+ }
237
+ /** Collapse families into one row each, as magpie does. */
238
+ export function collapseFamilies(raw) {
239
+ const out = [];
240
+ for (const f of familiesOf(raw)) {
241
+ const levels = new Map();
242
+ for (const v of f.variants)
243
+ if (!levels.has(v.level))
244
+ levels.set(v.level, v.id);
245
+ const def = defaultVariant(f);
246
+ levels.set('', def.id);
247
+ wireIds.set(def.id, { levels, base: f.base });
248
+ const aliases = f.variants.map((v) => v.id).filter((id) => id !== def.id);
249
+ out.push({
250
+ // The row keeps an id the account listed, so a selection made from it stays
251
+ // dispatchable; the name is what the picker shows.
252
+ id: def.id,
253
+ name: f.name,
254
+ ...aliases.length === 0 ? {} : { aliases },
255
+ contextWindow: Math.min(...f.variants.map((v) => v.contextWindow)),
256
+ inputModalities: def.inputModalities,
257
+ // Only the rungs this listing really serves: a level with no id of its own must
258
+ // not be offered as a choice the request would ignore.
259
+ reasoningEfforts: LEVEL_ORDER.filter((level) => levels.has(level)),
260
+ });
261
+ }
262
+ return out;
263
+ }
75
264
  /**
76
265
  * The ids the agent can actually call, in the order the response groups them.
77
266
  *
@@ -154,11 +343,11 @@ const NON_AGENT = /(?:^|[-_])(?:tab|tabs|commit|transcription|audio|embedding|co
154
343
  /**
155
344
  * Read the agent-callable catalog from a `fetchAvailableModels` response.
156
345
  *
157
- * Every listed id is a wire id the response itself named, so each row stays
158
- * dispatchable; the filter removes the surfaces the agent envelope cannot call
159
- * (tabs, commit messages, transcription, standalone image generation) and keeps the
160
- * response's own grouping order. A tier-suffixed id advertises no effort ladder,
161
- * because its suffix already fixes the tier.
346
+ * The response's own grouping decides which ids the agent envelope may call; this
347
+ * removes the surfaces it cannot (tabs, commit messages, transcription, standalone
348
+ * image generation) and keeps the response's own order. Tiers of one family then
349
+ * collapse into the single picker row that serves them, with a wire-id map so the
350
+ * request still names an id the account listed (see `wireIdFor`).
162
351
  *
163
352
  * @param payload - the decoded response.
164
353
  * @returns the catalog entries, in the response's order.
@@ -167,54 +356,21 @@ export function parseAntigravityModels(payload) {
167
356
  if (!isRecord(payload) || !isRecord(payload.models))
168
357
  return [];
169
358
  const models = payload.models;
170
- // TEMP diagnostics: write the raw model-id list to a file so an over-eager filter is
171
- // inspectable after a discovery run, since the node half's stdout is unreachable in
172
- // the desktop shell.
173
- try {
174
- const all = Object.keys(models);
175
- const kept = agentIds(payload);
176
- const { writeFileSync } = require('node:fs');
177
- const { homedir } = require('node:os');
178
- const { join } = require('node:path');
179
- writeFileSync(join(homedir(), '.dsh', 'antigravity-models-debug.json'), JSON.stringify({ all, kept, dropped: all.filter((id) => !kept.includes(id)) }, null, 2), 'utf8');
180
- }
181
- catch { /* diagnostics never break parsing */ }
182
- const entries = [];
183
- const seenBase = new Set();
359
+ const raw = [];
184
360
  for (const wireId of agentIds(payload)) {
185
- const info = modelInfo(models[wireId]);
186
- // Tier-marked ids (gemini-3.5-flash-low, gemini-3-flash-agent, …) are the family's
187
- // fixed tiers; they fold into the base family row so the picker shows one entry with
188
- // a ladder, not one row per tier. The base row is inserted the first time a tier of
189
- // it is seen.
190
- if (isTierMarked(wireId)) {
191
- const base = baseFamilyOf(wireId);
192
- if (seenBase.has(base))
193
- continue;
194
- seenBase.add(base);
195
- const baseInfo = modelInfo(models[base]) ?? info;
196
- entries.push({
197
- id: base,
198
- name: displayNameOf(base, models[base] ?? {}),
199
- contextWindow: baseInfo.maxTokens ?? baseInfo.maxInputTokens ?? baseInfo.inputTokenLimit ?? 1_048_576,
200
- inputModalities: baseInfo.supportsImages === true ? ['text', 'image'] : ['text'],
201
- reasoningEfforts: antigravityEffortsFor(base),
202
- });
361
+ // Image-generation models are never picker rows: they are agent-called, always on.
362
+ if (isImageModel(wireId))
203
363
  continue;
204
- }
205
- seenBase.add(wireId);
206
- const tiers = /-(?:low|medium|high)$/.test(wireId);
207
- const efforts = tiers ? [] : antigravityEffortsFor(wireId);
364
+ const info = modelInfo(models[wireId]);
208
365
  const contextWindow = info.maxTokens ?? info.maxInputTokens ?? info.inputTokenLimit ?? 1_048_576;
209
- entries.push({
366
+ raw.push({
210
367
  id: wireId,
211
368
  name: displayNameOf(wireId, models[wireId] ?? {}),
212
369
  contextWindow,
213
370
  inputModalities: info.supportsImages === true ? ['text', 'image'] : ['text'],
214
- ...efforts.length === 0 ? {} : { reasoningEfforts: efforts },
215
371
  });
216
372
  }
217
- return entries;
373
+ return collapseFamilies(raw);
218
374
  }
219
375
  /** A quota reading normalised to a percentage spent. */
220
376
  function usedPercentOf(raw) {
@@ -228,6 +228,8 @@ export declare class AntigravityAdapter extends LlmAdapter {
228
228
  private resetAt;
229
229
  /** Remember an account as spent (its exact reset is learned separately). */
230
230
  private noteSpent;
231
+ /** Resolve the default account's token without the preempt window. */
232
+ private resolveNoPreempt;
231
233
  /** Translate an auth failure into the message a user can act on. */
232
234
  private refreshHint;
233
235
  /** Build and send one streaming request for one model. */
@@ -20,7 +20,7 @@ import { ProviderController } from '../auth/provider-controller.js';
20
20
  import { proxiedFetch } from '../http.js';
21
21
  import { fallbackEntry, findEntry, resolveCatalog } from './catalog.js';
22
22
  import { idleWatchdog, mapFetchFailure, httpLlmError } from './common.js';
23
- import { parseAntigravityModelQuota, parseAntigravityModels, parseAntigravityQuotaSummary, } from './antigravity-catalog.js';
23
+ import { parseAntigravityModelQuota, parseAntigravityModels, parseAntigravityQuotaSummary, geminiTierId, wireIdFor, } from './antigravity-catalog.js';
24
24
  import { toAntigravityRequest, streamAntigravity, antigravityReasoning } from '../translate/antigravity.js';
25
25
  /** Route key this module registers. */
26
26
  export const ANTIGRAVITY_PROVIDER_ID = 'antigravity';
@@ -456,7 +456,14 @@ export class AntigravityAdapter extends LlmAdapter {
456
456
  // `auto` owns the fallback chain now, so this route serves exactly the
457
457
  // model it was asked for — but it draws on every account the provider holds, so a
458
458
  // spent allowance moves to the next account rather than failing the call.
459
- const model = options.model;
459
+ // A picker row is the family, not the wire id: `gemini-3.8-flash` is served as
460
+ // `gemini-3.8-flash-tiered` and `gpt-oss-120b` only exists as
461
+ // `gpt-oss-120b-medium`. The live listing's own mapping is the authority; the
462
+ // built-in tier names are the fallback for a cold start, before discovery ran.
463
+ const model = wireIdFor(options.model, options.reasoningEffort)
464
+ ?? (/^gemini-.*-flash$/.test(options.model)
465
+ ? geminiTierId(options.model, options.reasoningEffort ?? 'medium')
466
+ : options.model);
460
467
  // Accounts come from the manager when it can list them; a minimal manager (or a
461
468
  // test double) without `accounts()` means a single account, which is just the
462
469
  // default one.
@@ -467,7 +474,10 @@ export class AntigravityAdapter extends LlmAdapter {
467
474
  // diagnosable from the browser where the node stdout is hidden.
468
475
  const diag = [`pool=${accounts.join(',') || '(none)'}`];
469
476
  const tried = new Set();
470
- let token = await this.options.tokens.resolve();
477
+ // Resolve without preempt: use the stored token if it is still within its life,
478
+ // refresh only when it has actually expired. The 5-minute preempt was causing
479
+ // a refresh on every stream, which made Google show the consent page.
480
+ let token = await this.resolveNoPreempt();
471
481
  // Stay on the default account until it refuses. opencodex keeps account affinity —
472
482
  // a request stays on one account and only moves after a 429 — because switching
473
483
  // accounts on every request is what reads to Google as two accounts in play at once
@@ -571,6 +581,18 @@ export class AntigravityAdapter extends LlmAdapter {
571
581
  noteSpent(account) {
572
582
  this.options.onWarn?.(`antigravity: account ${account} allowance spent; moving to another account`);
573
583
  }
584
+ /** Resolve the default account's token without the preempt window. */
585
+ async resolveNoPreempt() {
586
+ const found = await this.options.tokens.session();
587
+ if (found === undefined) {
588
+ throw new ProviderError(`${ANTIGRAVITY_PROVIDER_NAME} no account is signed in`, REAUTH_REQUIRED);
589
+ }
590
+ const { account, session } = found;
591
+ if (Date.now() < session.expiresAt) {
592
+ return { account, accessToken: session.accessToken, expiresAt: session.expiresAt };
593
+ }
594
+ return await this.options.tokens.resolve(account, true);
595
+ }
574
596
  /** Translate an auth failure into the message a user can act on. */
575
597
  refreshHint(error) {
576
598
  if (error instanceof ProviderError && error.code === REAUTH_REQUIRED) {
@@ -116,8 +116,86 @@ interface GrokCliMetadata {
116
116
  export declare function isImageModel(id: string): boolean;
117
117
  /** True when a model id names a fast/lightspeed variant of a base model. */
118
118
  export declare function isFastVariant(id: string): boolean;
119
- /** The base model id a fast variant belongs to (`grok-4.7-fast` → `grok-4.7`). */
119
+ /**
120
+ * Every prefix of one model id, longest first, down to its first token.
121
+ *
122
+ * `grok-4.7-build-fast` → [`grok-4.7-build`, `grok-4.7`, `grok`]. A fast variant pairs
123
+ * with the longest prefix that is itself a listed model id, which is how
124
+ * `grok-4.7-build-fast` lands on `grok-4.7` (its own prefix `grok-4.7-build` is not a
125
+ * model) instead of inventing a base that no catalog row serves.
126
+ *
127
+ * @param id - model id.
128
+ * @returns the candidate bases, longest first.
129
+ */
130
+ export declare function baseCandidatesOf(id: string): readonly string[];
131
+ /**
132
+ * The base model id a fast variant belongs to, derived only from its own name.
133
+ *
134
+ * The fast token is removed, then any trailing generation qualifier (`build`, `beta`,
135
+ * `rc`, …) left behind by that removal, then any `@Nk` context suffix. What remains is
136
+ * the base (`grok-4.7-build-fast@500k` → `grok-4.7`, `kimi-for-coding-highspeed` →
137
+ * `kimi-for-coding`). Whether the base is a real catalog row is the index's job, not
138
+ * this function's.
139
+ */
120
140
  export declare function baseModelOf(id: string): string;
141
+ /**
142
+ * The fast variant of one picker row, from either side of the pair.
143
+ *
144
+ * Shared by every route that discovers fast ids, so the composer's quick-mode button
145
+ * behaves the same wherever a fast model is identifiable: it finds the pair when asked
146
+ * about the base row, and when asked about the fast id itself — which is what the
147
+ * selection becomes once the mode is on — so it can switch back off again.
148
+ */
149
+ export declare class FastVariantIndex {
150
+ private readonly byBase;
151
+ private readonly rowOfVariant;
152
+ private readonly knownIds;
153
+ /** Forget every pair and every known id, before re-reading a catalog. */
154
+ clear(): void;
155
+ /**
156
+ * Record one id the catalog listed.
157
+ *
158
+ * Listed ids are what a variant may pair with: the pair is the longest listed prefix
159
+ * of the variant's own name, so nothing is invented that no row actually serves.
160
+ *
161
+ * @param id - a wire id from a catalog.
162
+ */
163
+ noteListed(id: string): void;
164
+ /**
165
+ * Pair one fast id with its base, purely from its own name.
166
+ *
167
+ * Every listed prefix of the id is a candidate base, longest first; the first one
168
+ * that is itself a listed model wins. No curated alias or hardcoded map is needed:
169
+ * a provider that names its variants after the base model pairs them automatically.
170
+ *
171
+ * @param id - a wire id the catalog named.
172
+ * @returns the base id, or undefined when no listed prefix names one.
173
+ */
174
+ pair(id: string): string | undefined;
175
+ /**
176
+ * Record one id, if it names a fast variant.
177
+ *
178
+ * The first variant named for a row wins: a catalog that lists two spellings of the
179
+ * same variant (`kimi-for-coding-highspeed` and `kimi-k2.7-code-highspeed`) offers
180
+ * one button, and the earlier entry is the more specific one.
181
+ *
182
+ * @param id - a wire id from a catalog.
183
+ * @param rowOf - maps a base id to the picker row that serves it, when the catalog
184
+ * spells the base differently from the variant's own prefix.
185
+ */
186
+ note(id: string, rowOf?: (base: string) => string | undefined): void;
187
+ /** The fast variant's wire id for one picker row, when the catalog named one. */
188
+ variantOf(baseId: string): string | undefined;
189
+ /** The pair one picker id belongs to, from either side. */
190
+ pairOf(id: string): {
191
+ baseId: string;
192
+ wireId: string;
193
+ } | undefined;
194
+ }
195
+ /** Extract the context size from a model id's `@Nk` suffix, or undefined. */
196
+ export declare function contextOf(id: string): number | undefined;
197
+ /** Strip the `@Nk` context suffix from a model id. */
198
+ export declare function stripContext(id: string): string;
121
199
  /** Constructor dependencies for {@link GrokAdapter}. */
122
200
  export interface GrokAdapterOptions {
123
201
  /** API base; `<base>/responses` and `<base>/models`. */
@@ -190,6 +268,11 @@ export declare class GrokAdapter extends LlmAdapter {
190
268
  discover(ids: readonly string[], metadata?: ReadonlyMap<string, GrokCliMetadata>): void;
191
269
  /** The fast/lightspeed variant wire id for a base model, when discovery reported one. */
192
270
  fastVariant(baseId: string): string | undefined;
271
+ /** The fast pair one picker id belongs to, from either side. */
272
+ fastPairOf(id: string): {
273
+ baseId: string;
274
+ wireId: string;
275
+ } | undefined;
193
276
  /** The image-generation model id, when discovery reported one. */
194
277
  imageModelId(): string | undefined;
195
278
  /** The curated catalog, for the Settings page's model list. */
@@ -197,6 +280,12 @@ export declare class GrokAdapter extends LlmAdapter {
197
280
  /** What this route itself serves, without the ids added by hand. */
198
281
  ownCatalogEntries(): readonly CatalogEntry[];
199
282
  /** Build and send one request for one model. */
283
+ /** The default context variant per base model, set during discovery. */
284
+ private defaultContext;
285
+ /** Resolve a model id to the wire id the endpoint accepts (largest context). */
286
+ resolveModelId(model: string): string;
287
+ /** Record one default-context mapping. */
288
+ private noteContextVariant;
200
289
  private request;
201
290
  /** Resolve image blocks through the attachment service, when it is present. */
202
291
  private imageResolver;
@@ -206,8 +295,25 @@ export declare class GrokAdapter extends LlmAdapter {
206
295
  * @returns the model ids, or undefined when discovery is off or unavailable.
207
296
  */
208
297
  fetchCatalog(accessToken: string, signal?: AbortSignal): Promise<readonly string[] | undefined>;
209
- /** Read per-model capabilities from the CLI catalog; failures are silent. */
210
- private fetchCliMetadata;
298
+ /**
299
+ * Fold the fast and context variants of one id list into their base models.
300
+ *
301
+ * @param ids - wire ids to classify.
302
+ */
303
+ private noteWireVariants;
304
+ /**
305
+ * Read per-model capabilities from the CLI catalog; failures are silent.
306
+ *
307
+ * The catalog answers only for the signed-in account's own bearer — an anonymous
308
+ * request is refused — and it, unlike the public `/models` list, names the account's
309
+ * fast variant.
310
+ *
311
+ * @param fetchFn - fetch implementation.
312
+ * @param signal - caller cancellation.
313
+ * @param accessToken - the account's bearer.
314
+ * @returns the capability metadata and every id the catalog named.
315
+ */
316
+ private readCliCatalog;
211
317
  }
212
318
  /** Everything the `grok` route needs from the plugin configuration. */
213
319
  export interface GrokProviderOptions {