dsh-plugin-subscription-oauth 0.16.3 → 0.17.1
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/README.dev.md +1 -1
- package/README.md +7 -6
- package/README.zh.dev.md +1 -1
- package/README.zh.md +7 -6
- package/lib/client/FastModeButton.d.ts +0 -1
- package/lib/client/FastModeButton.js +48 -11
- package/lib/client.js +32 -9
- package/lib/client.js.map +1 -1
- package/lib/index.js +21 -0
- package/lib/protocol.d.ts +9 -0
- package/lib/providers/antigravity.d.ts +34 -4
- package/lib/providers/antigravity.js +100 -28
- package/lib/providers/ark-provider.d.ts +10 -0
- package/lib/providers/ark-provider.js +16 -2
- package/lib/providers/ark.d.ts +104 -10
- package/lib/providers/ark.js +186 -48
- package/lib/providers/catalog.d.ts +92 -0
- package/lib/providers/catalog.js +191 -0
- package/lib/providers/common.d.ts +14 -0
- package/lib/providers/common.js +23 -0
- package/lib/providers/glm-provider.d.ts +10 -0
- package/lib/providers/glm-provider.js +16 -2
- package/lib/providers/glm.d.ts +100 -16
- package/lib/providers/glm.js +182 -85
- package/lib/providers/grok.d.ts +114 -27
- package/lib/providers/grok.js +330 -129
- package/lib/providers/kimi-provider.d.ts +10 -0
- package/lib/providers/kimi-provider.js +16 -4
- package/lib/providers/kimi.d.ts +122 -32
- package/lib/providers/kimi.js +243 -122
- package/lib/providers/mimo-provider.d.ts +14 -1
- package/lib/providers/mimo-provider.js +17 -6
- package/lib/providers/mimo.d.ts +164 -19
- package/lib/providers/mimo.js +318 -19
- package/lib/providers/model-cache.d.ts +57 -0
- package/lib/providers/model-cache.js +105 -0
- package/lib/providers/responses-stream.d.ts +8 -0
- package/lib/providers/responses-stream.js +21 -5
- package/lib/providers/workbuddy-provider.d.ts +17 -0
- package/lib/providers/workbuddy-provider.js +14 -6
- package/lib/providers/workbuddy.d.ts +140 -13
- package/lib/providers/workbuddy.js +501 -59
- package/package.json +1 -1
package/lib/providers/grok.js
CHANGED
|
@@ -16,7 +16,7 @@ import { TokenStore, finiteInstant, nonEmpty } from '../auth/store.js';
|
|
|
16
16
|
import { TokenManager } from '../auth/tokens.js';
|
|
17
17
|
import { ProviderController } from '../auth/provider-controller.js';
|
|
18
18
|
import { proxiedFetch } from '../http.js';
|
|
19
|
-
import { fallbackEntry, findEntry, resolveCatalog } from './catalog.js';
|
|
19
|
+
import { composeCatalog, fallbackEntry, findEntry, liveEntry, liveModelFacts, resolveCatalog } from './catalog.js';
|
|
20
20
|
import { isMissingOrInvalidCredential } from './common.js';
|
|
21
21
|
import { streamProviderResponses, accountPoolHooks } from './responses-stream.js';
|
|
22
22
|
import { readGrokUsage } from './usage.js';
|
|
@@ -66,11 +66,14 @@ const EFFORT_LABELS = {
|
|
|
66
66
|
[FAST_RUNG]: { name: 'Fast', description: 'xAI fast variant of this model' },
|
|
67
67
|
};
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* The cold-start seed: what this build knows about xAI's models before either of the
|
|
70
|
+
* account's own listings has been read.
|
|
70
71
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
72
|
+
* A seed, never a roster. It decides which ids exist only until a listing has been read
|
|
73
|
+
* — the cached one at startup, or a fresh one on "refresh models" — and afterwards it
|
|
74
|
+
* describes only the fields those listings left silent. A fact the endpoint states,
|
|
75
|
+
* a context window included, always wins over anything here, so a model released,
|
|
76
|
+
* renamed or retired server-side reaches the picker without a plugin release.
|
|
74
77
|
*/
|
|
75
78
|
export const GROK_MODELS = [
|
|
76
79
|
{
|
|
@@ -276,6 +279,75 @@ export async function refreshGrokToken(input) {
|
|
|
276
279
|
}
|
|
277
280
|
return parseGrokTokenPayload(payload, input.refreshToken);
|
|
278
281
|
}
|
|
282
|
+
/** Description the picker shows for a model only the endpoint's listing describes. */
|
|
283
|
+
const GROK_LIVE_DESCRIPTION = 'Reported by the xAI model list';
|
|
284
|
+
/**
|
|
285
|
+
* Split one cached or fresh payload into the two listings it holds.
|
|
286
|
+
*
|
|
287
|
+
* The payload is this route's own object, so its shape is validated rather than trusted:
|
|
288
|
+
* a document written by an older build, or corrupted on disk, is a cache miss and never
|
|
289
|
+
* a crash on the way to the picker.
|
|
290
|
+
*
|
|
291
|
+
* @param payload - the cached or freshly assembled `{ public, cli }` payload.
|
|
292
|
+
* @returns the two row lists, or undefined when the payload is not this route's.
|
|
293
|
+
*/
|
|
294
|
+
function listingOf(payload) {
|
|
295
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload))
|
|
296
|
+
return undefined;
|
|
297
|
+
const record = payload;
|
|
298
|
+
const publicRows = record['public'];
|
|
299
|
+
if (!Array.isArray(publicRows))
|
|
300
|
+
return undefined;
|
|
301
|
+
const cliRows = record['cli'];
|
|
302
|
+
return { public: publicRows, cli: Array.isArray(cliRows) ? cliRows : [] };
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Read the capability metadata and the ids one CLI catalog answer named.
|
|
306
|
+
*
|
|
307
|
+
* The shape is xAI's own rather than the OpenAI-compatible one the shared row reader
|
|
308
|
+
* handles: a context window under `context_window`, and a ladder under
|
|
309
|
+
* `reasoning_efforts[].value` gated by `supports_reasoning_effort`. A row that states
|
|
310
|
+
* nothing usable contributes only its id, because the id list is what folds a fast
|
|
311
|
+
* variant onto its base row.
|
|
312
|
+
*
|
|
313
|
+
* @param rows - decoded rows of the CLI catalog answer.
|
|
314
|
+
* @returns the metadata by model id, and every id the catalog named.
|
|
315
|
+
*/
|
|
316
|
+
function cliListingOf(rows) {
|
|
317
|
+
const metadata = new Map();
|
|
318
|
+
const ids = [];
|
|
319
|
+
for (const row of rows) {
|
|
320
|
+
if (typeof row !== 'object' || row === null || Array.isArray(row))
|
|
321
|
+
continue;
|
|
322
|
+
const entry = row;
|
|
323
|
+
const rawId = entry['id'];
|
|
324
|
+
const id = typeof rawId === 'string' && rawId.length > 0 ? rawId : undefined;
|
|
325
|
+
if (id === undefined)
|
|
326
|
+
continue;
|
|
327
|
+
ids.push(id);
|
|
328
|
+
const rawContext = entry['context_window'];
|
|
329
|
+
const contextWindow = typeof rawContext === 'number' && Number.isFinite(rawContext) ? rawContext : undefined;
|
|
330
|
+
const supportsEffort = entry['supports_reasoning_effort'] === true;
|
|
331
|
+
const rawEfforts = entry['reasoning_efforts'];
|
|
332
|
+
const efforts = supportsEffort && Array.isArray(rawEfforts)
|
|
333
|
+
? rawEfforts
|
|
334
|
+
.map((effort) => (typeof effort === 'object' && effort !== null
|
|
335
|
+
? effort['value']
|
|
336
|
+
: undefined))
|
|
337
|
+
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
338
|
+
: [];
|
|
339
|
+
const requested = entry['reasoning_effort'];
|
|
340
|
+
const defaultEffort = supportsEffort && typeof requested === 'string' && efforts.includes(requested)
|
|
341
|
+
? requested
|
|
342
|
+
: undefined;
|
|
343
|
+
metadata.set(id, {
|
|
344
|
+
...contextWindow === undefined ? {} : { contextWindow },
|
|
345
|
+
...efforts.length === 0 ? {} : { reasoningEfforts: efforts },
|
|
346
|
+
...defaultEffort === undefined ? {} : { defaultEffort },
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return { metadata, ids };
|
|
350
|
+
}
|
|
279
351
|
/** Model ids that are clearly not chat models. */
|
|
280
352
|
const NON_CHAT_ID = /(?:^|[-_/])(?:image|video|imagine|embed|embedding|whisper|tts|vision-only)/i;
|
|
281
353
|
/** Model ids that are image-generation models, matched per provider's naming. */
|
|
@@ -438,13 +510,23 @@ export function stripContext(id) {
|
|
|
438
510
|
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
439
511
|
export class GrokAdapter extends LlmAdapter {
|
|
440
512
|
options;
|
|
441
|
-
/**
|
|
442
|
-
|
|
513
|
+
/**
|
|
514
|
+
* The catalog this route serves: entries derived from the endpoint's own two
|
|
515
|
+
* listings, either the pair just read or the pair the cache kept from the last read.
|
|
516
|
+
*
|
|
517
|
+
* Undefined means neither source has produced a listing yet, so the cold-start seed
|
|
518
|
+
* is all this build can honestly offer.
|
|
519
|
+
*/
|
|
520
|
+
live;
|
|
521
|
+
/** Whether the cached listing has been read; it is read at most once per session. */
|
|
522
|
+
hydrated = false;
|
|
443
523
|
/** Capability metadata from the CLI catalog, keyed by model id. */
|
|
444
524
|
metadata = new Map();
|
|
445
525
|
/** Fast-variant wire id per base model, for the picker's quick-mode button. */
|
|
446
526
|
fastVariants = new FastVariantIndex();
|
|
447
|
-
/**
|
|
527
|
+
/** The context variant one base row dispatches to, so `@500k` is never typed by hand. */
|
|
528
|
+
defaultContext = new Map();
|
|
529
|
+
/** Image-generation model id, when the listing reported one. */
|
|
448
530
|
imageModel;
|
|
449
531
|
/** Ids the user added by hand, cached so the sync catalog can name them. */
|
|
450
532
|
userAddedIds = [];
|
|
@@ -452,6 +534,146 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
452
534
|
super();
|
|
453
535
|
this.options = options;
|
|
454
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* Adopt the two listings of one live read as this route's catalog.
|
|
539
|
+
*
|
|
540
|
+
* Everything the picker shows is derived here from the endpoint's own answers: the
|
|
541
|
+
* public `/models` rows decide membership and state each model's display name, window
|
|
542
|
+
* and modalities, and the account's CLI catalog states the reasoning ladder and names
|
|
543
|
+
* the fast variants — the one thing the public list omits.
|
|
544
|
+
*
|
|
545
|
+
* A fast variant or an `@Nk` context variant rides with its base row instead of becoming
|
|
546
|
+
* a row of its own: the variant is a wire id the composer dispatches, never a picker
|
|
547
|
+
* row, so the user always has a way back to the standard model.
|
|
548
|
+
*
|
|
549
|
+
* @param payload - the cached or freshly assembled `{ public, cli }` payload.
|
|
550
|
+
* @returns the entries to publish, in the endpoint's own order; undefined when the
|
|
551
|
+
* payload names no model at all, so the caller keeps whatever it already had.
|
|
552
|
+
*/
|
|
553
|
+
adoptListing(payload) {
|
|
554
|
+
const listing = listingOf(payload);
|
|
555
|
+
if (listing === undefined)
|
|
556
|
+
return undefined;
|
|
557
|
+
const facts = listing.public
|
|
558
|
+
.map((row) => liveModelFacts(row))
|
|
559
|
+
.filter((fact) => fact !== undefined);
|
|
560
|
+
const cli = cliListingOf(listing.cli);
|
|
561
|
+
const chat = facts.filter((fact) => !NON_CHAT_ID.test(fact.id) && !isImageModel(fact.id));
|
|
562
|
+
// Membership comes from the public list, so a payload that named none of it is a miss
|
|
563
|
+
// rather than an empty picker.
|
|
564
|
+
if (chat.length === 0)
|
|
565
|
+
return undefined;
|
|
566
|
+
// One payload is the whole truth, so every table derived from an earlier read is
|
|
567
|
+
// replaced rather than merged: a model the endpoint has retired cannot survive in
|
|
568
|
+
// state left behind by a read that no longer names it.
|
|
569
|
+
this.metadata.clear();
|
|
570
|
+
this.fastVariants.clear();
|
|
571
|
+
this.defaultContext.clear();
|
|
572
|
+
for (const [id, meta] of cli.metadata)
|
|
573
|
+
this.metadata.set(id, meta);
|
|
574
|
+
// Image models are never picker rows: an agent calls them, always on.
|
|
575
|
+
this.imageModel = [...facts.map((fact) => fact.id), ...cli.ids].find((id) => isImageModel(id));
|
|
576
|
+
// Every id either listing named is what a variant may pair with; the pairing itself is
|
|
577
|
+
// the longest listed prefix of the variant's own name.
|
|
578
|
+
const wireIds = [
|
|
579
|
+
...chat.map((fact) => fact.id),
|
|
580
|
+
...cli.ids.filter((id) => !NON_CHAT_ID.test(id) && !isImageModel(id)),
|
|
581
|
+
];
|
|
582
|
+
for (const id of wireIds)
|
|
583
|
+
this.fastVariants.noteListed(id);
|
|
584
|
+
this.noteWireVariants(chat.map((fact) => fact.id));
|
|
585
|
+
this.noteWireVariants(cli.ids);
|
|
586
|
+
// Fold each variant onto its row, keeping the endpoint's order. A row's own facts are
|
|
587
|
+
// its identity and win outright; a variant only fills what they omit, except a context
|
|
588
|
+
// variant, whose window is the one the row actually dispatches to.
|
|
589
|
+
const own = new Map();
|
|
590
|
+
const folds = new Map();
|
|
591
|
+
const windows = new Map();
|
|
592
|
+
const order = [];
|
|
593
|
+
for (const fact of chat) {
|
|
594
|
+
const contextVariant = contextOf(fact.id) !== undefined;
|
|
595
|
+
const rowId = isFastVariant(fact.id)
|
|
596
|
+
? this.fastVariants.pair(fact.id) ?? baseModelOf(fact.id)
|
|
597
|
+
: contextVariant ? stripContext(fact.id) : fact.id;
|
|
598
|
+
if (rowId === '')
|
|
599
|
+
continue;
|
|
600
|
+
if (!own.has(rowId) && !folds.has(rowId))
|
|
601
|
+
order.push(rowId);
|
|
602
|
+
if (contextVariant)
|
|
603
|
+
windows.set(rowId, Math.max(windows.get(rowId) ?? 0, fact.contextWindow ?? 0));
|
|
604
|
+
if (rowId === fact.id) {
|
|
605
|
+
if (!own.has(rowId))
|
|
606
|
+
own.set(rowId, fact);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
folds.set(rowId, [...folds.get(rowId) ?? [], fact]);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return order.map((rowId) => {
|
|
613
|
+
const stated = own.get(rowId);
|
|
614
|
+
const variants = folds.get(rowId) ?? [];
|
|
615
|
+
const fill = (pick) => {
|
|
616
|
+
const ownValue = stated === undefined ? undefined : pick(stated);
|
|
617
|
+
if (ownValue !== undefined)
|
|
618
|
+
return ownValue;
|
|
619
|
+
for (const variant of variants) {
|
|
620
|
+
const value = pick(variant);
|
|
621
|
+
if (value !== undefined)
|
|
622
|
+
return value;
|
|
623
|
+
}
|
|
624
|
+
return undefined;
|
|
625
|
+
};
|
|
626
|
+
const widest = windows.get(rowId);
|
|
627
|
+
const statedWindow = stated?.contextWindow;
|
|
628
|
+
const contextWindow = widest === undefined || (statedWindow ?? 0) >= widest ? statedWindow : widest;
|
|
629
|
+
const inputModalities = fill((fact) => fact.inputModalities);
|
|
630
|
+
const reasoningEfforts = fill((fact) => fact.reasoningEfforts);
|
|
631
|
+
const defaultEffort = fill((fact) => fact.defaultEffort);
|
|
632
|
+
// The CLI catalog is a listing too, so its figures only fill what the public row
|
|
633
|
+
// left silent: a window or a ladder the endpoint already stated is never overridden,
|
|
634
|
+
// and the shipped seed never overrides either of them.
|
|
635
|
+
const meta = [rowId, ...variants.map((variant) => variant.id)]
|
|
636
|
+
.map((id) => this.metadata.get(id))
|
|
637
|
+
.find((entry) => entry !== undefined);
|
|
638
|
+
const context = contextWindow ?? meta?.contextWindow;
|
|
639
|
+
const efforts = reasoningEfforts ?? meta?.reasoningEfforts;
|
|
640
|
+
const metaDefault = meta?.defaultEffort;
|
|
641
|
+
// A default the ladder does not offer would leave the picker with a rung it cannot
|
|
642
|
+
// select, so it is stated only when the ladder that won actually contains it.
|
|
643
|
+
const fallback = defaultEffort
|
|
644
|
+
?? (metaDefault === undefined || efforts !== undefined && !efforts.includes(metaDefault) ? undefined : metaDefault);
|
|
645
|
+
const described = {
|
|
646
|
+
...stated ?? { id: rowId },
|
|
647
|
+
...context === undefined ? {} : { contextWindow: context },
|
|
648
|
+
...efforts === undefined ? {} : { reasoningEfforts: efforts },
|
|
649
|
+
...fallback === undefined ? {} : { defaultEffort: fallback },
|
|
650
|
+
...inputModalities === undefined ? {} : { inputModalities },
|
|
651
|
+
};
|
|
652
|
+
return liveEntry(described, GROK_MODELS, GROK_CONTEXT_WINDOW, GROK_LIVE_DESCRIPTION);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Read the cached listing, once, so the picker works without asking the endpoint.
|
|
657
|
+
*
|
|
658
|
+
* Called before the first catalog query, and fired at construction by the route's
|
|
659
|
+
* factory because the Settings page reads the catalog synchronously. A read failure is
|
|
660
|
+
* a cache miss, never a failure: the route simply has no listing until one is fetched.
|
|
661
|
+
*/
|
|
662
|
+
async hydrate() {
|
|
663
|
+
if (this.hydrated)
|
|
664
|
+
return;
|
|
665
|
+
this.hydrated = true;
|
|
666
|
+
if (this.live !== undefined || this.options.cachedListing === undefined)
|
|
667
|
+
return;
|
|
668
|
+
try {
|
|
669
|
+
const entries = this.adoptListing(await this.options.cachedListing());
|
|
670
|
+
if (entries !== undefined)
|
|
671
|
+
this.live = entries;
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
this.options.onWarn?.(`cached model catalog unavailable (${errorChain(error)})`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
455
677
|
providerInfo(provider) {
|
|
456
678
|
return { id: provider, name: GROK_PROVIDER_NAME };
|
|
457
679
|
}
|
|
@@ -464,6 +686,7 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
464
686
|
// must not appear in the picker, and Auto must not try it.
|
|
465
687
|
if (await this.options.tokens.session() === undefined)
|
|
466
688
|
return [];
|
|
689
|
+
await this.hydrate();
|
|
467
690
|
await this.syncUserModels();
|
|
468
691
|
const hidden = await this.hidden();
|
|
469
692
|
// One row per model, always under the model's own id: the host validates every GUI
|
|
@@ -491,14 +714,18 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
491
714
|
}
|
|
492
715
|
}
|
|
493
716
|
async resolveModel(_provider, model) {
|
|
717
|
+
await this.hydrate();
|
|
494
718
|
await this.syncUserModels();
|
|
495
719
|
// A fast variant is not a catalog row of its own: it answers with its base model's
|
|
496
|
-
// metadata — the same
|
|
720
|
+
// metadata — the same window, name and ladder the picker shows — while the id
|
|
497
721
|
// stays the fast wire id the request must send.
|
|
498
722
|
const pair = this.fastPairOf(model);
|
|
499
723
|
const entry = findEntry(this.catalog(), model)
|
|
500
724
|
?? (pair === undefined ? undefined : findEntry(this.catalog(), pair.baseId));
|
|
501
725
|
if (entry === undefined) {
|
|
726
|
+
// An unknown id is still dispatchable: the endpoint accepts the id it reports, and a
|
|
727
|
+
// model added after this build should not be refused. It gets the conservative
|
|
728
|
+
// defaults, described by the cold-start seed rather than by a guess.
|
|
502
729
|
const template = findEntry(GROK_MODELS, 'grok-4.6');
|
|
503
730
|
const reasoning = template === undefined ? undefined : reasoningInfo(template);
|
|
504
731
|
return {
|
|
@@ -567,45 +794,25 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
567
794
|
return [...this.shippedCatalog(), ...this.addedEntries()];
|
|
568
795
|
}
|
|
569
796
|
/**
|
|
570
|
-
* The catalog to present:
|
|
571
|
-
*
|
|
572
|
-
*
|
|
797
|
+
* The catalog to present: the models both of the endpoint's listings named, in the
|
|
798
|
+
* public list's own order, each with the metadata the listings stated.
|
|
799
|
+
*
|
|
800
|
+
* There is no shipped roster to fall back to. The listings are read once and cached
|
|
801
|
+
* (see {@link hydrate} and {@link fetchCatalog}), and that cached table is what the
|
|
802
|
+
* picker shows until the next read, so a model released, renamed or retired
|
|
803
|
+
* server-side is reflected without a plugin update or a request per refresh.
|
|
804
|
+
*
|
|
805
|
+
* {@link GROK_MODELS} is only the cold-start seed: it decides membership while neither
|
|
806
|
+
* listing has been read, and afterwards describes the fields the listings left silent.
|
|
807
|
+
* A live fact, a context window included, always wins over it.
|
|
573
808
|
*/
|
|
574
809
|
shippedCatalog() {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
seen.add(id);
|
|
581
|
-
const meta = this.metadata.get(id);
|
|
582
|
-
extra.push({
|
|
583
|
-
id,
|
|
584
|
-
name: id,
|
|
585
|
-
description: 'Reported by the xAI model list',
|
|
586
|
-
contextWindow: meta?.contextWindow ?? GROK_CONTEXT_WINDOW,
|
|
587
|
-
inputModalities: ['text', 'image'],
|
|
588
|
-
// A ladder is advertised only when the provider's own catalog gave one.
|
|
589
|
-
...meta?.reasoningEfforts === undefined ? {} : { reasoningEfforts: meta.reasoningEfforts },
|
|
590
|
-
...meta?.defaultEffort === undefined ? {} : { defaultEffort: meta.defaultEffort },
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
// The seeds also pick up live metadata (reasoning ladder, default effort). The
|
|
594
|
-
// curated context window is never narrowed by a live figure: the CLI catalog states
|
|
595
|
-
// 256k for `grok-4.7`, which serves 500k, and a live read may only widen what this
|
|
596
|
-
// build ships.
|
|
597
|
-
const seeded = GROK_MODELS.map((entry) => {
|
|
598
|
-
const meta = this.metadata.get(entry.id);
|
|
599
|
-
if (meta === undefined)
|
|
600
|
-
return entry;
|
|
601
|
-
return {
|
|
602
|
-
...entry,
|
|
603
|
-
contextWindow: Math.max(entry.contextWindow, meta.contextWindow ?? 0),
|
|
604
|
-
...meta.reasoningEfforts === undefined ? {} : { reasoningEfforts: meta.reasoningEfforts },
|
|
605
|
-
...meta.defaultEffort === undefined ? {} : { defaultEffort: meta.defaultEffort },
|
|
606
|
-
};
|
|
810
|
+
return composeCatalog({
|
|
811
|
+
curated: GROK_MODELS,
|
|
812
|
+
...this.live === undefined ? {} : { live: this.live },
|
|
813
|
+
fallbackContextWindow: GROK_CONTEXT_WINDOW,
|
|
814
|
+
liveDescription: GROK_LIVE_DESCRIPTION,
|
|
607
815
|
});
|
|
608
|
-
return [...seeded, ...extra];
|
|
609
816
|
}
|
|
610
817
|
/** Re-read the ids the user added by hand. */
|
|
611
818
|
async syncUserModels() {
|
|
@@ -633,13 +840,21 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
633
840
|
.filter((id) => !known.has(id))
|
|
634
841
|
.map((id) => fallbackEntry(id, 'Added by hand in Settings', GROK_CONTEXT_WINDOW));
|
|
635
842
|
}
|
|
636
|
-
/**
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
843
|
+
/**
|
|
844
|
+
* Adopt the ids a caller reports as the served set, without a listing.
|
|
845
|
+
*
|
|
846
|
+
* {@link fetchCatalog} already derived the entries from the rows it read, and the
|
|
847
|
+
* controller reports the union of every account's ids back here; the entries it
|
|
848
|
+
* recorded are kept, and an id no entry describes gets a conservative row described by
|
|
849
|
+
* the cold-start seed. An id the caller does not report is dropped: the reported set is
|
|
850
|
+
* the endpoint's whole answer.
|
|
851
|
+
*
|
|
852
|
+
* @param ids - model ids the caller reports.
|
|
853
|
+
*/
|
|
854
|
+
discover(ids) {
|
|
855
|
+
const known = new Map((this.live ?? []).map((entry) => [entry.id, entry]));
|
|
856
|
+
this.live = ids.map((id) => known.get(id)
|
|
857
|
+
?? liveEntry({ id }, GROK_MODELS, GROK_CONTEXT_WINDOW, GROK_LIVE_DESCRIPTION));
|
|
643
858
|
}
|
|
644
859
|
/** The fast/lightspeed variant wire id for a base model, when discovery reported one. */
|
|
645
860
|
fastVariant(baseId) {
|
|
@@ -653,6 +868,16 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
653
868
|
imageModelId() {
|
|
654
869
|
return this.imageModel;
|
|
655
870
|
}
|
|
871
|
+
/**
|
|
872
|
+
* Whether this route has read a model listing yet.
|
|
873
|
+
*
|
|
874
|
+
* A variant is derived from the listing, so until one has been read "this model has no
|
|
875
|
+
* variant" and "the listing has not arrived" look identical from outside; a caller that
|
|
876
|
+
* asks once (the composer's quick-mode button) needs to tell them apart.
|
|
877
|
+
*/
|
|
878
|
+
hasListing() {
|
|
879
|
+
return this.live !== undefined;
|
|
880
|
+
}
|
|
656
881
|
/** The curated catalog, for the Settings page's model list. */
|
|
657
882
|
catalogEntries() {
|
|
658
883
|
return this.catalog();
|
|
@@ -662,8 +887,6 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
662
887
|
return this.shippedCatalog();
|
|
663
888
|
}
|
|
664
889
|
/** Build and send one request for one model. */
|
|
665
|
-
/** The default context variant per base model, set during discovery. */
|
|
666
|
-
defaultContext = new Map();
|
|
667
890
|
/** Resolve a model id to the wire id the endpoint accepts (largest context). */
|
|
668
891
|
resolveModelId(model) {
|
|
669
892
|
return this.defaultContext.get(model) ?? model;
|
|
@@ -726,9 +949,18 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
726
949
|
};
|
|
727
950
|
}
|
|
728
951
|
/**
|
|
729
|
-
* Read the
|
|
730
|
-
*
|
|
731
|
-
*
|
|
952
|
+
* Read both of the endpoint's listings and adopt them as this route's catalog.
|
|
953
|
+
*
|
|
954
|
+
* This is the one place a model table is produced, and it is the endpoint's own answer:
|
|
955
|
+
* what the public list names is what the picker offers, and the account's CLI catalog
|
|
956
|
+
* states the capabilities and names the fast variants that list omits. The rows are also
|
|
957
|
+
* handed to {@link GrokAdapterOptions.saveListing}, so the next session starts from this
|
|
958
|
+
* table instead of asking again — the Settings "refresh models" action is what re-reads
|
|
959
|
+
* it, and a picker query never does.
|
|
960
|
+
*
|
|
961
|
+
* @param accessToken - bearer for the account.
|
|
962
|
+
* @param signal - optional cancellation.
|
|
963
|
+
* @returns the model ids now served, or undefined when the listing is unavailable.
|
|
732
964
|
*/
|
|
733
965
|
async fetchCatalog(accessToken, signal) {
|
|
734
966
|
if (!this.options.discovery)
|
|
@@ -743,56 +975,36 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
743
975
|
// A refused credential is reported by the card's account list; only a
|
|
744
976
|
// genuine discovery failure is worth a warning.
|
|
745
977
|
if (response.status !== 401 && response.status !== 403) {
|
|
746
|
-
this.options.onWarn?.(`xAI model discovery failed (HTTP ${response.status});
|
|
978
|
+
this.options.onWarn?.(`xAI model discovery failed (HTTP ${response.status}); keeping the last catalog`);
|
|
747
979
|
}
|
|
748
980
|
return undefined;
|
|
749
981
|
}
|
|
750
982
|
const payload = await response.json();
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
// Fast/lightspeed variants ride with their base model: they share the base's
|
|
758
|
-
// visibility switch and are reached by a picker button, not a separate row.
|
|
759
|
-
// Context variants (@500k) also fold into the base; the largest context wins.
|
|
760
|
-
this.fastVariants.clear();
|
|
761
|
-
this.defaultContext.clear();
|
|
762
|
-
for (const id of rawIds) {
|
|
763
|
-
this.fastVariants.noteListed(id);
|
|
764
|
-
}
|
|
765
|
-
this.noteWireVariants(rawIds);
|
|
766
|
-
const ids = rawIds
|
|
767
|
-
.map((id) => {
|
|
768
|
-
if (isFastVariant(id))
|
|
769
|
-
return this.fastVariants.pair(id) ?? baseModelOf(id);
|
|
770
|
-
const ctx = contextOf(id);
|
|
771
|
-
if (ctx !== undefined)
|
|
772
|
-
return stripContext(id);
|
|
773
|
-
return id;
|
|
774
|
-
})
|
|
775
|
-
.filter((id, index, list) => list.indexOf(id) === index);
|
|
776
|
-
if (ids.length === 0) {
|
|
777
|
-
this.options.onWarn?.('xAI model discovery returned an empty catalog; using the built-in catalog');
|
|
983
|
+
const publicRows = Array.isArray(payload.data) ? payload.data : [];
|
|
984
|
+
const cliRows = await this.readCliCatalog(fetchFn, signal, accessToken);
|
|
985
|
+
const listing = { public: publicRows, cli: cliRows };
|
|
986
|
+
const entries = this.adoptListing(listing);
|
|
987
|
+
if (entries === undefined) {
|
|
988
|
+
this.options.onWarn?.('xAI model discovery returned no usable catalog; keeping the last catalog');
|
|
778
989
|
return undefined;
|
|
779
990
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
991
|
+
this.live = entries;
|
|
992
|
+
// The cached table is the endpoint's answer, kept so the next session does not ask
|
|
993
|
+
// again. A cache failure never fails the read that produced it.
|
|
994
|
+
if (this.options.saveListing !== undefined) {
|
|
995
|
+
try {
|
|
996
|
+
await this.options.saveListing(listing);
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
this.options.onWarn?.(`model catalog cache write failed (${errorChain(error)})`);
|
|
1000
|
+
}
|
|
788
1001
|
}
|
|
789
|
-
|
|
790
|
-
return ids;
|
|
1002
|
+
return entries.map((entry) => entry.id);
|
|
791
1003
|
}
|
|
792
1004
|
catch (error) {
|
|
793
1005
|
if (signal?.aborted === true)
|
|
794
1006
|
return undefined;
|
|
795
|
-
this.options.onWarn?.(`xAI model discovery failed;
|
|
1007
|
+
this.options.onWarn?.(`xAI model discovery failed; keeping the last catalog (${errorChain(error)})`);
|
|
796
1008
|
return undefined;
|
|
797
1009
|
}
|
|
798
1010
|
}
|
|
@@ -818,57 +1030,34 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
818
1030
|
}
|
|
819
1031
|
}
|
|
820
1032
|
/**
|
|
821
|
-
* Read
|
|
1033
|
+
* Read the account's CLI catalog; a failure is an empty listing, not an error.
|
|
822
1034
|
*
|
|
823
1035
|
* The catalog answers only for the signed-in account's own bearer — an anonymous
|
|
824
|
-
* request is refused — and it, unlike the public `/models` list,
|
|
825
|
-
* fast variant.
|
|
1036
|
+
* request is refused — and it, unlike the public `/models` list, states per-model
|
|
1037
|
+
* context windows and reasoning ladders and names the account's fast variant. The rows
|
|
1038
|
+
* are returned verbatim so both the fresh catalog and the cached payload are derived
|
|
1039
|
+
* from the same parse.
|
|
826
1040
|
*
|
|
827
1041
|
* @param fetchFn - fetch implementation.
|
|
828
1042
|
* @param signal - caller cancellation.
|
|
829
1043
|
* @param accessToken - the account's bearer.
|
|
830
|
-
* @returns the
|
|
1044
|
+
* @returns the decoded rows, or an empty list when the catalog is unavailable.
|
|
831
1045
|
*/
|
|
832
1046
|
async readCliCatalog(fetchFn, signal, accessToken) {
|
|
833
|
-
const result = new Map();
|
|
834
|
-
const ids = [];
|
|
835
1047
|
try {
|
|
836
1048
|
const response = await fetchFn(GROK_CLI_MODELS_URL, {
|
|
837
1049
|
headers: attributedHeaders({ 'authorization': `Bearer ${accessToken}` }),
|
|
838
1050
|
...signal === undefined ? {} : { signal },
|
|
839
1051
|
});
|
|
840
1052
|
if (!response.ok)
|
|
841
|
-
return
|
|
1053
|
+
return [];
|
|
842
1054
|
const payload = await response.json();
|
|
843
|
-
|
|
844
|
-
const id = typeof entry.id === 'string' && entry.id.length > 0 ? entry.id : undefined;
|
|
845
|
-
if (id === undefined)
|
|
846
|
-
continue;
|
|
847
|
-
ids.push(id);
|
|
848
|
-
const contextWindow = typeof entry.context_window === 'number' && Number.isFinite(entry.context_window)
|
|
849
|
-
? entry.context_window
|
|
850
|
-
: undefined;
|
|
851
|
-
const supportsEffort = entry.supports_reasoning_effort === true;
|
|
852
|
-
const efforts = supportsEffort
|
|
853
|
-
? (entry.reasoning_efforts ?? [])
|
|
854
|
-
.map((effort) => effort.value)
|
|
855
|
-
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
856
|
-
: [];
|
|
857
|
-
const defaultEffort = supportsEffort && typeof entry.reasoning_effort === 'string'
|
|
858
|
-
&& efforts.includes(entry.reasoning_effort)
|
|
859
|
-
? entry.reasoning_effort
|
|
860
|
-
: undefined;
|
|
861
|
-
result.set(id, {
|
|
862
|
-
...contextWindow === undefined ? {} : { contextWindow },
|
|
863
|
-
...efforts.length === 0 ? {} : { reasoningEfforts: efforts },
|
|
864
|
-
...defaultEffort === undefined ? {} : { defaultEffort },
|
|
865
|
-
});
|
|
866
|
-
}
|
|
1055
|
+
return Array.isArray(payload.data) ? payload.data : [];
|
|
867
1056
|
}
|
|
868
1057
|
catch {
|
|
869
1058
|
// Capability metadata is a bonus; the id list is what matters.
|
|
1059
|
+
return [];
|
|
870
1060
|
}
|
|
871
|
-
return { metadata: result, ids };
|
|
872
1061
|
}
|
|
873
1062
|
}
|
|
874
1063
|
/**
|
|
@@ -960,8 +1149,13 @@ export function createGrokProvider(options) {
|
|
|
960
1149
|
const read = await readGrokUsage({ accessToken: token.accessToken, fetchFn: options.discoveryFetchFn ?? proxiedFetch });
|
|
961
1150
|
return read === undefined ? [] : read.windows;
|
|
962
1151
|
},
|
|
1152
|
+
...options.cachedListing === undefined ? {} : { cachedListing: options.cachedListing },
|
|
1153
|
+
...options.saveListing === undefined ? {} : { saveListing: options.saveListing },
|
|
963
1154
|
...options.discoveryFetchFn === undefined ? {} : { fetchFn: options.discoveryFetchFn },
|
|
964
1155
|
});
|
|
1156
|
+
// Read the cached table now rather than at the first picker query: the Settings page
|
|
1157
|
+
// reads the catalog synchronously, so a request-free start must already have it.
|
|
1158
|
+
void adapter.hydrate().catch(() => undefined);
|
|
965
1159
|
const spec = {
|
|
966
1160
|
id: GROK_PROVIDER_ID,
|
|
967
1161
|
displayName: GROK_PROVIDER_NAME,
|
|
@@ -1009,12 +1203,19 @@ export function createGrokProvider(options) {
|
|
|
1009
1203
|
removeModel: (id) => controller.removeModel(id),
|
|
1010
1204
|
usage: (force) => controller.usage(force),
|
|
1011
1205
|
reload: () => controller.reload(),
|
|
1012
|
-
modelVariants: (id) => {
|
|
1206
|
+
modelVariants: async (id) => {
|
|
1207
|
+
// A variant is known only once the route has read its listing, whose rows name it.
|
|
1208
|
+
// The composer asks exactly once, so the read is awaited here: "no fast variant"
|
|
1209
|
+
// must mean the route really has none, not that its table was still loading.
|
|
1210
|
+
await adapter.hydrate();
|
|
1013
1211
|
const pair = adapter.fastPairOf(id);
|
|
1014
1212
|
const image = adapter.imageModelId();
|
|
1015
1213
|
return {
|
|
1016
1214
|
...pair === undefined ? {} : { fast: pair.wireId, fastBase: pair.baseId, fastRung: fastWordOf(pair.wireId) },
|
|
1017
1215
|
...image === undefined ? {} : { image },
|
|
1216
|
+
// Nothing read yet: the answer above is provisional, and the caller is told so
|
|
1217
|
+
// rather than being left to read silence as "this model has no variant".
|
|
1218
|
+
...adapter.hasListing() ? {} : { pending: true },
|
|
1018
1219
|
};
|
|
1019
1220
|
},
|
|
1020
1221
|
};
|
|
@@ -26,6 +26,16 @@ export interface KimiProviderOptions {
|
|
|
26
26
|
defaultModel?: string;
|
|
27
27
|
/** Extra model ids published without shipped metadata. */
|
|
28
28
|
extraModels?: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* The endpoint's last live payload, from this plugin's cache.
|
|
31
|
+
*
|
|
32
|
+
* The cached table is what the picker shows before any request: the endpoint's own
|
|
33
|
+
* answer from the last read, so a release does not need a plugin update and a refresh
|
|
34
|
+
* does not need a request.
|
|
35
|
+
*/
|
|
36
|
+
cachedListing?: () => Promise<unknown>;
|
|
37
|
+
/** Record what a live listing just produced, replacing the cached table. */
|
|
38
|
+
saveListing?: (payload: unknown) => Promise<void>;
|
|
29
39
|
/** Shared model-visibility preferences. */
|
|
30
40
|
preferences: ModelPreferences;
|
|
31
41
|
/** Resolve the attachment service per request. */
|