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/ark.js
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* malformed one is told so (`401 The API key format is incorrect`). So the
|
|
12
12
|
* credential is a UUID key, sent as a plain bearer — no request signing.
|
|
13
13
|
* - `/api/coding/v3` is documented by the official CLI as "OpenTOP + CodingPlan
|
|
14
|
-
* API", and
|
|
14
|
+
* API", and its `/models` listing — read once and kept in this plugin's cache, see
|
|
15
|
+
* `hydrate` and `fetchCatalog` — is the roster this route publishes. Nothing in this
|
|
16
|
+
* module decides which models the plan serves, so one released, renamed or retired
|
|
17
|
+
* server-side reaches the picker without a plugin update.
|
|
15
18
|
*
|
|
16
19
|
* @module dsh-plugin-subscription-oauth/providers/ark
|
|
17
20
|
*/
|
|
@@ -20,10 +23,10 @@ import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
|
20
23
|
import { finiteInstant, nonEmpty } from '../auth/store.js';
|
|
21
24
|
import { callArkControl, parseArkFreeUsage, parseArkPlanUsage } from '../auth/ark-control.js';
|
|
22
25
|
import { proxiedFetch } from '../http.js';
|
|
23
|
-
import { findEntry, resolveCatalog } from './catalog.js';
|
|
26
|
+
import { composeCatalog, fallbackEntry, findEntry, liveEntry, liveModelFacts, resolveCatalog } from './catalog.js';
|
|
24
27
|
import { streamProviderResponses, accountPoolHooks } from './responses-stream.js';
|
|
25
28
|
import { toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
26
|
-
import { isImageModel } from './grok.js';
|
|
29
|
+
import { baseModelOf, FastVariantIndex, isFastVariant, isImageModel } from './grok.js';
|
|
27
30
|
/** Model ids that are clearly not chat models. */
|
|
28
31
|
const NON_CHAT_ID = /(?:^|[-_/])(?:image|video|imagine|embed|embedding|whisper|tts|vision-only)/i;
|
|
29
32
|
/** Provider route this module serves. */
|
|
@@ -43,23 +46,12 @@ export const ARK_DEFAULT_MODEL = 'ark-code-latest';
|
|
|
43
46
|
/**
|
|
44
47
|
* Context capacity advertised for the plan's models.
|
|
45
48
|
*
|
|
46
|
-
* The
|
|
47
|
-
* discovery refines this where
|
|
49
|
+
* The plan states no window in this build; the endpoint's own listing supplies one
|
|
50
|
+
* where it names a model, and discovery refines this where it does not.
|
|
48
51
|
*/
|
|
49
52
|
export const ARK_CONTEXT_WINDOW = 262_144;
|
|
50
53
|
/** Output budget used when the caller sets no cap. */
|
|
51
54
|
export const ARK_DEFAULT_MAX_TOKENS = 32_000;
|
|
52
|
-
/** The plan's model roster, as the reference registry records it. */
|
|
53
|
-
export const ARK_MODELS = [
|
|
54
|
-
{ id: 'ark-code-latest', name: 'Ark Code (latest)', description: 'Plan alias for the current coding model', contextWindow: ARK_CONTEXT_WINDOW, inputModalities: ['text'] },
|
|
55
|
-
{ id: 'doubao-seed-2.0-code', name: 'Doubao Seed 2.0 Code', description: 'Coding-tuned Doubao', contextWindow: ARK_CONTEXT_WINDOW, inputModalities: ['text'] },
|
|
56
|
-
{ id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', description: 'Fast DeepSeek', contextWindow: ARK_CONTEXT_WINDOW, inputModalities: ['text'] },
|
|
57
|
-
{ id: 'glm-5.3', name: 'GLM-5.3', description: 'Zhipu flagship', contextWindow: ARK_CONTEXT_WINDOW, inputModalities: ['text'] },
|
|
58
|
-
{ id: 'glm-5.3-flash', name: 'GLM-5.3 Flash', description: 'Fast GLM, reads images', contextWindow: ARK_CONTEXT_WINDOW, inputModalities: ['text', 'image'] },
|
|
59
|
-
{ id: 'glm-5.2', name: 'GLM-5.2', description: 'Previous Zhipu flagship', contextWindow: ARK_CONTEXT_WINDOW, inputModalities: ['text'] },
|
|
60
|
-
{ id: 'kimi-k2.6', name: 'Kimi K2.6', description: 'Moonshot model on Ark', contextWindow: ARK_CONTEXT_WINDOW, inputModalities: ['text'] },
|
|
61
|
-
{ id: 'minimax-m3', name: 'MiniMax M3', description: 'MiniMax model on Ark', contextWindow: ARK_CONTEXT_WINDOW, inputModalities: ['text', 'image'] },
|
|
62
|
-
];
|
|
63
55
|
/**
|
|
64
56
|
* Coerce one decoded credential entry into an Ark session.
|
|
65
57
|
*
|
|
@@ -108,20 +100,107 @@ function stsFromSession(value) {
|
|
|
108
100
|
}
|
|
109
101
|
return { accessKeyId, secretAccessKey, sessionToken, expiresAt };
|
|
110
102
|
}
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
/**
|
|
104
|
+
* The rows of one cached listing.
|
|
105
|
+
*
|
|
106
|
+
* The cache holds the rows this plugin stored; a payload written as the endpoint's own
|
|
107
|
+
* envelope is read too, so a differently shaped cache cannot silently empty the picker.
|
|
108
|
+
*
|
|
109
|
+
* @param payload - whatever `cachedListing` returned.
|
|
110
|
+
* @returns the rows to adopt, or an empty list when the payload names none.
|
|
111
|
+
*/
|
|
112
|
+
function listingRows(payload) {
|
|
113
|
+
if (Array.isArray(payload))
|
|
114
|
+
return payload;
|
|
115
|
+
const data = typeof payload === 'object' && payload !== null
|
|
116
|
+
? payload.data
|
|
117
|
+
: undefined;
|
|
118
|
+
return Array.isArray(data) ? data : [];
|
|
114
119
|
}
|
|
115
120
|
/** The adapter behind the `ark` route. */
|
|
116
121
|
export class ArkAdapter extends LlmAdapter {
|
|
117
122
|
options;
|
|
123
|
+
/**
|
|
124
|
+
* The catalog this route serves: entries derived from the endpoint's own listing,
|
|
125
|
+
* either the one just read or the one the cache kept from the last read.
|
|
126
|
+
*
|
|
127
|
+
* Undefined means neither source has produced a listing yet, so only the configured
|
|
128
|
+
* default model is offered until one does.
|
|
129
|
+
*/
|
|
118
130
|
live;
|
|
119
|
-
/**
|
|
131
|
+
/** Whether the cached listing has been read; it is read at most once per session. */
|
|
132
|
+
hydrated = false;
|
|
133
|
+
/** Image-generation model id, when the listing reported one. */
|
|
120
134
|
imageModel;
|
|
135
|
+
/**
|
|
136
|
+
* Fast-variant wire id per picker row.
|
|
137
|
+
*
|
|
138
|
+
* Derived from the listing rather than a curated alias map: a row whose name is a
|
|
139
|
+
* listed model plus the provider's own fast word rides that row instead of becoming
|
|
140
|
+
* one of its own.
|
|
141
|
+
*/
|
|
142
|
+
fastVariants = new FastVariantIndex();
|
|
121
143
|
constructor(options) {
|
|
122
144
|
super();
|
|
123
145
|
this.options = options;
|
|
124
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Adopt the rows of one live listing as this route's catalog.
|
|
149
|
+
*
|
|
150
|
+
* Everything the picker shows is derived here from the endpoint's own answer: the
|
|
151
|
+
* rows that become picker entries and the fast variant that pairs with each row.
|
|
152
|
+
* Nothing about which models exist, or what each one is, is decided by this build.
|
|
153
|
+
*
|
|
154
|
+
* @param rows - decoded rows of the endpoint's listing, in the endpoint's order.
|
|
155
|
+
* @returns the entries to publish, in picker order.
|
|
156
|
+
*/
|
|
157
|
+
adoptListing(rows) {
|
|
158
|
+
const facts = rows
|
|
159
|
+
.map((row) => liveModelFacts(row))
|
|
160
|
+
.filter((fact) => fact !== undefined);
|
|
161
|
+
// Image models are never picker rows: they are agent-called, always on.
|
|
162
|
+
this.imageModel = facts.find((fact) => isImageModel(fact.id))?.id;
|
|
163
|
+
this.fastVariants.clear();
|
|
164
|
+
for (const fact of facts)
|
|
165
|
+
this.fastVariants.noteListed(fact.id);
|
|
166
|
+
for (const fact of facts) {
|
|
167
|
+
const base = this.fastVariants.pair(fact.id);
|
|
168
|
+
this.fastVariants.note(fact.id, () => base ?? baseModelOf(fact.id));
|
|
169
|
+
}
|
|
170
|
+
// Fold a variant onto its row and drop the ids that are not chat models, keeping the
|
|
171
|
+
// endpoint's order. A row's own facts describe the row; a variant's describe the
|
|
172
|
+
// variant, so a row that is also listed is never overwritten by its variant.
|
|
173
|
+
const byId = new Map();
|
|
174
|
+
for (const fact of facts) {
|
|
175
|
+
if (NON_CHAT_ID.test(fact.id))
|
|
176
|
+
continue;
|
|
177
|
+
const id = isFastVariant(fact.id) ? this.fastVariants.pair(fact.id) ?? baseModelOf(fact.id) : fact.id;
|
|
178
|
+
if (id === fact.id || !byId.has(id))
|
|
179
|
+
byId.set(id, { ...fact, id });
|
|
180
|
+
}
|
|
181
|
+
return [...byId.values()].map((fact) => liveEntry(fact, [], ARK_CONTEXT_WINDOW, 'Reported by the Ark model list'));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Read the cached listing, once, so the picker works without asking the endpoint.
|
|
185
|
+
*
|
|
186
|
+
* Called before the first catalog query. A read failure is a cache miss, never a
|
|
187
|
+
* failure: the route simply has no listing until one is fetched.
|
|
188
|
+
*/
|
|
189
|
+
async hydrate() {
|
|
190
|
+
if (this.hydrated)
|
|
191
|
+
return;
|
|
192
|
+
this.hydrated = true;
|
|
193
|
+
if (this.live !== undefined || this.options.cachedListing === undefined)
|
|
194
|
+
return;
|
|
195
|
+
try {
|
|
196
|
+
const rows = listingRows(await this.options.cachedListing());
|
|
197
|
+
if (rows.length > 0)
|
|
198
|
+
this.live = this.adoptListing(rows);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
this.options.onWarn?.(`cached model catalog unavailable (${error instanceof Error ? error.message : String(error)})`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
125
204
|
providerInfo(provider) {
|
|
126
205
|
return { id: provider, name: ARK_PROVIDER_NAME };
|
|
127
206
|
}
|
|
@@ -133,6 +212,7 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
133
212
|
// must not appear in the picker, and Auto must not try it.
|
|
134
213
|
if (await this.options.tokens.session() === undefined)
|
|
135
214
|
return [];
|
|
215
|
+
await this.hydrate();
|
|
136
216
|
const hidden = await this.hidden();
|
|
137
217
|
// Image models are never picker rows: they are agent-called, always on.
|
|
138
218
|
const imageless = this.catalog().filter((entry) => !isImageModel(entry.id));
|
|
@@ -148,10 +228,21 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
148
228
|
imageModelId() {
|
|
149
229
|
return this.imageModel;
|
|
150
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Whether this route has read a model listing yet.
|
|
233
|
+
*
|
|
234
|
+
* The image model is a row of the listing, so until one has been read "there is no image
|
|
235
|
+
* model" and "the listing has not arrived" look identical from outside; a caller that asks
|
|
236
|
+
* once needs to tell them apart.
|
|
237
|
+
*/
|
|
238
|
+
hasListing() {
|
|
239
|
+
return this.live !== undefined;
|
|
240
|
+
}
|
|
151
241
|
async resolveModel(_provider, model) {
|
|
242
|
+
await this.hydrate();
|
|
152
243
|
const entry = findEntry(this.catalog(), model);
|
|
153
244
|
if (entry === undefined) {
|
|
154
|
-
const template = findEntry(this.catalog(),
|
|
245
|
+
const template = findEntry(this.catalog(), this.defaultModel()) ?? this.catalog()[0];
|
|
155
246
|
return {
|
|
156
247
|
provider: ARK_PROVIDER_ID,
|
|
157
248
|
id: model,
|
|
@@ -200,19 +291,30 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
200
291
|
catalog() {
|
|
201
292
|
return [...this.shippedCatalog(), ...this.addedEntries()];
|
|
202
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* The catalog to present: the models the endpoint's listing named, in its own order,
|
|
296
|
+
* each with the metadata that listing stated, then the configured extras.
|
|
297
|
+
*
|
|
298
|
+
* There is no shipped roster. A listing is read once and cached (see {@link hydrate}
|
|
299
|
+
* and {@link fetchCatalog}), and that cached table is what the picker shows until the
|
|
300
|
+
* next read, so a model released, renamed or retired server-side is reflected without
|
|
301
|
+
* a plugin update. Only the configured default is always offered, because the picker
|
|
302
|
+
* preselects it.
|
|
303
|
+
*/
|
|
203
304
|
shippedCatalog() {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
305
|
+
return composeCatalog({
|
|
306
|
+
curated: [],
|
|
307
|
+
...this.live === undefined ? {} : { live: this.live },
|
|
308
|
+
...this.options.extraModels === undefined ? {} : { extra: this.options.extraModels },
|
|
309
|
+
defaultModel: this.defaultModel(),
|
|
310
|
+
fallbackContextWindow: ARK_CONTEXT_WINDOW,
|
|
311
|
+
liveDescription: 'Reported by the Ark model list',
|
|
312
|
+
extraDescription: 'Extra model from the plugin configuration',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** The model the picker preselects, from configuration. */
|
|
316
|
+
defaultModel() {
|
|
317
|
+
return this.options.defaultModel ?? ARK_DEFAULT_MODEL;
|
|
216
318
|
}
|
|
217
319
|
addedEntries() {
|
|
218
320
|
const own = this.shippedCatalog();
|
|
@@ -221,7 +323,7 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
221
323
|
return [];
|
|
222
324
|
return (Array.isArray(added) ? added : [])
|
|
223
325
|
.filter((id) => findEntry(own, id) === undefined)
|
|
224
|
-
.map(
|
|
326
|
+
.map((id) => fallbackEntry(id, 'Added by hand in Settings', ARK_CONTEXT_WINDOW));
|
|
225
327
|
}
|
|
226
328
|
async hidden() {
|
|
227
329
|
if (this.options.hiddenModels === undefined)
|
|
@@ -233,15 +335,34 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
233
335
|
return new Set();
|
|
234
336
|
}
|
|
235
337
|
}
|
|
236
|
-
/**
|
|
338
|
+
/**
|
|
339
|
+
* Adopt the ids a caller reports as the served set, without a listing.
|
|
340
|
+
*
|
|
341
|
+
* {@link fetchCatalog} already derived the entries from the rows it read, and the
|
|
342
|
+
* controller reports those same ids back here; the entries it recorded are kept. A
|
|
343
|
+
* caller that only has ids (a test, another discovery path) gets conservative entries
|
|
344
|
+
* for them.
|
|
345
|
+
*
|
|
346
|
+
* @param ids - model ids the caller reports.
|
|
347
|
+
*/
|
|
237
348
|
discover(ids) {
|
|
238
|
-
this.live
|
|
349
|
+
const known = new Map((this.live ?? []).map((entry) => [entry.id, entry]));
|
|
350
|
+
this.live = ids.map((id) => known.get(id)
|
|
351
|
+
?? liveEntry({ id }, [], ARK_CONTEXT_WINDOW, 'Reported by the Ark model list'));
|
|
239
352
|
}
|
|
240
353
|
/**
|
|
241
|
-
* Read the plan's live model list.
|
|
354
|
+
* Read the plan's live model list and adopt it as this route's catalog.
|
|
242
355
|
*
|
|
243
|
-
* The Coding Plan answers the OpenAI models shape on its own base
|
|
244
|
-
*
|
|
356
|
+
* The Coding Plan answers the OpenAI models shape on its own base, so the rows are
|
|
357
|
+
* parsed exactly as the other routes parse theirs: what the listing names is what the
|
|
358
|
+
* picker offers, with the display name, window, modalities and ladder it states. The
|
|
359
|
+
* rows are also handed to {@link ArkAdapterOptions.saveListing}, so the next session
|
|
360
|
+
* starts from this table instead of asking again — the Settings "refresh models"
|
|
361
|
+
* action is what re-reads it.
|
|
362
|
+
*
|
|
363
|
+
* @param apiKey - bearer for the account.
|
|
364
|
+
* @param signal - optional cancellation.
|
|
365
|
+
* @returns the model ids now served, or undefined when the listing is unavailable.
|
|
245
366
|
*/
|
|
246
367
|
async fetchCatalog(apiKey, signal) {
|
|
247
368
|
if (!this.options.discovery)
|
|
@@ -253,23 +374,40 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
253
374
|
...signal === undefined ? {} : { signal },
|
|
254
375
|
});
|
|
255
376
|
if (!response.ok) {
|
|
256
|
-
this.options.onWarn?.(`Ark model list unavailable (HTTP ${response.status})`);
|
|
377
|
+
this.options.onWarn?.(`Ark model list unavailable (HTTP ${response.status}); keeping the last catalog`);
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
const payload = await response.json();
|
|
381
|
+
const rows = Array.isArray(payload.data) ? payload.data : [];
|
|
382
|
+
const entries = this.adoptListing(rows);
|
|
383
|
+
if (entries.length === 0) {
|
|
384
|
+
this.options.onWarn?.('Ark model discovery returned an empty catalog; keeping the last catalog');
|
|
257
385
|
return undefined;
|
|
258
386
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
this.imageModel = allIds.find((id) => isImageModel(id));
|
|
265
|
-
const ids = allIds.filter((id) => !NON_CHAT_ID.test(id));
|
|
266
|
-
return ids.length === 0 ? undefined : ids;
|
|
387
|
+
this.live = entries;
|
|
388
|
+
// The cached table is the endpoint's answer, kept so the next session does not ask
|
|
389
|
+
// again. A cache failure never fails the read that produced it.
|
|
390
|
+
await this.saveListing(rows);
|
|
391
|
+
return entries.map((entry) => entry.id);
|
|
267
392
|
}
|
|
268
393
|
catch (error) {
|
|
394
|
+
if (signal?.aborted === true)
|
|
395
|
+
return undefined;
|
|
269
396
|
this.options.onWarn?.(`Ark model list unavailable (${error instanceof Error ? error.message : String(error)})`);
|
|
270
397
|
return undefined;
|
|
271
398
|
}
|
|
272
399
|
}
|
|
400
|
+
/** Persist one live listing, reporting a cache failure without failing the read. */
|
|
401
|
+
async saveListing(rows) {
|
|
402
|
+
if (this.options.saveListing === undefined)
|
|
403
|
+
return;
|
|
404
|
+
try {
|
|
405
|
+
await this.options.saveListing(rows);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
this.options.onWarn?.(`model catalog cache write failed (${error instanceof Error ? error.message : String(error)})`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
273
411
|
/** Send one streaming request over the Responses wire. */
|
|
274
412
|
async request(options, model, apiKey, signal) {
|
|
275
413
|
const resolveImage = this.imageResolver(signal);
|
|
@@ -54,6 +54,98 @@ export interface ResolvedCatalog {
|
|
|
54
54
|
/** Every entry in catalog order, hidden ids included (the Settings list). */
|
|
55
55
|
ranked: CatalogEntry[];
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* One model as a live listing stated it.
|
|
59
|
+
*
|
|
60
|
+
* Every field but the id is optional, and an absent field means the listing was
|
|
61
|
+
* silent rather than that the model lacks the capability: a row states what it
|
|
62
|
+
* states, and a curated entry fills the gaps.
|
|
63
|
+
*/
|
|
64
|
+
export interface LiveModelFacts {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly name?: string | undefined;
|
|
67
|
+
readonly description?: string | undefined;
|
|
68
|
+
readonly contextWindow?: number | undefined;
|
|
69
|
+
readonly inputModalities?: readonly ModelModality[] | undefined;
|
|
70
|
+
readonly reasoningEfforts?: readonly string[] | undefined;
|
|
71
|
+
readonly defaultEffort?: string | undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Read the facts one live listing row states.
|
|
75
|
+
*
|
|
76
|
+
* The field names are the ones these providers actually use, including Kimi's
|
|
77
|
+
* `display_name`/`context_length`/`think_efforts` and the OpenAI-compatible
|
|
78
|
+
* `context_window`/`name` spellings; anything unrecognized is left absent rather than
|
|
79
|
+
* guessed, so a curated entry can still describe the model.
|
|
80
|
+
*
|
|
81
|
+
* @param row - one decoded row of a `/models` payload.
|
|
82
|
+
* @returns the facts, or undefined when the row names no model.
|
|
83
|
+
*/
|
|
84
|
+
export declare function liveModelFacts(row: unknown): LiveModelFacts | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* The entry to publish for one id the endpoint served.
|
|
87
|
+
*
|
|
88
|
+
* What the endpoint stated wins; the curated table fills only what the listing was
|
|
89
|
+
* silent about, so a live figure — the window a model actually serves today — is
|
|
90
|
+
* never overridden by a shipped guess, and a retired figure never survives a live
|
|
91
|
+
* read that contradicts it. A route whose ids are stable aliases the service re-points
|
|
92
|
+
* per release keeps its own labels instead (see the kimi route's alias labels).
|
|
93
|
+
*
|
|
94
|
+
* @param facts - what the listing stated for this id.
|
|
95
|
+
* @param curated - the curated metadata table.
|
|
96
|
+
* @param fallbackContextWindow - window to state when neither source names one.
|
|
97
|
+
* @param description - picker description when neither source states one.
|
|
98
|
+
* @returns the merged entry.
|
|
99
|
+
*/
|
|
100
|
+
export declare function liveEntry(facts: LiveModelFacts, curated: readonly CatalogEntry[], fallbackContextWindow: number, description: string): CatalogEntry;
|
|
101
|
+
/**
|
|
102
|
+
* How one provider's catalog is composed from its curated table and the entries the
|
|
103
|
+
* endpoint itself reported.
|
|
104
|
+
*
|
|
105
|
+
* The endpoint decides membership. A curated table is metadata, not a roster: the ids
|
|
106
|
+
* in it describe what the build knows about a model's name, window and ladder, and it
|
|
107
|
+
* is the membership only while no live list has been read. That is what lets a model
|
|
108
|
+
* released or retired server-side reach the picker without a plugin update, which a
|
|
109
|
+
* roster can never do.
|
|
110
|
+
*/
|
|
111
|
+
export interface CatalogPlan {
|
|
112
|
+
/** Curated entries, in curated order; metadata first, fallback membership second. */
|
|
113
|
+
readonly curated: readonly CatalogEntry[];
|
|
114
|
+
/**
|
|
115
|
+
* Entries the endpoint reported, in its own order.
|
|
116
|
+
*
|
|
117
|
+
* Undefined means discovery has not run (or failed), so the curated table decides
|
|
118
|
+
* membership and the picker is never empty.
|
|
119
|
+
*/
|
|
120
|
+
readonly live?: readonly CatalogEntry[] | undefined;
|
|
121
|
+
/** Ids the plugin configuration adds, on top of the membership. */
|
|
122
|
+
readonly extra?: readonly string[] | undefined;
|
|
123
|
+
/** The model the picker preselects, and therefore always offers. */
|
|
124
|
+
readonly defaultModel?: string | undefined;
|
|
125
|
+
/** Context advertised for an id no curated entry describes. */
|
|
126
|
+
readonly fallbackContextWindow: number;
|
|
127
|
+
/** Description shown for a live id no curated entry describes. */
|
|
128
|
+
readonly liveDescription?: string | undefined;
|
|
129
|
+
/** Description shown for a configured id no curated entry describes. */
|
|
130
|
+
readonly extraDescription?: string | undefined;
|
|
131
|
+
/** Description shown for the default model when no curated entry describes it. */
|
|
132
|
+
readonly defaultDescription?: string | undefined;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Compose one provider's published catalog.
|
|
136
|
+
*
|
|
137
|
+
* The membership is the live entries in the endpoint's own order when a listing has
|
|
138
|
+
* been read, and the curated order otherwise. The configured default comes first —
|
|
139
|
+
* the picker preselects it, so it must be selectable — and configured extras last.
|
|
140
|
+
* Metadata always comes from the source that described the id: a live row is never
|
|
141
|
+
* shadowed by the curated table or by the default seed, so what the endpoint states
|
|
142
|
+
* about a model is what the picker shows.
|
|
143
|
+
*
|
|
144
|
+
* @param plan - curated metadata, the live membership, and the fallbacks.
|
|
145
|
+
* @param refine - widens an entry with provider-side metadata, when the provider has it.
|
|
146
|
+
* @returns the entries to publish, in picker order.
|
|
147
|
+
*/
|
|
148
|
+
export declare function composeCatalog(plan: CatalogPlan, refine?: (entry: CatalogEntry) => CatalogEntry): CatalogEntry[];
|
|
57
149
|
/**
|
|
58
150
|
* Apply the user's model order and visibility to one provider's catalog.
|
|
59
151
|
*
|
package/lib/providers/catalog.js
CHANGED
|
@@ -42,6 +42,197 @@ export function fallbackEntry(id, description, contextWindow) {
|
|
|
42
42
|
defaultEffort: 'max',
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
/** Read a non-empty string field. */
|
|
46
|
+
function stringField(value) {
|
|
47
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
48
|
+
}
|
|
49
|
+
/** Read a positive integer field. */
|
|
50
|
+
function numberField(value) {
|
|
51
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
52
|
+
return Math.floor(value);
|
|
53
|
+
if (typeof value === 'string') {
|
|
54
|
+
const parsed = Number(value);
|
|
55
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
56
|
+
return Math.floor(parsed);
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The input modalities a listing states, in the harness vocabulary.
|
|
62
|
+
*
|
|
63
|
+
* Listings name modalities the harness has no word for (`video`, `audio`); those are
|
|
64
|
+
* dropped rather than smuggled through the type, so a capability is advertised only
|
|
65
|
+
* where the harness can act on it.
|
|
66
|
+
*/
|
|
67
|
+
function modalitiesOf(row) {
|
|
68
|
+
const stated = [];
|
|
69
|
+
const modalities = row['modalities'];
|
|
70
|
+
if (typeof modalities === 'object' && modalities !== null) {
|
|
71
|
+
const input = modalities['input'];
|
|
72
|
+
if (Array.isArray(input))
|
|
73
|
+
stated.push(...input);
|
|
74
|
+
}
|
|
75
|
+
for (const [field, modality] of [['supports_image_in', 'image'], ['supports_image_input', 'image']]) {
|
|
76
|
+
if (row[field] === true)
|
|
77
|
+
stated.push(modality);
|
|
78
|
+
}
|
|
79
|
+
if (stated.length === 0)
|
|
80
|
+
return undefined;
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const value of stated) {
|
|
83
|
+
if (value === 'text' && !out.includes('text'))
|
|
84
|
+
out.push('text');
|
|
85
|
+
if (value === 'image' && !out.includes('image'))
|
|
86
|
+
out.push('image');
|
|
87
|
+
}
|
|
88
|
+
return out.length === 0 ? undefined : out;
|
|
89
|
+
}
|
|
90
|
+
/** The reasoning ladder a listing states, from either of the shapes these APIs use. */
|
|
91
|
+
function effortsOf(row) {
|
|
92
|
+
const think = row['think_efforts'];
|
|
93
|
+
if (typeof think === 'object' && think !== null) {
|
|
94
|
+
const record = think;
|
|
95
|
+
const valid = Array.isArray(record['valid_efforts'])
|
|
96
|
+
? record['valid_efforts'].filter((effort) => typeof effort === 'string' && effort !== '')
|
|
97
|
+
: [];
|
|
98
|
+
const fallback = stringField(record['default_effort']);
|
|
99
|
+
return {
|
|
100
|
+
...valid.length === 0 ? {} : { efforts: valid },
|
|
101
|
+
...fallback === undefined ? {} : { defaultEffort: fallback },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const reasoning = row['reasoning'];
|
|
105
|
+
if (typeof reasoning === 'object' && reasoning !== null) {
|
|
106
|
+
const effort = reasoning['effort'];
|
|
107
|
+
if (typeof effort === 'object' && effort !== null) {
|
|
108
|
+
const record = effort;
|
|
109
|
+
const valid = Array.isArray(record['valid'])
|
|
110
|
+
? record['valid'].filter((value) => typeof value === 'string' && value !== '')
|
|
111
|
+
: [];
|
|
112
|
+
const fallback = stringField(record['default']);
|
|
113
|
+
return {
|
|
114
|
+
...valid.length === 0 ? {} : { efforts: valid },
|
|
115
|
+
...fallback === undefined ? {} : { defaultEffort: fallback },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Read the facts one live listing row states.
|
|
123
|
+
*
|
|
124
|
+
* The field names are the ones these providers actually use, including Kimi's
|
|
125
|
+
* `display_name`/`context_length`/`think_efforts` and the OpenAI-compatible
|
|
126
|
+
* `context_window`/`name` spellings; anything unrecognized is left absent rather than
|
|
127
|
+
* guessed, so a curated entry can still describe the model.
|
|
128
|
+
*
|
|
129
|
+
* @param row - one decoded row of a `/models` payload.
|
|
130
|
+
* @returns the facts, or undefined when the row names no model.
|
|
131
|
+
*/
|
|
132
|
+
export function liveModelFacts(row) {
|
|
133
|
+
if (typeof row !== 'object' || row === null || Array.isArray(row))
|
|
134
|
+
return undefined;
|
|
135
|
+
const record = row;
|
|
136
|
+
const id = stringField(record['id']) ?? stringField(record['model']);
|
|
137
|
+
if (id === undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
const name = stringField(record['display_name']) ?? stringField(record['name']);
|
|
140
|
+
const description = stringField(record['description']);
|
|
141
|
+
const limit = record['limit'];
|
|
142
|
+
const contextWindow = numberField(record['context_length'])
|
|
143
|
+
?? numberField(record['context_window'])
|
|
144
|
+
?? numberField(record['max_context_length'])
|
|
145
|
+
?? numberField(typeof limit === 'object' && limit !== null
|
|
146
|
+
? limit['context']
|
|
147
|
+
: undefined);
|
|
148
|
+
const inputModalities = modalitiesOf(record);
|
|
149
|
+
const { efforts, defaultEffort } = effortsOf(record);
|
|
150
|
+
return {
|
|
151
|
+
id,
|
|
152
|
+
...name === undefined ? {} : { name },
|
|
153
|
+
...description === undefined ? {} : { description },
|
|
154
|
+
...contextWindow === undefined ? {} : { contextWindow },
|
|
155
|
+
...inputModalities === undefined ? {} : { inputModalities },
|
|
156
|
+
...efforts === undefined ? {} : { reasoningEfforts: efforts },
|
|
157
|
+
...defaultEffort === undefined ? {} : { defaultEffort },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The entry to publish for one id the endpoint served.
|
|
162
|
+
*
|
|
163
|
+
* What the endpoint stated wins; the curated table fills only what the listing was
|
|
164
|
+
* silent about, so a live figure — the window a model actually serves today — is
|
|
165
|
+
* never overridden by a shipped guess, and a retired figure never survives a live
|
|
166
|
+
* read that contradicts it. A route whose ids are stable aliases the service re-points
|
|
167
|
+
* per release keeps its own labels instead (see the kimi route's alias labels).
|
|
168
|
+
*
|
|
169
|
+
* @param facts - what the listing stated for this id.
|
|
170
|
+
* @param curated - the curated metadata table.
|
|
171
|
+
* @param fallbackContextWindow - window to state when neither source names one.
|
|
172
|
+
* @param description - picker description when neither source states one.
|
|
173
|
+
* @returns the merged entry.
|
|
174
|
+
*/
|
|
175
|
+
export function liveEntry(facts, curated, fallbackContextWindow, description) {
|
|
176
|
+
const known = findEntry(curated, facts.id);
|
|
177
|
+
const contextWindow = facts.contextWindow ?? known?.contextWindow ?? fallbackContextWindow;
|
|
178
|
+
const name = facts.name ?? known?.name ?? facts.id;
|
|
179
|
+
const summary = facts.description ?? known?.description ?? description;
|
|
180
|
+
const inputModalities = facts.inputModalities ?? known?.inputModalities;
|
|
181
|
+
const reasoningEfforts = facts.reasoningEfforts ?? known?.reasoningEfforts;
|
|
182
|
+
const defaultEffort = facts.defaultEffort ?? known?.defaultEffort;
|
|
183
|
+
return {
|
|
184
|
+
id: known?.id ?? facts.id,
|
|
185
|
+
name,
|
|
186
|
+
description: summary,
|
|
187
|
+
contextWindow,
|
|
188
|
+
...inputModalities === undefined ? {} : { inputModalities },
|
|
189
|
+
...reasoningEfforts === undefined ? {} : { reasoningEfforts },
|
|
190
|
+
...defaultEffort === undefined ? {} : { defaultEffort },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Compose one provider's published catalog.
|
|
195
|
+
*
|
|
196
|
+
* The membership is the live entries in the endpoint's own order when a listing has
|
|
197
|
+
* been read, and the curated order otherwise. The configured default comes first —
|
|
198
|
+
* the picker preselects it, so it must be selectable — and configured extras last.
|
|
199
|
+
* Metadata always comes from the source that described the id: a live row is never
|
|
200
|
+
* shadowed by the curated table or by the default seed, so what the endpoint states
|
|
201
|
+
* about a model is what the picker shows.
|
|
202
|
+
*
|
|
203
|
+
* @param plan - curated metadata, the live membership, and the fallbacks.
|
|
204
|
+
* @param refine - widens an entry with provider-side metadata, when the provider has it.
|
|
205
|
+
* @returns the entries to publish, in picker order.
|
|
206
|
+
*/
|
|
207
|
+
export function composeCatalog(plan, refine = (entry) => entry) {
|
|
208
|
+
const membership = plan.live ?? plan.curated;
|
|
209
|
+
const described = new Map(membership.map((entry) => [entry.id, entry]));
|
|
210
|
+
const source = plan.live === undefined
|
|
211
|
+
? 'Built-in catalog'
|
|
212
|
+
: plan.liveDescription ?? 'Reported by the provider model list';
|
|
213
|
+
const wanted = [];
|
|
214
|
+
if (plan.defaultModel !== undefined && plan.defaultModel !== '') {
|
|
215
|
+
wanted.push({ id: plan.defaultModel, description: plan.defaultDescription ?? 'Default model for this route' });
|
|
216
|
+
}
|
|
217
|
+
for (const entry of membership)
|
|
218
|
+
wanted.push({ id: entry.id, description: source });
|
|
219
|
+
for (const id of plan.extra ?? []) {
|
|
220
|
+
wanted.push({ id, description: plan.extraDescription ?? 'Extra model from the plugin configuration' });
|
|
221
|
+
}
|
|
222
|
+
const rows = [];
|
|
223
|
+
const seen = new Set();
|
|
224
|
+
for (const { id, description } of wanted) {
|
|
225
|
+
const canonical = findEntry(plan.curated, id)?.id ?? id;
|
|
226
|
+
if (seen.has(canonical))
|
|
227
|
+
continue;
|
|
228
|
+
seen.add(canonical);
|
|
229
|
+
const entry = described.get(canonical)
|
|
230
|
+
?? findEntry(plan.curated, id)
|
|
231
|
+
?? fallbackEntry(id, description, plan.fallbackContextWindow);
|
|
232
|
+
rows.push(refine({ ...entry, id: canonical }));
|
|
233
|
+
}
|
|
234
|
+
return rows;
|
|
235
|
+
}
|
|
45
236
|
/**
|
|
46
237
|
* Apply the user's model order and visibility to one provider's catalog.
|
|
47
238
|
*
|
|
@@ -38,6 +38,20 @@ export declare function idleWatchdog(signal: AbortSignal | undefined, idleMs: nu
|
|
|
38
38
|
export declare function httpLlmError(response: Response, label: string): Promise<LlmError>;
|
|
39
39
|
/** Whether a failure means the stored credential no longer works. */
|
|
40
40
|
export declare function isMissingOrInvalidCredential(error: unknown): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The model id a provider's failure names as the one to use instead.
|
|
43
|
+
*
|
|
44
|
+
* A 401/403 is not always about the credential: a provider also answers it when the
|
|
45
|
+
* request names a model id the account does not serve — Kimi says `Your model id does
|
|
46
|
+
* not exist, recognized as other:k3[1m]. Please set model id as \`k3\``. That message
|
|
47
|
+
* is the endpoint's own answer to "which id should I have used", so it is worth acting
|
|
48
|
+
* on rather than reporting: refreshing a healthy token, or marking the account as
|
|
49
|
+
* needing a new login, would both be wrong answers to a model rename.
|
|
50
|
+
*
|
|
51
|
+
* @param message - the failure text.
|
|
52
|
+
* @returns the replacement id, or undefined when the failure is not about the model id.
|
|
53
|
+
*/
|
|
54
|
+
export declare function unknownModelReplacement(message: string): string | undefined;
|
|
41
55
|
/**
|
|
42
56
|
* Map a thrown transport failure onto a harness error, distinguishing caller
|
|
43
57
|
* cancellation from an idle timeout from a genuine network fault.
|