@dsh-plus/llm-pi 0.1.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/lib/client.js ADDED
@@ -0,0 +1,1904 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@dsh-plus/llm-pi",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ //#region rolldown:runtime
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+
29
+ //#endregion
30
+ let react = require("react");
31
+ react = __toESM(react);
32
+ let react_jsx_runtime = require("react/jsx-runtime");
33
+ react_jsx_runtime = __toESM(react_jsx_runtime);
34
+
35
+ //#region src/client/api.ts
36
+ const ROUTE_CONFIG = "/dsh-plus/llm-pi/config";
37
+ const ROUTE_CATALOG = "/dsh-plus/llm-pi/catalog";
38
+ async function parse(res) {
39
+ const body = await res.json();
40
+ if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
41
+ return body;
42
+ }
43
+ async function fetchConfig() {
44
+ return parse(await fetch(ROUTE_CONFIG, { credentials: "same-origin" }));
45
+ }
46
+ async function saveConfig(patch) {
47
+ return parse(await fetch(ROUTE_CONFIG, {
48
+ method: "PUT",
49
+ credentials: "same-origin",
50
+ headers: { "content-type": "application/json" },
51
+ body: JSON.stringify(patch)
52
+ }));
53
+ }
54
+ /** 目录查询:provider 为空时只返回该源的 provider 列表。 */
55
+ async function fetchCatalog(provider, source) {
56
+ const url = `${ROUTE_CATALOG}?provider=${encodeURIComponent(provider)}&source=${source}`;
57
+ return parse(await fetch(url, { credentials: "same-origin" }));
58
+ }
59
+ /** 手动拉取 models.dev 目录:POST /catalog/refresh → 最新快照状态。 */
60
+ async function refreshCatalog() {
61
+ return parse(await fetch(`${ROUTE_CATALOG}/refresh`, {
62
+ method: "POST",
63
+ credentials: "same-origin"
64
+ }));
65
+ }
66
+
67
+ //#endregion
68
+ //#region src/client/draft.ts
69
+ function numToText(value) {
70
+ return value === void 0 ? "" : String(value);
71
+ }
72
+ /** 数字文本 → 数值;空串/非法返回 undefined(不写入)。 */
73
+ function toNum(text) {
74
+ const trimmed = text.trim();
75
+ if (trimmed === "") return void 0;
76
+ const value = Number(trimmed);
77
+ return Number.isFinite(value) ? value : void 0;
78
+ }
79
+ function numTextOk(text) {
80
+ return text.trim() !== "" && Number.isFinite(Number(text.trim()));
81
+ }
82
+ function headersToPairs(headers) {
83
+ return Object.entries(headers ?? {}).map(([key, value]) => ({
84
+ key,
85
+ value
86
+ }));
87
+ }
88
+ function pairsToHeaders(pairs) {
89
+ const out = {};
90
+ for (const pair of pairs) if (pair.key.trim() !== "") out[pair.key.trim()] = pair.value;
91
+ return Object.keys(out).length > 0 ? out : void 0;
92
+ }
93
+ function inputFromWire(list) {
94
+ return {
95
+ text: list?.includes("text") ?? false,
96
+ image: list?.includes("image") ?? false
97
+ };
98
+ }
99
+ function inputToWire(input) {
100
+ const out = [];
101
+ if (input.text) out.push("text");
102
+ if (input.image) out.push("image");
103
+ return out.length > 0 ? out : void 0;
104
+ }
105
+ function reasoningFromWire(value) {
106
+ if (value === false) return {
107
+ nonReasoning: true,
108
+ levels: {}
109
+ };
110
+ const levels = {};
111
+ for (const [level, line] of Object.entries(value ?? {})) levels[level] = line ?? "";
112
+ return {
113
+ nonReasoning: false,
114
+ levels
115
+ };
116
+ }
117
+ function reasoningToWire(value) {
118
+ if (value.nonReasoning) return false;
119
+ const out = {};
120
+ for (const [level, line] of Object.entries(value.levels)) if (line.trim() !== "") out[level] = line.trim();
121
+ return Object.keys(out).length > 0 ? out : void 0;
122
+ }
123
+ function budgetFromWire(value) {
124
+ return {
125
+ minimal: numToText(value?.minimal),
126
+ low: numToText(value?.low),
127
+ medium: numToText(value?.medium),
128
+ high: numToText(value?.high)
129
+ };
130
+ }
131
+ function budgetToWire(value) {
132
+ const out = {};
133
+ for (const key of [
134
+ "minimal",
135
+ "low",
136
+ "medium",
137
+ "high"
138
+ ]) {
139
+ const num = toNum(value[key]);
140
+ if (num !== void 0) out[key] = num;
141
+ }
142
+ return Object.keys(out).length > 0 ? out : void 0;
143
+ }
144
+ /** 剔除空值:undefined / '' / 空数组 / 空对象。 */
145
+ function omitEmpty(obj) {
146
+ const out = {};
147
+ for (const [key, value] of Object.entries(obj)) {
148
+ if (value === void 0 || value === "") continue;
149
+ if (Array.isArray(value) && value.length === 0) continue;
150
+ if (typeof value === "object" && value !== null && Object.keys(value).length === 0) continue;
151
+ out[key] = value;
152
+ }
153
+ return out;
154
+ }
155
+ function modelDraftFromWire(model) {
156
+ return {
157
+ id: model.id,
158
+ extends: model.extends ?? "",
159
+ name: model.name ?? "",
160
+ contextWindow: numToText(model.contextWindow),
161
+ maxTokens: numToText(model.maxTokens),
162
+ input: inputFromWire(model.input),
163
+ reasoningEfforts: reasoningFromWire(model.reasoningEfforts),
164
+ compat: { ...model.compat ?? {} }
165
+ };
166
+ }
167
+ function modelToWire(model) {
168
+ const reasoningEfforts = reasoningToWire(model.reasoningEfforts);
169
+ return omitEmpty({
170
+ id: model.id.trim(),
171
+ extends: model.extends.trim(),
172
+ name: model.name.trim(),
173
+ contextWindow: toNum(model.contextWindow),
174
+ maxTokens: toNum(model.maxTokens),
175
+ input: inputToWire(model.input),
176
+ ...reasoningEfforts === void 0 ? {} : { reasoningEfforts },
177
+ compat: model.compat
178
+ });
179
+ }
180
+ function providerDraftFromWire(provider) {
181
+ return {
182
+ extends: provider.extends ?? "",
183
+ displayName: provider.displayName ?? "",
184
+ api: provider.api ?? "",
185
+ baseURL: provider.baseURL ?? "",
186
+ apiKeyEnv: provider.apiKeyEnv ?? "",
187
+ headers: headersToPairs(provider.headers),
188
+ compat: { ...provider.compat ?? {} },
189
+ defaultContextWindow: numToText(provider.defaultContextWindow),
190
+ defaultMaxTokens: numToText(provider.defaultMaxTokens),
191
+ input: inputFromWire(provider.defaultInput),
192
+ reasoning: provider.reasoning ?? "",
193
+ thinkingBudgets: budgetFromWire(provider.thinkingBudgets),
194
+ cacheRetention: provider.cacheRetention ?? "",
195
+ transport: provider.transport ?? "",
196
+ timeoutMs: numToText(provider.timeoutMs),
197
+ websocketConnectTimeoutMs: numToText(provider.websocketConnectTimeoutMs),
198
+ streamIdleTimeoutMs: numToText(provider.streamIdleTimeoutMs),
199
+ retryPolicy: provider.retryPolicy,
200
+ models: (provider.models ?? []).map(modelDraftFromWire)
201
+ };
202
+ }
203
+ function providerToWire(provider) {
204
+ return omitEmpty({
205
+ extends: provider.extends.trim(),
206
+ displayName: provider.displayName.trim(),
207
+ api: provider.api,
208
+ baseURL: provider.baseURL.trim(),
209
+ apiKeyEnv: provider.apiKeyEnv.trim(),
210
+ headers: pairsToHeaders(provider.headers),
211
+ compat: provider.compat,
212
+ defaultContextWindow: toNum(provider.defaultContextWindow),
213
+ defaultMaxTokens: toNum(provider.defaultMaxTokens),
214
+ input: inputToWire(provider.input),
215
+ reasoning: provider.reasoning,
216
+ thinkingBudgets: budgetToWire(provider.thinkingBudgets),
217
+ cacheRetention: provider.cacheRetention,
218
+ transport: provider.transport,
219
+ timeoutMs: toNum(provider.timeoutMs),
220
+ websocketConnectTimeoutMs: toNum(provider.websocketConnectTimeoutMs),
221
+ streamIdleTimeoutMs: toNum(provider.streamIdleTimeoutMs),
222
+ retryPolicy: provider.retryPolicy,
223
+ models: provider.models.map(modelToWire)
224
+ });
225
+ }
226
+ function emptyProviderDraft() {
227
+ return {
228
+ extends: "",
229
+ displayName: "",
230
+ api: "",
231
+ baseURL: "",
232
+ apiKeyEnv: "",
233
+ headers: [],
234
+ compat: {},
235
+ defaultContextWindow: "",
236
+ defaultMaxTokens: "",
237
+ input: {
238
+ text: false,
239
+ image: false
240
+ },
241
+ reasoning: "",
242
+ thinkingBudgets: {
243
+ minimal: "",
244
+ low: "",
245
+ medium: "",
246
+ high: ""
247
+ },
248
+ cacheRetention: "",
249
+ transport: "",
250
+ timeoutMs: "",
251
+ websocketConnectTimeoutMs: "",
252
+ streamIdleTimeoutMs: "",
253
+ retryPolicy: void 0,
254
+ models: []
255
+ };
256
+ }
257
+ function emptyModelDraft() {
258
+ return {
259
+ id: "",
260
+ extends: "",
261
+ name: "",
262
+ contextWindow: "",
263
+ maxTokens: "",
264
+ input: {
265
+ text: false,
266
+ image: false
267
+ },
268
+ reasoningEfforts: {
269
+ nonReasoning: false,
270
+ levels: {}
271
+ },
272
+ compat: {}
273
+ };
274
+ }
275
+ function draftFromWire(wire) {
276
+ return {
277
+ enabled: wire.enabled,
278
+ catalogUrl: wire.catalogUrl,
279
+ catalogRefreshHours: String(wire.catalogRefreshHours),
280
+ catalogProxy: wire.catalogProxy,
281
+ providers: Object.fromEntries(Object.entries(wire.providers).map(([route, provider]) => [route, providerDraftFromWire(provider)]))
282
+ };
283
+ }
284
+ /** 提交补丁:完整配置对象,providers 全量替换;空值一律剔除。 */
285
+ function toPatch(draft) {
286
+ return {
287
+ enabled: draft.enabled,
288
+ catalogUrl: draft.catalogUrl.trim(),
289
+ catalogRefreshHours: toNum(draft.catalogRefreshHours),
290
+ catalogProxy: draft.catalogProxy.trim(),
291
+ providers: Object.fromEntries(Object.entries(draft.providers).map(([route, provider]) => [route, providerToWire(provider)]))
292
+ };
293
+ }
294
+
295
+ //#endregion
296
+ //#region src/client/fields.tsx
297
+ function CollapseSection(props) {
298
+ const [open, setOpen] = (0, react.useState)(props.defaultOpen);
299
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
300
+ className: "lpc-collapse",
301
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
302
+ type: "button",
303
+ className: "lpc-collapseHead",
304
+ id: props.id,
305
+ "aria-expanded": open,
306
+ onClick: () => setOpen(!open),
307
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
308
+ className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}`,
309
+ children: "▾"
310
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
311
+ className: "lpc-collapseTitle",
312
+ children: props.title
313
+ })]
314
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
315
+ className: "lpc-collapseBody",
316
+ children: props.children
317
+ }) : null]
318
+ });
319
+ }
320
+ function TextField(props) {
321
+ const invalid = props.invalid === true;
322
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
323
+ className: `lpc-field${props.wide === true ? " lpc-wide" : ""}`,
324
+ children: [
325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
326
+ className: "lpc-head",
327
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
328
+ className: "lpc-label",
329
+ htmlFor: props.id,
330
+ children: props.label
331
+ })
332
+ }),
333
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
334
+ id: props.id,
335
+ className: `lpc-input${invalid ? " lpc-inputInvalid" : ""}`,
336
+ type: "text",
337
+ inputMode: props.numeric === true ? "numeric" : void 0,
338
+ list: props.list,
339
+ "aria-invalid": invalid || void 0,
340
+ value: props.value,
341
+ disabled: props.disabled === true,
342
+ onChange: (event) => props.onEdit(event.target.value)
343
+ }),
344
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
345
+ className: invalid ? "lpc-invalid" : "lpc-hint",
346
+ children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
347
+ })
348
+ ]
349
+ });
350
+ }
351
+ function CheckRow(props) {
352
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
353
+ className: "lpc-checkRow",
354
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
355
+ id: props.id,
356
+ type: "checkbox",
357
+ checked: props.checked,
358
+ disabled: props.disabled === true,
359
+ onChange: (event) => props.onEdit(event.target.checked)
360
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
361
+ htmlFor: props.id,
362
+ children: props.label
363
+ })]
364
+ });
365
+ }
366
+ function SelectField(props) {
367
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
368
+ className: `lpc-field${props.wide === true ? " lpc-wide" : ""}`,
369
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
370
+ className: "lpc-head",
371
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
372
+ className: "lpc-label",
373
+ htmlFor: props.id,
374
+ children: props.label
375
+ })
376
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
377
+ id: props.id,
378
+ className: "lpc-input lpc-select",
379
+ value: props.value,
380
+ disabled: props.disabled === true,
381
+ onChange: (event) => props.onEdit(event.target.value),
382
+ children: [props.unsetLabel !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
383
+ value: "",
384
+ children: props.unsetLabel
385
+ }) : null, props.options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
386
+ value: option,
387
+ children: option
388
+ }, option))]
389
+ })]
390
+ });
391
+ }
392
+ function KeyValueEditor(props) {
393
+ const update = (index, patch) => {
394
+ props.onEdit(props.pairs.map((pair, i) => i === index ? {
395
+ ...pair,
396
+ ...patch
397
+ } : pair));
398
+ };
399
+ const remove = (index) => {
400
+ props.onEdit(props.pairs.filter((_, i) => i !== index));
401
+ };
402
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
403
+ className: "lpc-field lpc-wide",
404
+ children: [
405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
406
+ className: "lpc-head",
407
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
408
+ className: "lpc-label",
409
+ htmlFor: props.id,
410
+ children: props.label
411
+ })
412
+ }),
413
+ props.pairs.map((pair, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
414
+ className: "lpc-kvRow",
415
+ children: [
416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
417
+ id: index === 0 ? props.id : void 0,
418
+ className: "lpc-input",
419
+ value: pair.key,
420
+ placeholder: props.keyPlaceholder,
421
+ disabled: props.disabled === true,
422
+ onChange: (event) => update(index, { key: event.target.value })
423
+ }),
424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
425
+ className: "lpc-input",
426
+ value: pair.value,
427
+ placeholder: props.valuePlaceholder,
428
+ disabled: props.disabled === true,
429
+ onChange: (event) => update(index, { value: event.target.value })
430
+ }),
431
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
432
+ type: "button",
433
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
434
+ disabled: props.disabled === true,
435
+ onClick: () => remove(index),
436
+ children: props.removeLabel
437
+ })
438
+ ]
439
+ }, index)),
440
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
441
+ className: "lpc-kvAdd",
442
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
443
+ type: "button",
444
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
445
+ disabled: props.disabled === true,
446
+ onClick: () => props.onEdit([...props.pairs, {
447
+ key: "",
448
+ value: ""
449
+ }]),
450
+ children: props.addLabel
451
+ })
452
+ }),
453
+ props.hint !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
454
+ className: "lpc-hint",
455
+ children: props.hint
456
+ }) : null
457
+ ]
458
+ });
459
+ }
460
+ function toJsonText(value) {
461
+ return value === void 0 ? "" : JSON.stringify(value, null, 2);
462
+ }
463
+ function parseJsonText(text) {
464
+ if (text.trim() === "") return {
465
+ ok: true,
466
+ value: void 0
467
+ };
468
+ try {
469
+ return {
470
+ ok: true,
471
+ value: JSON.parse(text)
472
+ };
473
+ } catch {
474
+ return { ok: false };
475
+ }
476
+ }
477
+ function JsonField(props) {
478
+ const [text, setText] = (0, react.useState)(() => toJsonText(props.value));
479
+ (0, react.useEffect)(() => {
480
+ setText(toJsonText(props.value));
481
+ }, [props.epoch]);
482
+ const parsed = parseJsonText(text);
483
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
484
+ className: `lpc-field${props.wide === true ? " lpc-wide" : ""}`,
485
+ children: [
486
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
487
+ className: "lpc-head",
488
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
489
+ className: "lpc-label",
490
+ htmlFor: props.id,
491
+ children: props.label
492
+ })
493
+ }),
494
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
495
+ id: props.id,
496
+ className: `lpc-input lpc-textarea${parsed.ok ? "" : " lpc-inputInvalid"}`,
497
+ rows: 4,
498
+ spellCheck: false,
499
+ "aria-invalid": parsed.ok ? void 0 : true,
500
+ value: text,
501
+ disabled: props.disabled === true,
502
+ onChange: (event) => {
503
+ setText(event.target.value);
504
+ const result = parseJsonText(event.target.value);
505
+ props.onEdit(result.ok ? result.value : void 0);
506
+ }
507
+ }),
508
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
509
+ className: parsed.ok ? "lpc-hint" : "lpc-invalid",
510
+ children: parsed.ok ? props.hint ?? "" : props.invalidText
511
+ })
512
+ ]
513
+ });
514
+ }
515
+
516
+ //#endregion
517
+ //#region src/client/constants.ts
518
+ /**
519
+ * 浏览器半内联常量:与服务端 packages/llm-pi/src/config.ts、compat.ts 逐字对齐。
520
+ * 浏览器半不能 import 服务端模块(tsdown 只打包 client 侧入口),
521
+ * 改动服务端这些常量时必须同步本文件。
522
+ * @module llm-pi/client/constants
523
+ */
524
+ /** 协议枚举(来源:config.ts PROTOCOL_IDS)。 */
525
+ const PROTOCOL_IDS = [
526
+ "openai-completions",
527
+ "openai-responses",
528
+ "anthropic-messages"
529
+ ];
530
+ /** thinking 档位(来源:config.ts THINKING_LEVELS)。 */
531
+ const THINKING_LEVELS = [
532
+ "off",
533
+ "minimal",
534
+ "low",
535
+ "medium",
536
+ "high",
537
+ "xhigh",
538
+ "max"
539
+ ];
540
+ /** 请求模态(来源:config.ts MODALITIES)。 */
541
+ const MODALITIES = ["text", "image"];
542
+ /** cacheRetention 枚举(来源:config.ts providerProfile.cacheRetention)。 */
543
+ const CACHE_RETENTION_OPTIONS = [
544
+ "none",
545
+ "short",
546
+ "long"
547
+ ];
548
+ /** transport 枚举(来源:config.ts providerProfile.transport)。 */
549
+ const TRANSPORT_OPTIONS = [
550
+ "sse",
551
+ "websocket",
552
+ "websocket-cached",
553
+ "auto"
554
+ ];
555
+ /** thinkingBudgets 档位键(来源:config.ts thinkingBudgets)。 */
556
+ const BUDGET_KEYS = [
557
+ "minimal",
558
+ "low",
559
+ "medium",
560
+ "high"
561
+ ];
562
+ /** 逐协议 compat 字段表(来源:compat.ts FIELDS_BY_PROTOCOL)。 */
563
+ const COMPAT_FIELDS = {
564
+ "openai-completions": {
565
+ supportsStore: "boolean",
566
+ supportsDeveloperRole: "boolean",
567
+ supportsReasoningEffort: "boolean",
568
+ supportsUsageInStreaming: "boolean",
569
+ maxTokensField: ["max_completion_tokens", "max_tokens"],
570
+ requiresToolResultName: "boolean",
571
+ requiresAssistantAfterToolResult: "boolean",
572
+ requiresThinkingAsText: "boolean",
573
+ requiresReasoningContentOnAssistantMessages: "boolean",
574
+ thinkingFormat: [
575
+ "openai",
576
+ "openrouter",
577
+ "deepseek",
578
+ "together",
579
+ "zai",
580
+ "qwen",
581
+ "chat-template",
582
+ "qwen-chat-template",
583
+ "string-thinking",
584
+ "ant-ling"
585
+ ],
586
+ chatTemplateKwargs: "object",
587
+ openRouterRouting: "object",
588
+ vercelGatewayRouting: "object",
589
+ zaiToolStream: "boolean",
590
+ supportsOpenAIGrammarTools: "boolean",
591
+ supportsStrictMode: "boolean",
592
+ cacheControlFormat: ["anthropic"],
593
+ sendSessionAffinityHeaders: "boolean",
594
+ deferredToolsMode: ["kimi"],
595
+ sessionAffinityFormat: [
596
+ "openai",
597
+ "openai-nosession",
598
+ "openrouter"
599
+ ],
600
+ supportsLongCacheRetention: "boolean"
601
+ },
602
+ "openai-responses": {
603
+ supportsDeveloperRole: "boolean",
604
+ sessionAffinityFormat: [
605
+ "openai",
606
+ "openai-nosession",
607
+ "openrouter"
608
+ ],
609
+ supportsLongCacheRetention: "boolean",
610
+ supportsStrictMode: "boolean",
611
+ supportsOpenAIGrammarTools: "boolean",
612
+ supportsToolSearch: "boolean",
613
+ supportsExplicitPromptCacheMode: "boolean"
614
+ },
615
+ "anthropic-messages": {
616
+ supportsEagerToolInputStreaming: "boolean",
617
+ supportsLongCacheRetention: "boolean",
618
+ sendSessionAffinityHeaders: "boolean",
619
+ supportsCacheControlOnTools: "boolean",
620
+ supportsTemperature: "boolean",
621
+ forceAdaptiveThinking: "boolean",
622
+ allowEmptySignature: "boolean",
623
+ supportsStrictTools: "boolean",
624
+ supportsToolReferences: "boolean"
625
+ }
626
+ };
627
+ /** 某协议的全部合法 compat 键(与服务端 compatFieldsOf 一致)。 */
628
+ function compatFieldsOf(api) {
629
+ return Object.keys(COMPAT_FIELDS[api] ?? {});
630
+ }
631
+ /** 某协议某字段的取值约束(与服务端 compatFieldSpec 一致)。 */
632
+ function compatFieldSpec(api, field) {
633
+ return COMPAT_FIELDS[api]?.[field];
634
+ }
635
+ /** api 未设置时的渲染回退组(最常见的协议;保存仍由后端按实际协议校验)。 */
636
+ const COMPAT_FALLBACK_API = "openai-completions";
637
+
638
+ //#endregion
639
+ //#region src/client/views/compat.tsx
640
+ /** api 变更后裁剪 compat:只保留新渲染组的字段,避免保存时被后端拒绝。 */
641
+ function pruneCompatForApi(compat, api) {
642
+ const group = api !== "" && compatFieldsOf(api).length > 0 ? api : COMPAT_FALLBACK_API;
643
+ const fields = new Set(compatFieldsOf(group));
644
+ const next = {};
645
+ for (const [key, value] of Object.entries(compat)) if (fields.has(key)) next[key] = value;
646
+ return next;
647
+ }
648
+ function CompatEditor(props) {
649
+ const effective = props.api !== "" && compatFieldsOf(props.api).length > 0 ? props.api : COMPAT_FALLBACK_API;
650
+ const fields = compatFieldsOf(effective);
651
+ const setField = (field, value) => {
652
+ const next = { ...props.compat };
653
+ if (value === void 0) delete next[field];
654
+ else next[field] = value;
655
+ props.onEdit(next);
656
+ };
657
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
658
+ className: `lpc-field lpc-wide`,
659
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapseSection, {
660
+ id: `${props.idPrefix}-collapse`,
661
+ title: props.t("compatGroup"),
662
+ defaultOpen: false,
663
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [props.api === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
664
+ className: "lpc-hint",
665
+ children: props.t("compatApiHint")
666
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
667
+ className: "lpc-grid",
668
+ children: fields.map((field) => {
669
+ const spec = compatFieldSpec(effective, field);
670
+ if (spec === "boolean") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
671
+ id: `${props.idPrefix}-${field}`,
672
+ label: field,
673
+ value: props.compat[field] === void 0 ? "" : String(props.compat[field]),
674
+ options: ["true", "false"],
675
+ unsetLabel: props.t("compatUnset"),
676
+ disabled: props.disabled === true,
677
+ onEdit: (value) => {
678
+ if (value === "") setField(field, void 0);
679
+ else setField(field, value === "true");
680
+ }
681
+ }, field);
682
+ if (spec === "object") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
683
+ id: `${props.idPrefix}-${field}`,
684
+ label: field,
685
+ value: props.compat[field],
686
+ epoch: props.epoch,
687
+ disabled: props.disabled === true,
688
+ invalidText: props.t("invalidJson"),
689
+ onEdit: (value) => setField(field, value)
690
+ }, field);
691
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
692
+ id: `${props.idPrefix}-${field}`,
693
+ label: field,
694
+ value: props.compat[field] === void 0 ? "" : String(props.compat[field]),
695
+ options: spec,
696
+ unsetLabel: props.t("compatUnset"),
697
+ disabled: props.disabled === true,
698
+ onEdit: (value) => {
699
+ if (value === "") setField(field, void 0);
700
+ else setField(field, value);
701
+ }
702
+ }, field);
703
+ })
704
+ })] })
705
+ })
706
+ });
707
+ }
708
+
709
+ //#endregion
710
+ //#region src/client/views/models.tsx
711
+ function catalogNote(status, t) {
712
+ if (status === void 0) return "";
713
+ if (status.error !== null) return `${t("modelsDevError")}${status.error}`;
714
+ return `${t("modelsDevStatusLine")}:${status.providers} 个 provider,快照 ${status.fetchedAt ?? "-"}`;
715
+ }
716
+ function ModelsTable(props) {
717
+ const { t } = props;
718
+ const [source, setSource] = (0, react.useState)("builtin");
719
+ const [providerIds, setProviderIds] = (0, react.useState)([]);
720
+ const [provider, setProvider] = (0, react.useState)("");
721
+ const [candidateModels, setCandidateModels] = (0, react.useState)([]);
722
+ const [note, setNote] = (0, react.useState)("");
723
+ const listId = `lpc-models-${props.route.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
724
+ (0, react.useEffect)(() => {
725
+ let alive = true;
726
+ setNote(t("catalogLoading"));
727
+ (async () => {
728
+ try {
729
+ const list = await fetchCatalog("", source);
730
+ if (!alive) return;
731
+ setProviderIds(list.providers);
732
+ const preferred = list.providers.includes(props.defaultProvider) ? props.defaultProvider : list.providers[0] ?? "";
733
+ setProvider(preferred);
734
+ setNote(catalogNote(list.status, t));
735
+ if (preferred === "") return;
736
+ const result = await fetchCatalog(preferred, source);
737
+ if (alive) setCandidateModels(result.models);
738
+ } catch {
739
+ if (alive) setNote(t("catalogFailed"));
740
+ }
741
+ })();
742
+ return () => {
743
+ alive = false;
744
+ };
745
+ }, [source]);
746
+ const onProviderChange = (value) => {
747
+ setProvider(value);
748
+ if (value === "") {
749
+ setCandidateModels([]);
750
+ return;
751
+ }
752
+ fetchCatalog(value, source).then((result) => setCandidateModels(result.models)).catch(() => setNote(t("catalogFailed")));
753
+ };
754
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
755
+ className: "lpc-models",
756
+ children: [
757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
758
+ className: "lpc-modelHead",
759
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
760
+ className: "lpc-modelTitle",
761
+ children: t("modelsGroup")
762
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
763
+ type: "button",
764
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
765
+ disabled: props.disabled === true,
766
+ onClick: () => props.onModels([...props.models, emptyModelDraft()]),
767
+ children: t("addModel")
768
+ })]
769
+ }),
770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
771
+ className: "lpc-catalogBar",
772
+ children: [
773
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
774
+ className: "lpc-catalogLabel",
775
+ htmlFor: `${listId}-source`,
776
+ children: t("catalogSource")
777
+ }),
778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
779
+ id: `${listId}-source`,
780
+ className: "lpc-input lpc-select lpc-catalogSelect",
781
+ value: source,
782
+ disabled: props.disabled === true,
783
+ onChange: (event) => setSource(event.target.value),
784
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
785
+ value: "builtin",
786
+ children: "builtin"
787
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
788
+ value: "models-dev",
789
+ children: "models-dev"
790
+ })]
791
+ }),
792
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
793
+ className: "lpc-catalogLabel",
794
+ htmlFor: `${listId}-provider`,
795
+ children: t("catalogProvider")
796
+ }),
797
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
798
+ id: `${listId}-provider`,
799
+ className: "lpc-input lpc-select lpc-catalogSelect",
800
+ value: provider,
801
+ disabled: props.disabled === true || providerIds.length === 0,
802
+ onChange: (event) => onProviderChange(event.target.value),
803
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
804
+ value: "",
805
+ children: "-"
806
+ }), providerIds.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
807
+ value: id,
808
+ children: id
809
+ }, id))]
810
+ })
811
+ ]
812
+ }),
813
+ note !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
814
+ className: "lpc-hint",
815
+ children: note
816
+ }) : null,
817
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
818
+ id: listId,
819
+ children: candidateModels.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { value: id }, id))
820
+ }),
821
+ props.models.map((model, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
822
+ index,
823
+ model,
824
+ api: props.api,
825
+ listId,
826
+ epoch: props.epoch,
827
+ disabled: props.disabled === true,
828
+ t,
829
+ onPatch: (patch) => props.onModels(props.models.map((m, i) => i === index ? {
830
+ ...m,
831
+ ...patch
832
+ } : m)),
833
+ onRemove: () => props.onModels(props.models.filter((_, i) => i !== index))
834
+ }, `${index}:${model.id}`))
835
+ ]
836
+ });
837
+ }
838
+ function ModelRow(props) {
839
+ const { model, t } = props;
840
+ const id = `${props.listId}-m${props.index}`;
841
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
842
+ className: "lpc-modelRow",
843
+ children: [
844
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
845
+ className: "lpc-modelHead",
846
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
847
+ className: "lpc-modelTitle",
848
+ children: [
849
+ t("modelRow"),
850
+ " ",
851
+ props.index + 1
852
+ ]
853
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
854
+ type: "button",
855
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
856
+ disabled: props.disabled === true,
857
+ onClick: props.onRemove,
858
+ children: t("deleteModel")
859
+ })]
860
+ }),
861
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
862
+ className: "lpc-grid",
863
+ children: [
864
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
865
+ id: `${id}-id`,
866
+ label: t("modelId"),
867
+ hint: t("modelIdHint"),
868
+ value: model.id,
869
+ disabled: props.disabled === true,
870
+ invalid: model.id.trim() === "",
871
+ invalidLabel: t("modelIdRequired"),
872
+ onEdit: (value) => props.onPatch({ id: value })
873
+ }),
874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
875
+ id: `${id}-extends`,
876
+ label: t("modelExtends"),
877
+ hint: t("modelExtendsHint"),
878
+ value: model.extends,
879
+ list: props.listId,
880
+ disabled: props.disabled === true,
881
+ onEdit: (value) => props.onPatch({ extends: value })
882
+ }),
883
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
884
+ id: `${id}-name`,
885
+ label: t("modelName"),
886
+ value: model.name,
887
+ disabled: props.disabled === true,
888
+ onEdit: (value) => props.onPatch({ name: value })
889
+ }),
890
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
891
+ id: `${id}-ctx`,
892
+ label: t("contextWindow"),
893
+ numeric: true,
894
+ value: model.contextWindow,
895
+ disabled: props.disabled === true,
896
+ onEdit: (value) => props.onPatch({ contextWindow: value })
897
+ }),
898
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
899
+ id: `${id}-max`,
900
+ label: t("maxTokens"),
901
+ numeric: true,
902
+ value: model.maxTokens,
903
+ disabled: props.disabled === true,
904
+ onEdit: (value) => props.onPatch({ maxTokens: value })
905
+ }),
906
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
907
+ className: "lpc-field",
908
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
909
+ className: "lpc-label",
910
+ children: t("input")
911
+ }), MODALITIES.map((modality) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
912
+ id: `${id}-input-${modality}`,
913
+ label: modality,
914
+ checked: model.input[modality],
915
+ disabled: props.disabled === true,
916
+ onEdit: (checked) => props.onPatch({ input: {
917
+ ...model.input,
918
+ [modality]: checked
919
+ } })
920
+ }, modality))]
921
+ })
922
+ ]
923
+ }),
924
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningEditor, {
925
+ idPrefix: `${id}-re`,
926
+ value: model.reasoningEfforts,
927
+ disabled: props.disabled === true,
928
+ t,
929
+ onEdit: (reasoningEfforts) => props.onPatch({ reasoningEfforts })
930
+ }),
931
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompatEditor, {
932
+ idPrefix: `${id}-compat`,
933
+ api: props.api,
934
+ compat: model.compat,
935
+ epoch: props.epoch,
936
+ disabled: props.disabled === true,
937
+ wide: true,
938
+ t,
939
+ onEdit: (compat) => props.onPatch({ compat })
940
+ })
941
+ ]
942
+ });
943
+ }
944
+ function ReasoningEditor(props) {
945
+ const { value } = props;
946
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
947
+ className: "lpc-field lpc-wide",
948
+ children: [
949
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
950
+ className: "lpc-head",
951
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
952
+ className: "lpc-label",
953
+ children: props.t("reasoningEfforts")
954
+ })
955
+ }),
956
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
957
+ className: "lpc-checkRow",
958
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
959
+ id: `${props.idPrefix}-nonreasoning`,
960
+ type: "checkbox",
961
+ checked: value.nonReasoning,
962
+ disabled: props.disabled === true,
963
+ onChange: (event) => props.onEdit({
964
+ ...value,
965
+ nonReasoning: event.target.checked
966
+ })
967
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
968
+ htmlFor: `${props.idPrefix}-nonreasoning`,
969
+ children: props.t("nonReasoning")
970
+ })]
971
+ }),
972
+ value.nonReasoning ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
973
+ className: "lpc-grid",
974
+ children: THINKING_LEVELS.map((level) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
975
+ id: `${props.idPrefix}-${level}`,
976
+ label: level,
977
+ value: value.levels[level] ?? "",
978
+ disabled: props.disabled === true,
979
+ onEdit: (text) => props.onEdit({
980
+ ...value,
981
+ levels: {
982
+ ...value.levels,
983
+ [level]: text
984
+ }
985
+ })
986
+ }, level))
987
+ })
988
+ ]
989
+ });
990
+ }
991
+
992
+ //#endregion
993
+ //#region src/client/views/provider-fields.tsx
994
+ function ProviderScalarFields(props) {
995
+ const { draft, t } = props;
996
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
997
+ className: "lpc-grid",
998
+ children: [
999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1000
+ id: `${props.id}-extends`,
1001
+ label: t("extends"),
1002
+ hint: t("extendsHint"),
1003
+ value: draft.extends,
1004
+ disabled: props.disabled === true,
1005
+ onEdit: (value) => props.onPatch({ extends: value })
1006
+ }),
1007
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1008
+ id: `${props.id}-displayName`,
1009
+ label: t("displayName"),
1010
+ value: draft.displayName,
1011
+ disabled: props.disabled === true,
1012
+ onEdit: (value) => props.onPatch({ displayName: value })
1013
+ }),
1014
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1015
+ id: `${props.id}-baseURL`,
1016
+ label: t("baseURL"),
1017
+ hint: t("baseURLHint"),
1018
+ value: draft.baseURL,
1019
+ disabled: props.disabled === true,
1020
+ onEdit: (value) => props.onPatch({ baseURL: value })
1021
+ }),
1022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1023
+ id: `${props.id}-apiKeyEnv`,
1024
+ label: t("apiKeyEnv"),
1025
+ hint: t("apiKeyEnvHint"),
1026
+ value: draft.apiKeyEnv,
1027
+ disabled: props.disabled === true,
1028
+ onEdit: (value) => props.onPatch({ apiKeyEnv: value })
1029
+ }),
1030
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1031
+ id: `${props.id}-defaultCtx`,
1032
+ label: t("defaultContextWindow"),
1033
+ numeric: true,
1034
+ value: draft.defaultContextWindow,
1035
+ disabled: props.disabled === true,
1036
+ onEdit: (value) => props.onPatch({ defaultContextWindow: value })
1037
+ }),
1038
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1039
+ id: `${props.id}-defaultMax`,
1040
+ label: t("defaultMaxTokens"),
1041
+ numeric: true,
1042
+ value: draft.defaultMaxTokens,
1043
+ disabled: props.disabled === true,
1044
+ onEdit: (value) => props.onPatch({ defaultMaxTokens: value })
1045
+ }),
1046
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1047
+ id: `${props.id}-timeout`,
1048
+ label: t("timeoutMs"),
1049
+ numeric: true,
1050
+ value: draft.timeoutMs,
1051
+ disabled: props.disabled === true,
1052
+ onEdit: (value) => props.onPatch({ timeoutMs: value })
1053
+ }),
1054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1055
+ id: `${props.id}-wsTimeout`,
1056
+ label: t("websocketConnectTimeoutMs"),
1057
+ numeric: true,
1058
+ value: draft.websocketConnectTimeoutMs,
1059
+ disabled: props.disabled === true,
1060
+ onEdit: (value) => props.onPatch({ websocketConnectTimeoutMs: value })
1061
+ }),
1062
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1063
+ id: `${props.id}-streamIdle`,
1064
+ label: t("streamIdleTimeoutMs"),
1065
+ numeric: true,
1066
+ value: draft.streamIdleTimeoutMs,
1067
+ disabled: props.disabled === true,
1068
+ onEdit: (value) => props.onPatch({ streamIdleTimeoutMs: value })
1069
+ }),
1070
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1071
+ className: "lpc-field",
1072
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1073
+ className: "lpc-label",
1074
+ children: t("defaultInput")
1075
+ }), MODALITIES.map((modality) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1076
+ id: `${props.id}-input-${modality}`,
1077
+ label: modality,
1078
+ checked: draft.input[modality],
1079
+ disabled: props.disabled === true,
1080
+ onEdit: (checked) => props.onPatch({ input: {
1081
+ ...draft.input,
1082
+ [modality]: checked
1083
+ } })
1084
+ }, modality))]
1085
+ }),
1086
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1087
+ className: "lpc-field",
1088
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1089
+ className: "lpc-label",
1090
+ children: t("thinkingBudgets")
1091
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1092
+ className: "lpc-grid lpc-gridNested",
1093
+ children: BUDGET_KEYS.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1094
+ id: `${props.id}-budget-${key}`,
1095
+ label: key,
1096
+ numeric: true,
1097
+ value: draft.thinkingBudgets[key],
1098
+ disabled: props.disabled === true,
1099
+ onEdit: (value) => props.onPatch({ thinkingBudgets: {
1100
+ ...draft.thinkingBudgets,
1101
+ [key]: value
1102
+ } })
1103
+ }, key))
1104
+ })]
1105
+ })
1106
+ ]
1107
+ });
1108
+ }
1109
+ /** 协议/档位/缓存/传输四个下拉组。 */
1110
+ function ProviderSelectFields(props) {
1111
+ const { draft, t } = props;
1112
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1113
+ className: "lpc-grid",
1114
+ children: [
1115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1116
+ id: `${props.id}-api`,
1117
+ label: t("api"),
1118
+ value: draft.api,
1119
+ options: PROTOCOL_IDS,
1120
+ unsetLabel: t("compatUnset"),
1121
+ disabled: props.disabled === true,
1122
+ onEdit: (value) => props.onPatch({
1123
+ api: value,
1124
+ compat: pruneCompatForApi(draft.compat, value)
1125
+ })
1126
+ }),
1127
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1128
+ id: `${props.id}-reasoning`,
1129
+ label: t("reasoning"),
1130
+ value: draft.reasoning,
1131
+ options: THINKING_LEVELS,
1132
+ unsetLabel: t("compatUnset"),
1133
+ disabled: props.disabled === true,
1134
+ onEdit: (value) => props.onPatch({ reasoning: value })
1135
+ }),
1136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1137
+ id: `${props.id}-cache`,
1138
+ label: t("cacheRetention"),
1139
+ value: draft.cacheRetention,
1140
+ options: CACHE_RETENTION_OPTIONS,
1141
+ unsetLabel: t("compatUnset"),
1142
+ disabled: props.disabled === true,
1143
+ onEdit: (value) => props.onPatch({ cacheRetention: value })
1144
+ }),
1145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1146
+ id: `${props.id}-transport`,
1147
+ label: t("transport"),
1148
+ value: draft.transport,
1149
+ options: TRANSPORT_OPTIONS,
1150
+ unsetLabel: t("compatUnset"),
1151
+ disabled: props.disabled === true,
1152
+ onEdit: (value) => props.onPatch({ transport: value })
1153
+ })
1154
+ ]
1155
+ });
1156
+ }
1157
+
1158
+ //#endregion
1159
+ //#region src/client/views/providers.tsx
1160
+ function ProvidersSection(props) {
1161
+ const [newRoute, setNewRoute] = (0, react.useState)("");
1162
+ const [routeError, setRouteError] = (0, react.useState)("");
1163
+ const submitAdd = () => {
1164
+ const key = newRoute.trim();
1165
+ if (key === "") {
1166
+ setRouteError(props.t("routeEmpty"));
1167
+ return;
1168
+ }
1169
+ if (props.providers[key] !== void 0) {
1170
+ setRouteError(props.t("routeDuplicate"));
1171
+ return;
1172
+ }
1173
+ props.onAddRoute(key);
1174
+ setNewRoute("");
1175
+ setRouteError("");
1176
+ };
1177
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1178
+ className: "lpc-section",
1179
+ children: [
1180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1181
+ className: "lpc-groupLabel",
1182
+ children: props.t("providersGroup")
1183
+ }),
1184
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1185
+ className: "lpc-addRoute",
1186
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1187
+ className: `lpc-input${routeError !== "" ? " lpc-inputInvalid" : ""}`,
1188
+ value: newRoute,
1189
+ disabled: props.disabled === true,
1190
+ placeholder: props.t("addRoutePlaceholder"),
1191
+ onChange: (event) => {
1192
+ setNewRoute(event.target.value);
1193
+ setRouteError("");
1194
+ }
1195
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1196
+ type: "button",
1197
+ className: "lpc-btn lpc-btnGhost",
1198
+ disabled: props.disabled === true,
1199
+ onClick: submitAdd,
1200
+ children: props.t("addRoute")
1201
+ })]
1202
+ }),
1203
+ routeError !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1204
+ className: "lpc-invalid",
1205
+ children: routeError
1206
+ }) : null,
1207
+ Object.entries(props.providers).map(([route, draft]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderSection, {
1208
+ route,
1209
+ draft,
1210
+ epoch: props.epoch,
1211
+ disabled: props.disabled === true,
1212
+ t: props.t,
1213
+ onRemove: () => props.onRemoveRoute(route),
1214
+ onPatch: (patch) => props.onPatchProvider(route, patch)
1215
+ }, route))
1216
+ ]
1217
+ });
1218
+ }
1219
+ function ProviderSection(props) {
1220
+ const [open, setOpen] = (0, react.useState)(false);
1221
+ const { route, draft, t } = props;
1222
+ const id = route.replace(/[^a-zA-Z0-9_-]/g, "_");
1223
+ const summary = draft.api !== "" ? draft.api : draft.extends !== "" ? `extends ${draft.extends}` : "";
1224
+ const fieldProps = {
1225
+ id,
1226
+ draft,
1227
+ disabled: props.disabled === true,
1228
+ t,
1229
+ onPatch: props.onPatch
1230
+ };
1231
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1232
+ className: "lpc-route",
1233
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1234
+ className: "lpc-routeHead",
1235
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1236
+ type: "button",
1237
+ className: "lpc-routeToggle",
1238
+ "aria-expanded": open,
1239
+ onClick: () => setOpen(!open),
1240
+ children: [
1241
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1242
+ className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}`,
1243
+ children: "▾"
1244
+ }),
1245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1246
+ className: "lpc-routeKey",
1247
+ children: route
1248
+ }),
1249
+ summary !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1250
+ className: "lpc-routeApi",
1251
+ children: summary
1252
+ }) : null
1253
+ ]
1254
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1255
+ type: "button",
1256
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1257
+ disabled: props.disabled === true,
1258
+ onClick: props.onRemove,
1259
+ children: t("deleteRoute")
1260
+ })]
1261
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1262
+ className: "lpc-routeBody",
1263
+ children: [
1264
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1265
+ className: "lpc-groupLabel",
1266
+ children: t("providerFields")
1267
+ }),
1268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderScalarFields, { ...fieldProps }),
1269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderSelectFields, { ...fieldProps }),
1270
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyValueEditor, {
1271
+ id: `${id}-headers`,
1272
+ label: t("headers"),
1273
+ hint: t("headersHint"),
1274
+ pairs: draft.headers,
1275
+ disabled: props.disabled === true,
1276
+ keyPlaceholder: t("key"),
1277
+ valuePlaceholder: t("value"),
1278
+ addLabel: t("add"),
1279
+ removeLabel: t("remove"),
1280
+ onEdit: (headers) => props.onPatch({ headers })
1281
+ }),
1282
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapseSection, {
1283
+ id: `${id}-advanced`,
1284
+ title: t("advancedGroup"),
1285
+ defaultOpen: false,
1286
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
1287
+ id: `${id}-retry`,
1288
+ label: t("retryPolicy"),
1289
+ hint: t("retryPolicyHint"),
1290
+ invalidText: t("invalidJson"),
1291
+ value: draft.retryPolicy,
1292
+ epoch: props.epoch,
1293
+ disabled: props.disabled === true,
1294
+ wide: true,
1295
+ onEdit: (retryPolicy) => props.onPatch({ retryPolicy })
1296
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompatEditor, {
1297
+ idPrefix: `${id}-compat`,
1298
+ api: draft.api,
1299
+ compat: draft.compat,
1300
+ epoch: props.epoch,
1301
+ disabled: props.disabled === true,
1302
+ wide: true,
1303
+ t,
1304
+ onEdit: (compat) => props.onPatch({ compat })
1305
+ })] })
1306
+ }),
1307
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsTable, {
1308
+ route,
1309
+ api: draft.api,
1310
+ defaultProvider: draft.extends,
1311
+ models: draft.models,
1312
+ epoch: props.epoch,
1313
+ disabled: props.disabled === true,
1314
+ t,
1315
+ onModels: (models) => props.onPatch({ models })
1316
+ })
1317
+ ]
1318
+ }) : null]
1319
+ });
1320
+ }
1321
+
1322
+ //#endregion
1323
+ //#region src/client/card.tsx
1324
+ const IDLE_STATUS = {
1325
+ kind: "idle",
1326
+ text: ""
1327
+ };
1328
+ function modelsDevText(status, t) {
1329
+ if (status === null) return t("modelsDevEmpty");
1330
+ if (status.error !== null) return `${t("modelsDevError")}${status.error}`;
1331
+ if (status.fetchedAt === null) return t("modelsDevEmpty");
1332
+ return `${t("modelsDevStatusLine")}:${status.providers} 个 provider,快照 ${status.fetchedAt}`;
1333
+ }
1334
+ function LlmPiCard(props) {
1335
+ const { t } = props;
1336
+ const [open, setOpen] = (0, react.useState)(false);
1337
+ const [wire, setWire] = (0, react.useState)(null);
1338
+ const [draft, setDraft] = (0, react.useState)(null);
1339
+ const [epoch, setEpoch] = (0, react.useState)(0);
1340
+ const [failed, setFailed] = (0, react.useState)(false);
1341
+ const [saving, setSaving] = (0, react.useState)(false);
1342
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
1343
+ const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
1344
+ (0, react.useEffect)(() => {
1345
+ let alive = true;
1346
+ fetchConfig().then((loaded) => {
1347
+ if (!alive) return;
1348
+ setWire(loaded);
1349
+ setDraft(draftFromWire(loaded));
1350
+ }).catch(() => {
1351
+ if (alive) setFailed(true);
1352
+ });
1353
+ return () => {
1354
+ alive = false;
1355
+ };
1356
+ }, []);
1357
+ const dirty = (0, react.useMemo)(() => wire !== null && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromWire(wire))), [wire, draft]);
1358
+ const invalid = (0, react.useMemo)(() => {
1359
+ if (draft === null) return false;
1360
+ return !numTextOk(draft.catalogRefreshHours) || Object.values(draft.providers).some((provider) => provider.models.some((model) => model.id.trim() === ""));
1361
+ }, [draft]);
1362
+ if (failed) return null;
1363
+ if (wire === null || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
1364
+ className: "lpc-card",
1365
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1366
+ className: "lpc-readOnly",
1367
+ children: t("loading")
1368
+ })
1369
+ });
1370
+ const setProvider = (route, patch) => {
1371
+ const current = draft.providers[route] ?? emptyProviderDraft();
1372
+ setDraft({
1373
+ ...draft,
1374
+ providers: {
1375
+ ...draft.providers,
1376
+ [route]: {
1377
+ ...current,
1378
+ ...patch
1379
+ }
1380
+ }
1381
+ });
1382
+ setStatus(IDLE_STATUS);
1383
+ };
1384
+ const onAddRoute = (key) => {
1385
+ setDraft({
1386
+ ...draft,
1387
+ providers: {
1388
+ ...draft.providers,
1389
+ [key]: emptyProviderDraft()
1390
+ }
1391
+ });
1392
+ setStatus(IDLE_STATUS);
1393
+ };
1394
+ const onRemoveRoute = (route) => {
1395
+ const next = { ...draft.providers };
1396
+ delete next[route];
1397
+ setDraft({
1398
+ ...draft,
1399
+ providers: next
1400
+ });
1401
+ setStatus(IDLE_STATUS);
1402
+ };
1403
+ const onSave = () => {
1404
+ setSaving(true);
1405
+ saveConfig(toPatch(draft)).then((saved) => {
1406
+ setWire(saved);
1407
+ setDraft(draftFromWire(saved));
1408
+ setEpoch((value) => value + 1);
1409
+ setStatus({
1410
+ kind: "ok",
1411
+ text: t("saveOk")
1412
+ });
1413
+ }).catch((error) => {
1414
+ const message = error instanceof Error ? error.message : String(error);
1415
+ setStatus({
1416
+ kind: "error",
1417
+ text: `${t("saveFailed")}${message}`
1418
+ });
1419
+ }).finally(() => setSaving(false));
1420
+ };
1421
+ const onDiscard = () => {
1422
+ setDraft(draftFromWire(wire));
1423
+ setEpoch((value) => value + 1);
1424
+ setStatus(IDLE_STATUS);
1425
+ };
1426
+ const onRefreshCatalog = () => {
1427
+ setRefreshing(true);
1428
+ refreshCatalog().then((result) => {
1429
+ setWire({
1430
+ ...wire,
1431
+ modelsDevStatus: result.status
1432
+ });
1433
+ setStatus({
1434
+ kind: "ok",
1435
+ text: t("refreshOk")
1436
+ });
1437
+ }).catch((error) => {
1438
+ const message = error instanceof Error ? error.message : String(error);
1439
+ setStatus({
1440
+ kind: "error",
1441
+ text: `${t("refreshFailed")}${message}`
1442
+ });
1443
+ }).finally(() => setRefreshing(false));
1444
+ };
1445
+ const disabled = !wire.writable;
1446
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1447
+ className: `lpc-card${open ? " lpc-cardOpen" : ""}`,
1448
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1449
+ type: "button",
1450
+ className: "lpc-header",
1451
+ "aria-expanded": open,
1452
+ "aria-label": `${t(open ? "collapse" : "expand")}: ${t("title")}`,
1453
+ onClick: () => setOpen(!open),
1454
+ children: [
1455
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1456
+ className: "lpc-headText",
1457
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1458
+ className: "lpc-name",
1459
+ children: t("title")
1460
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1461
+ className: "lpc-description",
1462
+ children: t("description")
1463
+ })]
1464
+ }),
1465
+ dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1466
+ className: "lpc-pending",
1467
+ children: t("unsaved")
1468
+ }) : null,
1469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1470
+ className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}`,
1471
+ children: "▾"
1472
+ })
1473
+ ]
1474
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1475
+ className: "lpc-body",
1476
+ children: [
1477
+ disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1478
+ className: "lpc-readOnly",
1479
+ role: "status",
1480
+ children: t("readOnly")
1481
+ }) : null,
1482
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1483
+ id: "lpc-enabled",
1484
+ label: t("enabled"),
1485
+ checked: draft.enabled,
1486
+ disabled,
1487
+ onEdit: (value) => {
1488
+ setDraft({
1489
+ ...draft,
1490
+ enabled: value
1491
+ });
1492
+ setStatus(IDLE_STATUS);
1493
+ }
1494
+ }),
1495
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1496
+ id: "lpc-catalogUrl",
1497
+ label: t("catalogUrl"),
1498
+ hint: t("catalogUrlHint"),
1499
+ value: draft.catalogUrl,
1500
+ disabled,
1501
+ onEdit: (value) => {
1502
+ setDraft({
1503
+ ...draft,
1504
+ catalogUrl: value
1505
+ });
1506
+ setStatus(IDLE_STATUS);
1507
+ }
1508
+ }),
1509
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1510
+ id: "lpc-catalogRefresh",
1511
+ label: t("catalogRefreshHours"),
1512
+ hint: t("catalogRefreshHoursHint"),
1513
+ value: draft.catalogRefreshHours,
1514
+ numeric: true,
1515
+ disabled,
1516
+ invalid: !numTextOk(draft.catalogRefreshHours),
1517
+ invalidLabel: t("invalidNumber"),
1518
+ onEdit: (value) => {
1519
+ setDraft({
1520
+ ...draft,
1521
+ catalogRefreshHours: value
1522
+ });
1523
+ setStatus(IDLE_STATUS);
1524
+ }
1525
+ }),
1526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1527
+ id: "lpc-catalogProxy",
1528
+ label: t("catalogProxy"),
1529
+ hint: t("catalogProxyHint"),
1530
+ value: draft.catalogProxy,
1531
+ disabled,
1532
+ onEdit: (value) => {
1533
+ setDraft({
1534
+ ...draft,
1535
+ catalogProxy: value
1536
+ });
1537
+ setStatus(IDLE_STATUS);
1538
+ }
1539
+ }),
1540
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1541
+ className: "lpc-statusRow",
1542
+ children: [
1543
+ t("kitSource"),
1544
+ ":",
1545
+ wire.kitSource
1546
+ ]
1547
+ }),
1548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1549
+ className: "lpc-statusRow",
1550
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1551
+ t("modelsDevStatus"),
1552
+ ":",
1553
+ modelsDevText(wire.modelsDevStatus, t)
1554
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1555
+ type: "button",
1556
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall lpc-refreshBtn",
1557
+ disabled: disabled || refreshing,
1558
+ onClick: onRefreshCatalog,
1559
+ children: t(refreshing ? "refreshingCatalog" : "refreshCatalog")
1560
+ })]
1561
+ }),
1562
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidersSection, {
1563
+ providers: draft.providers,
1564
+ epoch,
1565
+ disabled,
1566
+ t,
1567
+ onAddRoute,
1568
+ onRemoveRoute,
1569
+ onPatchProvider: setProvider
1570
+ }),
1571
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1572
+ className: "lpc-footer",
1573
+ children: [
1574
+ status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1575
+ className: `lpc-status${status.kind === "error" ? " lpc-statusError" : ""}`,
1576
+ role: "status",
1577
+ children: status.text
1578
+ }) : null,
1579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1580
+ type: "button",
1581
+ className: "lpc-btn lpc-btnGhost",
1582
+ disabled: !dirty || saving,
1583
+ onClick: onDiscard,
1584
+ children: t("discard")
1585
+ }),
1586
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1587
+ type: "button",
1588
+ className: "lpc-btn lpc-btnPrimary",
1589
+ disabled: !dirty || invalid || saving || disabled,
1590
+ onClick: onSave,
1591
+ children: t(saving ? "saving" : "save")
1592
+ })
1593
+ ]
1594
+ })
1595
+ ]
1596
+ }) : null]
1597
+ });
1598
+ }
1599
+
1600
+ //#endregion
1601
+ //#region src/client/i18n.ts
1602
+ /**
1603
+ * 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
1604
+ * @module llm-pi/client/i18n
1605
+ */
1606
+ const NS = "dsh-plus-llm-pi";
1607
+ const zh = {
1608
+ title: "LLM 路由(llm-pi)",
1609
+ description: "自定义 LLM 路由:协议、compat、模型目录与 models.dev 目录兜底。",
1610
+ enabled: "启用插件",
1611
+ catalogUrl: "models.dev 目录数据端点",
1612
+ catalogUrlHint: "快照数据源;一般无需修改。",
1613
+ catalogRefreshHours: "目录刷新间隔(小时)",
1614
+ catalogRefreshHoursHint: "0 = 不自动拉取(可手动拉取或读已有缓存);>0 = 每 N 小时自动拉取。",
1615
+ catalogProxy: "拉取代理地址",
1616
+ catalogProxyHint: "HTTP 代理(如 http://127.0.0.1:7890);留空直连。",
1617
+ kitSource: "模块来源",
1618
+ modelsDevStatus: "models.dev 快照",
1619
+ modelsDevEmpty: "未拉取(无缓存数据;可手动拉取)",
1620
+ modelsDevStatusLine: "已加载",
1621
+ modelsDevError: "加载失败:",
1622
+ refreshCatalog: "手动拉取",
1623
+ refreshingCatalog: "拉取中…",
1624
+ refreshOk: "目录已拉取。",
1625
+ refreshFailed: "拉取失败:",
1626
+ advancedGroup: "高级设置(retryPolicy / compat)",
1627
+ providersGroup: "Provider 路由",
1628
+ addRoute: "新增 route",
1629
+ addRoutePlaceholder: "新 route 键名(如 my-llm)",
1630
+ routeEmpty: "route 键名不能为空。",
1631
+ routeDuplicate: "该 route 已存在。",
1632
+ deleteRoute: "删除 route",
1633
+ providerFields: "基础字段",
1634
+ extends: "继承内置 provider",
1635
+ extendsHint: "如 openai、anthropic;提供 api/baseURL 默认值与模型查找源。",
1636
+ displayName: "显示名",
1637
+ api: "线协议",
1638
+ baseURL: "端点 URL",
1639
+ baseURLHint: "缺省继承 extends 源的端点。",
1640
+ apiKeyEnv: "凭据引用名",
1641
+ apiKeyEnvHint: "如 NEWAPI_API_KEY(凭据服务或环境变量)。",
1642
+ defaultContextWindow: "默认上下文容量",
1643
+ defaultMaxTokens: "默认输出上限",
1644
+ defaultInput: "默认输入模态",
1645
+ reasoning: "默认 reasoning 档位",
1646
+ thinkingBudgets: "thinking 档位预算",
1647
+ cacheRetention: "提示缓存保留",
1648
+ transport: "流式传输",
1649
+ timeoutMs: "HTTP 超时(毫秒)",
1650
+ websocketConnectTimeoutMs: "WebSocket 连接超时(毫秒)",
1651
+ streamIdleTimeoutMs: "流空闲超时(毫秒)",
1652
+ headers: "请求头",
1653
+ headersHint: "键值对;键为空的整行会被忽略。",
1654
+ key: "键",
1655
+ value: "值",
1656
+ add: "添加",
1657
+ remove: "删除",
1658
+ retryPolicy: "重试策略(JSON)",
1659
+ retryPolicyHint: "dsh-llm RetryPolicy 形状;非法 JSON 不会提交。",
1660
+ invalidJson: "JSON 格式错误(该字段不会提交)。",
1661
+ compatGroup: "Compat 覆盖",
1662
+ compatApiHint: "api 未设置时暂按 openai-completions 字段组渲染;保存时后端按实际协议校验。",
1663
+ compatUnset: "未设置",
1664
+ modelsGroup: "模型目录",
1665
+ addModel: "添加模型",
1666
+ deleteModel: "删除",
1667
+ modelRow: "模型",
1668
+ modelId: "模型 id",
1669
+ modelIdHint: "发送给 provider 的标识,必填。",
1670
+ modelIdRequired: "模型 id 必填。",
1671
+ modelExtends: "继承源",
1672
+ modelExtendsHint: "\"provider/model\" 或裸 model id(候选来自下方目录)。",
1673
+ modelName: "显示名",
1674
+ contextWindow: "上下文容量",
1675
+ maxTokens: "输出上限",
1676
+ input: "输入模态",
1677
+ reasoningEfforts: "reasoningEfforts",
1678
+ nonReasoning: "非推理模型",
1679
+ catalogSource: "候选来源",
1680
+ catalogProvider: "候选 provider",
1681
+ catalogLoading: "目录加载中…",
1682
+ catalogFailed: "目录加载失败。",
1683
+ save: "保存",
1684
+ saving: "保存中…",
1685
+ discard: "放弃",
1686
+ unsaved: "未保存",
1687
+ saveOk: "已保存。",
1688
+ saveFailed: "保存失败:",
1689
+ loading: "加载中…",
1690
+ readOnly: "当前部署无 settings provider,配置为只读;请编辑 settings.yaml。",
1691
+ expand: "展开",
1692
+ collapse: "收起",
1693
+ invalidNumber: "请输入有效数字"
1694
+ };
1695
+ const en = {
1696
+ title: "LLM routes (llm-pi)",
1697
+ description: "Custom LLM routes: protocol, compat, model catalog and models.dev fallback.",
1698
+ enabled: "Enable plugin",
1699
+ catalogUrl: "models.dev catalog endpoint",
1700
+ catalogUrlHint: "Snapshot data source; usually no change needed.",
1701
+ catalogRefreshHours: "Catalog refresh (hours)",
1702
+ catalogRefreshHoursHint: "0 = no auto refresh (manual refresh or existing cache); >0 = refresh every N hours.",
1703
+ catalogProxy: "Fetch proxy",
1704
+ catalogProxyHint: "HTTP proxy (e.g. http://127.0.0.1:7890); leave empty for direct.",
1705
+ kitSource: "Module source",
1706
+ modelsDevStatus: "models.dev snapshot",
1707
+ modelsDevEmpty: "Not fetched (no cached data; use manual refresh)",
1708
+ modelsDevStatusLine: "Loaded",
1709
+ modelsDevError: "Load failed: ",
1710
+ refreshCatalog: "Refresh now",
1711
+ refreshingCatalog: "Refreshing…",
1712
+ refreshOk: "Catalog refreshed.",
1713
+ refreshFailed: "Refresh failed: ",
1714
+ advancedGroup: "Advanced (retryPolicy / compat)",
1715
+ providersGroup: "Provider routes",
1716
+ addRoute: "Add route",
1717
+ addRoutePlaceholder: "New route key (e.g. my-llm)",
1718
+ routeEmpty: "Route key must not be empty.",
1719
+ routeDuplicate: "This route already exists.",
1720
+ deleteRoute: "Delete route",
1721
+ providerFields: "Basic fields",
1722
+ extends: "Inherit built-in provider",
1723
+ extendsHint: "e.g. openai, anthropic; provides api/baseURL defaults and the model lookup source.",
1724
+ displayName: "Display name",
1725
+ api: "Wire protocol",
1726
+ baseURL: "Base URL",
1727
+ baseURLHint: "Defaults to the extends source endpoint.",
1728
+ apiKeyEnv: "Credential reference",
1729
+ apiKeyEnvHint: "e.g. NEWAPI_API_KEY (credential service or environment variable).",
1730
+ defaultContextWindow: "Default context window",
1731
+ defaultMaxTokens: "Default max tokens",
1732
+ defaultInput: "Default input modalities",
1733
+ reasoning: "Default reasoning level",
1734
+ thinkingBudgets: "Thinking budgets",
1735
+ cacheRetention: "Cache retention",
1736
+ transport: "Streaming transport",
1737
+ timeoutMs: "HTTP timeout (ms)",
1738
+ websocketConnectTimeoutMs: "WebSocket connect timeout (ms)",
1739
+ streamIdleTimeoutMs: "Stream idle timeout (ms)",
1740
+ headers: "Request headers",
1741
+ headersHint: "Key/value pairs; rows with an empty key are ignored.",
1742
+ key: "Key",
1743
+ value: "Value",
1744
+ add: "Add",
1745
+ remove: "Remove",
1746
+ retryPolicy: "Retry policy (JSON)",
1747
+ retryPolicyHint: "dsh-llm RetryPolicy shape; invalid JSON is not submitted.",
1748
+ invalidJson: "Invalid JSON (this field will not be submitted).",
1749
+ compatGroup: "Compat overrides",
1750
+ compatApiHint: "When api is unset, fields render per openai-completions; the backend validates per the effective protocol.",
1751
+ compatUnset: "Unset",
1752
+ modelsGroup: "Model catalog",
1753
+ addModel: "Add model",
1754
+ deleteModel: "Delete",
1755
+ modelRow: "Model",
1756
+ modelId: "Model id",
1757
+ modelIdHint: "The identifier sent to the provider; required.",
1758
+ modelIdRequired: "Model id is required.",
1759
+ modelExtends: "Inherits from",
1760
+ modelExtendsHint: "\"provider/model\" or a bare model id (candidates from the catalog below).",
1761
+ modelName: "Display name",
1762
+ contextWindow: "Context window",
1763
+ maxTokens: "Max tokens",
1764
+ input: "Input modalities",
1765
+ reasoningEfforts: "reasoningEfforts",
1766
+ nonReasoning: "Non-reasoning model",
1767
+ catalogSource: "Candidate source",
1768
+ catalogProvider: "Candidate provider",
1769
+ catalogLoading: "Loading catalog…",
1770
+ catalogFailed: "Failed to load catalog.",
1771
+ save: "Save",
1772
+ saving: "Saving…",
1773
+ discard: "Discard",
1774
+ unsaved: "Unsaved",
1775
+ saveOk: "Saved.",
1776
+ saveFailed: "Save failed: ",
1777
+ loading: "Loading…",
1778
+ readOnly: "No settings provider in this deployment; edit settings.yaml instead.",
1779
+ expand: "Expand",
1780
+ collapse: "Collapse",
1781
+ invalidNumber: "Enter a valid number"
1782
+ };
1783
+
1784
+ //#endregion
1785
+ //#region src/client/styles.ts
1786
+ /**
1787
+ * 配置卡片样式:沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),
1788
+ * 视觉对齐官方卡片(--dsw-alias-* 变量),不覆盖上游任何选择器。
1789
+ * @module llm-pi/client/styles
1790
+ */
1791
+ const PLUGIN_ID = "@dsh-plus/llm-pi";
1792
+ const STYLE_TAG_ID = `${PLUGIN_ID}/card.css`;
1793
+ const cardCss = `
1794
+ .lpc-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
1795
+ .lpc-card:hover{border-color:var(--dsw-alias-label-dimmed)}
1796
+ .lpc-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
1797
+ .lpc-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
1798
+ .lpc-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
1799
+ .lpc-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
1800
+ .lpc-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
1801
+ .lpc-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
1802
+ .lpc-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;font-size:12px}
1803
+ .lpc-chevronOpen{transform:rotate(180deg)}
1804
+ .lpc-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
1805
+ .lpc-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
1806
+ .lpc-field{flex-direction:column;gap:6px;padding:12px 0;display:flex;min-width:0}
1807
+ .lpc-field+.lpc-field{border-top:1px solid var(--dsw-alias-border-l2)}
1808
+ .lpc-head{align-items:center;gap:8px;display:flex}
1809
+ .lpc-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
1810
+ .lpc-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;width:100%;box-sizing:border-box;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}
1811
+ .lpc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
1812
+ .lpc-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
1813
+ .lpc-inputInvalid{border-color:var(--dsw-alias-label-error)}
1814
+ .lpc-select{appearance:none}
1815
+ .lpc-textarea{height:auto;min-height:72px;padding:8px 12px;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;resize:vertical}
1816
+ .lpc-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
1817
+ .lpc-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
1818
+ .lpc-checkRow{align-items:center;gap:8px;display:flex;padding:3px 0}
1819
+ .lpc-checkRow input{accent-color:var(--dsw-alias-brand-primary)}
1820
+ .lpc-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer}
1821
+ .lpc-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:14px 0 2px;margin:0}
1822
+ .lpc-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
1823
+ .lpc-statusRow{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px;line-height:1.6;word-break:break-all}
1824
+ .lpc-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
1825
+ .lpc-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
1826
+ .lpc-statusError{color:var(--dsw-alias-label-error)}
1827
+ .lpc-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5;flex:none}
1828
+ .lpc-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
1829
+ .lpc-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
1830
+ .lpc-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
1831
+ .lpc-btn:disabled{opacity:.4;cursor:default}
1832
+ .lpc-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
1833
+ .lpc-btnSmall{padding:2px 10px;font-size:12px}
1834
+ .lpc-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
1835
+ .lpc-gridNested{margin-top:2px}
1836
+ .lpc-wide{grid-column:1 / -1}
1837
+ .lpc-addRoute{display:flex;gap:8px;align-items:center;padding:10px 0}
1838
+ .lpc-addRoute .lpc-input{flex:1;min-width:0}
1839
+ .lpc-route{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin:10px 0;background:var(--dsw-alias-bg-layer-3)}
1840
+ .lpc-routeHead{display:flex;align-items:center;gap:8px;padding:6px 10px}
1841
+ .lpc-routeToggle{appearance:none;background:0 0;border:0;font:inherit;color:inherit;cursor:pointer;display:flex;align-items:center;gap:8px;flex:1;min-width:0;text-align:left;padding:4px 0;border-radius:6px}
1842
+ .lpc-routeToggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
1843
+ .lpc-routeKey{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1844
+ .lpc-routeApi{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1845
+ .lpc-routeBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 14px;padding-bottom:6px}
1846
+ .lpc-kvRow{display:flex;gap:8px;align-items:center}
1847
+ .lpc-kvRow .lpc-input{flex:1;min-width:0}
1848
+ .lpc-kvAdd{padding-top:8px}
1849
+ .lpc-modelRow{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;margin:10px 0;padding:0 14px;background:var(--dsw-alias-bg-layer-3)}
1850
+ .lpc-modelHead{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 0}
1851
+ .lpc-modelTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}
1852
+ .lpc-catalogBar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:4px 0}
1853
+ .lpc-catalogLabel{color:var(--dsw-alias-label-tertiary);font-size:12px}
1854
+ .lpc-catalogSelect{width:auto;height:30px}
1855
+ .lpc-collapse{border-top:1px solid var(--dsw-alias-border-l2);margin:2px 0}
1856
+ .lpc-collapseHead{appearance:none;background:0 0;border:0;font:inherit;color:inherit;cursor:pointer;display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:10px 0;border-radius:6px}
1857
+ .lpc-collapseHead:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
1858
+ .lpc-collapseTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}
1859
+ .lpc-collapseBody{border-top:1px dashed var(--dsw-alias-border-l2);padding-bottom:6px}
1860
+ .lpc-refreshBtn{margin-left:8px;vertical-align:middle}
1861
+ `;
1862
+ /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
1863
+ function injectStyle() {
1864
+ if (typeof document === "undefined") return null;
1865
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`) !== null) return null;
1866
+ const tag = document.createElement("style");
1867
+ tag.dataset.plugin = PLUGIN_ID;
1868
+ tag.dataset.pluginCss = STYLE_TAG_ID;
1869
+ tag.textContent = cardCss;
1870
+ document.head.appendChild(tag);
1871
+ return tag;
1872
+ }
1873
+
1874
+ //#endregion
1875
+ //#region src/client/client.ts
1876
+ const name = "dsh-plus-llm-pi";
1877
+ /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
1878
+ const inject = ["slots", "locale"];
1879
+ function apply(ctx) {
1880
+ const c = ctx;
1881
+ const tag = injectStyle();
1882
+ c.effect(() => () => {
1883
+ tag?.remove();
1884
+ }, "llm-pi: style");
1885
+ c.effect(() => c.locale.register(NS, {
1886
+ zh,
1887
+ en
1888
+ }), "llm-pi: locale");
1889
+ c.slots.inject("settings.plugin.item", () => c.slots.register({
1890
+ name: "settings.plugin.item",
1891
+ id: "llm-pi",
1892
+ order: 110,
1893
+ locale: NS,
1894
+ inject: () => ({ t: c.locale.bind(NS) })
1895
+ }, LlmPiCard));
1896
+ }
1897
+
1898
+ //#endregion
1899
+ exports.apply = apply;
1900
+ exports.inject = inject;
1901
+ exports.name = name;
1902
+ return module.exports;
1903
+ }
1904
+ });