dsh-plugin-subscription-oauth 0.16.3 → 0.17.0
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/index.js +21 -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 +6 -0
- package/lib/providers/ark.d.ts +96 -10
- package/lib/providers/ark.js +176 -48
- package/lib/providers/catalog.d.ts +91 -0
- package/lib/providers/catalog.js +190 -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 +6 -0
- package/lib/providers/glm.d.ts +92 -16
- package/lib/providers/glm.js +172 -85
- package/lib/providers/grok.d.ts +106 -27
- package/lib/providers/grok.js +312 -128
- package/lib/providers/kimi-provider.d.ts +10 -0
- package/lib/providers/kimi-provider.js +8 -3
- package/lib/providers/kimi.d.ts +104 -32
- package/lib/providers/kimi.js +198 -129
- 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));
|
|
@@ -149,9 +229,10 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
149
229
|
return this.imageModel;
|
|
150
230
|
}
|
|
151
231
|
async resolveModel(_provider, model) {
|
|
232
|
+
await this.hydrate();
|
|
152
233
|
const entry = findEntry(this.catalog(), model);
|
|
153
234
|
if (entry === undefined) {
|
|
154
|
-
const template = findEntry(this.catalog(),
|
|
235
|
+
const template = findEntry(this.catalog(), this.defaultModel()) ?? this.catalog()[0];
|
|
155
236
|
return {
|
|
156
237
|
provider: ARK_PROVIDER_ID,
|
|
157
238
|
id: model,
|
|
@@ -200,19 +281,30 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
200
281
|
catalog() {
|
|
201
282
|
return [...this.shippedCatalog(), ...this.addedEntries()];
|
|
202
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* The catalog to present: the models the endpoint's listing named, in its own order,
|
|
286
|
+
* each with the metadata that listing stated, then the configured extras.
|
|
287
|
+
*
|
|
288
|
+
* There is no shipped roster. A listing is read once and cached (see {@link hydrate}
|
|
289
|
+
* and {@link fetchCatalog}), and that cached table is what the picker shows until the
|
|
290
|
+
* next read, so a model released, renamed or retired server-side is reflected without
|
|
291
|
+
* a plugin update. Only the configured default is always offered, because the picker
|
|
292
|
+
* preselects it.
|
|
293
|
+
*/
|
|
203
294
|
shippedCatalog() {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
295
|
+
return composeCatalog({
|
|
296
|
+
curated: [],
|
|
297
|
+
...this.live === undefined ? {} : { live: this.live },
|
|
298
|
+
...this.options.extraModels === undefined ? {} : { extra: this.options.extraModels },
|
|
299
|
+
defaultModel: this.defaultModel(),
|
|
300
|
+
fallbackContextWindow: ARK_CONTEXT_WINDOW,
|
|
301
|
+
liveDescription: 'Reported by the Ark model list',
|
|
302
|
+
extraDescription: 'Extra model from the plugin configuration',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/** The model the picker preselects, from configuration. */
|
|
306
|
+
defaultModel() {
|
|
307
|
+
return this.options.defaultModel ?? ARK_DEFAULT_MODEL;
|
|
216
308
|
}
|
|
217
309
|
addedEntries() {
|
|
218
310
|
const own = this.shippedCatalog();
|
|
@@ -221,7 +313,7 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
221
313
|
return [];
|
|
222
314
|
return (Array.isArray(added) ? added : [])
|
|
223
315
|
.filter((id) => findEntry(own, id) === undefined)
|
|
224
|
-
.map(
|
|
316
|
+
.map((id) => fallbackEntry(id, 'Added by hand in Settings', ARK_CONTEXT_WINDOW));
|
|
225
317
|
}
|
|
226
318
|
async hidden() {
|
|
227
319
|
if (this.options.hiddenModels === undefined)
|
|
@@ -233,15 +325,34 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
233
325
|
return new Set();
|
|
234
326
|
}
|
|
235
327
|
}
|
|
236
|
-
/**
|
|
328
|
+
/**
|
|
329
|
+
* Adopt the ids a caller reports as the served set, without a listing.
|
|
330
|
+
*
|
|
331
|
+
* {@link fetchCatalog} already derived the entries from the rows it read, and the
|
|
332
|
+
* controller reports those same ids back here; the entries it recorded are kept. A
|
|
333
|
+
* caller that only has ids (a test, another discovery path) gets conservative entries
|
|
334
|
+
* for them.
|
|
335
|
+
*
|
|
336
|
+
* @param ids - model ids the caller reports.
|
|
337
|
+
*/
|
|
237
338
|
discover(ids) {
|
|
238
|
-
this.live
|
|
339
|
+
const known = new Map((this.live ?? []).map((entry) => [entry.id, entry]));
|
|
340
|
+
this.live = ids.map((id) => known.get(id)
|
|
341
|
+
?? liveEntry({ id }, [], ARK_CONTEXT_WINDOW, 'Reported by the Ark model list'));
|
|
239
342
|
}
|
|
240
343
|
/**
|
|
241
|
-
* Read the plan's live model list.
|
|
344
|
+
* Read the plan's live model list and adopt it as this route's catalog.
|
|
345
|
+
*
|
|
346
|
+
* The Coding Plan answers the OpenAI models shape on its own base, so the rows are
|
|
347
|
+
* parsed exactly as the other routes parse theirs: what the listing names is what the
|
|
348
|
+
* picker offers, with the display name, window, modalities and ladder it states. The
|
|
349
|
+
* rows are also handed to {@link ArkAdapterOptions.saveListing}, so the next session
|
|
350
|
+
* starts from this table instead of asking again — the Settings "refresh models"
|
|
351
|
+
* action is what re-reads it.
|
|
242
352
|
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
353
|
+
* @param apiKey - bearer for the account.
|
|
354
|
+
* @param signal - optional cancellation.
|
|
355
|
+
* @returns the model ids now served, or undefined when the listing is unavailable.
|
|
245
356
|
*/
|
|
246
357
|
async fetchCatalog(apiKey, signal) {
|
|
247
358
|
if (!this.options.discovery)
|
|
@@ -253,23 +364,40 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
253
364
|
...signal === undefined ? {} : { signal },
|
|
254
365
|
});
|
|
255
366
|
if (!response.ok) {
|
|
256
|
-
this.options.onWarn?.(`Ark model list unavailable (HTTP ${response.status})`);
|
|
367
|
+
this.options.onWarn?.(`Ark model list unavailable (HTTP ${response.status}); keeping the last catalog`);
|
|
257
368
|
return undefined;
|
|
258
369
|
}
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
370
|
+
const payload = await response.json();
|
|
371
|
+
const rows = Array.isArray(payload.data) ? payload.data : [];
|
|
372
|
+
const entries = this.adoptListing(rows);
|
|
373
|
+
if (entries.length === 0) {
|
|
374
|
+
this.options.onWarn?.('Ark model discovery returned an empty catalog; keeping the last catalog');
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
this.live = entries;
|
|
378
|
+
// The cached table is the endpoint's answer, kept so the next session does not ask
|
|
379
|
+
// again. A cache failure never fails the read that produced it.
|
|
380
|
+
await this.saveListing(rows);
|
|
381
|
+
return entries.map((entry) => entry.id);
|
|
267
382
|
}
|
|
268
383
|
catch (error) {
|
|
384
|
+
if (signal?.aborted === true)
|
|
385
|
+
return undefined;
|
|
269
386
|
this.options.onWarn?.(`Ark model list unavailable (${error instanceof Error ? error.message : String(error)})`);
|
|
270
387
|
return undefined;
|
|
271
388
|
}
|
|
272
389
|
}
|
|
390
|
+
/** Persist one live listing, reporting a cache failure without failing the read. */
|
|
391
|
+
async saveListing(rows) {
|
|
392
|
+
if (this.options.saveListing === undefined)
|
|
393
|
+
return;
|
|
394
|
+
try {
|
|
395
|
+
await this.options.saveListing(rows);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
this.options.onWarn?.(`model catalog cache write failed (${error instanceof Error ? error.message : String(error)})`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
273
401
|
/** Send one streaming request over the Responses wire. */
|
|
274
402
|
async request(options, model, apiKey, signal) {
|
|
275
403
|
const resolveImage = this.imageResolver(signal);
|
|
@@ -54,6 +54,97 @@ 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.
|
|
92
|
+
*
|
|
93
|
+
* @param facts - what the listing stated for this id.
|
|
94
|
+
* @param curated - the curated metadata table.
|
|
95
|
+
* @param fallbackContextWindow - window to state when neither source names one.
|
|
96
|
+
* @param description - picker description when neither source states one.
|
|
97
|
+
* @returns the merged entry.
|
|
98
|
+
*/
|
|
99
|
+
export declare function liveEntry(facts: LiveModelFacts, curated: readonly CatalogEntry[], fallbackContextWindow: number, description: string): CatalogEntry;
|
|
100
|
+
/**
|
|
101
|
+
* How one provider's catalog is composed from its curated table and the entries the
|
|
102
|
+
* endpoint itself reported.
|
|
103
|
+
*
|
|
104
|
+
* The endpoint decides membership. A curated table is metadata, not a roster: the ids
|
|
105
|
+
* in it describe what the build knows about a model's name, window and ladder, and it
|
|
106
|
+
* is the membership only while no live list has been read. That is what lets a model
|
|
107
|
+
* released or retired server-side reach the picker without a plugin update, which a
|
|
108
|
+
* roster can never do.
|
|
109
|
+
*/
|
|
110
|
+
export interface CatalogPlan {
|
|
111
|
+
/** Curated entries, in curated order; metadata first, fallback membership second. */
|
|
112
|
+
readonly curated: readonly CatalogEntry[];
|
|
113
|
+
/**
|
|
114
|
+
* Entries the endpoint reported, in its own order.
|
|
115
|
+
*
|
|
116
|
+
* Undefined means discovery has not run (or failed), so the curated table decides
|
|
117
|
+
* membership and the picker is never empty.
|
|
118
|
+
*/
|
|
119
|
+
readonly live?: readonly CatalogEntry[] | undefined;
|
|
120
|
+
/** Ids the plugin configuration adds, on top of the membership. */
|
|
121
|
+
readonly extra?: readonly string[] | undefined;
|
|
122
|
+
/** The model the picker preselects, and therefore always offers. */
|
|
123
|
+
readonly defaultModel?: string | undefined;
|
|
124
|
+
/** Context advertised for an id no curated entry describes. */
|
|
125
|
+
readonly fallbackContextWindow: number;
|
|
126
|
+
/** Description shown for a live id no curated entry describes. */
|
|
127
|
+
readonly liveDescription?: string | undefined;
|
|
128
|
+
/** Description shown for a configured id no curated entry describes. */
|
|
129
|
+
readonly extraDescription?: string | undefined;
|
|
130
|
+
/** Description shown for the default model when no curated entry describes it. */
|
|
131
|
+
readonly defaultDescription?: string | undefined;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Compose one provider's published catalog.
|
|
135
|
+
*
|
|
136
|
+
* The membership is the live entries in the endpoint's own order when a listing has
|
|
137
|
+
* been read, and the curated order otherwise. The configured default comes first —
|
|
138
|
+
* the picker preselects it, so it must be selectable — and configured extras last.
|
|
139
|
+
* Metadata always comes from the source that described the id: a live row is never
|
|
140
|
+
* shadowed by the curated table or by the default seed, so what the endpoint states
|
|
141
|
+
* about a model is what the picker shows.
|
|
142
|
+
*
|
|
143
|
+
* @param plan - curated metadata, the live membership, and the fallbacks.
|
|
144
|
+
* @param refine - widens an entry with provider-side metadata, when the provider has it.
|
|
145
|
+
* @returns the entries to publish, in picker order.
|
|
146
|
+
*/
|
|
147
|
+
export declare function composeCatalog(plan: CatalogPlan, refine?: (entry: CatalogEntry) => CatalogEntry): CatalogEntry[];
|
|
57
148
|
/**
|
|
58
149
|
* Apply the user's model order and visibility to one provider's catalog.
|
|
59
150
|
*
|
package/lib/providers/catalog.js
CHANGED
|
@@ -42,6 +42,196 @@ 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.
|
|
167
|
+
*
|
|
168
|
+
* @param facts - what the listing stated for this id.
|
|
169
|
+
* @param curated - the curated metadata table.
|
|
170
|
+
* @param fallbackContextWindow - window to state when neither source names one.
|
|
171
|
+
* @param description - picker description when neither source states one.
|
|
172
|
+
* @returns the merged entry.
|
|
173
|
+
*/
|
|
174
|
+
export function liveEntry(facts, curated, fallbackContextWindow, description) {
|
|
175
|
+
const known = findEntry(curated, facts.id);
|
|
176
|
+
const contextWindow = facts.contextWindow ?? known?.contextWindow ?? fallbackContextWindow;
|
|
177
|
+
const name = facts.name ?? known?.name ?? facts.id;
|
|
178
|
+
const summary = facts.description ?? known?.description ?? description;
|
|
179
|
+
const inputModalities = facts.inputModalities ?? known?.inputModalities;
|
|
180
|
+
const reasoningEfforts = facts.reasoningEfforts ?? known?.reasoningEfforts;
|
|
181
|
+
const defaultEffort = facts.defaultEffort ?? known?.defaultEffort;
|
|
182
|
+
return {
|
|
183
|
+
id: known?.id ?? facts.id,
|
|
184
|
+
name,
|
|
185
|
+
description: summary,
|
|
186
|
+
contextWindow,
|
|
187
|
+
...inputModalities === undefined ? {} : { inputModalities },
|
|
188
|
+
...reasoningEfforts === undefined ? {} : { reasoningEfforts },
|
|
189
|
+
...defaultEffort === undefined ? {} : { defaultEffort },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Compose one provider's published catalog.
|
|
194
|
+
*
|
|
195
|
+
* The membership is the live entries in the endpoint's own order when a listing has
|
|
196
|
+
* been read, and the curated order otherwise. The configured default comes first —
|
|
197
|
+
* the picker preselects it, so it must be selectable — and configured extras last.
|
|
198
|
+
* Metadata always comes from the source that described the id: a live row is never
|
|
199
|
+
* shadowed by the curated table or by the default seed, so what the endpoint states
|
|
200
|
+
* about a model is what the picker shows.
|
|
201
|
+
*
|
|
202
|
+
* @param plan - curated metadata, the live membership, and the fallbacks.
|
|
203
|
+
* @param refine - widens an entry with provider-side metadata, when the provider has it.
|
|
204
|
+
* @returns the entries to publish, in picker order.
|
|
205
|
+
*/
|
|
206
|
+
export function composeCatalog(plan, refine = (entry) => entry) {
|
|
207
|
+
const membership = plan.live ?? plan.curated;
|
|
208
|
+
const described = new Map(membership.map((entry) => [entry.id, entry]));
|
|
209
|
+
const source = plan.live === undefined
|
|
210
|
+
? 'Built-in catalog'
|
|
211
|
+
: plan.liveDescription ?? 'Reported by the provider model list';
|
|
212
|
+
const wanted = [];
|
|
213
|
+
if (plan.defaultModel !== undefined && plan.defaultModel !== '') {
|
|
214
|
+
wanted.push({ id: plan.defaultModel, description: plan.defaultDescription ?? 'Default model for this route' });
|
|
215
|
+
}
|
|
216
|
+
for (const entry of membership)
|
|
217
|
+
wanted.push({ id: entry.id, description: source });
|
|
218
|
+
for (const id of plan.extra ?? []) {
|
|
219
|
+
wanted.push({ id, description: plan.extraDescription ?? 'Extra model from the plugin configuration' });
|
|
220
|
+
}
|
|
221
|
+
const rows = [];
|
|
222
|
+
const seen = new Set();
|
|
223
|
+
for (const { id, description } of wanted) {
|
|
224
|
+
const canonical = findEntry(plan.curated, id)?.id ?? id;
|
|
225
|
+
if (seen.has(canonical))
|
|
226
|
+
continue;
|
|
227
|
+
seen.add(canonical);
|
|
228
|
+
const entry = described.get(canonical)
|
|
229
|
+
?? findEntry(plan.curated, id)
|
|
230
|
+
?? fallbackEntry(id, description, plan.fallbackContextWindow);
|
|
231
|
+
rows.push(refine({ ...entry, id: canonical }));
|
|
232
|
+
}
|
|
233
|
+
return rows;
|
|
234
|
+
}
|
|
45
235
|
/**
|
|
46
236
|
* Apply the user's model order and visibility to one provider's catalog.
|
|
47
237
|
*
|
|
@@ -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.
|
package/lib/providers/common.js
CHANGED
|
@@ -141,6 +141,29 @@ export function isMissingOrInvalidCredential(error) {
|
|
|
141
141
|
}
|
|
142
142
|
return false;
|
|
143
143
|
}
|
|
144
|
+
/** Body text that says the request named a model the account does not serve. */
|
|
145
|
+
const UNKNOWN_MODEL = /model id does not exist|unknown model|model (?:id )?not found|does not exist, recognized as/i;
|
|
146
|
+
/** Body text that names the model id to use instead. */
|
|
147
|
+
const MODEL_REPLACEMENT = /set model id as\s*`?([A-Za-z0-9][A-Za-z0-9._:@[\]/-]*)`?/i;
|
|
148
|
+
/**
|
|
149
|
+
* The model id a provider's failure names as the one to use instead.
|
|
150
|
+
*
|
|
151
|
+
* A 401/403 is not always about the credential: a provider also answers it when the
|
|
152
|
+
* request names a model id the account does not serve — Kimi says `Your model id does
|
|
153
|
+
* not exist, recognized as other:k3[1m]. Please set model id as \`k3\``. That message
|
|
154
|
+
* is the endpoint's own answer to "which id should I have used", so it is worth acting
|
|
155
|
+
* on rather than reporting: refreshing a healthy token, or marking the account as
|
|
156
|
+
* needing a new login, would both be wrong answers to a model rename.
|
|
157
|
+
*
|
|
158
|
+
* @param message - the failure text.
|
|
159
|
+
* @returns the replacement id, or undefined when the failure is not about the model id.
|
|
160
|
+
*/
|
|
161
|
+
export function unknownModelReplacement(message) {
|
|
162
|
+
if (!UNKNOWN_MODEL.test(message))
|
|
163
|
+
return undefined;
|
|
164
|
+
const replacement = MODEL_REPLACEMENT.exec(message)?.[1];
|
|
165
|
+
return replacement === undefined || replacement === '' ? undefined : replacement;
|
|
166
|
+
}
|
|
144
167
|
/**
|
|
145
168
|
* Map a thrown transport failure onto a harness error, distinguishing caller
|
|
146
169
|
* cancellation from an idle timeout from a genuine network fault.
|