@dsh-plus/llm-pi 0.1.6 → 0.1.7

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