@fonlan/dsh-model-router 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fonlan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # dsh-model-router
2
+
3
+ **dsh-model-router** 是一个 [DSH](https://github.com/deepseek-ai/dsh) 插件,向 harness 注册一个虚拟模型提供商 `model-router`:它汇聚用户已添加的**所有**提供商及其模型(按 model id 严格归并),并在每次 LLM 请求发出时把请求实时路由到该模型**当前激活**的提供商。配套的独立设置页支持按模型对提供商拖拽排序(供后续自动切换使用)与点击切换当前提供商;切换后全局立即生效——下一次发出的该模型请求即走新提供商。
4
+
5
+ ## 功能
6
+
7
+ - **虚拟提供商 `model-router`**:出现在输入框下方的模型选择器(含默认模型设置等所有选择入口)中,列出全部已配置提供商的所有模型,模型条目副文本标注当前激活的提供商(如 `via OpenCode Go`)。
8
+ - **严格按 model id 归并**:同一模型出现在多个提供商时归并为一条;展示名、上下文窗口、推理档位等参数跟随当前激活的提供商。
9
+ - **请求时实时路由**:会话中始终保存 `provider=model-router`,每次请求发出时进程内解析当前激活提供商并委派给真实 adapter(凭据解析、重试、流式、计量全部走原管线),主 agent、子代理、工具 LLM 调用一视同仁。
10
+ - **独立设置页**(设置 → 模型路由):每个模型下列出其全部提供商——点击切换当前使用;拖拽调整优先级顺序(供后续自动切换策略使用);未配置 API 密钥的提供商标灰不可选。
11
+ - **自动配置维护**:新增/删除提供商或模型后配置自动同步(消失的提供商从排序中清理、激活自动回落到剩余第一位、新提供商追加到排序末尾);未配置过的模型自动选择排序第一位。
12
+ - **失败即报错**:当前提供商请求失败直接返回错误(自动切换为后续规划功能)。
13
+ - **双语**:设置页文案中英双语,跟随界面语言。
14
+ - **通用运行时**:核心路由逻辑与 UI 解耦——web profile 提供设置页与选择器;CLI / headless profile 装上同样按配置路由(可直接手改 `settings.yaml` 的 `model-router:` 段)。
15
+
16
+ ## 配置
17
+
18
+ 路由配置持久化在 settings 文档的 `model-router` 命名空间(`~/.dsh/settings.yaml`):
19
+
20
+ ```yaml
21
+ model-router:
22
+ models:
23
+ deepseek-v4-flash:
24
+ order: # 优先级顺序(供后续自动切换使用)
25
+ - opencode-go
26
+ - deepseek-official
27
+ active: opencode-go # 当前实际使用的提供商
28
+ ```
29
+
30
+ `order` 与 `active` 分开维护:点击切换只改 `active`,拖拽排序只改 `order`。
31
+
32
+ ## 安装
33
+
34
+ ### 方式一:从 npm 发布包安装
35
+
36
+ ```sh
37
+ dsh plugin --profile web add @fonlan/dsh-model-router
38
+ ```
39
+
40
+ ### 方式二:本地源码链接安装
41
+
42
+ 在仓库目录下构建出 `lib/` 产物后以本地目录方式添加:
43
+
44
+ ```sh
45
+ pnpm build
46
+ dsh plugin --profile web add .
47
+ ```
48
+
49
+ 安装后插件通过 `cordis.patch.yml` 自动挂载到目标 profile:服务端注册 `model-router` 提供商与路由配置,web 端注册 `settings.section` 设置页。重启 profile 后生效(web profile 重启后,输入框下方的模型选择器即出现 Model Router 分组)。
50
+
51
+ ## 开发
52
+
53
+ ```sh
54
+ pnpm install # 安装依赖
55
+ pnpm build # 构建 lib/(tsc 声明 + tsdown 双端产物)
56
+ pnpm test # vitest 单测(归并/路由解析/委派等纯逻辑)
57
+ pnpm typecheck # server + client 双 tsconfig 类型检查
58
+ ```
59
+
60
+ - 修改服务端后需重启 profile。
61
+ - 客户端改动走 web 端 HMR 链路,`pnpm run dev:web` 同启时生效。
62
+
63
+ ## License
64
+
65
+ MIT — 详见 [LICENSE](./LICENSE)。
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: model-router
3
+ name: '@fonlan/dsh-model-router'
package/lib/client.js ADDED
@@ -0,0 +1,368 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@fonlan/dsh-model-router",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/locales.ts
10
+ const LOCALE_NS = "model-router";
11
+ const zh = {
12
+ settingsTitle: "模型路由",
13
+ settingsDescription: "每个模型按 model id 聚合所有提供商;点击提供商切换该模型当前使用的路由,拖拽调整优先级顺序(供后续自动切换使用)。",
14
+ loading: "加载中…",
15
+ loadFailed: "加载失败:{message}",
16
+ retry: "重试",
17
+ refresh: "刷新",
18
+ empty: "没有可路由的提供商或模型,请先在设置中添加提供商。",
19
+ modelsCount: "共 {count} 个模型",
20
+ activeBadge: "当前",
21
+ noCredential: "未配置 API 密钥,不可选",
22
+ dragHint: "拖拽调整优先级顺序",
23
+ switchFailed: "切换失败:{message}",
24
+ reorderFailed: "排序失败:{message}",
25
+ providerCount: "{count} 个提供商"
26
+ };
27
+ const en = {
28
+ settingsTitle: "Model Router",
29
+ settingsDescription: "Each model aggregates its providers by exact model id. Click a provider to switch where this model currently routes; drag to adjust the preference order (used by automatic failover later).",
30
+ loading: "Loading…",
31
+ loadFailed: "Load failed: {message}",
32
+ retry: "Retry",
33
+ refresh: "Refresh",
34
+ empty: "No routable providers or models yet — add providers in settings first.",
35
+ modelsCount: "{count} models",
36
+ activeBadge: "active",
37
+ noCredential: "No API key configured — not selectable",
38
+ dragHint: "Drag to adjust preference order",
39
+ switchFailed: "Switch failed: {message}",
40
+ reorderFailed: "Reorder failed: {message}",
41
+ providerCount: "{count} providers"
42
+ };
43
+ //#endregion
44
+ //#region src/client/api.ts
45
+ var ModelRouterApiError = class extends Error {
46
+ code;
47
+ constructor(code, message) {
48
+ super(message);
49
+ this.code = code;
50
+ }
51
+ };
52
+ async function call(method, payload = {}) {
53
+ let response;
54
+ try {
55
+ response = await fetch(`/plugins/@fonlan/dsh-model-router/api/${method}`, {
56
+ method: "POST",
57
+ headers: { "content-type": "application/json" },
58
+ body: JSON.stringify(payload)
59
+ });
60
+ } catch (error) {
61
+ throw new ModelRouterApiError("network", error instanceof Error ? error.message : String(error));
62
+ }
63
+ const parsed = await response.json().catch(() => null);
64
+ if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === void 0) throw new ModelRouterApiError(parsed?.error?.code ?? "http", parsed?.error?.message ?? `HTTP ${response.status}`);
65
+ return parsed.value;
66
+ }
67
+ const api = {
68
+ state: () => call("state"),
69
+ switchActive: (modelId, providerId) => call("switch-active", {
70
+ modelId,
71
+ providerId
72
+ }),
73
+ reorder: (modelId, order) => call("reorder", {
74
+ modelId,
75
+ order
76
+ }),
77
+ refresh: () => call("refresh")
78
+ };
79
+ //#endregion
80
+ //#region \0dsh-css:/Users/fonlan/Repos/dsh-model-router/src/client/settings-section.css.mjs
81
+ const css = "/* Model Router settings page — theme-consistent via DSW alias variables. */\n.mr-root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 4px 0 24px;\n}\n\n.mr-description {\n margin: 0;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.mr-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-button {\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: var(--dsw-alias-interactive-bg-hover);\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 8px;\n font-size: 12px;\n line-height: 18px;\n padding: 3px 10px;\n}\n\n.mr-button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n filter: brightness(1.08);\n}\n\n.mr-button:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n.mr-error {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n color: var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n padding: 7px 8px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 16px 0;\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-models {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.mr-card {\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 12px;\n padding: 10px 12px;\n background: var(--dsw-alias-bg-module-platform);\n transition: opacity 0.12s;\n}\n\n.mr-card-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-card-header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.mr-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n\n.mr-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n line-height: 18px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n flex: 1;\n}\n\n.mr-provider-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-drag-hint {\n margin: 2px 0 6px;\n color: var(--dsw-alias-label-caption);\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-providers {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-provider {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid transparent;\n border-radius: 10px;\n padding: 2px 8px 2px 4px;\n}\n\n.mr-provider-active {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l3);\n}\n\n.mr-provider-nocred {\n opacity: 0.55;\n}\n\n.mr-provider-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-grip {\n color: var(--dsw-alias-label-caption);\n cursor: grab;\n font-size: 12px;\n line-height: 20px;\n flex: none;\n user-select: none;\n}\n\n.mr-grip:active {\n cursor: grabbing;\n}\n\n.mr-provider-button {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 6px;\n color: var(--dsw-alias-label-primary);\n background: none;\n border: none;\n text-align: left;\n cursor: pointer;\n font: inherit;\n padding: 4px 0;\n}\n\n.mr-provider-button:disabled {\n cursor: default;\n}\n\n.mr-provider-name {\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.mr-provider-nocred-label {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-active-badge {\n flex: none;\n color: var(--dsw-alias-label-inverted, #fff);\n background: var(--dsw-alias-border-brand, #4d6bfe);\n border-radius: 999px;\n font-size: 11px;\n line-height: 16px;\n padding: 0 8px;\n}\n";
82
+ const tagId = "@fonlan/dsh-model-router/settings-section.css";
83
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
84
+ const tag = document.createElement("style");
85
+ tag.dataset.plugin = "@fonlan/dsh-model-router";
86
+ tag.dataset.pluginCss = tagId;
87
+ tag.textContent = css;
88
+ document.head.appendChild(tag);
89
+ }
90
+ //#endregion
91
+ //#region src/client/settings-section.tsx
92
+ /**
93
+ * The Model Router settings page: one card per merged model, provider chips
94
+ * that switch the active route on click and reorder by native HTML5 drag &
95
+ * drop (preference order for future automatic failover). All reads/mutations
96
+ * go through the plugin's fenced API; the server persists through the
97
+ * settings service, so a switch here is live for the next request globally.
98
+ */
99
+ function arrayMove(list, from, to) {
100
+ const next = [...list];
101
+ const [item] = next.splice(from, 1);
102
+ next.splice(to, 0, item);
103
+ return next;
104
+ }
105
+ function useLocaleRevision(ctx) {
106
+ const subscribe = (0, react.useCallback)((onChange) => {
107
+ try {
108
+ return ctx.locale.subscribe(onChange);
109
+ } catch {
110
+ return () => {};
111
+ }
112
+ }, [ctx]);
113
+ const getSnapshot = (0, react.useCallback)(() => {
114
+ try {
115
+ return ctx.locale.getLocale().revision;
116
+ } catch {
117
+ return 0;
118
+ }
119
+ }, [ctx]);
120
+ return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
121
+ }
122
+ function makeSettingsSection(ctx) {
123
+ const t = (() => {
124
+ try {
125
+ return ctx.locale.bind(LOCALE_NS);
126
+ } catch {
127
+ return (key) => key;
128
+ }
129
+ })();
130
+ return function ModelRouterSettingsSection() {
131
+ useLocaleRevision(ctx);
132
+ const [state, setState] = (0, react.useState)(null);
133
+ const [error, setError] = (0, react.useState)(null);
134
+ const [loading, setLoading] = (0, react.useState)(true);
135
+ const [busyModel, setBusyModel] = (0, react.useState)(null);
136
+ const [drag, setDrag] = (0, react.useState)(null);
137
+ const load = (0, react.useCallback)(async () => {
138
+ setLoading(true);
139
+ try {
140
+ setState(await api.state());
141
+ setError(null);
142
+ } catch (cause) {
143
+ setError(cause instanceof Error ? cause.message : String(cause));
144
+ } finally {
145
+ setLoading(false);
146
+ }
147
+ }, []);
148
+ (0, react.useEffect)(() => {
149
+ load();
150
+ }, [load]);
151
+ const switchActive = (0, react.useCallback)(async (model, providerId) => {
152
+ if (model.active === providerId) return;
153
+ setBusyModel(model.id);
154
+ try {
155
+ setState(await api.switchActive(model.id, providerId));
156
+ setError(null);
157
+ } catch (cause) {
158
+ setError(t("switchFailed", { message: cause instanceof Error ? cause.message : String(cause) }));
159
+ } finally {
160
+ setBusyModel(null);
161
+ }
162
+ }, [t]);
163
+ const reorder = (0, react.useCallback)(async (model, from, to) => {
164
+ if (from === to) return;
165
+ const order = arrayMove(model.order, from, to);
166
+ setState((prev) => prev === null ? prev : {
167
+ ...prev,
168
+ models: prev.models.map((m) => m.id === model.id ? {
169
+ ...m,
170
+ order
171
+ } : m)
172
+ });
173
+ setBusyModel(model.id);
174
+ try {
175
+ setState(await api.reorder(model.id, order));
176
+ setError(null);
177
+ } catch (cause) {
178
+ setError(t("reorderFailed", { message: cause instanceof Error ? cause.message : String(cause) }));
179
+ setState((prev) => prev === null ? prev : {
180
+ ...prev,
181
+ models: prev.models.map((m) => m.id === model.id ? {
182
+ ...m,
183
+ order: model.order
184
+ } : m)
185
+ });
186
+ } finally {
187
+ setBusyModel(null);
188
+ }
189
+ }, [t]);
190
+ const models = (0, react.useMemo)(() => state?.models ?? [], [state]);
191
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192
+ className: "mr-root",
193
+ children: [
194
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
195
+ className: "mr-description",
196
+ children: t("settingsDescription")
197
+ }),
198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
199
+ className: "mr-toolbar",
200
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
201
+ className: "mr-count",
202
+ children: t("modelsCount", { count: models.length })
203
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
204
+ type: "button",
205
+ className: "mr-button",
206
+ onClick: () => void load(),
207
+ disabled: loading,
208
+ children: t("refresh")
209
+ })]
210
+ }),
211
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
212
+ className: "mr-error",
213
+ role: "alert",
214
+ children: error
215
+ }),
216
+ loading && models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
217
+ className: "mr-empty",
218
+ children: t("loading")
219
+ }),
220
+ !loading && models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
221
+ className: "mr-empty",
222
+ children: t("empty")
223
+ }),
224
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
225
+ className: "mr-models",
226
+ children: models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCard, {
227
+ model,
228
+ t,
229
+ busy: busyModel === model.id,
230
+ dragging: drag,
231
+ onDragStart: (index) => setDrag({
232
+ modelId: model.id,
233
+ index
234
+ }),
235
+ onDragEnd: () => setDrag(null),
236
+ onDrop: (index) => {
237
+ if (drag !== null && drag.modelId === model.id) reorder(model, drag.index, index);
238
+ setDrag(null);
239
+ },
240
+ onSwitch: (providerId) => void switchActive(model, providerId)
241
+ }, model.id))
242
+ })
243
+ ]
244
+ });
245
+ };
246
+ }
247
+ function ModelCard(props) {
248
+ const { model, t, busy, dragging } = props;
249
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
250
+ className: `mr-card${busy ? " mr-card-busy" : ""}`,
251
+ children: [
252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
253
+ className: "mr-card-header",
254
+ children: [
255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
256
+ className: "mr-model-name",
257
+ children: model.name
258
+ }),
259
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
260
+ className: "mr-model-id",
261
+ children: model.id
262
+ }),
263
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
264
+ className: "mr-provider-count",
265
+ children: t("providerCount", { count: model.providers.length })
266
+ })
267
+ ]
268
+ }),
269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
270
+ className: "mr-drag-hint",
271
+ children: t("dragHint")
272
+ }),
273
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
274
+ className: "mr-providers",
275
+ children: model.providers.map((provider, index) => {
276
+ const active = provider.id === model.active;
277
+ const selectable = provider.credentialConfigured;
278
+ const dropTarget = dragging !== null && dragging.modelId === model.id && dragging.index !== index;
279
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
280
+ className: [
281
+ "mr-provider",
282
+ active ? "mr-provider-active" : "",
283
+ !selectable ? "mr-provider-nocred" : "",
284
+ dropTarget ? "mr-provider-drop" : ""
285
+ ].filter(Boolean).join(" "),
286
+ draggable: !busy && model.providers.length > 1,
287
+ title: selectable ? void 0 : t("noCredential"),
288
+ onDragStart: (event) => {
289
+ props.onDragStart(index);
290
+ event.dataTransfer.effectAllowed = "move";
291
+ try {
292
+ event.dataTransfer.setData("text/plain", model.id);
293
+ } catch {}
294
+ },
295
+ onDragOver: (event) => {
296
+ if (dragging !== null && dragging.modelId === model.id && dragging.index !== index) {
297
+ event.preventDefault();
298
+ event.dataTransfer.dropEffect = "move";
299
+ }
300
+ },
301
+ onDrop: (event) => {
302
+ if (dragging !== null && dragging.modelId === model.id) {
303
+ event.preventDefault();
304
+ props.onDrop(index);
305
+ }
306
+ },
307
+ onDragEnd: () => props.onDragEnd(),
308
+ children: [
309
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
310
+ className: "mr-grip",
311
+ "aria-hidden": "true",
312
+ children: "⠿"
313
+ }),
314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
315
+ type: "button",
316
+ className: "mr-provider-button",
317
+ disabled: !selectable || busy || active,
318
+ onClick: () => props.onSwitch(provider.id),
319
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
320
+ className: "mr-provider-name",
321
+ children: provider.name
322
+ }), !selectable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
323
+ className: "mr-provider-nocred-label",
324
+ children: ["· ", t("noCredential")]
325
+ })]
326
+ }),
327
+ active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
328
+ className: "mr-active-badge",
329
+ children: t("activeBadge")
330
+ })
331
+ ]
332
+ }, provider.id);
333
+ })
334
+ })
335
+ ]
336
+ });
337
+ }
338
+ //#endregion
339
+ //#region src/client/index.tsx
340
+ /** Services required before mounting (provided by the client runtime). */
341
+ const inject = ["slots", "locale"];
342
+ /** Client plugin body. */
343
+ function apply(ctx) {
344
+ const t = ctx.locale.bind(LOCALE_NS);
345
+ ctx.effect(() => {
346
+ const off = ctx.locale.register(LOCALE_NS, {
347
+ zh,
348
+ en
349
+ });
350
+ return () => off();
351
+ }, "model-router: dictionaries");
352
+ const SettingsSection = makeSettingsSection(ctx);
353
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
354
+ name: "settings.section",
355
+ id: "model-router",
356
+ order: 300,
357
+ label: () => t("settingsTitle"),
358
+ locale: LOCALE_NS
359
+ }, SettingsSection));
360
+ }
361
+ //#endregion
362
+ exports.apply = apply;
363
+ exports.inject = inject;
364
+ return module.exports;
365
+ }
366
+ });
367
+
368
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["useCallback","useSyncExternalStore","useState","useMemo"],"sources":["../../../src/client/locales.ts","../../../src/client/api.ts","../../../src/client/settings-section.tsx","../../../src/client/index.tsx"],"sourcesContent":["// Locale namespace = the plugin's cordis id (kebab). DSH flat namespace keys\n// (settings + locale) use the internal id `model-router`; the scoped npm\n// package name is reserved for package-bound identifiers (exported `name`,\n// module-loader id, the /plugins/<pkg>/… route base).\nexport const LOCALE_NS = 'model-router'\n\nexport const zh = {\n settingsTitle: '模型路由',\n settingsDescription: '每个模型按 model id 聚合所有提供商;点击提供商切换该模型当前使用的路由,拖拽调整优先级顺序(供后续自动切换使用)。',\n loading: '加载中…',\n loadFailed: '加载失败:{message}',\n retry: '重试',\n refresh: '刷新',\n empty: '没有可路由的提供商或模型,请先在设置中添加提供商。',\n modelsCount: '共 {count} 个模型',\n activeBadge: '当前',\n noCredential: '未配置 API 密钥,不可选',\n dragHint: '拖拽调整优先级顺序',\n switchFailed: '切换失败:{message}',\n reorderFailed: '排序失败:{message}',\n providerCount: '{count} 个提供商',\n}\n\nexport const en = {\n settingsTitle: 'Model Router',\n settingsDescription: 'Each model aggregates its providers by exact model id. Click a provider to switch where this model currently routes; drag to adjust the preference order (used by automatic failover later).',\n loading: 'Loading…',\n loadFailed: 'Load failed: {message}',\n retry: 'Retry',\n refresh: 'Refresh',\n empty: 'No routable providers or models yet — add providers in settings first.',\n modelsCount: '{count} models',\n activeBadge: 'active',\n noCredential: 'No API key configured — not selectable',\n dragHint: 'Drag to adjust preference order',\n switchFailed: 'Switch failed: {message}',\n reorderFailed: 'Reorder failed: {message}',\n providerCount: '{count} providers',\n}\n","/** Client transport for the model-router settings API. */\nexport interface RouterProviderView {\n id: string\n name: string\n credentialConfigured: boolean\n}\n\nexport interface RouterModelView {\n id: string\n name: string\n active: string | null\n order: string[]\n providers: RouterProviderView[]\n}\n\nexport interface ModelRouterState {\n providers: RouterProviderView[]\n models: RouterModelView[]\n}\n\nexport class ModelRouterApiError extends Error {\n constructor(readonly code: string, message: string) {\n super(message)\n }\n}\n\nasync function call<T>(method: string, payload: Record<string, unknown> = {}): Promise<T> {\n let response: Response\n try {\n response = await fetch(`/plugins/@fonlan/dsh-model-router/api/${method}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch (error) {\n throw new ModelRouterApiError('network', error instanceof Error ? error.message : String(error))\n }\n const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null\n = await response.json().catch(() => null)\n if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {\n throw new ModelRouterApiError(\n parsed?.error?.code ?? 'http',\n parsed?.error?.message ?? `HTTP ${response.status}`,\n )\n }\n return parsed.value as T\n}\n\nexport const api = {\n state: () => call<ModelRouterState>('state'),\n switchActive: (modelId: string, providerId: string) =>\n call<ModelRouterState>('switch-active', { modelId, providerId }),\n reorder: (modelId: string, order: string[]) =>\n call<ModelRouterState>('reorder', { modelId, order }),\n refresh: () => call<ModelRouterState>('refresh'),\n}\n","/**\n * The Model Router settings page: one card per merged model, provider chips\n * that switch the active route on click and reorder by native HTML5 drag &\n * drop (preference order for future automatic failover). All reads/mutations\n * go through the plugin's fenced API; the server persists through the\n * settings service, so a switch here is live for the next request globally.\n */\nimport { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { Translate } from '@deepseek-ai/dsh-client-ui-slots'\nimport { LOCALE_NS } from './locales'\nimport { api, type ModelRouterState, type RouterModelView } from './api'\nimport './settings-section.css'\n\nfunction arrayMove<T>(list: readonly T[], from: number, to: number): T[] {\n const next = [...list]\n const [item] = next.splice(from, 1)\n next.splice(to, 0, item)\n return next\n}\n\nfunction useLocaleRevision(ctx: ClientContext): number {\n const subscribe = useCallback(\n (onChange: () => void) => {\n try {\n return ctx.locale.subscribe(onChange)\n } catch {\n return () => {}\n }\n },\n [ctx],\n )\n const getSnapshot = useCallback(() => {\n try {\n return ctx.locale.getLocale().revision\n } catch {\n return 0\n }\n }, [ctx])\n return useSyncExternalStore(subscribe, getSnapshot)\n}\n\nexport function makeSettingsSection(ctx: ClientContext): () => JSX.Element {\n // Bound translation is namespace-typed; the section's props use the plain\n // Translate face (string keys), which the dict satisfies structurally.\n const t: Translate = (() => {\n try {\n return ctx.locale.bind(LOCALE_NS) as unknown as Translate\n } catch {\n return (key: string) => key\n }\n })()\n\n return function ModelRouterSettingsSection(): JSX.Element {\n useLocaleRevision(ctx)\n const [state, setState] = useState<ModelRouterState | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(true)\n const [busyModel, setBusyModel] = useState<string | null>(null)\n const [drag, setDrag] = useState<{ modelId: string; index: number } | null>(null)\n\n const load = useCallback(async () => {\n setLoading(true)\n try {\n setState(await api.state())\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setLoading(false)\n }\n }, [])\n\n useEffect(() => {\n void load()\n }, [load])\n\n const switchActive = useCallback(async (model: RouterModelView, providerId: string) => {\n if (model.active === providerId) return\n setBusyModel(model.id)\n try {\n setState(await api.switchActive(model.id, providerId))\n setError(null)\n } catch (cause) {\n setError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n } finally {\n setBusyModel(null)\n }\n }, [t])\n\n const reorder = useCallback(async (model: RouterModelView, from: number, to: number) => {\n if (from === to) return\n const order = arrayMove(model.order, from, to)\n setState(prev => prev === null ? prev : {\n ...prev,\n models: prev.models.map(m => (m.id === model.id ? { ...m, order } : m)),\n })\n setBusyModel(model.id)\n try {\n setState(await api.reorder(model.id, order))\n setError(null)\n } catch (cause) {\n setError(t('reorderFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n setState(prev => prev === null ? prev : {\n ...prev,\n models: prev.models.map(m => (m.id === model.id ? { ...m, order: model.order } : m)),\n })\n } finally {\n setBusyModel(null)\n }\n }, [t])\n\n const models = useMemo(() => state?.models ?? [], [state])\n\n return (\n <div className=\"mr-root\">\n <p className=\"mr-description\">{t('settingsDescription')}</p>\n <div className=\"mr-toolbar\">\n <span className=\"mr-count\">{t('modelsCount', { count: models.length })}</span>\n <button type=\"button\" className=\"mr-button\" onClick={() => void load()} disabled={loading}>\n {t('refresh')}\n </button>\n </div>\n {error !== null && (\n <div className=\"mr-error\" role=\"alert\">{error}</div>\n )}\n {loading && models.length === 0 && (\n <div className=\"mr-empty\">{t('loading')}</div>\n )}\n {!loading && models.length === 0 && (\n <div className=\"mr-empty\">{t('empty')}</div>\n )}\n <div className=\"mr-models\">\n {models.map(model => (\n <ModelCard\n key={model.id}\n model={model}\n t={t}\n busy={busyModel === model.id}\n dragging={drag}\n onDragStart={index => setDrag({ modelId: model.id, index })}\n onDragEnd={() => setDrag(null)}\n onDrop={index => {\n if (drag !== null && drag.modelId === model.id) {\n void reorder(model, drag.index, index)\n }\n setDrag(null)\n }}\n onSwitch={providerId => void switchActive(model, providerId)}\n />\n ))}\n </div>\n </div>\n )\n }\n}\n\ninterface ModelCardProps {\n model: RouterModelView\n t: Translate\n busy: boolean\n dragging: { modelId: string; index: number } | null\n onDragStart: (index: number) => void\n onDragEnd: () => void\n onDrop: (index: number) => void\n onSwitch: (providerId: string) => void\n}\n\nfunction ModelCard(props: ModelCardProps): JSX.Element {\n const { model, t, busy, dragging } = props\n return (\n <section className={`mr-card${busy ? ' mr-card-busy' : ''}`}>\n <header className=\"mr-card-header\">\n <span className=\"mr-model-name\">{model.name}</span>\n <span className=\"mr-model-id\">{model.id}</span>\n <span className=\"mr-provider-count\">{t('providerCount', { count: model.providers.length })}</span>\n </header>\n <p className=\"mr-drag-hint\">{t('dragHint')}</p>\n <ul className=\"mr-providers\">\n {model.providers.map((provider, index) => {\n const active = provider.id === model.active\n const selectable = provider.credentialConfigured\n const dropTarget = dragging !== null && dragging.modelId === model.id && dragging.index !== index\n return (\n <li\n key={provider.id}\n className={[\n 'mr-provider',\n active ? 'mr-provider-active' : '',\n !selectable ? 'mr-provider-nocred' : '',\n dropTarget ? 'mr-provider-drop' : '',\n ].filter(Boolean).join(' ')}\n draggable={!busy && model.providers.length > 1}\n title={selectable ? undefined : t('noCredential')}\n onDragStart={(event) => {\n props.onDragStart(index)\n event.dataTransfer.effectAllowed = 'move'\n try {\n event.dataTransfer.setData('text/plain', model.id)\n } catch {\n // drag data is cosmetic for our own handler\n }\n }}\n onDragOver={(event) => {\n if (dragging !== null && dragging.modelId === model.id && dragging.index !== index) {\n event.preventDefault()\n event.dataTransfer.dropEffect = 'move'\n }\n }}\n onDrop={(event) => {\n if (dragging !== null && dragging.modelId === model.id) {\n event.preventDefault()\n props.onDrop(index)\n }\n }}\n onDragEnd={() => props.onDragEnd()}\n >\n <span className=\"mr-grip\" aria-hidden=\"true\">⠿</span>\n <button\n type=\"button\"\n className=\"mr-provider-button\"\n disabled={!selectable || busy || active}\n onClick={() => props.onSwitch(provider.id)}\n >\n <span className=\"mr-provider-name\">{provider.name}</span>\n {!selectable && <span className=\"mr-provider-nocred-label\">· {t('noCredential')}</span>}\n </button>\n {active && <span className=\"mr-active-badge\">{t('activeBadge')}</span>}\n </li>\n )\n })}\n </ul>\n </section>\n )\n}\n","/**\n * @fonlan/dsh-model-router client half: the Model Router settings page plus\n * bilingual dictionaries. The composer model picker needs no client\n * injection — the host catalog already renders the `model-router` group and\n * each model's description carries the active provider.\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\nimport { LOCALE_NS, zh, en } from './locales'\nimport { makeSettingsSection } from './settings-section'\n\n/** Services required before mounting (provided by the client runtime). */\nexport const inject = ['slots', 'locale']\n\n/** Client plugin body. */\nexport function apply(ctx: ClientContext): void {\n const t = ctx.locale.bind(LOCALE_NS)\n ctx.effect(() => {\n const off = ctx.locale.register(LOCALE_NS, { zh, en })\n return () => off()\n }, 'model-router: dictionaries')\n\n const SettingsSection = makeSettingsSection(ctx)\n ctx.slots.inject('settings.section', () =>\n ctx.slots.register({\n name: 'settings.section',\n id: 'model-router',\n order: 300,\n label: () => t('settingsTitle'),\n locale: LOCALE_NS,\n }, SettingsSection as never),\n )\n}\n"],"mappings":"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"}