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