@fonlan/dsh-model-router 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  - **虚拟提供商 `model-router`**:出现在输入框下方的模型选择器(含默认模型设置等所有选择入口)中,列出全部已配置提供商的所有模型,模型条目副文本标注当前激活的提供商(如 `via OpenCode Go`)。
8
8
  - **快速路由切换**:当当前会话选择的是 `model-router` 模型时,输入栏模型选择器右侧显示当前 provider 按钮;点击打开 provider 面板,可直接切换该模型的激活 provider。切换对所有会话的后续请求生效,完整排序和全量管理仍在设置 → 模型路由中完成。可在设置 → 模型路由中通过「在输入栏显示快速路由切换按钮」开关控制该按钮是否显示。
9
9
  - **严格按 model id 归并**:同一模型出现在多个提供商时归并为一条;展示名、上下文窗口、推理档位等参数跟随当前激活的提供商。
10
+ - **忽略模型 ID 前缀**(默认开启):`deepseek/deepseek-v4-flash` 与 `deepseek-v4-flash` 视为同一模型,统一显示不带前缀的模型 id;请求时传入带前缀的 id 也能命中同一路由。每个提供商条目保留自己的原始 id,委派时按原始 id 发给真实提供商。可在设置 → 模型路由中通过「匹配时忽略模型 ID 前缀」开关控制。
10
11
  - **请求时实时路由**:会话中始终保存 `provider=model-router`,每次请求发出时进程内解析当前激活提供商并委派给真实 adapter(凭据解析、重试、流式、计量全部走原管线),主 agent、子代理、工具 LLM 调用一视同仁。
11
12
  - **独立设置页**(设置 → 模型路由):每个模型下列出其全部提供商——点击切换当前使用;拖拽调整优先级顺序(供后续自动切换策略使用);未配置 API 密钥的提供商标灰不可选。
12
13
  - **自动配置维护**:新增/删除提供商或模型后配置自动同步(消失的提供商从排序中清理、激活自动回落到剩余第一位、新提供商追加到排序末尾);未配置过的模型自动选择排序第一位。
@@ -21,6 +22,7 @@
21
22
  ```yaml
22
23
  model-router:
23
24
  showQuickSwitch: true # 是否在输入栏显示快速路由切换按钮(默认 true)
25
+ ignoreModelIdPrefix: true # 匹配时忽略模型 ID 前缀(默认 true)
24
26
  models:
25
27
  deepseek-v4-flash:
26
28
  order: # 优先级顺序(供后续自动切换使用)
package/lib/client.js CHANGED
@@ -33,7 +33,9 @@ window.__ModuleLoader__.load({
33
33
  noCredentialShort: "未配置凭据",
34
34
  globalRouteHint: "切换后对所有会话的后续请求生效",
35
35
  showQuickSwitchLabel: "在输入栏显示快速路由切换按钮",
36
- showQuickSwitchDescription: "开启后,当当前模型是 model-router 路由时,在输入框模型选择器右侧显示 provider 快速切换按钮。"
36
+ showQuickSwitchDescription: "开启后,当当前模型是 model-router 路由时,在输入框模型选择器右侧显示 provider 快速切换按钮。",
37
+ ignorePrefixLabel: "匹配时忽略模型 ID 前缀",
38
+ ignorePrefixDescription: "开启后,deepseek/deepseek-v4-flash 与 deepseek-v4-flash 视为同一模型,统一显示不带前缀的模型 id(默认开启)。"
37
39
  };
38
40
  const en = {
39
41
  settingsTitle: "Model Router",
@@ -59,7 +61,9 @@ window.__ModuleLoader__.load({
59
61
  noCredentialShort: "No credentials",
60
62
  globalRouteHint: "Changes apply to subsequent requests in all sessions",
61
63
  showQuickSwitchLabel: "Show quick route switcher in the composer",
62
- showQuickSwitchDescription: "When enabled, a provider quick-switch button appears next to the model selector when the current model is a model-router route."
64
+ showQuickSwitchDescription: "When enabled, a provider quick-switch button appears next to the model selector when the current model is a model-router route.",
65
+ ignorePrefixLabel: "Ignore model ID prefix when matching",
66
+ ignorePrefixDescription: "When enabled, deepseek/deepseek-v4-flash and deepseek-v4-flash are treated as the same model and shown without the prefix (on by default)."
63
67
  };
64
68
  //#endregion
65
69
  //#region src/client/api.ts
@@ -96,7 +100,8 @@ window.__ModuleLoader__.load({
96
100
  order
97
101
  }),
98
102
  refresh: () => call("refresh"),
99
- setShowQuickSwitch: (value) => call("set-show-quick-switch", { value })
103
+ setShowQuickSwitch: (value) => call("set-show-quick-switch", { value }),
104
+ setIgnoreModelIdPrefix: (value) => call("set-ignore-model-id-prefix", { value })
100
105
  };
101
106
  //#endregion
102
107
  //#region \0dsh-css:/Users/fonlan/Repos/dsh-model-router/src/client/settings-section.css.mjs
@@ -157,6 +162,7 @@ window.__ModuleLoader__.load({
157
162
  const [busyModel, setBusyModel] = (0, react.useState)(null);
158
163
  const [drag, setDrag] = (0, react.useState)(null);
159
164
  const [quickSwitchBusy, setQuickSwitchBusy] = (0, react.useState)(false);
165
+ const [ignorePrefixBusy, setIgnorePrefixBusy] = (0, react.useState)(false);
160
166
  const load = (0, react.useCallback)(async () => {
161
167
  setLoading(true);
162
168
  try {
@@ -231,6 +237,27 @@ window.__ModuleLoader__.load({
231
237
  setQuickSwitchBusy(false);
232
238
  }
233
239
  }, [state]);
240
+ const toggleIgnorePrefix = (0, react.useCallback)(async (value) => {
241
+ if (state === null) return;
242
+ const previous = state.ignoreModelIdPrefix;
243
+ setState((prev) => prev === null ? prev : {
244
+ ...prev,
245
+ ignoreModelIdPrefix: value
246
+ });
247
+ setIgnorePrefixBusy(true);
248
+ try {
249
+ setState(await api.setIgnoreModelIdPrefix(value));
250
+ setError(null);
251
+ } catch (cause) {
252
+ setState((prev) => prev === null ? prev : {
253
+ ...prev,
254
+ ignoreModelIdPrefix: previous
255
+ });
256
+ setError(cause instanceof Error ? cause.message : String(cause));
257
+ } finally {
258
+ setIgnorePrefixBusy(false);
259
+ }
260
+ }, [state]);
234
261
  const models = (0, react.useMemo)(() => state?.models ?? [], [state]);
235
262
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
236
263
  className: "mr-root",
@@ -257,6 +284,24 @@ window.__ModuleLoader__.load({
257
284
  })]
258
285
  })]
259
286
  }),
287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
288
+ className: "mr-quick-switch",
289
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
290
+ type: "checkbox",
291
+ checked: state?.ignoreModelIdPrefix ?? true,
292
+ disabled: loading || ignorePrefixBusy,
293
+ onChange: (event) => void toggleIgnorePrefix(event.target.checked)
294
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
295
+ className: "mr-quick-switch-copy",
296
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
297
+ className: "mr-quick-switch-label",
298
+ children: t("ignorePrefixLabel")
299
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
300
+ className: "mr-quick-switch-desc",
301
+ children: t("ignorePrefixDescription")
302
+ })]
303
+ })]
304
+ }),
260
305
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
261
306
  className: "mr-toolbar",
262
307
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useCallback","useSyncExternalStore","useState","useMemo","useSyncExternalStore","useState","useCallback","useMemo","Menu","IconGlobeOutline14","IconChevronDownOutline14"],"sources":["../../../src/client/locales.ts","../../../src/client/api.ts","../../../src/client/settings-section.tsx","../../../src/client/route-switcher.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 routeButtonLabel: '快速切换模型路由',\n routeButtonTitle: '切换当前模型的提供商',\n routeButtonAria: '切换当前模型提供商,当前为 {provider}',\n routeMenuTitle: '{model} · 选择提供商',\n routeLoading: '加载路由…',\n unknownProvider: '未知提供商',\n noCredentialShort: '未配置凭据',\n globalRouteHint: '切换后对所有会话的后续请求生效',\n showQuickSwitchLabel: '在输入栏显示快速路由切换按钮',\n showQuickSwitchDescription: '开启后,当当前模型是 model-router 路由时,在输入框模型选择器右侧显示 provider 快速切换按钮。',\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 routeButtonLabel: 'Quick model route switcher',\n routeButtonTitle: 'Switch the current model provider',\n routeButtonAria: 'Switch current model provider, currently {provider}',\n routeMenuTitle: '{model} · Choose provider',\n routeLoading: 'Loading routes…',\n unknownProvider: 'Unknown provider',\n noCredentialShort: 'No credentials',\n globalRouteHint: 'Changes apply to subsequent requests in all sessions',\n showQuickSwitchLabel: 'Show quick route switcher in the composer',\n showQuickSwitchDescription: 'When enabled, a provider quick-switch button appears next to the model selector when the current model is a model-router route.',\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 /** Whether the composer quick route-switcher button is enabled. */\n showQuickSwitch: boolean\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 setShowQuickSwitch: (value: boolean) =>\n call<ModelRouterState>('set-show-quick-switch', { value }),\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 const [quickSwitchBusy, setQuickSwitchBusy] = useState(false)\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 toggleQuickSwitch = useCallback(async (value: boolean) => {\n if (state === null) return\n const previous = state.showQuickSwitch\n setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: value })\n setQuickSwitchBusy(true)\n try {\n setState(await api.setShowQuickSwitch(value))\n setError(null)\n } catch (cause) {\n setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: previous })\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setQuickSwitchBusy(false)\n }\n }, [state])\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 <label className=\"mr-quick-switch\">\n <input\n type=\"checkbox\"\n checked={state?.showQuickSwitch ?? true}\n disabled={loading || quickSwitchBusy}\n onChange={(event) => void toggleQuickSwitch(event.target.checked)}\n />\n <span className=\"mr-quick-switch-copy\">\n <span className=\"mr-quick-switch-label\">{t('showQuickSwitchLabel')}</span>\n <span className=\"mr-quick-switch-desc\">{t('showQuickSwitchDescription')}</span>\n </span>\n </label>\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","import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'\nimport type { ClientContext, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { IconChevronDownOutline14, IconGlobeOutline14, Menu, type MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport { api, type ModelRouterState, type RouterModelView } from './api'\nimport { LOCALE_NS } from './locales'\nimport './route-switcher.css'\n\nconst ROUTER_PROVIDER_ID = 'model-router'\n\ninterface RouteSwitcherInjected {\n available: boolean\n directory: SnapshotStore<ModelDirectoryState>\n load: () => void\n}\n\ntype RouteSwitcherProps = PropsRuntime<'conversation.input.right'>\n & PropsLocale<typeof LOCALE_NS>\n & RouteSwitcherInjected\n\ntype RouteProvider = RouterModelView['providers'][number]\n\n/**\n * The quick route switcher lives beside DSH's model seat in the composer row.\n * It reads the same per-session model directory as the host model picker, but\n * only appears when the selected model is the virtual model-router route.\n */\nexport function RouteSwitcher({\n session,\n available,\n directory,\n load,\n t,\n}: RouteSwitcherProps): JSX.Element | null {\n const directoryState = useSyncExternalStore(\n onChange => directory.subscribe(onChange),\n () => directory.getSnapshot(),\n () => directory.getSnapshot(),\n )\n const current = directoryState.current\n const [routerState, setRouterState] = useState<ModelRouterState | null>(null)\n const [routerLoading, setRouterLoading] = useState(false)\n const [routerError, setRouterError] = useState<string | null>(null)\n const [open, setOpen] = useState(false)\n const [busyProvider, setBusyProvider] = useState<string | null>(null)\n\n const isRouterModel = available && current?.provider === ROUTER_PROVIDER_ID\n const modelId = isRouterModel ? current.model : null\n\n const loadRouterState = useCallback(async () => {\n setRouterLoading(true)\n try {\n setRouterState(await api.state())\n setRouterError(null)\n } catch (cause) {\n setRouterError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setRouterLoading(false)\n }\n }, [])\n\n useEffect(() => {\n if (!available) return\n load()\n }, [available, load])\n\n // Fetch the global toggle (and the model's provider list) on mount too, so a\n // \"hide quick switcher\" setting applies even before the model directory\n // settles. The button stays visible while the value is unknown (default on).\n useEffect(() => {\n if (!available) return\n void loadRouterState()\n }, [available])\n\n useEffect(() => {\n setOpen(false)\n setBusyProvider(null)\n setRouterState(null)\n setRouterError(null)\n }, [modelId])\n\n // The model-selection directory is refreshed when another session changes\n // the router settings document. Re-read the global router state after that\n // refresh so this button never shows a stale active provider. Include the\n // model id: switching between two router models can leave the directory in\n // the same ready state while changing the API row we need to display.\n useEffect(() => {\n if (!isRouterModel || modelId === null || directoryState.status !== 'ready') return\n void loadRouterState()\n }, [directoryState.status, isRouterModel, loadRouterState, modelId])\n\n const routeModel = useMemo(\n () => modelId === null\n ? undefined\n : routerState?.models.find(model => model.id === modelId),\n [modelId, routerState],\n )\n\n const providers = useMemo(() => {\n if (routeModel === undefined) return []\n const byId = new Map(routeModel.providers.map(provider => [provider.id, provider]))\n const ordered = routeModel.order\n .map(id => byId.get(id))\n .filter((provider): provider is RouteProvider => provider !== undefined)\n const seen = new Set(ordered.map(provider => provider.id))\n return [...ordered, ...routeModel.providers.filter(provider => !seen.has(provider.id))]\n }, [routeModel])\n\n const switchProvider = useCallback(async (providerId: string) => {\n if (modelId === null || routeModel === undefined || routeModel.active === providerId) {\n setOpen(false)\n return\n }\n setBusyProvider(providerId)\n try {\n const next = await api.switchActive(modelId, providerId)\n setRouterState(next)\n setRouterError(null)\n setOpen(false)\n // The model directory also listens for settings updates, but an explicit\n // refresh keeps the host model picker description in sync immediately.\n load()\n } catch (cause) {\n setRouterError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n } finally {\n setBusyProvider(null)\n }\n }, [load, modelId, routeModel, t])\n\n if (session.removed || !isRouterModel) return null\n if (routerState?.showQuickSwitch === false) return null\n\n const activeProvider = providers.find(provider => provider.id === routeModel?.active)\n const activeName = activeProvider?.name ?? routeModel?.active ?? t('routeLoading')\n if (routeModel !== undefined && providers.length < 2) return null\n\n const menuItems: MenuEntry[] = routeModel === undefined\n ? [{\n type: 'label',\n id: 'route-status',\n text: routerError ?? t('routeLoading'),\n }]\n : [\n {\n type: 'label',\n id: 'route-model',\n text: t('routeMenuTitle', { model: modelId }),\n },\n ...providers.map(provider => ({\n id: provider.id,\n label: (\n <span className=\"mr-route-item-copy\">\n <span className=\"mr-route-item-name\">{provider.name}</span>\n {!provider.credentialConfigured && (\n <span className=\"mr-route-item-note\">{t('noCredentialShort')}</span>\n )}\n </span>\n ),\n disabled: !provider.credentialConfigured || busyProvider !== null,\n })),\n ...(routerError === null ? [] : [{\n id: 'route-error',\n label: routerError,\n disabled: true,\n danger: true,\n }]),\n ]\n\n return (\n <Menu\n className=\"mr-route-anchor\"\n open={open}\n side=\"top\"\n align=\"end\"\n portal\n compact\n dense\n items={menuItems}\n selectedId={routeModel?.active ?? undefined}\n onSelect={id => {\n if (id === 'route-error' || routeModel === undefined) {\n void loadRouterState()\n load()\n return\n }\n void switchProvider(id)\n }}\n onClose={() => setOpen(false)}\n footer={[\n {\n type: 'label',\n id: 'route-global-hint',\n text: t('globalRouteHint'),\n },\n ]}\n anchor={(\n <button\n type=\"button\"\n className=\"mr-route-trigger\"\n aria-label={t('routeButtonAria', { provider: activeName })}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n disabled={routerLoading || busyProvider !== null}\n onClick={() => {\n setOpen(value => !value)\n if (!open) {\n void loadRouterState()\n load()\n }\n }}\n title={t('routeButtonTitle')}\n >\n <IconGlobeOutline14 />\n <span className=\"mr-route-trigger-label\">{activeName}</span>\n <span className=\"mr-route-trigger-chevron\" aria-hidden>\n <IconChevronDownOutline14 />\n </span>\n </button>\n )}\n />\n )\n}\n\n/** Register the quick route switcher into the composer right-side tool row. */\nexport function registerRouteSwitcher(ctx: ClientContext): void {\n const t = ctx.locale.bind(LOCALE_NS)\n ctx.inject(['slots', 'modelDirectories'], scope => {\n const models = scope.modelDirectories\n const sessions = scope.sessions\n scope.slots.inject('conversation.input.right', () => scope.slots.register({\n name: 'conversation.input.right',\n id: 'model-router-route-switcher',\n order: 100,\n label: () => t('routeButtonLabel'),\n locale: LOCALE_NS,\n inject: (sessionId) => {\n const directory = models.directoryFor(sessionId)\n const available = sessions.subagentAddress(sessionId) === undefined\n return {\n available,\n directory: directory.store,\n load: () => {\n if (available) directory.load().catch(() => {})\n },\n }\n },\n }, RouteSwitcher as never))\n })\n}","/**\n * @fonlan/dsh-model-router client half: the Model Router settings page plus\n * a quick provider switcher beside DSH's composer model selector.\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 type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport { LOCALE_NS, zh, en } from './locales'\nimport { makeSettingsSection } from './settings-section'\nimport { registerRouteSwitcher } from './route-switcher'\n\n/** Services required before mounting (provided by the client runtime). */\nexport const inject = ['slots', 'locale', 'modelDirectories', 'sessions']\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 registerRouteSwitcher(ctx)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useCallback","useSyncExternalStore","useState","useMemo","useSyncExternalStore","useState","useCallback","useMemo","Menu","IconGlobeOutline14","IconChevronDownOutline14"],"sources":["../../../src/client/locales.ts","../../../src/client/api.ts","../../../src/client/settings-section.tsx","../../../src/client/route-switcher.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 routeButtonLabel: '快速切换模型路由',\n routeButtonTitle: '切换当前模型的提供商',\n routeButtonAria: '切换当前模型提供商,当前为 {provider}',\n routeMenuTitle: '{model} · 选择提供商',\n routeLoading: '加载路由…',\n unknownProvider: '未知提供商',\n noCredentialShort: '未配置凭据',\n globalRouteHint: '切换后对所有会话的后续请求生效',\n showQuickSwitchLabel: '在输入栏显示快速路由切换按钮',\n showQuickSwitchDescription: '开启后,当当前模型是 model-router 路由时,在输入框模型选择器右侧显示 provider 快速切换按钮。',\n ignorePrefixLabel: '匹配时忽略模型 ID 前缀',\n ignorePrefixDescription: '开启后,deepseek/deepseek-v4-flash 与 deepseek-v4-flash 视为同一模型,统一显示不带前缀的模型 id(默认开启)。',\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 routeButtonLabel: 'Quick model route switcher',\n routeButtonTitle: 'Switch the current model provider',\n routeButtonAria: 'Switch current model provider, currently {provider}',\n routeMenuTitle: '{model} · Choose provider',\n routeLoading: 'Loading routes…',\n unknownProvider: 'Unknown provider',\n noCredentialShort: 'No credentials',\n globalRouteHint: 'Changes apply to subsequent requests in all sessions',\n showQuickSwitchLabel: 'Show quick route switcher in the composer',\n showQuickSwitchDescription: 'When enabled, a provider quick-switch button appears next to the model selector when the current model is a model-router route.',\n ignorePrefixLabel: 'Ignore model ID prefix when matching',\n ignorePrefixDescription: 'When enabled, deepseek/deepseek-v4-flash and deepseek-v4-flash are treated as the same model and shown without the prefix (on by default).',\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 /** Whether the composer quick route-switcher button is enabled. */\n showQuickSwitch: boolean\n /** Whether model ids are matched with their leading vendor/ prefix ignored. */\n ignoreModelIdPrefix: boolean\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 setShowQuickSwitch: (value: boolean) =>\n call<ModelRouterState>('set-show-quick-switch', { value }),\n setIgnoreModelIdPrefix: (value: boolean) =>\n call<ModelRouterState>('set-ignore-model-id-prefix', { value }),\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 const [quickSwitchBusy, setQuickSwitchBusy] = useState(false)\n const [ignorePrefixBusy, setIgnorePrefixBusy] = useState(false)\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 toggleQuickSwitch = useCallback(async (value: boolean) => {\n if (state === null) return\n const previous = state.showQuickSwitch\n setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: value })\n setQuickSwitchBusy(true)\n try {\n setState(await api.setShowQuickSwitch(value))\n setError(null)\n } catch (cause) {\n setState(prev => prev === null ? prev : { ...prev, showQuickSwitch: previous })\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setQuickSwitchBusy(false)\n }\n }, [state])\n\n const toggleIgnorePrefix = useCallback(async (value: boolean) => {\n if (state === null) return\n const previous = state.ignoreModelIdPrefix\n setState(prev => prev === null ? prev : { ...prev, ignoreModelIdPrefix: value })\n setIgnorePrefixBusy(true)\n try {\n setState(await api.setIgnoreModelIdPrefix(value))\n setError(null)\n } catch (cause) {\n setState(prev => prev === null ? prev : { ...prev, ignoreModelIdPrefix: previous })\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setIgnorePrefixBusy(false)\n }\n }, [state])\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 <label className=\"mr-quick-switch\">\n <input\n type=\"checkbox\"\n checked={state?.showQuickSwitch ?? true}\n disabled={loading || quickSwitchBusy}\n onChange={(event) => void toggleQuickSwitch(event.target.checked)}\n />\n <span className=\"mr-quick-switch-copy\">\n <span className=\"mr-quick-switch-label\">{t('showQuickSwitchLabel')}</span>\n <span className=\"mr-quick-switch-desc\">{t('showQuickSwitchDescription')}</span>\n </span>\n </label>\n <label className=\"mr-quick-switch\">\n <input\n type=\"checkbox\"\n checked={state?.ignoreModelIdPrefix ?? true}\n disabled={loading || ignorePrefixBusy}\n onChange={(event) => void toggleIgnorePrefix(event.target.checked)}\n />\n <span className=\"mr-quick-switch-copy\">\n <span className=\"mr-quick-switch-label\">{t('ignorePrefixLabel')}</span>\n <span className=\"mr-quick-switch-desc\">{t('ignorePrefixDescription')}</span>\n </span>\n </label>\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","import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'\nimport type { ClientContext, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { IconChevronDownOutline14, IconGlobeOutline14, Menu, type MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport { api, type ModelRouterState, type RouterModelView } from './api'\nimport { LOCALE_NS } from './locales'\nimport './route-switcher.css'\n\nconst ROUTER_PROVIDER_ID = 'model-router'\n\ninterface RouteSwitcherInjected {\n available: boolean\n directory: SnapshotStore<ModelDirectoryState>\n load: () => void\n}\n\ntype RouteSwitcherProps = PropsRuntime<'conversation.input.right'>\n & PropsLocale<typeof LOCALE_NS>\n & RouteSwitcherInjected\n\ntype RouteProvider = RouterModelView['providers'][number]\n\n/**\n * The quick route switcher lives beside DSH's model seat in the composer row.\n * It reads the same per-session model directory as the host model picker, but\n * only appears when the selected model is the virtual model-router route.\n */\nexport function RouteSwitcher({\n session,\n available,\n directory,\n load,\n t,\n}: RouteSwitcherProps): JSX.Element | null {\n const directoryState = useSyncExternalStore(\n onChange => directory.subscribe(onChange),\n () => directory.getSnapshot(),\n () => directory.getSnapshot(),\n )\n const current = directoryState.current\n const [routerState, setRouterState] = useState<ModelRouterState | null>(null)\n const [routerLoading, setRouterLoading] = useState(false)\n const [routerError, setRouterError] = useState<string | null>(null)\n const [open, setOpen] = useState(false)\n const [busyProvider, setBusyProvider] = useState<string | null>(null)\n\n const isRouterModel = available && current?.provider === ROUTER_PROVIDER_ID\n const modelId = isRouterModel ? current.model : null\n\n const loadRouterState = useCallback(async () => {\n setRouterLoading(true)\n try {\n setRouterState(await api.state())\n setRouterError(null)\n } catch (cause) {\n setRouterError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setRouterLoading(false)\n }\n }, [])\n\n useEffect(() => {\n if (!available) return\n load()\n }, [available, load])\n\n // Fetch the global toggle (and the model's provider list) on mount too, so a\n // \"hide quick switcher\" setting applies even before the model directory\n // settles. The button stays visible while the value is unknown (default on).\n useEffect(() => {\n if (!available) return\n void loadRouterState()\n }, [available])\n\n useEffect(() => {\n setOpen(false)\n setBusyProvider(null)\n setRouterState(null)\n setRouterError(null)\n }, [modelId])\n\n // The model-selection directory is refreshed when another session changes\n // the router settings document. Re-read the global router state after that\n // refresh so this button never shows a stale active provider. Include the\n // model id: switching between two router models can leave the directory in\n // the same ready state while changing the API row we need to display.\n useEffect(() => {\n if (!isRouterModel || modelId === null || directoryState.status !== 'ready') return\n void loadRouterState()\n }, [directoryState.status, isRouterModel, loadRouterState, modelId])\n\n const routeModel = useMemo(\n () => modelId === null\n ? undefined\n : routerState?.models.find(model => model.id === modelId),\n [modelId, routerState],\n )\n\n const providers = useMemo(() => {\n if (routeModel === undefined) return []\n const byId = new Map(routeModel.providers.map(provider => [provider.id, provider]))\n const ordered = routeModel.order\n .map(id => byId.get(id))\n .filter((provider): provider is RouteProvider => provider !== undefined)\n const seen = new Set(ordered.map(provider => provider.id))\n return [...ordered, ...routeModel.providers.filter(provider => !seen.has(provider.id))]\n }, [routeModel])\n\n const switchProvider = useCallback(async (providerId: string) => {\n if (modelId === null || routeModel === undefined || routeModel.active === providerId) {\n setOpen(false)\n return\n }\n setBusyProvider(providerId)\n try {\n const next = await api.switchActive(modelId, providerId)\n setRouterState(next)\n setRouterError(null)\n setOpen(false)\n // The model directory also listens for settings updates, but an explicit\n // refresh keeps the host model picker description in sync immediately.\n load()\n } catch (cause) {\n setRouterError(t('switchFailed', { message: cause instanceof Error ? cause.message : String(cause) }))\n } finally {\n setBusyProvider(null)\n }\n }, [load, modelId, routeModel, t])\n\n if (session.removed || !isRouterModel) return null\n if (routerState?.showQuickSwitch === false) return null\n\n const activeProvider = providers.find(provider => provider.id === routeModel?.active)\n const activeName = activeProvider?.name ?? routeModel?.active ?? t('routeLoading')\n if (routeModel !== undefined && providers.length < 2) return null\n\n const menuItems: MenuEntry[] = routeModel === undefined\n ? [{\n type: 'label',\n id: 'route-status',\n text: routerError ?? t('routeLoading'),\n }]\n : [\n {\n type: 'label',\n id: 'route-model',\n text: t('routeMenuTitle', { model: modelId }),\n },\n ...providers.map(provider => ({\n id: provider.id,\n label: (\n <span className=\"mr-route-item-copy\">\n <span className=\"mr-route-item-name\">{provider.name}</span>\n {!provider.credentialConfigured && (\n <span className=\"mr-route-item-note\">{t('noCredentialShort')}</span>\n )}\n </span>\n ),\n disabled: !provider.credentialConfigured || busyProvider !== null,\n })),\n ...(routerError === null ? [] : [{\n id: 'route-error',\n label: routerError,\n disabled: true,\n danger: true,\n }]),\n ]\n\n return (\n <Menu\n className=\"mr-route-anchor\"\n open={open}\n side=\"top\"\n align=\"end\"\n portal\n compact\n dense\n items={menuItems}\n selectedId={routeModel?.active ?? undefined}\n onSelect={id => {\n if (id === 'route-error' || routeModel === undefined) {\n void loadRouterState()\n load()\n return\n }\n void switchProvider(id)\n }}\n onClose={() => setOpen(false)}\n footer={[\n {\n type: 'label',\n id: 'route-global-hint',\n text: t('globalRouteHint'),\n },\n ]}\n anchor={(\n <button\n type=\"button\"\n className=\"mr-route-trigger\"\n aria-label={t('routeButtonAria', { provider: activeName })}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n disabled={routerLoading || busyProvider !== null}\n onClick={() => {\n setOpen(value => !value)\n if (!open) {\n void loadRouterState()\n load()\n }\n }}\n title={t('routeButtonTitle')}\n >\n <IconGlobeOutline14 />\n <span className=\"mr-route-trigger-label\">{activeName}</span>\n <span className=\"mr-route-trigger-chevron\" aria-hidden>\n <IconChevronDownOutline14 />\n </span>\n </button>\n )}\n />\n )\n}\n\n/** Register the quick route switcher into the composer right-side tool row. */\nexport function registerRouteSwitcher(ctx: ClientContext): void {\n const t = ctx.locale.bind(LOCALE_NS)\n ctx.inject(['slots', 'modelDirectories'], scope => {\n const models = scope.modelDirectories\n const sessions = scope.sessions\n scope.slots.inject('conversation.input.right', () => scope.slots.register({\n name: 'conversation.input.right',\n id: 'model-router-route-switcher',\n order: 100,\n label: () => t('routeButtonLabel'),\n locale: LOCALE_NS,\n inject: (sessionId) => {\n const directory = models.directoryFor(sessionId)\n const available = sessions.subagentAddress(sessionId) === undefined\n return {\n available,\n directory: directory.store,\n load: () => {\n if (available) directory.load().catch(() => {})\n },\n }\n },\n }, RouteSwitcher as never))\n })\n}","/**\n * @fonlan/dsh-model-router client half: the Model Router settings page plus\n * a quick provider switcher beside DSH's composer model selector.\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 type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'\nimport { LOCALE_NS, zh, en } from './locales'\nimport { makeSettingsSection } from './settings-section'\nimport { registerRouteSwitcher } from './route-switcher'\n\n/** Services required before mounting (provided by the client runtime). */\nexport const inject = ['slots', 'locale', 'modelDirectories', 'sessions']\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 registerRouteSwitcher(ctx)\n}\n"],"mappings":"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"}