@klarkxy/dsh-model-center 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1946 @@
1
+ function __createDshNodeShims() {
2
+ class EventEmitter {
3
+ constructor() {
4
+ this._e = Object.create(null)
5
+ }
6
+ on(type, fn) {
7
+ (this._e[type] ||= []).push(fn)
8
+ return this
9
+ }
10
+ addListener(type, fn) {
11
+ return this.on(type, fn)
12
+ }
13
+ once(type, fn) {
14
+ const wrap = (...args) => {
15
+ this.off(type, wrap)
16
+ fn(...args)
17
+ }
18
+ return this.on(type, wrap)
19
+ }
20
+ off(type, fn) {
21
+ const list = this._e[type]
22
+ if (list) this._e[type] = list.filter((item) => item !== fn)
23
+ return this
24
+ }
25
+ removeListener(type, fn) {
26
+ return this.off(type, fn)
27
+ }
28
+ emit(type, ...args) {
29
+ const list = this._e[type]
30
+ if (!list) return false
31
+ for (const fn of list.slice()) fn(...args)
32
+ return true
33
+ }
34
+ removeAllListeners(type) {
35
+ if (type) delete this._e[type]
36
+ else this._e = Object.create(null)
37
+ return this
38
+ }
39
+ listeners(type) {
40
+ return (this._e[type] || []).slice()
41
+ }
42
+ setMaxListeners() {
43
+ return this
44
+ }
45
+ }
46
+
47
+ class Buffer extends Uint8Array {
48
+ static from(value, encodingOrOffset, length) {
49
+ if (typeof value === 'string') return new Buffer(new TextEncoder().encode(value))
50
+ if (value instanceof ArrayBuffer) return new Buffer(new Uint8Array(value, encodingOrOffset, length))
51
+ if (ArrayBuffer.isView(value)) return new Buffer(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))
52
+ if (Array.isArray(value) || value && typeof value.length === 'number') return new Buffer(Uint8Array.from(value))
53
+ return new Buffer(0)
54
+ }
55
+ static alloc(size, fill) {
56
+ const buf = new Buffer(size)
57
+ if (fill !== undefined && fill !== 0) {
58
+ if (typeof fill === 'string') {
59
+ const bytes = new TextEncoder().encode(fill)
60
+ for (let i = 0; i < size; i += 1) buf[i] = bytes[i % bytes.length]
61
+ } else buf.fill(fill)
62
+ }
63
+ return buf
64
+ }
65
+ static allocUnsafe(size) {
66
+ return new Buffer(size)
67
+ }
68
+ static allocUnsafeSlow(size) {
69
+ return new Buffer(size)
70
+ }
71
+ static concat(list, totalLength) {
72
+ const length = totalLength ?? list.reduce((sum, item) => sum + item.length, 0)
73
+ const out = new Buffer(length)
74
+ let offset = 0
75
+ for (const item of list) {
76
+ out.set(item, offset)
77
+ offset += item.length
78
+ }
79
+ return out
80
+ }
81
+ static isBuffer(value) {
82
+ return value instanceof Buffer
83
+ }
84
+ static byteLength(value) {
85
+ if (typeof value === 'string') return new TextEncoder().encode(value).length
86
+ return value?.byteLength ?? value?.length ?? 0
87
+ }
88
+ static isEncoding(encoding) {
89
+ return /^(utf8|utf-8|ascii|latin1|binary|hex|base64|base64url)$/i.test(String(encoding || ''))
90
+ }
91
+ toString(encoding) {
92
+ if (encoding === 'hex') return [...this].map((byte) => byte.toString(16).padStart(2, '0')).join('')
93
+ if (encoding === 'base64') {
94
+ let binary = ''
95
+ this.forEach((byte) => {
96
+ binary += String.fromCharCode(byte)
97
+ })
98
+ return btoa(binary)
99
+ }
100
+ return new TextDecoder().decode(this)
101
+ }
102
+ slice(start, end) {
103
+ return Buffer.from(super.subarray(start, end))
104
+ }
105
+ copy(target, targetStart = 0, start = 0, end = this.length) {
106
+ const sliced = this.subarray(start, end)
107
+ target.set(sliced, targetStart)
108
+ return sliced.length
109
+ }
110
+ write(string, offset = 0) {
111
+ const bytes = new TextEncoder().encode(string)
112
+ const written = Math.min(bytes.length, this.length - offset)
113
+ this.set(bytes.subarray(0, written), offset)
114
+ return written
115
+ }
116
+ }
117
+
118
+ function inherits(ctor, superCtor) {
119
+ ctor.super_ = superCtor
120
+ ctor.prototype = Object.create(superCtor.prototype, {
121
+ constructor: { value: ctor, writable: true, configurable: true },
122
+ })
123
+ }
124
+
125
+ if (!globalThis.Buffer) globalThis.Buffer = Buffer
126
+
127
+ return {
128
+ buffer: { Buffer, SlowBuffer: Buffer, INSPECT_MAX_BYTES: 50, kMaxLength: 0x7fffffff },
129
+ stream: {
130
+ Stream: EventEmitter,
131
+ Readable: EventEmitter,
132
+ Writable: EventEmitter,
133
+ Duplex: EventEmitter,
134
+ Transform: EventEmitter,
135
+ PassThrough: EventEmitter,
136
+ },
137
+ events: { EventEmitter },
138
+ util: {
139
+ deprecate(fn) {
140
+ return fn
141
+ },
142
+ inherits,
143
+ inspect() {
144
+ return ''
145
+ },
146
+ format(value) {
147
+ return String(value)
148
+ },
149
+ types: { isBuffer: Buffer.isBuffer },
150
+ },
151
+ }
152
+ }
153
+
154
+ window.__ModuleLoader__.load({
155
+ id: "@klarkxy/dsh-model-center",
156
+ factory: (dshRequire) => {
157
+ var __nodeShims;
158
+ var process = globalThis.process || { env: {}, nextTick: function (fn) { var args = [].slice.call(arguments, 1); queueMicrotask(function () { fn.apply(null, args); }); } };
159
+ var require = function (id) {
160
+ if (id === 'buffer' || id === 'stream' || id === 'util' || id === 'events') {
161
+ if (!__nodeShims) __nodeShims = __createDshNodeShims();
162
+ return __nodeShims[id];
163
+ }
164
+ return dshRequire(id);
165
+ };
166
+ var module = { exports: {} };
167
+ var exports = module.exports;
168
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
169
+ let react = require("react");
170
+ let react_jsx_runtime = require("react/jsx-runtime");
171
+ //#region src/catalogue.ts
172
+ /** DSH GenerateOptions.purpose values. Shown only as labels for live registrations. */
173
+ const BUILTIN_PURPOSE_CATALOGUE = [{
174
+ id: "compaction",
175
+ label: "会话压缩",
176
+ defaultTarget: {
177
+ kind: "role",
178
+ role: "weak"
179
+ }
180
+ }, {
181
+ id: "session-title",
182
+ label: "会话标题",
183
+ defaultTarget: {
184
+ kind: "role",
185
+ role: "weak"
186
+ }
187
+ }];
188
+ function purposeLabel(id, registeredLabel, locale = "zh") {
189
+ if (registeredLabel) return registeredLabel;
190
+ const known = BUILTIN_PURPOSE_CATALOGUE.find((item) => item.id === id);
191
+ if (known) {
192
+ if (locale === "en") return id === "compaction" ? "Compaction" : "Session title";
193
+ return known.label;
194
+ }
195
+ return id;
196
+ }
197
+ //#endregion
198
+ //#region ../dsh-ai-services/src/contracts.ts
199
+ const AI_RPC_CHANNEL = "/dsh-ai-services";
200
+ const MODEL_SETTINGS_SLOT = "dsh-editor.settings.models";
201
+ //#endregion
202
+ //#region src/contracts.ts
203
+ const MODEL_CENTER_RPC_CHANNEL = "/dsh-model-center";
204
+ const MODEL_ROLES = [
205
+ "weak",
206
+ "normal",
207
+ "strong",
208
+ "fantasy"
209
+ ];
210
+ function isModelRole(value) {
211
+ return value === "normal" || value === "weak" || value === "strong" || value === "fantasy";
212
+ }
213
+ function isBoundRoute(route) {
214
+ return Boolean(route && route.provider.trim() && route.model.trim());
215
+ }
216
+ //#endregion
217
+ //#region src/client-view.ts
218
+ const SETTINGS_SECTION_SLOT = "settings.section";
219
+ const MODEL_CENTER_SLOT_ID = "model-center";
220
+ const SETTINGS_SEAT = {
221
+ replacement: MODEL_SETTINGS_SLOT,
222
+ fallback: SETTINGS_SECTION_SLOT,
223
+ exclusive: true
224
+ };
225
+ var RpcCallError = class extends Error {
226
+ code;
227
+ constructor(message, code) {
228
+ super(message);
229
+ this.code = code;
230
+ this.name = "RpcCallError";
231
+ }
232
+ };
233
+ function unwrapRpc(result) {
234
+ if (!result || typeof result !== "object") throw new RpcCallError("请求失败。");
235
+ const row = result;
236
+ if ("ok" in row) {
237
+ if (!row.ok) throw new RpcCallError(row.error.message || "请求失败。", row.error.code);
238
+ return row.value;
239
+ }
240
+ return result;
241
+ }
242
+ function isHostEnabledStatus(value) {
243
+ try {
244
+ return unwrapRpc(value).enabled === true;
245
+ } catch {
246
+ return false;
247
+ }
248
+ }
249
+ function tablistKey(current, key) {
250
+ if (key === "Home") return "policy";
251
+ if (key === "End") return "providers";
252
+ if (key === "ArrowRight" || key === "ArrowLeft") {
253
+ const tabs = [
254
+ "policy",
255
+ "runtime",
256
+ "providers"
257
+ ];
258
+ return tabs[(tabs.indexOf(current) + (key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length];
259
+ }
260
+ }
261
+ function tabLabel(tab, locale) {
262
+ if (tab === "providers") return locale === "en" ? "Providers" : "供应商";
263
+ if (tab === "runtime") return locale === "en" ? "Runtime" : "运行设置";
264
+ return locale === "en" ? "Model routing" : "模型配置";
265
+ }
266
+ function centerLabel(locale) {
267
+ return locale === "en" ? "Model Center" : "模型中心";
268
+ }
269
+ function modelSettingsSlotSpec(locale) {
270
+ return {
271
+ name: MODEL_SETTINGS_SLOT,
272
+ id: MODEL_CENTER_SLOT_ID,
273
+ order: 0,
274
+ label: centerLabel(locale)
275
+ };
276
+ }
277
+ function settingsSectionSlotSpec(locale) {
278
+ return {
279
+ name: SETTINGS_SECTION_SLOT,
280
+ id: MODEL_CENTER_SLOT_ID,
281
+ order: 40,
282
+ label: centerLabel(locale)
283
+ };
284
+ }
285
+ /** Bind the replacement seat when declared; otherwise the standalone settings.section. Never both. */
286
+ function registerExclusiveSettingsSeats(slots, render, locale = "zh") {
287
+ let replacementLive = false;
288
+ let dropFallbackInject = () => {};
289
+ let dropFallbackRegister = () => {};
290
+ const dropReplacement = slots.inject(SETTINGS_SEAT.replacement, () => {
291
+ replacementLive = true;
292
+ dropFallbackRegister();
293
+ dropFallbackRegister = () => {};
294
+ dropFallbackInject();
295
+ dropFallbackInject = () => {};
296
+ return slots.register(modelSettingsSlotSpec(locale), render);
297
+ });
298
+ if (!replacementLive) dropFallbackInject = slots.inject(SETTINGS_SEAT.fallback, () => {
299
+ if (replacementLive) return () => {};
300
+ dropFallbackRegister = slots.register(settingsSectionSlotSpec(locale), render);
301
+ return () => {
302
+ dropFallbackRegister();
303
+ };
304
+ });
305
+ return () => {
306
+ dropReplacement();
307
+ dropFallbackInject();
308
+ dropFallbackRegister();
309
+ };
310
+ }
311
+ const HOST_STATUS_ENDPOINT = "status";
312
+ function hostChannel() {
313
+ return MODEL_CENTER_RPC_CHANNEL;
314
+ }
315
+ async function readHostEnabled(call, signal) {
316
+ try {
317
+ return isHostEnabledStatus(await call(MODEL_CENTER_RPC_CHANNEL, HOST_STATUS_ENDPOINT, {}, signal));
318
+ } catch {
319
+ return false;
320
+ }
321
+ }
322
+ const COPY = {
323
+ zh: {
324
+ loading: "正在读取模型中心…",
325
+ reconnect: "重新连接",
326
+ hostOff: "模型中心未启用。",
327
+ policyMissing: "无法读取 AI 策略。",
328
+ save: "保存",
329
+ saved: "已保存。",
330
+ retry: "重试",
331
+ limits: "限额与超时",
332
+ retryLimit: "失败重试次数",
333
+ concurrency: "并发",
334
+ timeout: "超时(毫秒)",
335
+ maxInput: "输入字符上限",
336
+ maxOutput: "输出 token 上限",
337
+ roles: "模型档位",
338
+ fantasyPreset: "幻想",
339
+ capabilityHint: "跟随档位时,模型与思考强度会一起更新。",
340
+ capabilities: "能力默认值",
341
+ unsaved: "有未保存的更改",
342
+ notConfigured: "请先设置对话档",
343
+ purposes: "其他功能",
344
+ noPurposes: "没有其他功能需要单独配置。",
345
+ unbound: "跟随对话",
346
+ bindNormal: "对话",
347
+ commonModels: "常用功能",
348
+ otherModels: "其他功能",
349
+ advanced: "高级设置",
350
+ defaultPreset: "对话",
351
+ efficientPreset: "快速",
352
+ qualityPreset: "思考",
353
+ followDefault: "跟随对话",
354
+ followEfficient: "跟随快速",
355
+ followQuality: "跟随思考",
356
+ useModel: "使用模型",
357
+ followSession: "跟随当前会话",
358
+ explicit: "单独设置",
359
+ provider: "供应商",
360
+ model: "模型",
361
+ effort: "思考强度",
362
+ chooseModel: "选择模型",
363
+ catalog: "模型",
364
+ resolved: "解析结果",
365
+ source: "配置来源",
366
+ conflict: "冲突",
367
+ discovery: "发现模型",
368
+ discovering: "正在发现…",
369
+ noProviders: "没有可列出的供应商。",
370
+ live: "已激活",
371
+ dormant: "未激活",
372
+ openNative: "打开原生设置",
373
+ nativeHint: "凭据请在原生设置中编辑。",
374
+ editorHint: "供应商编辑由宿主提供。",
375
+ revisionConflict: "配置已被更新,请刷新后重试。",
376
+ storageFailed: "策略存储失败。",
377
+ defaultEffort: "默认",
378
+ unknownOp: "未知操作。"
379
+ },
380
+ en: {
381
+ loading: "Loading Model Center…",
382
+ reconnect: "Reconnect",
383
+ hostOff: "Model Center is off.",
384
+ policyMissing: "Could not read AI policy.",
385
+ save: "Save",
386
+ saved: "Saved.",
387
+ retry: "Retry",
388
+ limits: "Limits and timeouts",
389
+ retryLimit: "Retry attempts",
390
+ concurrency: "Concurrency",
391
+ timeout: "Timeout (ms)",
392
+ maxInput: "Input character cap",
393
+ maxOutput: "Output token cap",
394
+ roles: "Model tiers",
395
+ fantasyPreset: "Fantasy",
396
+ capabilityHint: "Following a tier inherits both its model and reasoning effort.",
397
+ capabilities: "Capability defaults",
398
+ unsaved: "Unsaved changes",
399
+ notConfigured: "Configure the Chat tier first",
400
+ purposes: "Other features",
401
+ noPurposes: "No other features need a separate model.",
402
+ unbound: "Follow Chat",
403
+ bindNormal: "Chat",
404
+ commonModels: "Common features",
405
+ otherModels: "Other features",
406
+ advanced: "Advanced settings",
407
+ defaultPreset: "Chat",
408
+ efficientPreset: "Quick",
409
+ qualityPreset: "Thinking",
410
+ followDefault: "Follow Chat",
411
+ followEfficient: "Follow Quick",
412
+ followQuality: "Follow Thinking",
413
+ useModel: "Use model",
414
+ followSession: "Follow current session",
415
+ explicit: "Custom",
416
+ provider: "Provider",
417
+ model: "Model",
418
+ effort: "Reasoning",
419
+ chooseModel: "Choose a model",
420
+ catalog: "Model",
421
+ resolved: "Resolved",
422
+ source: "Source",
423
+ conflict: "Conflict",
424
+ discovery: "Discover models",
425
+ discovering: "Discovering…",
426
+ noProviders: "No providers to list.",
427
+ live: "Live",
428
+ dormant: "Dormant",
429
+ openNative: "Open native settings",
430
+ nativeHint: "Edit credentials in native settings.",
431
+ editorHint: "The host supplies the provider editor.",
432
+ revisionConflict: "Policy changed; refresh and retry.",
433
+ storageFailed: "Policy storage failed.",
434
+ defaultEffort: "Default",
435
+ unknownOp: "Unknown operation."
436
+ }
437
+ };
438
+ function copy(locale) {
439
+ return COPY[locale];
440
+ }
441
+ //#endregion
442
+ //#region src/model-catalog.ts
443
+ const KEY_SEP = "";
444
+ function isRecord$1(value) {
445
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
446
+ }
447
+ function text(value) {
448
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
449
+ }
450
+ function parseEffort(value) {
451
+ if (!isRecord$1(value)) return void 0;
452
+ const id = text(value.id);
453
+ if (!id) return void 0;
454
+ return {
455
+ id,
456
+ name: text(value.name) ?? id
457
+ };
458
+ }
459
+ function parseModel(value) {
460
+ if (!isRecord$1(value)) return void 0;
461
+ const id = text(value.id);
462
+ if (!id) return void 0;
463
+ const name = text(value.name) ?? id;
464
+ if (value.reasoning === void 0) return {
465
+ id,
466
+ name
467
+ };
468
+ if (!isRecord$1(value.reasoning) || !Array.isArray(value.reasoning.efforts)) return {
469
+ id,
470
+ name
471
+ };
472
+ const efforts = value.reasoning.efforts.map(parseEffort).filter((item) => Boolean(item));
473
+ const defaultEffort = text(value.reasoning.defaultEffort);
474
+ return {
475
+ id,
476
+ name,
477
+ reasoning: defaultEffort ? {
478
+ efforts,
479
+ defaultEffort
480
+ } : { efforts }
481
+ };
482
+ }
483
+ function parseGroup(value) {
484
+ if (!isRecord$1(value)) return void 0;
485
+ const id = text(value.id);
486
+ if (!id || !Array.isArray(value.models)) return void 0;
487
+ const models = value.models.map(parseModel).filter((item) => Boolean(item));
488
+ return {
489
+ id,
490
+ name: text(value.name) ?? id,
491
+ models
492
+ };
493
+ }
494
+ function parseModelCatalog(value) {
495
+ const root = isRecord$1(value) ? value : {};
496
+ const groups = Array.isArray(root.groups) ? root.groups.map(parseGroup).filter((item) => Boolean(item)) : [];
497
+ const fallback = isRecord$1(root.default) ? root.default : void 0;
498
+ const provider = fallback ? text(fallback.provider) : void 0;
499
+ const model = fallback ? text(fallback.model) : void 0;
500
+ const reasoningEffort = fallback ? text(fallback.reasoningEffort) : void 0;
501
+ const catalog = { groups };
502
+ if (provider && model) catalog.default = reasoningEffort ? {
503
+ provider,
504
+ model,
505
+ reasoningEffort
506
+ } : {
507
+ provider,
508
+ model
509
+ };
510
+ return catalog;
511
+ }
512
+ function catalogChoices(catalog, bound) {
513
+ const choices = [];
514
+ for (const group of catalog.groups) for (const model of group.models) choices.push({
515
+ provider: group.id,
516
+ model: model.id,
517
+ label: `${group.name} / ${model.name}`,
518
+ efforts: model.reasoning?.efforts ?? [],
519
+ defaultEffort: model.reasoning?.defaultEffort
520
+ });
521
+ if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
522
+ provider: bound.provider,
523
+ model: bound.model,
524
+ label: `${bound.provider} / ${bound.model}`,
525
+ efforts: []
526
+ });
527
+ return choices;
528
+ }
529
+ function choiceOf(choices, provider, model) {
530
+ return choices.find((item) => item.provider === provider && item.model === model);
531
+ }
532
+ /** Advertised efforts plus the current value if it is not in the list. Unlisted IDs stay selectable. */
533
+ function effortOptions(choice, current) {
534
+ const listed = choice?.efforts.map((item) => ({ ...item })) ?? [];
535
+ if (current && !listed.some((item) => item.id === current)) listed.push({
536
+ id: current,
537
+ name: current
538
+ });
539
+ return listed;
540
+ }
541
+ function routeKey(provider, model) {
542
+ return `${provider}${KEY_SEP}${model}`;
543
+ }
544
+ function parseRouteKey(value) {
545
+ const index = value.indexOf(KEY_SEP);
546
+ if (index <= 0) return void 0;
547
+ const provider = value.slice(0, index);
548
+ const model = value.slice(index + 1);
549
+ if (!provider || !model) return void 0;
550
+ return {
551
+ provider,
552
+ model
553
+ };
554
+ }
555
+ function emptyCatalog() {
556
+ return { groups: [] };
557
+ }
558
+ //#endregion
559
+ //#region src/policy.ts
560
+ function roleRoute(policy, role) {
561
+ const bound = policy.roles[role];
562
+ if (isBoundRoute(bound)) return { route: bound };
563
+ if (role !== "normal") {
564
+ const normal = policy.roles.normal;
565
+ if (isBoundRoute(normal)) return {
566
+ route: normal,
567
+ inheritedRole: "normal"
568
+ };
569
+ }
570
+ if (role === "normal") return { error: "对话档尚未设置模型。" };
571
+ return { error: `${roleLabel(role)}档尚未设置模型,且对话档不可用。` };
572
+ }
573
+ function roleLabel(role, locale = "zh") {
574
+ if (locale === "en") return {
575
+ normal: "Chat",
576
+ weak: "Quick",
577
+ strong: "Thinking",
578
+ fantasy: "Fantasy"
579
+ }[role];
580
+ return {
581
+ normal: "对话",
582
+ weak: "快速",
583
+ strong: "思考",
584
+ fantasy: "幻想"
585
+ }[role];
586
+ }
587
+ function resolveTarget(target, policy, session) {
588
+ if (target.kind === "session") {
589
+ if (!isBoundRoute(session)) return { error: "当前会话没有可用模型。" };
590
+ return { route: session };
591
+ }
592
+ if (target.kind === "model") {
593
+ if (!isBoundRoute(target)) return { error: "指定模型不完整。" };
594
+ return { route: {
595
+ provider: target.provider,
596
+ model: target.model,
597
+ reasoningEffort: target.reasoningEffort
598
+ } };
599
+ }
600
+ return roleRoute(policy, target.role);
601
+ }
602
+ function purposeTarget(policy, purpose, specs, override) {
603
+ if (override) return {
604
+ target: override,
605
+ source: "override"
606
+ };
607
+ const configured = policy.purposes[purpose];
608
+ if (configured) return {
609
+ target: configured,
610
+ source: "purpose"
611
+ };
612
+ const spec = specs.find((item) => item.id === purpose);
613
+ if (spec) return {
614
+ target: spec.defaultTarget,
615
+ source: "default"
616
+ };
617
+ return { error: "未配置该用途。" };
618
+ }
619
+ function previewResolve(policy, purpose, options = {}) {
620
+ const selected = purposeTarget(policy, purpose, options.specs ?? [], options.override);
621
+ if ("error" in selected) return {
622
+ ok: false,
623
+ error: selected.error,
624
+ policyRevision: policy.revision
625
+ };
626
+ const resolved = resolveTarget(selected.target, policy, options.session);
627
+ if ("error" in resolved) return {
628
+ ok: false,
629
+ error: resolved.error,
630
+ target: selected.target,
631
+ policyRevision: policy.revision
632
+ };
633
+ let conflict;
634
+ if (options.knownProviders && !options.knownProviders.has(resolved.route.provider)) conflict = "供应商当前不可用。";
635
+ else if (options.knownModels && !options.knownModels.has(`${resolved.route.provider}/${resolved.route.model}`)) conflict = "模型当前不可用。";
636
+ return {
637
+ ok: true,
638
+ conflict,
639
+ route: {
640
+ ...resolved.route,
641
+ source: selected.source,
642
+ target: selected.target,
643
+ policyRevision: policy.revision,
644
+ inheritedRole: resolved.inheritedRole
645
+ }
646
+ };
647
+ }
648
+ function purposeRows(policy, registered) {
649
+ return registered.map((spec) => {
650
+ const configured = policy.purposes[spec.id];
651
+ return {
652
+ ...spec,
653
+ target: configured ?? spec.defaultTarget,
654
+ source: configured ? "purpose" : "default"
655
+ };
656
+ });
657
+ }
658
+ function formatRoute(route) {
659
+ const effort = route.reasoningEffort ? ` · ${route.reasoningEffort}` : "";
660
+ return `${route.provider} / ${route.model}${effort}`;
661
+ }
662
+ //#endregion
663
+ //#region src/providers.ts
664
+ function providerIdOf(entry) {
665
+ if (typeof entry.provider === "string" && entry.provider.length > 0) return entry.provider;
666
+ if (typeof entry.id === "string" && entry.id.length > 0) return entry.id;
667
+ return "";
668
+ }
669
+ function displayNameOf(entry) {
670
+ if (typeof entry.displayName === "string" && entry.displayName.length > 0) return entry.displayName;
671
+ if (typeof entry.name === "string" && entry.name.length > 0) return entry.name;
672
+ return providerIdOf(entry) || "provider";
673
+ }
674
+ function settingsPathOf(entry) {
675
+ return Array.isArray(entry.settingsPath) ? entry.settingsPath.filter((item) => typeof item === "string") : [];
676
+ }
677
+ function profileAt(value, path) {
678
+ let current = value;
679
+ for (const segment of path) {
680
+ if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
681
+ current = current[segment];
682
+ }
683
+ return current;
684
+ }
685
+ function apiKeyEnvOf(profile) {
686
+ if (!profile || typeof profile !== "object" || Array.isArray(profile)) return void 0;
687
+ const ref = profile.apiKeyEnv;
688
+ return typeof ref === "string" && ref.trim() ? ref.trim() : void 0;
689
+ }
690
+ function namespacesOf(snapshot) {
691
+ const map = /* @__PURE__ */ new Map();
692
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return map;
693
+ const view = snapshot.view;
694
+ const namespaces = view && typeof view === "object" && !Array.isArray(view) ? view.namespaces : void 0;
695
+ if (!Array.isArray(namespaces)) return map;
696
+ for (const entry of namespaces) {
697
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
698
+ const ns = entry.ns;
699
+ if (typeof ns === "string" && ns) map.set(ns, entry.value);
700
+ }
701
+ return map;
702
+ }
703
+ /** Resolve the profile's apiKeyEnv. Never invent PROVIDER_API_KEY. */
704
+ function resolveApiKeyEnv(namespaces, settingsNs, settingsPath, schema) {
705
+ if (!settingsNs) return void 0;
706
+ const namespace = namespaces.get(settingsNs);
707
+ return apiKeyEnvOf(schema?.getPath ? schema.getPath(namespace, settingsPath) : profileAt(namespace, settingsPath));
708
+ }
709
+ function authOf(credential) {
710
+ if (credential?.configured === true) return "configured";
711
+ if (credential?.configured === false) return "missing";
712
+ return "unknown";
713
+ }
714
+ function joinProviderListings(configurable, live, credentials = {}, apiKeyEnvs = /* @__PURE__ */ new Map()) {
715
+ const liveIds = new Set(live.map((item) => typeof item.id === "string" ? item.id : "").filter(Boolean));
716
+ const rows = configurable.map((entry) => {
717
+ const id = providerIdOf(entry);
718
+ const credentialRef = id ? apiKeyEnvs.get(id) : void 0;
719
+ const credential = credentialRef ? credentials[credentialRef] : void 0;
720
+ return {
721
+ id: id || displayNameOf(entry),
722
+ displayName: displayNameOf(entry),
723
+ settingsNs: typeof entry.settingsNs === "string" ? entry.settingsNs : "",
724
+ settingsPath: settingsPathOf(entry),
725
+ live: id ? liveIds.has(id) : false,
726
+ declared: entry.declared === true ? true : entry.declared === false ? false : void 0,
727
+ error: typeof entry.error === "string" && entry.error ? entry.error : void 0,
728
+ auth: authOf(credential),
729
+ writable: typeof credential?.writable === "boolean" ? credential.writable : void 0,
730
+ credentialRef
731
+ };
732
+ });
733
+ for (const item of live) {
734
+ const id = typeof item.id === "string" ? item.id : "";
735
+ if (!id || rows.some((row) => row.id === id)) continue;
736
+ const credentialRef = apiKeyEnvs.get(id);
737
+ const credential = credentialRef ? credentials[credentialRef] : void 0;
738
+ rows.push({
739
+ id,
740
+ displayName: typeof item.name === "string" && item.name ? item.name : id,
741
+ settingsNs: "",
742
+ settingsPath: [],
743
+ live: true,
744
+ declared: void 0,
745
+ error: void 0,
746
+ auth: authOf(credential),
747
+ writable: typeof credential?.writable === "boolean" ? credential.writable : void 0,
748
+ credentialRef
749
+ });
750
+ }
751
+ return rows;
752
+ }
753
+ function credentialRefsToDescribe(rows) {
754
+ return [...new Set(rows.flatMap((row) => row.credentialRef ? [row.credentialRef] : []))];
755
+ }
756
+ function shouldLoadNativeProviders(renderProviders) {
757
+ return typeof renderProviders !== "function";
758
+ }
759
+ function authLabel(auth, locale = "zh") {
760
+ if (auth === "configured") return locale === "en" ? "Key configured" : "已配置密钥";
761
+ if (auth === "missing") return locale === "en" ? "Key missing" : "未配置密钥";
762
+ return locale === "en" ? "Auth unknown" : "密钥状态未知";
763
+ }
764
+ function canDiscoverModels(llm) {
765
+ return typeof llm?.discoverModels === "function";
766
+ }
767
+ function canOpenSettingsDocument(settings) {
768
+ return typeof settings?.openSettingsDocument === "function";
769
+ }
770
+ function nativeSettingsNote(locale = "zh") {
771
+ return locale === "en" ? "Open native settings for provider credentials." : "凭据请在原生设置中编辑。";
772
+ }
773
+ //#endregion
774
+ //#region src/schema.ts
775
+ const LIMIT_BOUNDS = {
776
+ concurrency: {
777
+ min: 1,
778
+ max: 8
779
+ },
780
+ timeoutMs: {
781
+ min: 1e3,
782
+ max: 3e5
783
+ },
784
+ maxInputChars: {
785
+ min: 1,
786
+ max: 2e5
787
+ },
788
+ maxOutputTokens: {
789
+ min: 1,
790
+ max: 8192
791
+ },
792
+ maxAttempts: {
793
+ min: 1,
794
+ max: 5
795
+ }
796
+ };
797
+ const ROUTE_BOUNDS = {
798
+ provider: 128,
799
+ model: 256,
800
+ effort: 64,
801
+ purpose: 80
802
+ };
803
+ function isRecord(value) {
804
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
805
+ }
806
+ function fail(error) {
807
+ return {
808
+ ok: false,
809
+ error
810
+ };
811
+ }
812
+ function boundedInt(value, min, max) {
813
+ return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max ? value : void 0;
814
+ }
815
+ function boundedText(value, max) {
816
+ if (typeof value !== "string") return void 0;
817
+ const text = value.trim();
818
+ if (!text || text.length > max) return void 0;
819
+ return text;
820
+ }
821
+ function parseModelRoute(value) {
822
+ if (!isRecord(value)) return void 0;
823
+ const provider = boundedText(value.provider, ROUTE_BOUNDS.provider);
824
+ const model = boundedText(value.model, ROUTE_BOUNDS.model);
825
+ if (!provider || !model) return void 0;
826
+ if (value.reasoningEffort === void 0) return {
827
+ provider,
828
+ model
829
+ };
830
+ if (typeof value.reasoningEffort !== "string") return void 0;
831
+ const reasoningEffort = value.reasoningEffort.trim();
832
+ if (!reasoningEffort) return {
833
+ provider,
834
+ model
835
+ };
836
+ if (reasoningEffort.length > ROUTE_BOUNDS.effort) return void 0;
837
+ return {
838
+ provider,
839
+ model,
840
+ reasoningEffort
841
+ };
842
+ }
843
+ function parseModelTarget(value) {
844
+ if (!isRecord(value) || typeof value.kind !== "string") return void 0;
845
+ if (value.kind === "session") return { kind: "session" };
846
+ if (value.kind === "role" && isModelRole(value.role)) return {
847
+ kind: "role",
848
+ role: value.role
849
+ };
850
+ if (value.kind === "model") {
851
+ const route = parseModelRoute(value);
852
+ return route ? {
853
+ kind: "model",
854
+ ...route
855
+ } : void 0;
856
+ }
857
+ }
858
+ function parseLimits(value) {
859
+ if (!isRecord(value)) return void 0;
860
+ const concurrency = boundedInt(value.concurrency, LIMIT_BOUNDS.concurrency.min, LIMIT_BOUNDS.concurrency.max);
861
+ const timeoutMs = boundedInt(value.timeoutMs, LIMIT_BOUNDS.timeoutMs.min, LIMIT_BOUNDS.timeoutMs.max);
862
+ const maxInputChars = boundedInt(value.maxInputChars, LIMIT_BOUNDS.maxInputChars.min, LIMIT_BOUNDS.maxInputChars.max);
863
+ const maxOutputTokens = boundedInt(value.maxOutputTokens, LIMIT_BOUNDS.maxOutputTokens.min, LIMIT_BOUNDS.maxOutputTokens.max);
864
+ const maxAttempts = boundedInt(value.maxAttempts, LIMIT_BOUNDS.maxAttempts.min, LIMIT_BOUNDS.maxAttempts.max);
865
+ if (concurrency === void 0 || timeoutMs === void 0 || maxInputChars === void 0 || maxOutputTokens === void 0 || maxAttempts === void 0) return;
866
+ return {
867
+ concurrency,
868
+ timeoutMs,
869
+ maxInputChars,
870
+ maxOutputTokens,
871
+ maxAttempts
872
+ };
873
+ }
874
+ function parseRoles(value) {
875
+ if (!isRecord(value)) return void 0;
876
+ const roles = {};
877
+ for (const role of [
878
+ "normal",
879
+ "weak",
880
+ "strong",
881
+ "fantasy"
882
+ ]) {
883
+ if (value[role] === void 0) continue;
884
+ const route = parseModelRoute(value[role]);
885
+ if (!route) return void 0;
886
+ roles[role] = route;
887
+ }
888
+ return roles;
889
+ }
890
+ function parsePurposes(value) {
891
+ if (!isRecord(value)) return void 0;
892
+ const keys = Object.keys(value);
893
+ if (keys.length > 200) return void 0;
894
+ const purposes = {};
895
+ for (const key of keys) {
896
+ if (!key || key.length > ROUTE_BOUNDS.purpose) return void 0;
897
+ const target = parseModelTarget(value[key]);
898
+ if (!target) return void 0;
899
+ purposes[key] = target;
900
+ }
901
+ return purposes;
902
+ }
903
+ function parsePolicyBody(value) {
904
+ if (!isRecord(value)) return void 0;
905
+ const roles = parseRoles(value.roles);
906
+ const purposes = parsePurposes(value.purposes);
907
+ const limits = parseLimits(value.limits);
908
+ if (!roles || !purposes || !limits) return void 0;
909
+ return {
910
+ roles,
911
+ purposes,
912
+ limits
913
+ };
914
+ }
915
+ function parseAiPolicy(value) {
916
+ if (!isRecord(value)) return void 0;
917
+ const revision = boundedInt(value.revision, 0, Number.MAX_SAFE_INTEGER);
918
+ const body = parsePolicyBody(value);
919
+ if (revision === void 0 || !body) return void 0;
920
+ return {
921
+ revision,
922
+ ...body
923
+ };
924
+ }
925
+ function parsePurposeSpec(value) {
926
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.label !== "string" || typeof value.plugin !== "string") return void 0;
927
+ if (!value.id || value.id.length > ROUTE_BOUNDS.purpose || !value.label || !value.plugin) return void 0;
928
+ const defaultTarget = parseModelTarget(value.defaultTarget);
929
+ if (!defaultTarget) return void 0;
930
+ return {
931
+ id: value.id,
932
+ label: value.label,
933
+ plugin: value.plugin,
934
+ defaultTarget,
935
+ maxOutputTokens: typeof value.maxOutputTokens === "number" ? value.maxOutputTokens : void 0,
936
+ maxInputChars: typeof value.maxInputChars === "number" ? value.maxInputChars : void 0,
937
+ timeoutMs: typeof value.timeoutMs === "number" ? value.timeoutMs : void 0
938
+ };
939
+ }
940
+ function parsePolicyUpdate(payload) {
941
+ if (!isRecord(payload)) return fail("AI 策略格式无效。");
942
+ const expectedRevision = boundedInt(payload.expectedRevision, 0, Number.MAX_SAFE_INTEGER);
943
+ const policy = parsePolicyBody(payload.policy);
944
+ if (expectedRevision === void 0 || !policy) return fail("AI 策略格式无效。");
945
+ return {
946
+ ok: true,
947
+ value: {
948
+ expectedRevision,
949
+ policy
950
+ }
951
+ };
952
+ }
953
+ function parseAiServicesStatus(value) {
954
+ if (!isRecord(value) || value.policy === void 0 || !Array.isArray(value.purposes)) return fail("AI 策略接口与约定不一致。");
955
+ const policy = parseAiPolicy(value.policy);
956
+ if (!policy) return fail("AI 策略接口与约定不一致。");
957
+ const purposes = value.purposes.map(parsePurposeSpec);
958
+ if (purposes.some((item) => !item)) return fail("AI 策略接口与约定不一致。");
959
+ if (value.storageFailed !== void 0 && typeof value.storageFailed !== "boolean") return fail("AI 策略接口与约定不一致。");
960
+ return {
961
+ ok: true,
962
+ value: {
963
+ policy,
964
+ purposes,
965
+ storageFailed: value.storageFailed === true
966
+ }
967
+ };
968
+ }
969
+ function parseResolvedRoute(value) {
970
+ if (!isRecord(value)) return { success: false };
971
+ const route = parseModelRoute(value);
972
+ const target = parseModelTarget(value.target);
973
+ const policyRevision = boundedInt(value.policyRevision, 0, Number.MAX_SAFE_INTEGER);
974
+ const source = value.source;
975
+ if (!route || !target || policyRevision === void 0 || source !== "override" && source !== "purpose" && source !== "default") return { success: false };
976
+ const inheritedRole = value.inheritedRole;
977
+ if (inheritedRole !== void 0 && !isModelRole(inheritedRole)) return { success: false };
978
+ return {
979
+ success: true,
980
+ data: {
981
+ ...route,
982
+ source,
983
+ target,
984
+ policyRevision,
985
+ inheritedRole
986
+ }
987
+ };
988
+ }
989
+ function editablePolicy(policy) {
990
+ return {
991
+ roles: { ...policy.roles },
992
+ purposes: { ...policy.purposes },
993
+ limits: { ...policy.limits }
994
+ };
995
+ }
996
+ function isPolicyConflict(error) {
997
+ if (!error || typeof error !== "object") return false;
998
+ if (("code" in error ? String(error.code) : "") === "AI_POLICY_CONFLICT") return true;
999
+ const message = error instanceof Error ? error.message : "";
1000
+ return /策略已被|revision|conflict/i.test(message);
1001
+ }
1002
+ //#endregion
1003
+ //#region src/load.ts
1004
+ function createGenerationGate(start = 0) {
1005
+ let current = start;
1006
+ return {
1007
+ current: () => current,
1008
+ next: () => {
1009
+ current += 1;
1010
+ return current;
1011
+ },
1012
+ isCurrent: (token) => token === current
1013
+ };
1014
+ }
1015
+ function stillCurrent(token, gate, signal) {
1016
+ return gate.isCurrent(token) && !signal?.aborted;
1017
+ }
1018
+ function asList(result) {
1019
+ try {
1020
+ const value = unwrapRpc(result);
1021
+ return Array.isArray(value) ? value : [];
1022
+ } catch {
1023
+ return [];
1024
+ }
1025
+ }
1026
+ function statusValue(result) {
1027
+ try {
1028
+ return unwrapRpc(result);
1029
+ } catch {
1030
+ return result;
1031
+ }
1032
+ }
1033
+ async function loadModelCenter(input, isCurrent) {
1034
+ const { signal } = input;
1035
+ const aiResult = await input.call(AI_RPC_CHANNEL, "status", {}, signal);
1036
+ if (!isCurrent()) return void 0;
1037
+ const ai = parseAiServicesStatus(statusValue(aiResult));
1038
+ if (!ai.ok) throw new Error(ai.error);
1039
+ let catalog = emptyCatalog();
1040
+ if (input.session?.modelCatalog) {
1041
+ try {
1042
+ catalog = parseModelCatalog(statusValue(await input.session.modelCatalog()));
1043
+ } catch {
1044
+ catalog = emptyCatalog();
1045
+ }
1046
+ if (!isCurrent()) return void 0;
1047
+ }
1048
+ let providers = [];
1049
+ if (input.loadProviders && input.llm) {
1050
+ const configurableResult = await input.llm.listConfigurableProviders();
1051
+ if (!isCurrent()) return void 0;
1052
+ const liveResult = input.llm.listProviders ? await input.llm.listProviders() : {
1053
+ ok: true,
1054
+ value: []
1055
+ };
1056
+ if (!isCurrent()) return void 0;
1057
+ const configurable = asList(configurableResult);
1058
+ const live = asList(liveResult);
1059
+ let namespaces = /* @__PURE__ */ new Map();
1060
+ const described = input.configForms?.describe?.();
1061
+ if (described?.ensure) {
1062
+ try {
1063
+ await described.ensure();
1064
+ } catch {}
1065
+ if (!isCurrent()) return void 0;
1066
+ }
1067
+ if (described?.getSnapshot) namespaces = namespacesOf(described.getSnapshot());
1068
+ const apiKeyEnvs = /* @__PURE__ */ new Map();
1069
+ for (const entry of configurable) {
1070
+ const id = typeof entry.provider === "string" && entry.provider ? entry.provider : typeof entry.id === "string" ? entry.id : "";
1071
+ if (!id) continue;
1072
+ const settingsNs = typeof entry.settingsNs === "string" ? entry.settingsNs : "";
1073
+ const settingsPath = Array.isArray(entry.settingsPath) ? entry.settingsPath.filter((item) => typeof item === "string") : [];
1074
+ const ref = resolveApiKeyEnv(namespaces, settingsNs, settingsPath, input.settingsSchema);
1075
+ if (ref) apiKeyEnvs.set(id, ref);
1076
+ }
1077
+ const refs = credentialRefsToDescribe(joinProviderListings(configurable, live, {}, apiKeyEnvs));
1078
+ let credentials = {};
1079
+ if (refs.length && input.credentials?.describe) {
1080
+ try {
1081
+ credentials = unwrapRpc(await input.credentials.describe(refs));
1082
+ } catch {
1083
+ credentials = {};
1084
+ }
1085
+ if (!isCurrent()) return void 0;
1086
+ }
1087
+ providers = joinProviderListings(configurable, live, credentials, apiKeyEnvs);
1088
+ }
1089
+ const resolved = {};
1090
+ for (const spec of ai.value.purposes) {
1091
+ try {
1092
+ const raw = unwrapRpc(await input.call(AI_RPC_CHANNEL, "resolve", {
1093
+ purpose: spec.id,
1094
+ sessionId: input.sessionId
1095
+ }, signal));
1096
+ if (!isCurrent()) return void 0;
1097
+ const parsed = parseResolvedRoute(raw);
1098
+ if (parsed.success) {
1099
+ resolved[spec.id] = parsed.data;
1100
+ continue;
1101
+ }
1102
+ } catch {
1103
+ if (!isCurrent()) return void 0;
1104
+ }
1105
+ const local = previewResolve(ai.value.policy, spec.id, { specs: ai.value.purposes });
1106
+ resolved[spec.id] = local.ok ? local.route : { error: local.error };
1107
+ }
1108
+ if (!isCurrent()) return void 0;
1109
+ return {
1110
+ policy: ai.value.policy,
1111
+ purposes: ai.value.purposes,
1112
+ storageFailed: ai.value.storageFailed,
1113
+ catalog,
1114
+ providers,
1115
+ resolved
1116
+ };
1117
+ }
1118
+ async function savePolicyUpdate(call, request, isCurrent, signal) {
1119
+ const parsed = parsePolicyUpdate(request);
1120
+ if (!parsed.ok) return {
1121
+ ok: false,
1122
+ reason: "invalid",
1123
+ error: parsed.error
1124
+ };
1125
+ try {
1126
+ const raw = await call(AI_RPC_CHANNEL, "update", parsed.value, signal);
1127
+ if (!isCurrent()) return {
1128
+ ok: false,
1129
+ reason: "stale",
1130
+ error: ""
1131
+ };
1132
+ const policy = parseAiPolicy(unwrapRpc(raw));
1133
+ if (!policy) return {
1134
+ ok: false,
1135
+ reason: "invalid",
1136
+ error: "AI 策略接口与约定不一致。"
1137
+ };
1138
+ return {
1139
+ ok: true,
1140
+ policy
1141
+ };
1142
+ } catch (error) {
1143
+ if (!isCurrent()) return {
1144
+ ok: false,
1145
+ reason: "stale",
1146
+ error: ""
1147
+ };
1148
+ if (isPolicyConflict(error)) return {
1149
+ ok: false,
1150
+ reason: "conflict",
1151
+ error: error instanceof Error ? error.message : "策略冲突。"
1152
+ };
1153
+ return {
1154
+ ok: false,
1155
+ reason: "invalid",
1156
+ error: error instanceof Error ? error.message : "保存失败。"
1157
+ };
1158
+ }
1159
+ }
1160
+ //#endregion
1161
+ //#region src/client.tsx
1162
+ const name = "dsh-model-center-client";
1163
+ const inject = [
1164
+ "slots",
1165
+ "connection",
1166
+ "remote",
1167
+ "remote.llm",
1168
+ "remote.settings",
1169
+ "remote.session",
1170
+ "remote.credentials",
1171
+ "configForms",
1172
+ "settingsSchema"
1173
+ ];
1174
+ const COMMON_PURPOSES = [
1175
+ "chat",
1176
+ "manuscript.completion",
1177
+ "manuscript.rewrite"
1178
+ ];
1179
+ function presetLabel(role, locale) {
1180
+ const text = copy(locale);
1181
+ return role === "normal" ? text.defaultPreset : role === "weak" ? text.efficientPreset : role === "strong" ? text.qualityPreset : text.fantasyPreset;
1182
+ }
1183
+ function purposeTargetValue(target) {
1184
+ if (target.kind === "session") return "session";
1185
+ if (target.kind === "role") return `role:${target.role}`;
1186
+ return `model:${routeKey(target.provider, target.model)}`;
1187
+ }
1188
+ function purposeTargetFromValue(value) {
1189
+ if (value === "session") return { kind: "session" };
1190
+ if (value.startsWith("role:")) {
1191
+ const role = value.slice(5);
1192
+ if (MODEL_ROLES.includes(role)) return {
1193
+ kind: "role",
1194
+ role
1195
+ };
1196
+ }
1197
+ if (value.startsWith("model:")) {
1198
+ const parsed = parseRouteKey(value.slice(6));
1199
+ if (parsed) return {
1200
+ kind: "model",
1201
+ ...parsed
1202
+ };
1203
+ }
1204
+ return {
1205
+ kind: "role",
1206
+ role: "normal"
1207
+ };
1208
+ }
1209
+ function registerModelCenterSlots(client, render, locale = "zh") {
1210
+ return registerExclusiveSettingsSeats(client.slots, render, locale);
1211
+ }
1212
+ async function activateClientUi(client, cancelled, signal) {
1213
+ const enabled = await readHostEnabled((channel, endpoint, payload, next) => client.connection.rpc.call(channel, endpoint, payload, next ?? signal), signal);
1214
+ if (cancelled() || signal?.aborted || !shouldAttach(enabled)) return () => {};
1215
+ return registerModelCenterSlots(client, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCenterSettings, {
1216
+ client,
1217
+ ...props
1218
+ }));
1219
+ }
1220
+ function shouldAttach(enabled) {
1221
+ return enabled === true;
1222
+ }
1223
+ function apply(ctx) {
1224
+ const client = ctx;
1225
+ ctx.effect(() => {
1226
+ if (typeof document === "undefined") return () => {};
1227
+ const style = document.createElement("style");
1228
+ style.setAttribute("data-plugin", name);
1229
+ style.textContent = styles;
1230
+ document.head.appendChild(style);
1231
+ return () => style.remove();
1232
+ }, "model-center.styles");
1233
+ ctx.effect(() => {
1234
+ const controller = new AbortController();
1235
+ let disposeSlots = () => {};
1236
+ activateClientUi(client, () => controller.signal.aborted, controller.signal).then((dispose) => {
1237
+ if (controller.signal.aborted) {
1238
+ dispose();
1239
+ return;
1240
+ }
1241
+ disposeSlots = dispose;
1242
+ });
1243
+ return () => {
1244
+ controller.abort();
1245
+ disposeSlots();
1246
+ };
1247
+ }, "model-center.ui");
1248
+ }
1249
+ function ModelCenterSettings(props) {
1250
+ const locale = props.locale === "en" ? "en" : "zh";
1251
+ const text = copy(locale);
1252
+ const tabsId = (0, react.useId)();
1253
+ const generation = (0, react.useRef)(createGenerationGate());
1254
+ const [tab, setTab] = (0, react.useState)("policy");
1255
+ const [busy, setBusy] = (0, react.useState)(false);
1256
+ const [error, setError] = (0, react.useState)("");
1257
+ const [note, setNote] = (0, react.useState)("");
1258
+ const [policy, setPolicy] = (0, react.useState)();
1259
+ const [savedPolicy, setSavedPolicy] = (0, react.useState)("");
1260
+ const [purposes, setPurposes] = (0, react.useState)([]);
1261
+ const [providers, setProviders] = (0, react.useState)([]);
1262
+ const [resolved, setResolved] = (0, react.useState)({});
1263
+ const [catalog, setCatalog] = (0, react.useState)(emptyCatalog());
1264
+ const [discovered, setDiscovered] = (0, react.useState)({});
1265
+ const [enabled, setEnabled] = (0, react.useState)(void 0);
1266
+ const [storageFailed, setStorageFailed] = (0, react.useState)(false);
1267
+ const hostedProviders = !shouldLoadNativeProviders(props.renderProviders);
1268
+ async function hostStatus(signal) {
1269
+ return isHostEnabledStatus(await props.client.connection.rpc.call(hostChannel(), "status", {}, signal));
1270
+ }
1271
+ async function load(token, signal) {
1272
+ const gate = generation.current;
1273
+ const live = await hostStatus(signal);
1274
+ if (!stillCurrent(token, gate, signal)) return;
1275
+ setEnabled(live);
1276
+ if (!live) {
1277
+ setError(text.hostOff);
1278
+ return;
1279
+ }
1280
+ const snapshot = await loadModelCenter({
1281
+ call: (channel, endpoint, payload, next) => props.client.connection.rpc.call(channel, endpoint, payload, next ?? signal),
1282
+ sessionId: props.sessionId,
1283
+ loadProviders: !hostedProviders,
1284
+ llm: props.client.remote.llm,
1285
+ credentials: props.client.remote.credentials,
1286
+ session: props.client.remote.session,
1287
+ configForms: props.client.configForms,
1288
+ settingsSchema: props.client.settingsSchema,
1289
+ signal
1290
+ }, () => stillCurrent(token, gate, signal));
1291
+ if (!stillCurrent(token, gate, signal) || !snapshot) return;
1292
+ setPolicy(snapshot.policy);
1293
+ setSavedPolicy(JSON.stringify(editablePolicy(snapshot.policy)));
1294
+ setPurposes(snapshot.purposes);
1295
+ setProviders(snapshot.providers);
1296
+ setResolved(snapshot.resolved);
1297
+ setCatalog(snapshot.catalog);
1298
+ setStorageFailed(snapshot.storageFailed);
1299
+ }
1300
+ (0, react.useEffect)(() => {
1301
+ const token = generation.current.next();
1302
+ const controller = new AbortController();
1303
+ setError("");
1304
+ setNote("");
1305
+ load(token, controller.signal).catch((cause) => {
1306
+ if (!stillCurrent(token, generation.current, controller.signal)) return;
1307
+ setError(cause instanceof Error ? cause.message : text.policyMissing);
1308
+ });
1309
+ return () => {
1310
+ controller.abort();
1311
+ generation.current.next();
1312
+ };
1313
+ }, [
1314
+ props.client,
1315
+ props.sessionId,
1316
+ locale,
1317
+ hostedProviders
1318
+ ]);
1319
+ async function action(run) {
1320
+ const token = generation.current.current();
1321
+ const signal = new AbortController().signal;
1322
+ setBusy(true);
1323
+ setNote("");
1324
+ setError("");
1325
+ try {
1326
+ await run();
1327
+ } catch (cause) {
1328
+ if (!stillCurrent(token, generation.current, signal)) return;
1329
+ setError(cause instanceof Error ? cause.message : text.policyMissing);
1330
+ } finally {
1331
+ if (stillCurrent(token, generation.current, signal)) setBusy(false);
1332
+ }
1333
+ }
1334
+ if (enabled === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1335
+ className: "model-center",
1336
+ "aria-label": centerLabel(locale),
1337
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1338
+ role: "status",
1339
+ children: text.hostOff
1340
+ })
1341
+ });
1342
+ if (!policy && enabled === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1343
+ className: "model-center",
1344
+ "aria-label": centerLabel(locale),
1345
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1346
+ role: "status",
1347
+ children: error || text.loading
1348
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1349
+ type: "button",
1350
+ onClick: () => {
1351
+ const token = generation.current.next();
1352
+ const controller = new AbortController();
1353
+ action(() => load(token, controller.signal));
1354
+ },
1355
+ children: text.reconnect
1356
+ })]
1357
+ });
1358
+ const draft = policy;
1359
+ const rows = draft ? purposeRows(draft, purposes) : [];
1360
+ const saveDraft = () => {
1361
+ if (!draft) return;
1362
+ action(async () => {
1363
+ const token = generation.current.current();
1364
+ const result = await savePolicyUpdate((channel, endpoint, payload, signal) => props.client.connection.rpc.call(channel, endpoint, payload, signal), {
1365
+ expectedRevision: draft.revision,
1366
+ policy: editablePolicy(draft)
1367
+ }, () => generation.current.isCurrent(token));
1368
+ if (!generation.current.isCurrent(token)) return;
1369
+ if (!result.ok) {
1370
+ if (result.reason === "stale") return;
1371
+ throw new Error(result.reason === "conflict" ? text.revisionConflict : result.error);
1372
+ }
1373
+ setPolicy(result.policy);
1374
+ const reload = generation.current.current();
1375
+ await load(reload, new AbortController().signal);
1376
+ if (!generation.current.isCurrent(reload)) return;
1377
+ setNote(text.saved);
1378
+ });
1379
+ };
1380
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1381
+ className: "model-center",
1382
+ "data-testid": "model-center",
1383
+ "aria-label": centerLabel(locale),
1384
+ children: [
1385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1386
+ className: "model-center-tabs",
1387
+ role: "tablist",
1388
+ "aria-label": centerLabel(locale),
1389
+ onKeyDown: (event) => {
1390
+ const next = tablistKey(tab, event.key);
1391
+ if (!next) return;
1392
+ event.preventDefault();
1393
+ setTab(next);
1394
+ event.currentTarget.querySelector(`[data-tab="${next}"]`)?.focus();
1395
+ },
1396
+ children: [
1397
+ "policy",
1398
+ "runtime",
1399
+ "providers"
1400
+ ].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1401
+ type: "button",
1402
+ role: "tab",
1403
+ "data-tab": key,
1404
+ id: `${tabsId}-${key}-tab`,
1405
+ "aria-controls": `${tabsId}-${key}-panel`,
1406
+ "aria-selected": tab === key,
1407
+ tabIndex: tab === key ? 0 : -1,
1408
+ onClick: () => setTab(key),
1409
+ children: tabLabel(key, locale)
1410
+ }, key))
1411
+ }),
1412
+ storageFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1413
+ role: "alert",
1414
+ className: "model-center-error",
1415
+ children: text.storageFailed
1416
+ }) : null,
1417
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1418
+ role: "alert",
1419
+ className: "model-center-error",
1420
+ children: error
1421
+ }) : null,
1422
+ note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1423
+ role: "status",
1424
+ children: note
1425
+ }) : null,
1426
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1427
+ role: "tabpanel",
1428
+ id: `${tabsId}-providers-panel`,
1429
+ "aria-labelledby": `${tabsId}-providers-tab`,
1430
+ hidden: tab !== "providers",
1431
+ tabIndex: 0,
1432
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidersPanel, {
1433
+ locale,
1434
+ busy,
1435
+ providers,
1436
+ discovered,
1437
+ renderProviders: props.renderProviders,
1438
+ canDiscover: canDiscoverModels(props.client.remote.llm),
1439
+ canOpenNative: canOpenSettingsDocument(props.client.remote.settings),
1440
+ onDiscover: (row) => void action(async () => {
1441
+ if (!props.client.remote.llm.discoverModels) throw new Error(text.noProviders);
1442
+ const token = generation.current.current();
1443
+ const result = unwrapRpc(await props.client.remote.llm.discoverModels(row.settingsNs, { provider: row.id }));
1444
+ if (!generation.current.isCurrent(token)) return;
1445
+ setDiscovered((current) => ({
1446
+ ...current,
1447
+ [row.id]: Array.isArray(result) ? result : []
1448
+ }));
1449
+ }),
1450
+ onOpenNative: () => void action(async () => {
1451
+ unwrapRpc(await props.client.remote.settings.openSettingsDocument?.());
1452
+ })
1453
+ })
1454
+ }),
1455
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1456
+ role: "tabpanel",
1457
+ id: `${tabsId}-policy-panel`,
1458
+ "aria-labelledby": `${tabsId}-policy-tab`,
1459
+ hidden: tab !== "policy",
1460
+ tabIndex: 0,
1461
+ children: draft ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PolicyPanel, {
1462
+ locale,
1463
+ busy,
1464
+ dirty: JSON.stringify(editablePolicy(draft)) !== savedPolicy,
1465
+ policy: draft,
1466
+ catalog,
1467
+ rows,
1468
+ resolved,
1469
+ renderChatModel: props.renderChatModel,
1470
+ onRoles: (roles) => {
1471
+ setNote("");
1472
+ setPolicy({
1473
+ ...draft,
1474
+ roles
1475
+ });
1476
+ },
1477
+ onPurpose: (id, target) => {
1478
+ setNote("");
1479
+ setPolicy({
1480
+ ...draft,
1481
+ purposes: {
1482
+ ...draft.purposes,
1483
+ [id]: target
1484
+ }
1485
+ });
1486
+ },
1487
+ onSave: saveDraft
1488
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1489
+ role: "alert",
1490
+ children: text.policyMissing
1491
+ })
1492
+ }),
1493
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1494
+ role: "tabpanel",
1495
+ id: `${tabsId}-runtime-panel`,
1496
+ "aria-labelledby": `${tabsId}-runtime-tab`,
1497
+ hidden: tab !== "runtime",
1498
+ tabIndex: 0,
1499
+ children: draft ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimePanel, {
1500
+ locale,
1501
+ busy,
1502
+ limits: draft.limits,
1503
+ onLimits: (limits) => {
1504
+ setNote("");
1505
+ setPolicy({
1506
+ ...draft,
1507
+ limits
1508
+ });
1509
+ },
1510
+ onSave: saveDraft
1511
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1512
+ role: "alert",
1513
+ children: text.policyMissing
1514
+ })
1515
+ })
1516
+ ]
1517
+ });
1518
+ }
1519
+ function ProvidersPanel(props) {
1520
+ const text = copy(props.locale);
1521
+ const hosted = typeof props.renderProviders === "function" ? props.renderProviders({ includeWritingRoutes: false }) : void 0;
1522
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1523
+ className: "model-center-providers",
1524
+ children: hosted !== void 0 && hosted !== null ? hosted : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1526
+ className: "model-center-meta",
1527
+ children: nativeSettingsNote(props.locale)
1528
+ }),
1529
+ props.providers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1530
+ className: "model-center-meta",
1531
+ children: text.noProviders
1532
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1533
+ className: "model-center-list",
1534
+ children: props.providers.map((row) => {
1535
+ const models = props.discovered[row.id] ?? [];
1536
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
1537
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1538
+ className: "model-center-provider-head",
1539
+ children: [
1540
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: row.displayName }),
1541
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: row.live ? text.live : text.dormant }),
1542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: authLabel(row.auth, props.locale) })
1543
+ ]
1544
+ }),
1545
+ row.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1546
+ role: "alert",
1547
+ className: "model-center-error",
1548
+ children: row.error
1549
+ }) : null,
1550
+ row.settingsNs ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1551
+ className: "model-center-meta",
1552
+ children: [row.settingsNs, row.settingsPath.length ? ` / ${row.settingsPath.join("/")}` : ""]
1553
+ }) : null,
1554
+ row.credentialRef ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1555
+ className: "model-center-meta",
1556
+ children: row.credentialRef
1557
+ }) : null,
1558
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1559
+ className: "model-center-actions",
1560
+ children: props.canDiscover && row.settingsNs ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1561
+ type: "button",
1562
+ disabled: props.busy,
1563
+ "aria-label": `${text.discovery} ${row.displayName}`,
1564
+ onClick: () => props.onDiscover(row),
1565
+ children: text.discovery
1566
+ }) : null
1567
+ }),
1568
+ models.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1569
+ className: "model-center-models",
1570
+ "aria-label": `${row.displayName} ${text.model}`,
1571
+ children: models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: model.name ? `${model.name} (${model.id})` : model.id }, model.id))
1572
+ }) : null
1573
+ ] }, row.id);
1574
+ })
1575
+ }),
1576
+ props.canOpenNative ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1577
+ type: "button",
1578
+ disabled: props.busy,
1579
+ onClick: props.onOpenNative,
1580
+ children: text.openNative
1581
+ }) : null
1582
+ ] })
1583
+ });
1584
+ }
1585
+ function RuntimePanel(props) {
1586
+ const text = copy(props.locale);
1587
+ const limits = [
1588
+ [
1589
+ "concurrency",
1590
+ text.concurrency,
1591
+ LIMIT_BOUNDS.concurrency.min,
1592
+ LIMIT_BOUNDS.concurrency.max
1593
+ ],
1594
+ [
1595
+ "timeoutMs",
1596
+ text.timeout,
1597
+ LIMIT_BOUNDS.timeoutMs.min,
1598
+ LIMIT_BOUNDS.timeoutMs.max
1599
+ ],
1600
+ [
1601
+ "maxInputChars",
1602
+ text.maxInput,
1603
+ LIMIT_BOUNDS.maxInputChars.min,
1604
+ LIMIT_BOUNDS.maxInputChars.max
1605
+ ],
1606
+ [
1607
+ "maxOutputTokens",
1608
+ text.maxOutput,
1609
+ LIMIT_BOUNDS.maxOutputTokens.min,
1610
+ LIMIT_BOUNDS.maxOutputTokens.max
1611
+ ],
1612
+ [
1613
+ "maxAttempts",
1614
+ text.retryLimit,
1615
+ LIMIT_BOUNDS.maxAttempts.min,
1616
+ LIMIT_BOUNDS.maxAttempts.max
1617
+ ]
1618
+ ];
1619
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1620
+ className: "model-center-runtime",
1621
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1622
+ className: "model-center-limits",
1623
+ disabled: props.busy,
1624
+ "aria-label": text.limits,
1625
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: text.limits }), limits.map(([key, label, min, max]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1626
+ type: "number",
1627
+ min,
1628
+ max,
1629
+ step: 1,
1630
+ value: props.limits[key],
1631
+ "aria-label": label,
1632
+ onChange: (event) => props.onLimits({
1633
+ ...props.limits,
1634
+ [key]: Number(event.target.value)
1635
+ })
1636
+ })] }, key))]
1637
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1638
+ type: "button",
1639
+ disabled: props.busy,
1640
+ onClick: props.onSave,
1641
+ children: text.save
1642
+ })]
1643
+ });
1644
+ }
1645
+ function PolicyPanel(props) {
1646
+ const text = copy(props.locale);
1647
+ const rows = [...COMMON_PURPOSES.flatMap((id) => props.rows.filter((row) => row.id === id)), ...props.rows.filter((row) => !COMMON_PURPOSES.includes(row.id))];
1648
+ const describe = (route) => {
1649
+ const selected = choiceOf(catalogChoices(props.catalog, route), route.provider, route.model);
1650
+ const effort = route.reasoningEffort ?? selected?.defaultEffort;
1651
+ return (selected?.label ?? formatRoute(route)) + (effort ? " · " + reasoningLabel(effort, selected?.efforts.find((item) => item.id === effort)?.name ?? effort, props.locale) : "");
1652
+ };
1653
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1654
+ className: "model-center-policy",
1655
+ children: [
1656
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1657
+ className: "model-center-tier-section",
1658
+ "aria-label": text.roles,
1659
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: text.roles }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1660
+ className: "model-center-tiers",
1661
+ disabled: props.busy,
1662
+ "aria-label": text.roles,
1663
+ children: [props.catalog.groups.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1664
+ className: "model-center-tier-head",
1665
+ "aria-hidden": "true",
1666
+ children: [
1667
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {}),
1668
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text.model }),
1669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text.effort })
1670
+ ]
1671
+ }) : null, MODEL_ROLES.map((role) => {
1672
+ const route = props.policy.roles[role] ?? {
1673
+ provider: "",
1674
+ model: ""
1675
+ };
1676
+ const label = presetLabel(role, props.locale);
1677
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1678
+ className: "model-center-tier",
1679
+ "data-tier": role,
1680
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1681
+ className: "model-center-tier-name",
1682
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: label })
1683
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteFields, {
1684
+ locale: props.locale,
1685
+ route,
1686
+ disabled: props.busy,
1687
+ catalog: props.catalog,
1688
+ ariaPrefix: label,
1689
+ emptyLabel: role === "normal" ? text.chooseModel : text.followDefault,
1690
+ onChange: (next) => {
1691
+ const roles = { ...props.policy.roles };
1692
+ if (!next.provider && !next.model && !next.reasoningEffort) delete roles[role];
1693
+ else roles[role] = next;
1694
+ props.onRoles(roles);
1695
+ }
1696
+ })]
1697
+ }, role);
1698
+ })]
1699
+ })]
1700
+ }),
1701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1702
+ className: "model-center-capabilities",
1703
+ "aria-label": text.capabilities,
1704
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: text.capabilities }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1705
+ className: "model-center-meta",
1706
+ children: text.capabilityHint
1707
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("fieldset", {
1708
+ disabled: props.busy,
1709
+ "aria-label": text.capabilities,
1710
+ children: rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1711
+ className: "model-center-meta",
1712
+ children: text.noPurposes
1713
+ }) : rows.map((row) => {
1714
+ const label = purposeLabel(row.id, row.label, props.locale);
1715
+ const preview = previewResolve(props.policy, row.id, { specs: props.rows });
1716
+ const custom = row.target.kind === "model";
1717
+ const value = custom ? "custom" : purposeTargetValue(row.target);
1718
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1719
+ className: "model-center-capability",
1720
+ "data-purpose": row.id,
1721
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1722
+ className: "model-center-capability-heading",
1723
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1724
+ htmlFor: "capability-" + row.id,
1725
+ children: label
1726
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1727
+ id: "capability-" + row.id,
1728
+ "aria-label": label + " " + text.useModel,
1729
+ value,
1730
+ onChange: (event) => {
1731
+ if (event.target.value === "custom") {
1732
+ const route = preview.ok ? preview.route : props.policy.roles.normal ?? props.catalog.default ?? {
1733
+ provider: "",
1734
+ model: ""
1735
+ };
1736
+ props.onPurpose(row.id, {
1737
+ kind: "model",
1738
+ provider: route.provider,
1739
+ model: route.model,
1740
+ ...route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {}
1741
+ });
1742
+ } else props.onPurpose(row.id, purposeTargetFromValue(event.target.value));
1743
+ },
1744
+ children: [
1745
+ MODEL_ROLES.map((role) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1746
+ value: "role:" + role,
1747
+ children: presetLabel(role, props.locale)
1748
+ }, role)),
1749
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1750
+ value: "custom",
1751
+ children: text.explicit
1752
+ }),
1753
+ row.id !== "chat" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1754
+ value: "session",
1755
+ children: text.followSession
1756
+ }) : null
1757
+ ]
1758
+ })]
1759
+ }), custom ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteFields, {
1760
+ locale: props.locale,
1761
+ route: row.target,
1762
+ disabled: props.busy,
1763
+ catalog: props.catalog,
1764
+ ariaPrefix: label,
1765
+ onChange: (route) => props.onPurpose(row.id, {
1766
+ kind: "model",
1767
+ ...route
1768
+ })
1769
+ }) : row.target.kind === "role" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1770
+ className: "model-center-meta model-center-route-summary",
1771
+ children: preview.ok ? describe(preview.route) : text.notConfigured
1772
+ }) : null]
1773
+ }, row.id);
1774
+ })
1775
+ })]
1776
+ }),
1777
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1778
+ className: "model-center-savebar",
1779
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1780
+ type: "button",
1781
+ disabled: props.busy || props.dirty === false,
1782
+ onClick: props.onSave,
1783
+ children: text.save
1784
+ }), props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1785
+ className: "model-center-meta",
1786
+ role: "status",
1787
+ children: text.unsaved
1788
+ }) : null]
1789
+ })
1790
+ ]
1791
+ });
1792
+ }
1793
+ function reasoningLabel(id, name, locale) {
1794
+ if (locale === "en") return name;
1795
+ return {
1796
+ off: "关",
1797
+ none: "关",
1798
+ minimal: "极低",
1799
+ low: "低",
1800
+ medium: "中",
1801
+ high: "高",
1802
+ xhigh: "极高",
1803
+ max: "最高"
1804
+ }[id] ?? name;
1805
+ }
1806
+ function withEffort(route, reasoningEffort) {
1807
+ return reasoningEffort ? {
1808
+ ...route,
1809
+ reasoningEffort
1810
+ } : {
1811
+ provider: route.provider,
1812
+ model: route.model
1813
+ };
1814
+ }
1815
+ function RouteFields(props) {
1816
+ const text = copy(props.locale);
1817
+ const choices = catalogChoices(props.catalog, props.route);
1818
+ const selected = choiceOf(choices, props.route.provider, props.route.model);
1819
+ const efforts = effortOptions(selected, props.route.reasoningEffort);
1820
+ const key = props.route.provider && props.route.model ? routeKey(props.route.provider, props.route.model) : "";
1821
+ const hasCatalog = choices.length > 0;
1822
+ const defaultLabel = selected?.defaultEffort ? `${text.defaultEffort} (${selected.defaultEffort})` : text.defaultEffort;
1823
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1824
+ className: "model-center-route",
1825
+ "data-catalog": hasCatalog,
1826
+ children: [hasCatalog ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [text.catalog, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1827
+ value: key,
1828
+ disabled: props.disabled,
1829
+ "aria-label": `${props.ariaPrefix} ${text.catalog}`,
1830
+ onChange: (event) => {
1831
+ const parsed = parseRouteKey(event.target.value);
1832
+ if (!parsed) {
1833
+ props.onChange({
1834
+ provider: "",
1835
+ model: ""
1836
+ });
1837
+ return;
1838
+ }
1839
+ props.onChange(withEffort({
1840
+ provider: parsed.provider,
1841
+ model: parsed.model
1842
+ }, props.route.reasoningEffort ?? ""));
1843
+ },
1844
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1845
+ value: "",
1846
+ children: props.emptyLabel ?? text.chooseModel
1847
+ }), choices.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1848
+ value: routeKey(item.provider, item.model),
1849
+ children: item.label
1850
+ }, routeKey(item.provider, item.model)))]
1851
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [text.provider, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1852
+ value: props.route.provider,
1853
+ disabled: props.disabled,
1854
+ "aria-label": `${props.ariaPrefix} ${text.provider}`,
1855
+ onChange: (event) => props.onChange({
1856
+ ...props.route,
1857
+ provider: event.target.value
1858
+ })
1859
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [text.model, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1860
+ value: props.route.model,
1861
+ disabled: props.disabled,
1862
+ "aria-label": `${props.ariaPrefix} ${text.model}`,
1863
+ onChange: (event) => props.onChange({
1864
+ ...props.route,
1865
+ model: event.target.value
1866
+ })
1867
+ })] })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [text.effort, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1868
+ value: props.route.reasoningEffort ?? "",
1869
+ disabled: props.disabled || !key,
1870
+ "aria-label": `${props.ariaPrefix} ${text.effort}`,
1871
+ onChange: (event) => props.onChange(withEffort(props.route, event.target.value)),
1872
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1873
+ value: "",
1874
+ children: defaultLabel
1875
+ }), efforts.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1876
+ value: item.id,
1877
+ children: reasoningLabel(item.id, item.name, props.locale)
1878
+ }, item.id))]
1879
+ })] })]
1880
+ });
1881
+ }
1882
+ const styles = `
1883
+ .model-center{max-width:760px;display:grid;gap:20px;color:inherit;font:400 var(--font-size-2,14px)/1.5 var(--default-font-family,system-ui,sans-serif)}
1884
+ .model-center p,.model-center h3{margin:0}
1885
+ .model-center-meta,.model-center small{font-size:var(--font-size-1,13px);color:var(--gray-11,inherit)}
1886
+ .model-center-error{color:var(--red-11,#b42318)}
1887
+ .model-center-tabs{display:flex;gap:20px;border-bottom:1px solid var(--gray-6,color-mix(in srgb,currentColor 15%,transparent))}
1888
+ .model-center-tabs button[role="tab"]{padding:8px 0;border:0;border-bottom:2px solid transparent;border-radius:0;background:transparent;color:var(--gray-11,inherit);font:inherit;cursor:pointer;transition:color 150ms ease,border-color 150ms ease}
1889
+ .model-center-tabs button[role="tab"]:hover{color:inherit}
1890
+ .model-center-tabs button[aria-selected="true"]{border-bottom-color:var(--accent-9,#3b82f6);color:var(--accent-11,inherit);font-weight:600}
1891
+ .model-center [role="tabpanel"]{border-radius:12px}
1892
+ .model-center fieldset{margin:0;border:1px solid var(--gray-6,color-mix(in srgb,currentColor 15%,transparent));border-radius:10px;padding:12px;display:grid;gap:12px}
1893
+ .model-center label{display:grid;gap:6px}
1894
+ .model-center input,.model-center select{box-sizing:border-box;width:100%;min-width:0;padding:8px 10px;border:1px solid var(--gray-6,color-mix(in srgb,currentColor 22%,transparent));border-radius:8px;background:var(--color-surface,transparent);color:inherit;font:inherit;transition:border-color 150ms ease,box-shadow 150ms ease}
1895
+ .model-center input:focus,.model-center select:focus{border-color:var(--accent-9,#3b82f6);box-shadow:0 0 0 2px color-mix(in srgb,var(--accent-9,#3b82f6) 25%,transparent)}
1896
+ .model-center button{min-height:34px;padding:6px 12px;border:1px solid color-mix(in srgb,currentColor 25%,transparent);border-radius:8px;background:transparent;color:inherit;cursor:pointer;font:inherit;justify-self:start;transition:background-color 150ms ease,color 150ms ease,border-color 150ms ease,box-shadow 150ms ease,transform 150ms ease}
1897
+ .model-center button:not([role="tab"]):hover:not(:disabled){background:var(--gray-3,color-mix(in srgb,currentColor 6%,transparent));border-color:color-mix(in srgb,currentColor 35%,transparent)}
1898
+ .model-center button:not([role="tab"]):active:not(:disabled){transform:scale(.97)}
1899
+ .model-center button:disabled{opacity:.45;cursor:not-allowed}
1900
+ .model-center :focus-visible{outline:2px solid var(--accent-9,currentColor);outline-offset:3px}
1901
+ .model-center-list,.model-center-models{margin:0;padding:0;list-style:none;display:grid;gap:12px}
1902
+ .model-center-list>li{display:grid;gap:8px;padding:12px 4px;border-top:1px solid var(--gray-6,color-mix(in srgb,currentColor 15%,transparent));border-radius:10px}
1903
+ .model-center-provider-head,.model-center-actions,.model-center-route{display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center}
1904
+ .model-center-route label{flex:1 1 160px}
1905
+ .model-center-policy,.model-center-runtime{display:grid;gap:24px}
1906
+ .model-center-limits{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 20px}
1907
+ .model-center-tier-section,.model-center-capabilities{display:grid;gap:14px}
1908
+ .model-center header{display:grid;gap:4px}
1909
+ .model-center h3{font-size:16px;font-weight:600}
1910
+ .model-center .model-center-tiers,.model-center-capabilities>fieldset{padding:0;border:0;border-radius:0;gap:0;min-width:0}
1911
+ .model-center-tier{display:grid;grid-template-columns:68px minmax(0,1fr);gap:4px 14px;padding:8px 0;border-bottom:1px solid var(--gray-6)}
1912
+ .model-center-tier-name{padding-top:9px}
1913
+ .model-center-tier .model-center-route{display:grid;grid-template-columns:minmax(0,1fr) 132px;align-items:start}
1914
+ .model-center-tier .model-center-route label{font-size:12px;color:var(--gray-11)}
1915
+ .model-center-tier-head{display:grid;grid-template-columns:68px minmax(0,1fr) 132px;gap:14px;font-size:12px;color:var(--gray-11)}
1916
+ .model-center-tier .model-center-route[data-catalog="true"]>label{font-size:0;gap:0}
1917
+ .model-center-tier .model-center-route select,.model-center-tier .model-center-route input{font-size:14px;color:var(--gray-12)}
1918
+ .model-center-capability{display:grid;gap:10px;padding:14px 0;border-bottom:1px solid var(--gray-6)}
1919
+ .model-center-capability-heading{display:grid;grid-template-columns:minmax(0,1fr) 180px;gap:16px;align-items:center}
1920
+ .model-center-capability-heading>label{font-weight:500}
1921
+ .model-center-route-summary{overflow-wrap:anywhere}
1922
+ .model-center-savebar{position:sticky;bottom:0;display:flex;align-items:center;gap:14px;padding:12px 0;background:var(--color-panel-solid,var(--gray-1));border-top:1px solid var(--gray-6);z-index:1}
1923
+ .model-center-savebar button{background:var(--accent-9);color:var(--accent-contrast,white);border-color:transparent;min-width:80px}
1924
+ .model-center .model-center-savebar button:hover:not(:disabled){background:var(--accent-10,var(--accent-9,#3b82f6))}
1925
+ @media(max-width:560px){.model-center-tier-head{display:none}.model-center-tier .model-center-route[data-catalog="true"]>label{font-size:12px;gap:6px}.model-center-tier{grid-template-columns:1fr}.model-center-tier-name{padding-top:0}.model-center-capability-heading{grid-template-columns:minmax(0,1fr) 150px}}
1926
+ @media(max-width:560px){.model-center-limits{grid-template-columns:minmax(0,1fr)}}
1927
+ @media(prefers-reduced-motion:reduce){.model-center button,.model-center input,.model-center select,.model-center-tabs button[role="tab"]{transition:none}}
1928
+ `;
1929
+ //#endregion
1930
+ exports.ModelCenterSettings = ModelCenterSettings;
1931
+ exports.PolicyPanel = PolicyPanel;
1932
+ exports.activateClientUi = activateClientUi;
1933
+ exports.apply = apply;
1934
+ exports.inject = inject;
1935
+ exports.name = name;
1936
+ exports.presetLabel = presetLabel;
1937
+ exports.purposeTargetFromValue = purposeTargetFromValue;
1938
+ exports.purposeTargetValue = purposeTargetValue;
1939
+ exports.registerModelCenterSlots = registerModelCenterSlots;
1940
+ exports.shouldAttach = shouldAttach;
1941
+ exports.styles = styles;
1942
+
1943
+ //# sourceMappingURL=client.inner.cjs.map
1944
+ return module.exports;
1945
+ }
1946
+ });