dsh-model-extension 1.1.0 → 1.2.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.md +5 -1
- package/lib/client.js +720 -94
- package/lib/client.js.map +1 -1
- package/lib/index.js +402 -5
- package/lib/index.js.map +1 -1
- package/package.json +4 -3
package/lib/client.js
CHANGED
|
@@ -217,8 +217,10 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
217
217
|
/** Same-origin routes served by the server half. */
|
|
218
218
|
const INDEX_URL = "/plugins/dsh-model-extension/models-index";
|
|
219
219
|
const DOWNLOAD_URL = "/plugins/dsh-model-extension/models-download";
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
const PI_AI_URL = "/plugins/dsh-model-extension/pi-ai-catalog";
|
|
221
|
+
/** Module-level cache: one page load fetches each source at most once. */
|
|
222
|
+
let cachedIndex;
|
|
223
|
+
/** Normalize the models.dev route payload. */
|
|
222
224
|
function normalize(raw) {
|
|
223
225
|
if (!Array.isArray(raw)) return [];
|
|
224
226
|
const entries = [];
|
|
@@ -237,23 +239,137 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
237
239
|
}
|
|
238
240
|
return entries;
|
|
239
241
|
}
|
|
242
|
+
/** Narrow a modality list to what this UI offers. */
|
|
243
|
+
function supportedInput(value) {
|
|
244
|
+
if (!Array.isArray(value)) return [];
|
|
245
|
+
return value.filter((m) => typeof m === "string" && (m === "text" || m === "image"));
|
|
246
|
+
}
|
|
247
|
+
/** Read one object-valued field off a raw record. */
|
|
248
|
+
function objectField(source, key) {
|
|
249
|
+
const value = source[key];
|
|
250
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The provider a models.dev id belongs to.
|
|
254
|
+
*
|
|
255
|
+
* models.dev ids are directory paths (`zhipuai/glm-5.3-flash`), so the leading
|
|
256
|
+
* segment names the provider. Reading it here gives the picker the same
|
|
257
|
+
* provider-first navigation that pi-ai's per-provider catalog has for free —
|
|
258
|
+
* and it is what lets a user narrow by vendor instead of guessing between
|
|
259
|
+
* same-named ids.
|
|
260
|
+
* @param id - the models.dev directory id.
|
|
261
|
+
* @returns the provider segment, or undefined when the id carries none.
|
|
262
|
+
*/
|
|
263
|
+
function modelsDevProvider(id) {
|
|
264
|
+
const slash = id.indexOf("/");
|
|
265
|
+
return slash > 0 ? id.slice(0, slash) : void 0;
|
|
266
|
+
}
|
|
267
|
+
/** Turn models.dev rows into prefill candidates. */
|
|
268
|
+
function toModelsDevEntries(rows) {
|
|
269
|
+
return rows.map((row) => ({
|
|
270
|
+
source: "models.dev",
|
|
271
|
+
key: `models.dev\u0000${row.id}`,
|
|
272
|
+
id: row.id,
|
|
273
|
+
provider: modelsDevProvider(row.id),
|
|
274
|
+
context: row.context,
|
|
275
|
+
output: row.output,
|
|
276
|
+
input: row.input,
|
|
277
|
+
reasoning: row.reasoning
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
/** Read the diagnostics warnings the server attaches to a catalog payload. */
|
|
281
|
+
function diagnosticsWarnings(payload) {
|
|
282
|
+
if (typeof payload !== "object" || payload === null) return [];
|
|
283
|
+
const diagnostics = payload["diagnostics"];
|
|
284
|
+
if (typeof diagnostics !== "object" || diagnostics === null) return [];
|
|
285
|
+
const warnings = diagnostics["warnings"];
|
|
286
|
+
if (!Array.isArray(warnings)) return [];
|
|
287
|
+
return warnings.filter((warning) => typeof warning === "string");
|
|
288
|
+
}
|
|
289
|
+
/** Turn a pi-ai route payload into prefill candidates, keeping both ids' provenance. */
|
|
290
|
+
function toPiAiEntries(payload) {
|
|
291
|
+
const warnings = diagnosticsWarnings(payload);
|
|
292
|
+
if (typeof payload !== "object" || payload === null) return {
|
|
293
|
+
entries: [],
|
|
294
|
+
warnings
|
|
295
|
+
};
|
|
296
|
+
const body = payload;
|
|
297
|
+
if (!Array.isArray(body["models"])) return {
|
|
298
|
+
entries: [],
|
|
299
|
+
warnings
|
|
300
|
+
};
|
|
301
|
+
const entries = [];
|
|
302
|
+
for (const item of body["models"]) {
|
|
303
|
+
if (typeof item !== "object" || item === null) continue;
|
|
304
|
+
const row = item;
|
|
305
|
+
const id = row["id"];
|
|
306
|
+
const provider = row["provider"];
|
|
307
|
+
if (typeof id !== "string" || id.length === 0) continue;
|
|
308
|
+
if (typeof provider !== "string" || provider.length === 0) continue;
|
|
309
|
+
entries.push({
|
|
310
|
+
source: "pi-ai",
|
|
311
|
+
key: `pi-ai\u0000${provider}\u0000${id}`,
|
|
312
|
+
id,
|
|
313
|
+
provider,
|
|
314
|
+
name: typeof row["name"] === "string" && row["name"].length > 0 ? row["name"] : void 0,
|
|
315
|
+
context: typeof row["contextWindow"] === "number" ? row["contextWindow"] : void 0,
|
|
316
|
+
output: typeof row["maxTokens"] === "number" ? row["maxTokens"] : void 0,
|
|
317
|
+
input: supportedInput(row["input"]),
|
|
318
|
+
reasoning: row["reasoning"] === true,
|
|
319
|
+
api: typeof row["api"] === "string" ? row["api"] : void 0,
|
|
320
|
+
thinkingLevelMap: objectField(row, "thinkingLevelMap"),
|
|
321
|
+
compat: objectField(row, "compat")
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
entries,
|
|
326
|
+
warnings,
|
|
327
|
+
version: typeof body["piAiVersion"] === "string" ? body["piAiVersion"] : void 0,
|
|
328
|
+
generatedAt: typeof body["generatedAt"] === "string" ? body["generatedAt"] : void 0
|
|
329
|
+
};
|
|
330
|
+
}
|
|
240
331
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
332
|
+
* Fetch one JSON route, keeping a refusal distinguishable from an empty source.
|
|
333
|
+
*
|
|
334
|
+
* The distinction matters for the pi-ai half: "the host ships no catalog" and
|
|
335
|
+
* "the host's catalog could not be called" both leave the list empty, but only
|
|
336
|
+
* the second one has a fix, and the server names it in the refusal body.
|
|
337
|
+
* @param url - the same-origin route.
|
|
338
|
+
* @returns the parsed payload, or the reason it is missing.
|
|
243
339
|
*/
|
|
244
|
-
async function
|
|
245
|
-
if (cached !== void 0) return cached;
|
|
340
|
+
async function fetchJson(url) {
|
|
246
341
|
try {
|
|
247
|
-
const response = await fetch(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
342
|
+
const response = await fetch(url);
|
|
343
|
+
const payload = await response.json().catch(() => void 0);
|
|
344
|
+
if (response.ok) return { payload };
|
|
345
|
+
const message = typeof payload === "object" && payload !== null ? payload["message"] : void 0;
|
|
346
|
+
return { failure: typeof message === "string" ? message : `HTTP ${String(response.status)}` };
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return { failure: error instanceof Error ? error.message : String(error) };
|
|
253
349
|
}
|
|
254
350
|
}
|
|
255
351
|
/**
|
|
256
|
-
*
|
|
352
|
+
* The prefill index, fetched once per page load. Both sources are requested
|
|
353
|
+
* together and either may come back empty; an entirely empty index is a
|
|
354
|
+
* legitimate answer the caller renders as "nothing to load yet".
|
|
355
|
+
* @returns the merged index.
|
|
356
|
+
*/
|
|
357
|
+
async function getPrefillIndex() {
|
|
358
|
+
if (cachedIndex !== void 0) return cachedIndex;
|
|
359
|
+
const [piAiResult, modelsDevResult] = await Promise.all([fetchJson(PI_AI_URL), fetchJson(INDEX_URL)]);
|
|
360
|
+
const piAi = toPiAiEntries(piAiResult.payload);
|
|
361
|
+
cachedIndex = {
|
|
362
|
+
piAi: piAi.entries,
|
|
363
|
+
modelsDev: toModelsDevEntries(normalize(modelsDevResult.payload)),
|
|
364
|
+
piAiVersion: piAi.version,
|
|
365
|
+
piAiGeneratedAt: piAi.generatedAt,
|
|
366
|
+
piAiWarnings: piAi.warnings,
|
|
367
|
+
...piAiResult.failure !== void 0 ? { piAiFailure: piAiResult.failure } : {}
|
|
368
|
+
};
|
|
369
|
+
return cachedIndex;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Download/refresh the models.dev file through the server half (user-triggered
|
|
257
373
|
* from the title-row button). A success also refreshes the local cache.
|
|
258
374
|
*/
|
|
259
375
|
async function downloadMetadata() {
|
|
@@ -264,7 +380,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
264
380
|
ok: false,
|
|
265
381
|
message: body?.message ?? `HTTP ${String(response.status)}`
|
|
266
382
|
};
|
|
267
|
-
|
|
383
|
+
cachedIndex = void 0;
|
|
268
384
|
return { ok: true };
|
|
269
385
|
} catch (error) {
|
|
270
386
|
return {
|
|
@@ -274,24 +390,61 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
274
390
|
}
|
|
275
391
|
}
|
|
276
392
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
|
|
282
|
-
|
|
393
|
+
* How many candidates the picker renders before it stops and reports the rest.
|
|
394
|
+
*
|
|
395
|
+
* Ten was too few once one id could arrive from a dozen providers; fifty shows
|
|
396
|
+
* a vendor's catalog without pouring a 1354-row index into the DOM.
|
|
397
|
+
*/
|
|
398
|
+
const DEFAULT_PREFILL_LIMIT = 50;
|
|
399
|
+
/**
|
|
400
|
+
* The providers a source offers, alphabetically.
|
|
401
|
+
*
|
|
402
|
+
* This is what turns "which `glm-5.3-flash` did you mean?" into a choice the
|
|
403
|
+
* user can actually make: the same id ships under several providers, and each
|
|
404
|
+
* provider's copy can carry different capacities and compat switches.
|
|
405
|
+
* @param index - the fetched index, or undefined while the page still loads.
|
|
406
|
+
* @param source - the source to enumerate.
|
|
407
|
+
* @returns the provider names; empty when the source has none.
|
|
283
408
|
*/
|
|
284
|
-
function
|
|
409
|
+
function providersOf(index, source) {
|
|
285
410
|
if (index === void 0) return [];
|
|
286
|
-
const
|
|
287
|
-
|
|
411
|
+
const entries = source === "pi-ai" ? index.piAi : index.modelsDev;
|
|
412
|
+
const providers = /* @__PURE__ */ new Set();
|
|
413
|
+
for (const entry of entries) if (entry.provider !== void 0 && entry.provider.length > 0) providers.add(entry.provider);
|
|
414
|
+
return [...providers].sort((a, b) => a.localeCompare(b));
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Candidates matching one source, one provider and one id substring.
|
|
418
|
+
*
|
|
419
|
+
* Either filter may be blank, and a blank one *widens* rather than matching
|
|
420
|
+
* nothing: a provider with no id typed lists that provider's whole catalog,
|
|
421
|
+
* which is how a user browses a vendor whose model names they have not
|
|
422
|
+
* memorized. Both blank lists the source's first page.
|
|
423
|
+
* @param index - the fetched index, or undefined while the page still loads.
|
|
424
|
+
* @param query - the source, provider, id substring and cap.
|
|
425
|
+
* @returns the capped candidates and the total match count.
|
|
426
|
+
*/
|
|
427
|
+
function searchPrefill(index, query) {
|
|
428
|
+
if (index === void 0) return {
|
|
429
|
+
hits: [],
|
|
430
|
+
total: 0
|
|
431
|
+
};
|
|
432
|
+
const entries = query.source === "pi-ai" ? index.piAi : index.modelsDev;
|
|
433
|
+
const provider = query.provider?.trim() ?? "";
|
|
434
|
+
const needle = query.text?.trim().toLowerCase() ?? "";
|
|
435
|
+
const limit = query.limit ?? DEFAULT_PREFILL_LIMIT;
|
|
288
436
|
const hits = [];
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (
|
|
437
|
+
let total = 0;
|
|
438
|
+
for (const entry of entries) {
|
|
439
|
+
if (provider.length > 0 && entry.provider !== provider) continue;
|
|
440
|
+
if (needle.length > 0 && !entry.id.toLowerCase().includes(needle)) continue;
|
|
441
|
+
total += 1;
|
|
442
|
+
if (hits.length < limit) hits.push(entry);
|
|
293
443
|
}
|
|
294
|
-
return
|
|
444
|
+
return {
|
|
445
|
+
hits,
|
|
446
|
+
total
|
|
447
|
+
};
|
|
295
448
|
}
|
|
296
449
|
//#endregion
|
|
297
450
|
//#region src/client/models-plus/compat.ts
|
|
@@ -494,6 +647,85 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
494
647
|
"string-thinking",
|
|
495
648
|
"ant-ling"
|
|
496
649
|
];
|
|
650
|
+
/**
|
|
651
|
+
* The compatibility switches llm-pi-ai *offers*, by wire protocol (the `offer`
|
|
652
|
+
* set of its COMPAT_GATES @ 0.1.5-rc.2). Only these may reach a model row: the
|
|
653
|
+
* adapter refuses a switch no protocol offers rather than ignoring it, so
|
|
654
|
+
* copying pi-ai's catalog `compat` across verbatim would fail resolution on
|
|
655
|
+
* the withheld fields it also records (routing preferences, session affinity,
|
|
656
|
+
* deferred tools, grammar tools). Re-check against the host's COMPAT_GATES on
|
|
657
|
+
* every adapter-anchor bump — the same drift gate the host keeps for itself.
|
|
658
|
+
*/
|
|
659
|
+
const COMPAT_FIELDS = {
|
|
660
|
+
"openai-completions": [
|
|
661
|
+
"supportsStore",
|
|
662
|
+
"supportsDeveloperRole",
|
|
663
|
+
"supportsReasoningEffort",
|
|
664
|
+
"supportsUsageInStreaming",
|
|
665
|
+
"supportsFinishReason",
|
|
666
|
+
"maxTokensField",
|
|
667
|
+
"requiresToolResultName",
|
|
668
|
+
"requiresAssistantAfterToolResult",
|
|
669
|
+
"requiresThinkingAsText",
|
|
670
|
+
"requiresReasoningContentOnAssistantMessages",
|
|
671
|
+
"thinkingFormat",
|
|
672
|
+
"chatTemplateKwargs",
|
|
673
|
+
"chatTemplateArgs",
|
|
674
|
+
"supportsThinkingTokenBudget",
|
|
675
|
+
"thinkingTokenBudgetField",
|
|
676
|
+
"vllmPriority",
|
|
677
|
+
"supportsStrictMode",
|
|
678
|
+
"cacheControlFormat",
|
|
679
|
+
"supportsLongCacheRetention"
|
|
680
|
+
],
|
|
681
|
+
"openai-responses": [
|
|
682
|
+
"supportsDeveloperRole",
|
|
683
|
+
"supportsMaxOutputTokens",
|
|
684
|
+
"supportsStrictMode",
|
|
685
|
+
"supportsLongCacheRetention"
|
|
686
|
+
],
|
|
687
|
+
"azure-openai-responses": [
|
|
688
|
+
"supportsDeveloperRole",
|
|
689
|
+
"supportsMaxOutputTokens",
|
|
690
|
+
"supportsStrictMode",
|
|
691
|
+
"supportsLongCacheRetention"
|
|
692
|
+
],
|
|
693
|
+
"openai-codex-responses": [
|
|
694
|
+
"supportsDeveloperRole",
|
|
695
|
+
"supportsMaxOutputTokens",
|
|
696
|
+
"supportsStrictMode",
|
|
697
|
+
"supportsLongCacheRetention"
|
|
698
|
+
],
|
|
699
|
+
"anthropic-messages": [
|
|
700
|
+
"supportsEagerToolInputStreaming",
|
|
701
|
+
"supportsLongCacheRetention",
|
|
702
|
+
"supportsCacheControlOnTools",
|
|
703
|
+
"supportsTemperature",
|
|
704
|
+
"forceAdaptiveThinking",
|
|
705
|
+
"allowEmptySignature",
|
|
706
|
+
"supportsStrictTools"
|
|
707
|
+
],
|
|
708
|
+
"bedrock-converse-stream": ["supportsStrictMode"]
|
|
709
|
+
};
|
|
710
|
+
/**
|
|
711
|
+
* Switches the base panel already renders a dedicated control for; the
|
|
712
|
+
* "other compatibility switches" block lists everything else.
|
|
713
|
+
*/
|
|
714
|
+
const COMPAT_BASE_FIELDS = [
|
|
715
|
+
"supportsDeveloperRole",
|
|
716
|
+
"supportsReasoningEffort",
|
|
717
|
+
"thinkingFormat"
|
|
718
|
+
];
|
|
719
|
+
/**
|
|
720
|
+
* The offered compat field names for one wire protocol.
|
|
721
|
+
* @param api - the model's protocol, when known.
|
|
722
|
+
* @returns the field names; empty for an unknown or absent protocol.
|
|
723
|
+
*/
|
|
724
|
+
function offeredCompatFields(api) {
|
|
725
|
+
if (api === void 0) return [];
|
|
726
|
+
const fields = COMPAT_FIELDS[api];
|
|
727
|
+
return fields === void 0 ? [] : fields;
|
|
728
|
+
}
|
|
497
729
|
/** Extra copy for the extension, keyed like the official dictionaries (en is authoritative). */
|
|
498
730
|
const en = {
|
|
499
731
|
nav: "Models+",
|
|
@@ -719,6 +951,104 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
719
951
|
thinkingFormat: "思考格式"
|
|
720
952
|
};
|
|
721
953
|
//#endregion
|
|
954
|
+
//#region src/client/models-plus/prefill.ts
|
|
955
|
+
/**
|
|
956
|
+
* Mapping pi-ai catalog values onto a Models+ row.
|
|
957
|
+
*
|
|
958
|
+
* The two sides describe the same thing, but two conventions differ in a way
|
|
959
|
+
* that makes a straight copy wrong:
|
|
960
|
+
*
|
|
961
|
+
* 1. **`null` means the opposite in each.** pi-ai's `thinkingLevelMap[level]`
|
|
962
|
+
* is *unsupported* when null; our `reasoningEfforts[level]` is *supported,
|
|
963
|
+
* send nothing* when null (the adapter then drops the key entirely, and
|
|
964
|
+
* pi-ai reads an absent `off` key as "supported but send no parameter").
|
|
965
|
+
* Copying one across would invert the meaning for 338 of the catalog's
|
|
966
|
+
* models, so values are mapped rather than copied: a pi-ai `null` becomes
|
|
967
|
+
* "omit our key", and an absent pi-ai key becomes an explicit entry.
|
|
968
|
+
* 2. **An absent base level has a protocol-specific default.** pi-ai resolves
|
|
969
|
+
* an absent `off`/`minimal`/`low`/`medium`/`high` by falling back to the
|
|
970
|
+
* level name — except on the Anthropic protocols, where `minimal` collapses
|
|
971
|
+
* onto `low`, and on Mistral, where every level resolves to `high`. Writing
|
|
972
|
+
* the level name there would send a spelling those providers never see.
|
|
973
|
+
*
|
|
974
|
+
* `xhigh` and `max` are the one pair pi-ai treats as unsupported when absent,
|
|
975
|
+
* so they are only written when the catalog declares them.
|
|
976
|
+
*
|
|
977
|
+
* @module dsh-model-extension/prefill
|
|
978
|
+
*/
|
|
979
|
+
/** The seven levels, in pi-ai's escalation order. */
|
|
980
|
+
const LEVELS = [
|
|
981
|
+
"off",
|
|
982
|
+
"minimal",
|
|
983
|
+
"low",
|
|
984
|
+
"medium",
|
|
985
|
+
"high",
|
|
986
|
+
"xhigh",
|
|
987
|
+
"max"
|
|
988
|
+
];
|
|
989
|
+
/**
|
|
990
|
+
* The wire spelling pi-ai falls back to for one absent base level, per
|
|
991
|
+
* protocol. This mirrors the fallbacks inside pi-ai's own dispatch branches.
|
|
992
|
+
* @param api - the model's wire protocol.
|
|
993
|
+
* @param level - the level whose key was absent.
|
|
994
|
+
* @returns the spelling pi-ai would send for that level.
|
|
995
|
+
*/
|
|
996
|
+
function defaultWireSpelling(api, level) {
|
|
997
|
+
if (api === "anthropic-messages" || api === "bedrock-converse-stream") return level === "minimal" ? "low" : level;
|
|
998
|
+
if (api === "mistral-conversations") return "high";
|
|
999
|
+
return level;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Convert a catalog `thinkingLevelMap` into our `reasoningEfforts` dict.
|
|
1003
|
+
*
|
|
1004
|
+
* A model that does not reason gets `false`. An outcome that offers nothing
|
|
1005
|
+
* beyond `off` also becomes `false`, because the adapter rejects a dict whose
|
|
1006
|
+
* only level is `off` — and the two describe the same request anyway.
|
|
1007
|
+
* @param levelMap - the catalog's map, when it carries one.
|
|
1008
|
+
* @param api - the model's wire protocol, which picks the fallback spellings.
|
|
1009
|
+
* @param reasoning - whether the model reasons at all.
|
|
1010
|
+
* @returns the dict to write into the row, or `false` for a non-reasoning model.
|
|
1011
|
+
*/
|
|
1012
|
+
function reasoningEffortsFrom(levelMap, api, reasoning) {
|
|
1013
|
+
if (!reasoning) return false;
|
|
1014
|
+
const map = levelMap ?? {};
|
|
1015
|
+
const efforts = {};
|
|
1016
|
+
for (const level of LEVELS) {
|
|
1017
|
+
if (!(level in map)) {
|
|
1018
|
+
if (level === "off") {
|
|
1019
|
+
efforts[level] = null;
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
if (level === "xhigh" || level === "max") continue;
|
|
1023
|
+
efforts[level] = defaultWireSpelling(api, level);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const wire = map[level];
|
|
1027
|
+
if (wire === null) continue;
|
|
1028
|
+
efforts[level] = wire;
|
|
1029
|
+
}
|
|
1030
|
+
return Object.keys(efforts).some((level) => level !== "off") ? efforts : false;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Project a catalog `compat` block onto the switches the host offers for one
|
|
1034
|
+
* protocol.
|
|
1035
|
+
*
|
|
1036
|
+
* The filter is load-bearing: pi-ai's catalog also records withheld switches
|
|
1037
|
+
* (routing preferences, session affinity, deferred tools, grammar tools), and
|
|
1038
|
+
* the adapter *refuses* a switch no protocol offers instead of ignoring it — a
|
|
1039
|
+
* verbatim copy would make the route fail to resolve.
|
|
1040
|
+
* @param compat - the catalog's compat record, when it carries one.
|
|
1041
|
+
* @param api - the model's wire protocol.
|
|
1042
|
+
* @returns the writable subset; empty when nothing survives.
|
|
1043
|
+
*/
|
|
1044
|
+
function compatFrom(compat, api) {
|
|
1045
|
+
const offered = offeredCompatFields(api);
|
|
1046
|
+
if (offered.length === 0 || compat === void 0) return {};
|
|
1047
|
+
const next = {};
|
|
1048
|
+
for (const field of offered) if (field in compat) next[field] = compat[field];
|
|
1049
|
+
return next;
|
|
1050
|
+
}
|
|
1051
|
+
//#endregion
|
|
722
1052
|
//#region src/client/models-plus/models-plus.module.css
|
|
723
1053
|
var models_plus_module_default = {
|
|
724
1054
|
"addActions": "J84-9a_addActions",
|
|
@@ -812,33 +1142,164 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
812
1142
|
//#endregion
|
|
813
1143
|
//#region src/client/models-plus/QuickLoad.tsx
|
|
814
1144
|
/**
|
|
815
|
-
* Quick-load:
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
1145
|
+
* Quick-load: source → provider → model id, three narrowing steps.
|
|
1146
|
+
*
|
|
1147
|
+
* The single search box could not answer the question it raised. One model id
|
|
1148
|
+
* ships under a dozen providers (`glm-5.3-flash` exists in zai, opencode,
|
|
1149
|
+
* opencode-go, baseten, together, huggingface and two gateways), and each
|
|
1150
|
+
* provider's copy can carry different capacities and compat switches — so a
|
|
1151
|
+
* flat list of ten same-looking ids left the user to pick blind, or to decode
|
|
1152
|
+
* a suffix the plugin had appended for want of a better idea.
|
|
1153
|
+
*
|
|
1154
|
+
* Splitting the axis fixes it at the source: the user states *which catalog*
|
|
1155
|
+
* and *whose copy* first, and the id search then runs inside that slice.
|
|
1156
|
+
*
|
|
1157
|
+
* Both drop-downs are plugin-rendered rather than native. A `<datalist>` was
|
|
1158
|
+
* tried for the provider field and abandoned: the browser positions its popup
|
|
1159
|
+
* itself (it opened across unrelated fields) and none of it is themeable, so
|
|
1160
|
+
* it could not look like the list beside it. One `quickList` now serves both,
|
|
1161
|
+
* which also keeps only one popup on screen at a time.
|
|
1162
|
+
*/
|
|
1163
|
+
/** The two sources, in the order the picker offers them; pi-ai leads by default. */
|
|
1164
|
+
const SOURCES = [{
|
|
1165
|
+
id: "pi-ai",
|
|
1166
|
+
label: "llm-pi-ai"
|
|
1167
|
+
}, {
|
|
1168
|
+
id: "models.dev",
|
|
1169
|
+
label: "models.dev"
|
|
1170
|
+
}];
|
|
1171
|
+
/**
|
|
1172
|
+
* One line describing what the selected source currently offers, shown while
|
|
1173
|
+
* nothing is typed. The pi-ai version and snapshot date matter here: a DSH home
|
|
1174
|
+
* can hold more than one pi-ai copy, and knowing which one answered is the
|
|
1175
|
+
* difference between a diagnosis and a mystery.
|
|
1176
|
+
* @param index - the fetched index.
|
|
1177
|
+
* @param source - the selected source.
|
|
1178
|
+
* @returns the summary line.
|
|
820
1179
|
*/
|
|
1180
|
+
function sourceSummary(index, source) {
|
|
1181
|
+
if (source === "pi-ai") {
|
|
1182
|
+
if (index.piAi.length === 0) return index.piAiFailure === void 0 ? "pi-ai 目录不可用" : `pi-ai 目录不可用:${index.piAiFailure}`;
|
|
1183
|
+
const version = index.piAiVersion === void 0 ? "" : ` v${index.piAiVersion}`;
|
|
1184
|
+
const when = index.piAiGeneratedAt === void 0 ? "" : ` · ${index.piAiGeneratedAt.slice(0, 10)}`;
|
|
1185
|
+
return `pi-ai 目录${version},${String(index.piAi.length)} 个模型${when};选择提供方或输入 model id`;
|
|
1186
|
+
}
|
|
1187
|
+
if (index.modelsDev.length === 0) return "models.dev 元数据未下载——点击页首「下载/更新元数据」";
|
|
1188
|
+
return `models.dev,${String(index.modelsDev.length)} 条;选择提供方或输入 model id`;
|
|
1189
|
+
}
|
|
821
1190
|
/**
|
|
822
|
-
* Render the quick-load
|
|
1191
|
+
* Render the three-step quick-load picker and its candidate list.
|
|
823
1192
|
* @param props - the pick handler and disabled flag.
|
|
824
|
-
* @returns the quick-load
|
|
1193
|
+
* @returns the quick-load block.
|
|
825
1194
|
*/
|
|
826
1195
|
function QuickLoad(props) {
|
|
827
|
-
const [
|
|
828
|
-
const [
|
|
1196
|
+
const [source, setSource] = (0, react.useState)("pi-ai");
|
|
1197
|
+
const [provider, setProvider] = (0, react.useState)("");
|
|
1198
|
+
const [text, setText] = (0, react.useState)("");
|
|
1199
|
+
const [panel, setPanel] = (0, react.useState)(null);
|
|
829
1200
|
const [index, setIndex] = (0, react.useState)(void 0);
|
|
1201
|
+
const textField = (0, react.useRef)(null);
|
|
830
1202
|
const ensureIndex = () => {
|
|
831
|
-
|
|
832
|
-
getMetadataIndex().then((loaded) => {
|
|
1203
|
+
getPrefillIndex().then((loaded) => {
|
|
833
1204
|
setIndex(loaded);
|
|
834
1205
|
});
|
|
835
1206
|
};
|
|
836
|
-
const
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1207
|
+
const providers = providersOf(index, source);
|
|
1208
|
+
const chosenProvider = providers.includes(provider) ? provider : "";
|
|
1209
|
+
const { hits, total } = searchPrefill(index, {
|
|
1210
|
+
source,
|
|
1211
|
+
provider: chosenProvider,
|
|
1212
|
+
text
|
|
1213
|
+
});
|
|
1214
|
+
const pick = (entry) => {
|
|
1215
|
+
setPanel(null);
|
|
1216
|
+
setText(entry.id);
|
|
1217
|
+
props.onPick(entry);
|
|
1218
|
+
};
|
|
1219
|
+
const switchSource = (next) => {
|
|
1220
|
+
setSource(next);
|
|
1221
|
+
setProvider("");
|
|
1222
|
+
setText("");
|
|
1223
|
+
setPanel(null);
|
|
1224
|
+
};
|
|
1225
|
+
/**
|
|
1226
|
+
* Commit one provider choice and move straight on to the id field, so the
|
|
1227
|
+
* flow is pick → type rather than pick → click → type.
|
|
1228
|
+
* @param name - the provider to select; blank means "every provider".
|
|
1229
|
+
*/
|
|
1230
|
+
const chooseProvider = (name) => {
|
|
1231
|
+
setProvider(name);
|
|
1232
|
+
setText("");
|
|
1233
|
+
textField.current?.focus();
|
|
1234
|
+
};
|
|
1235
|
+
const providerBody = () => {
|
|
1236
|
+
if (index === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1237
|
+
className: models_plus_module_default["quickEmpty"],
|
|
1238
|
+
children: "正在载入预填数据…"
|
|
1239
|
+
});
|
|
1240
|
+
const needle = provider.trim().toLowerCase();
|
|
1241
|
+
const matches = needle.length === 0 ? providers : providers.filter((name) => name.toLowerCase().includes(needle));
|
|
1242
|
+
if (providers.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1243
|
+
className: models_plus_module_default["quickEmpty"],
|
|
1244
|
+
children: "该数据源没有提供方信息"
|
|
1245
|
+
});
|
|
1246
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1247
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1248
|
+
type: "button",
|
|
1249
|
+
className: models_plus_module_default["quickItem"],
|
|
1250
|
+
onMouseDown: (event) => {
|
|
1251
|
+
event.preventDefault();
|
|
1252
|
+
chooseProvider("");
|
|
1253
|
+
},
|
|
1254
|
+
children: "全部(不限提供方)"
|
|
1255
|
+
}),
|
|
1256
|
+
matches.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1257
|
+
type: "button",
|
|
1258
|
+
className: models_plus_module_default["quickItem"],
|
|
1259
|
+
onMouseDown: (event) => {
|
|
1260
|
+
event.preventDefault();
|
|
1261
|
+
chooseProvider(name);
|
|
1262
|
+
},
|
|
1263
|
+
children: name
|
|
1264
|
+
}, name)),
|
|
1265
|
+
matches.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1266
|
+
className: models_plus_module_default["quickEmpty"],
|
|
1267
|
+
children: "没有匹配的提供方"
|
|
1268
|
+
}) : null
|
|
1269
|
+
] });
|
|
1270
|
+
};
|
|
1271
|
+
const modelBody = () => {
|
|
1272
|
+
if (index === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1273
|
+
className: models_plus_module_default["quickEmpty"],
|
|
1274
|
+
children: "正在载入预填数据…"
|
|
1275
|
+
});
|
|
1276
|
+
if (chosenProvider.length === 0 && text.trim().length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1277
|
+
className: models_plus_module_default["quickEmpty"],
|
|
1278
|
+
children: sourceSummary(index, source)
|
|
1279
|
+
}), source === "pi-ai" && index.piAiWarnings.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1280
|
+
className: models_plus_module_default["error"],
|
|
1281
|
+
children: index.piAiWarnings.join(";")
|
|
1282
|
+
}) : null] });
|
|
1283
|
+
if (hits.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1284
|
+
className: models_plus_module_default["quickEmpty"],
|
|
1285
|
+
children: "没有匹配的模型"
|
|
1286
|
+
});
|
|
1287
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [hits.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1288
|
+
type: "button",
|
|
1289
|
+
className: models_plus_module_default["quickItem"],
|
|
1290
|
+
onMouseDown: (event) => {
|
|
1291
|
+
event.preventDefault();
|
|
1292
|
+
pick(entry);
|
|
1293
|
+
},
|
|
1294
|
+
children: entry.id
|
|
1295
|
+
}, entry.key)), total > hits.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1296
|
+
className: models_plus_module_default["quickEmpty"],
|
|
1297
|
+
children: [
|
|
1298
|
+
"另有 ",
|
|
1299
|
+
total - hits.length,
|
|
1300
|
+
" 项匹配——继续输入或选定提供方以缩小范围"
|
|
1301
|
+
]
|
|
1302
|
+
}) : null] });
|
|
842
1303
|
};
|
|
843
1304
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
844
1305
|
className: models_plus_module_default["quickWrap"],
|
|
@@ -847,42 +1308,88 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
847
1308
|
className: models_plus_module_default["extLabel"],
|
|
848
1309
|
children: "快速装入"
|
|
849
1310
|
}),
|
|
850
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
"aria-label": "快速装入",
|
|
856
|
-
disabled: props.disabled,
|
|
857
|
-
onFocus: () => {
|
|
858
|
-
ensureIndex();
|
|
859
|
-
setOpen(true);
|
|
860
|
-
},
|
|
861
|
-
onBlur: () => {
|
|
862
|
-
setOpen(false);
|
|
1311
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1312
|
+
style: {
|
|
1313
|
+
display: "flex",
|
|
1314
|
+
gap: 6,
|
|
1315
|
+
alignItems: "center"
|
|
863
1316
|
},
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1317
|
+
children: [
|
|
1318
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
1319
|
+
className: `${models_plus_module_default["input"]} ${models_plus_module_default["selectInput"]}`,
|
|
1320
|
+
style: {
|
|
1321
|
+
flex: "0 0 108px",
|
|
1322
|
+
minWidth: 0
|
|
1323
|
+
},
|
|
1324
|
+
value: source,
|
|
1325
|
+
disabled: props.disabled,
|
|
1326
|
+
"aria-label": "数据源",
|
|
1327
|
+
onChange: (event) => {
|
|
1328
|
+
switchSource(event.target.value);
|
|
1329
|
+
},
|
|
1330
|
+
children: SOURCES.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1331
|
+
value: option.id,
|
|
1332
|
+
children: option.label
|
|
1333
|
+
}, option.id))
|
|
1334
|
+
}),
|
|
1335
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1336
|
+
className: models_plus_module_default["input"],
|
|
1337
|
+
style: {
|
|
1338
|
+
flex: "0 0 132px",
|
|
1339
|
+
minWidth: 0
|
|
1340
|
+
},
|
|
1341
|
+
type: "text",
|
|
1342
|
+
value: provider,
|
|
1343
|
+
placeholder: "提供方:全部",
|
|
1344
|
+
"aria-label": "提供方",
|
|
1345
|
+
autoComplete: "off",
|
|
1346
|
+
disabled: props.disabled,
|
|
1347
|
+
onFocus: () => {
|
|
1348
|
+
ensureIndex();
|
|
1349
|
+
setPanel("provider");
|
|
1350
|
+
},
|
|
1351
|
+
onBlur: () => {
|
|
1352
|
+
setPanel(null);
|
|
1353
|
+
},
|
|
1354
|
+
onChange: (event) => {
|
|
1355
|
+
setProvider(event.target.value);
|
|
1356
|
+
setPanel("provider");
|
|
1357
|
+
}
|
|
1358
|
+
}),
|
|
1359
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1360
|
+
className: models_plus_module_default["input"],
|
|
1361
|
+
style: {
|
|
1362
|
+
flex: "1 1 auto",
|
|
1363
|
+
minWidth: 0
|
|
1364
|
+
},
|
|
1365
|
+
type: "text",
|
|
1366
|
+
ref: textField,
|
|
1367
|
+
value: text,
|
|
1368
|
+
placeholder: "model id(如 glm)…",
|
|
1369
|
+
"aria-label": "model id",
|
|
1370
|
+
autoComplete: "off",
|
|
1371
|
+
disabled: props.disabled,
|
|
1372
|
+
onFocus: () => {
|
|
1373
|
+
ensureIndex();
|
|
1374
|
+
setPanel("model");
|
|
1375
|
+
},
|
|
1376
|
+
onBlur: () => {
|
|
1377
|
+
setPanel(null);
|
|
1378
|
+
},
|
|
1379
|
+
onChange: (event) => {
|
|
1380
|
+
setText(event.target.value);
|
|
1381
|
+
setPanel("model");
|
|
1382
|
+
}
|
|
1383
|
+
})
|
|
1384
|
+
]
|
|
868
1385
|
}),
|
|
869
|
-
|
|
1386
|
+
panel === "provider" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
870
1387
|
className: models_plus_module_default["quickList"],
|
|
871
|
-
children:
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
children: query.trim().length === 0 ? "输入关键字搜索模型 id" : "没有匹配的模型"
|
|
877
|
-
}) : hits.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
878
|
-
type: "button",
|
|
879
|
-
className: models_plus_module_default["quickItem"],
|
|
880
|
-
onMouseDown: (event) => {
|
|
881
|
-
event.preventDefault();
|
|
882
|
-
pick(id);
|
|
883
|
-
},
|
|
884
|
-
children: id
|
|
885
|
-
}, id))
|
|
1388
|
+
children: providerBody()
|
|
1389
|
+
}) : null,
|
|
1390
|
+
panel === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1391
|
+
className: models_plus_module_default["quickList"],
|
|
1392
|
+
children: modelBody()
|
|
886
1393
|
}) : null
|
|
887
1394
|
]
|
|
888
1395
|
});
|
|
@@ -900,6 +1407,15 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
900
1407
|
* the effort gate drops `reasoningEfforts` AND `compat.thinkingFormat` in ONE
|
|
901
1408
|
* commit so partial updates never overwrite each other (no orphan settings).
|
|
902
1409
|
*/
|
|
1410
|
+
/**
|
|
1411
|
+
* Whether a draft field is absent or whitespace-only.
|
|
1412
|
+
* @param value - the field's current value.
|
|
1413
|
+
* @returns true when the field carries nothing worth keeping.
|
|
1414
|
+
*/
|
|
1415
|
+
function isBlank(value) {
|
|
1416
|
+
if (value === void 0 || value === null) return true;
|
|
1417
|
+
return typeof value === "string" && value.trim().length === 0;
|
|
1418
|
+
}
|
|
903
1419
|
/** The row's compat sub-object as a plain record (absent → empty). */
|
|
904
1420
|
function compatOf(model) {
|
|
905
1421
|
const value = model["compat"];
|
|
@@ -944,21 +1460,44 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
944
1460
|
const setInput = (modalities) => {
|
|
945
1461
|
setKey("input", modalities.length === 0 ? void 0 : [...modalities]);
|
|
946
1462
|
};
|
|
1463
|
+
/**
|
|
1464
|
+
* Apply one prefill candidate.
|
|
1465
|
+
*
|
|
1466
|
+
* Every value is *restated* from the picked source rather than merged into
|
|
1467
|
+
* whatever was already there: capacities, modalities, reasoning levels and
|
|
1468
|
+
* compat are replaced wholesale. A prefill answers "what does this catalog
|
|
1469
|
+
* say about the model", so an absent field clears — leaving another source's
|
|
1470
|
+
* value behind would describe a model neither catalog claims, and switching
|
|
1471
|
+
* sources then re-picking is meant to be a clean rewrite.
|
|
1472
|
+
*
|
|
1473
|
+
* Two things do not follow that rule. `id` and `name` are filled only into a
|
|
1474
|
+
* blank row, because a catalog id is the request id for the vendor's *own*
|
|
1475
|
+
* endpoint while behind a gateway the string on the wire is often different.
|
|
1476
|
+
* And pi-ai's level map is *mapped* rather than copied, because `null` means
|
|
1477
|
+
* the opposite on each side.
|
|
1478
|
+
* @param entry - the picked candidate.
|
|
1479
|
+
*/
|
|
947
1480
|
const applyMetadata = (entry) => {
|
|
948
1481
|
const next = { ...model };
|
|
949
1482
|
if (entry.context === void 0) Reflect.deleteProperty(next, "contextWindow");
|
|
950
1483
|
else next["contextWindow"] = entry.context;
|
|
951
1484
|
if (entry.output === void 0) Reflect.deleteProperty(next, "maxTokens");
|
|
952
1485
|
else next["maxTokens"] = entry.output;
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1486
|
+
const modality = entry.input.filter((m) => m === "text" || m === "image");
|
|
1487
|
+
if (modality.length > 0) next["input"] = modality;
|
|
1488
|
+
else Reflect.deleteProperty(next, "input");
|
|
1489
|
+
if (entry.source === "pi-ai") {
|
|
1490
|
+
if (isBlank(next["id"])) next["id"] = entry.id;
|
|
1491
|
+
if (isBlank(next["name"]) && entry.name !== void 0) next["name"] = entry.name;
|
|
1492
|
+
next["reasoningEfforts"] = reasoningEffortsFrom(entry.thinkingLevelMap, entry.api, entry.reasoning);
|
|
1493
|
+
const compat = compatFrom(entry.compat, entry.api);
|
|
1494
|
+
if (Object.keys(compat).length === 0) Reflect.deleteProperty(next, "compat");
|
|
1495
|
+
else next["compat"] = compat;
|
|
1496
|
+
} else {
|
|
957
1497
|
Reflect.deleteProperty(next, "reasoningEfforts");
|
|
1498
|
+
if (entry.reasoning) next["compat"] = { supportsReasoningEffort: true };
|
|
1499
|
+
else Reflect.deleteProperty(next, "compat");
|
|
958
1500
|
}
|
|
959
|
-
setKey("compat", Object.keys(compat).length === 0 ? void 0 : compat);
|
|
960
|
-
const supported = entry.input.filter((m) => m === "text" || m === "image");
|
|
961
|
-
if (supported.length > 0) next["input"] = supported;
|
|
962
1501
|
onChange(next);
|
|
963
1502
|
setCapacityText(/* @__PURE__ */ new Map());
|
|
964
1503
|
};
|
|
@@ -998,8 +1537,48 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
998
1537
|
};
|
|
999
1538
|
const compat = compatOf(model);
|
|
1000
1539
|
const developerRole = compat["supportsDeveloperRole"] !== false;
|
|
1001
|
-
|
|
1540
|
+
/**
|
|
1541
|
+
* Switches a prefill wrote that the block above has no control for. Listing
|
|
1542
|
+
* them keeps a prefilled value from becoming invisible state: the pi-ai
|
|
1543
|
+
* catalog records far more of the compat surface than this panel models
|
|
1544
|
+
* explicitly, and that API refuses an unknown switch rather than ignoring
|
|
1545
|
+
* it — so a value the user cannot see is a value the user cannot fix.
|
|
1546
|
+
*/
|
|
1547
|
+
const extraCompat = Object.entries(compat).filter(([key]) => !COMPAT_BASE_FIELDS.includes(key)).sort(([a], [b]) => a.localeCompare(b));
|
|
1548
|
+
/**
|
|
1549
|
+
* Read an edited switch back as the type it already had.
|
|
1550
|
+
* @param previous - the value the switch carried when rendered.
|
|
1551
|
+
* @param raw - the field text.
|
|
1552
|
+
* @returns the coerced value, or the previous one when the text is unreadable.
|
|
1553
|
+
*/
|
|
1554
|
+
const readCompatValue = (previous, raw) => {
|
|
1555
|
+
if (typeof previous === "boolean") return raw === "true";
|
|
1556
|
+
if (typeof previous === "number") {
|
|
1557
|
+
const parsed = Number(raw);
|
|
1558
|
+
return Number.isNaN(parsed) ? previous : parsed;
|
|
1559
|
+
}
|
|
1560
|
+
if (typeof previous === "string") return raw;
|
|
1561
|
+
try {
|
|
1562
|
+
return JSON.parse(raw);
|
|
1563
|
+
} catch {
|
|
1564
|
+
return previous;
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
/** The switch as a wire field: whether the endpoint takes `reasoning_effort`. */
|
|
1568
|
+
const gateOn = compat["supportsReasoningEffort"] === true;
|
|
1002
1569
|
const efforts = effortsOf(model);
|
|
1570
|
+
/**
|
|
1571
|
+
* Whether the reasoning area is worth showing.
|
|
1572
|
+
*
|
|
1573
|
+
* The switch and the area answer different questions: the switch tells the
|
|
1574
|
+
* adapter the endpoint accepts `reasoning_effort`, while the area edits what
|
|
1575
|
+
* *this model* offers. A pi-ai prefill can leave the switch unset on purpose
|
|
1576
|
+
* — an absent key lets the adapter auto-detect from the endpoint — while
|
|
1577
|
+
* still carrying levels worth editing, and those levels reach the provider
|
|
1578
|
+
* through the protocol's own thinking parameter either way. So the area keys
|
|
1579
|
+
* off either signal, not the switch alone.
|
|
1580
|
+
*/
|
|
1581
|
+
const reasoningAreaShown = gateOn || efforts !== void 0 || compat["thinkingFormat"] !== void 0;
|
|
1003
1582
|
const toggleGate = (checked) => {
|
|
1004
1583
|
if (checked) {
|
|
1005
1584
|
setCompatKey("supportsReasoningEffort", true);
|
|
@@ -1107,24 +1686,28 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
1107
1686
|
})]
|
|
1108
1687
|
})
|
|
1109
1688
|
}),
|
|
1110
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1111
1690
|
className: models_plus_module_default["extGroup"],
|
|
1112
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1691
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1113
1692
|
className: models_plus_module_default["switchRow"],
|
|
1114
1693
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1115
1694
|
type: "checkbox",
|
|
1116
1695
|
className: models_plus_module_default["extCheckbox"],
|
|
1117
|
-
checked:
|
|
1696
|
+
checked: gateOn,
|
|
1118
1697
|
disabled,
|
|
1119
1698
|
onChange: (event) => {
|
|
1120
1699
|
toggleGate(event.target.checked);
|
|
1121
1700
|
}
|
|
1122
1701
|
}), "端点接受推理挡位参数(reasoning_effort)"]
|
|
1123
|
-
})
|
|
1702
|
+
}), !gateOn && reasoningAreaShown ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1703
|
+
className: models_plus_module_default["extHint"],
|
|
1704
|
+
style: { marginTop: 6 },
|
|
1705
|
+
children: "此项未设置:由 llm-pi-ai 按其 baseURL 检测结果决定是否发送 reasoning_effort; 下面的挡位不受影响,仍通过各协议自己的 thinking 参数生效。"
|
|
1706
|
+
}) : null]
|
|
1124
1707
|
}),
|
|
1125
1708
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1126
1709
|
className: models_plus_module_default["extGated"],
|
|
1127
|
-
hidden: !
|
|
1710
|
+
hidden: !reasoningAreaShown,
|
|
1128
1711
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1129
1712
|
className: models_plus_module_default["extField"],
|
|
1130
1713
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1218,7 +1801,50 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
1218
1801
|
}) : null
|
|
1219
1802
|
]
|
|
1220
1803
|
})]
|
|
1221
|
-
})
|
|
1804
|
+
}),
|
|
1805
|
+
extraCompat.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1806
|
+
className: models_plus_module_default["extGroup"],
|
|
1807
|
+
children: [
|
|
1808
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1809
|
+
className: models_plus_module_default["extLabel"],
|
|
1810
|
+
style: { marginBottom: 6 },
|
|
1811
|
+
children: "其他兼容开关"
|
|
1812
|
+
}),
|
|
1813
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1814
|
+
style: {
|
|
1815
|
+
display: "flex",
|
|
1816
|
+
flexDirection: "column",
|
|
1817
|
+
gap: 6
|
|
1818
|
+
},
|
|
1819
|
+
children: extraCompat.map(([key, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1820
|
+
style: {
|
|
1821
|
+
display: "flex",
|
|
1822
|
+
flexDirection: "column",
|
|
1823
|
+
gap: 2
|
|
1824
|
+
},
|
|
1825
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1826
|
+
className: models_plus_module_default["extHint"],
|
|
1827
|
+
children: key
|
|
1828
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1829
|
+
className: models_plus_module_default["input"],
|
|
1830
|
+
type: "text",
|
|
1831
|
+
defaultValue: typeof value === "string" ? value : JSON.stringify(value) ?? "",
|
|
1832
|
+
"aria-label": key,
|
|
1833
|
+
disabled,
|
|
1834
|
+
onBlur: (event) => {
|
|
1835
|
+
const raw = event.target.value.trim();
|
|
1836
|
+
setCompatKey(key, raw.length === 0 ? void 0 : readCompatValue(value, raw));
|
|
1837
|
+
}
|
|
1838
|
+
})]
|
|
1839
|
+
}, key))
|
|
1840
|
+
}),
|
|
1841
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1842
|
+
className: models_plus_module_default["extHint"],
|
|
1843
|
+
style: { marginTop: 6 },
|
|
1844
|
+
children: "这些开关由 pi-ai 目录预填;清空输入框即移除该开关,回到目录默认。"
|
|
1845
|
+
})
|
|
1846
|
+
]
|
|
1847
|
+
}) : null
|
|
1222
1848
|
]
|
|
1223
1849
|
});
|
|
1224
1850
|
}
|
|
@@ -1468,7 +2094,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
1468
2094
|
onChange(models.map((row, at) => at === index ? next : row));
|
|
1469
2095
|
}
|
|
1470
2096
|
}) : null]
|
|
1471
|
-
},
|
|
2097
|
+
}, String(index)))
|
|
1472
2098
|
}),
|
|
1473
2099
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1474
2100
|
type: "button",
|
|
@@ -1778,7 +2404,7 @@ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin
|
|
|
1778
2404
|
role: "tabpanel",
|
|
1779
2405
|
hidden: tab !== "provider",
|
|
1780
2406
|
children: [
|
|
1781
|
-
layout === "pi-ai"
|
|
2407
|
+
layout === "pi-ai" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1782
2408
|
className: models_plus_module_default["field"],
|
|
1783
2409
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1784
2410
|
className: models_plus_module_default["fieldLabel"],
|