@fonlan/dsh-model-parameters 0.1.5 → 0.1.6

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.en.md CHANGED
@@ -14,18 +14,21 @@ could not match.
14
14
 
15
15
  ## How it works
16
16
 
17
- 1. The plugin listens to the `llm-pi-ai` settings namespace (`settings/updated`).
18
- Any committed change — a freshly synced provider, an edited provider card, a
19
- manual `settings.yaml` edit — triggers a reconcile.
17
+ 1. The plugin listens to the `llm-pi-ai` settings namespace
18
+ (`settings/document-updated`). Any committed change — a freshly synced
19
+ provider, an edited provider card, a manual profile-patch edit — triggers a
20
+ reconcile.
20
21
  2. On plugin start it also runs one backfill over the existing configuration.
21
22
  3. For every model entry missing any of `name` / `contextWindow` / `maxTokens` /
22
23
  `reasoningEfforts` / `input`, it resolves the best catalog entry and fills
23
24
  only the missing fields (fill-only: present values are never overwritten).
24
25
  4. The write goes through the settings seam (`ctx.settings.mutate`), i.e. it
25
- lands in `~/.dsh/settings.yaml` — the same file you would edit by hand.
26
+ lands in the active profile's patch file
27
+ (`~/.dsh/profiles/<profile>/cordis.patch.yml`) — the same file you would
28
+ edit by hand.
26
29
 
27
30
  The reconcile is fill-only by construction, so its own writes cannot loop: the
28
- next `settings/updated` sees the fields present and produces no ops.
31
+ next `settings/document-updated` sees the fields present and produces no ops.
29
32
 
30
33
  ## Matching
31
34
 
@@ -87,25 +90,26 @@ cards, collapsed by default):
87
90
  - the last fill report: fields filled, providers touched, and the list of
88
91
  unmatched model ids.
89
92
 
90
- Plugin configuration persists in the `model-parameters` settings namespace
91
- (`~/.dsh/settings.yaml`), e.g.:
93
+ Plugin configuration IS the plugin's own loader entry config: the
94
+ `model-parameters` entry in the active profile's patch file, e.g.:
92
95
 
93
96
  ```yaml
94
- model-parameters:
95
- enabled: true
96
- ttlDays: 7
97
- fillName: true
98
- fillContext: true
99
- fillMaxTokens: true
100
- fillReasoning: true
101
- fillInput: true
102
- providerMap: {}
103
- officialProviders:
104
- - deepseek
105
- - openai
106
- - anthropic
107
- - google
108
- - xai
97
+ - id: model-parameters
98
+ config:
99
+ enabled: true
100
+ ttlDays: 7
101
+ fillName: true
102
+ fillContext: true
103
+ fillMaxTokens: true
104
+ fillReasoning: true
105
+ fillInput: true
106
+ providerMap: {}
107
+ officialProviders:
108
+ - deepseek
109
+ - openai
110
+ - anthropic
111
+ - google
112
+ - xai
109
113
  ```
110
114
 
111
115
  ## Installation
package/README.md CHANGED
@@ -8,12 +8,12 @@ DSH 插件:当 provider 的模型同步(或任何设置写入)留下缺失
8
8
 
9
9
  ## 工作原理
10
10
 
11
- 1. 插件监听 `llm-pi-ai` 设置命名空间(`settings/updated`)。任何已提交的变更——新同步的 provider、编辑过的 provider 卡片、手动编辑的 `settings.yaml`——都会触发一次 reconcile。
11
+ 1. 插件监听 `llm-pi-ai` 设置命名空间(`settings/document-updated`)。任何已提交的变更——新同步的 provider、编辑过的 provider 卡片、手动编辑的 profile 补丁——都会触发一次 reconcile。
12
12
  2. 插件启动时还会对现有配置执行一次全量回填。
13
13
  3. 对每个缺少 `name` / `contextWindow` / `maxTokens` / `reasoningEfforts` / `input` 任一字段的模型条目,解析出最佳目录条目,只补全缺失字段(fill-only:已有值绝不覆盖)。
14
- 4. 写入走设置接缝(`ctx.settings.mutate`),即落到 `~/.dsh/settings.yaml`——与你手动编辑的是同一个文件。
14
+ 4. 写入走设置接缝(`ctx.settings.mutate`),即落到当前 profile 的补丁文件(`~/.dsh/profiles/<profile>/cordis.patch.yml`)——与你手动编辑的是同一个文件。
15
15
 
16
- reconcile 天然是 fill-only 的,因此它自己的写入不会造成循环:下一次 `settings/updated` 看到字段已存在,不会产生任何操作。
16
+ reconcile 天然是 fill-only 的,因此它自己的写入不会造成循环:下一次 `settings/document-updated` 看到字段已存在,不会产生任何操作。
17
17
 
18
18
  ## 匹配逻辑
19
19
 
@@ -58,24 +58,25 @@ models.dev 把同一模型挂在许多 provider 名下(`deepseek-v4-flash` 出
58
58
  - 目录新鲜度(条目数、provider 数、上次更新);
59
59
  - 上次补全报告:补全的字段、涉及的 provider、未匹配的模型 id 列表。
60
60
 
61
- 插件配置保存在 `model-parameters` 设置命名空间(`~/.dsh/settings.yaml`),例如:
61
+ 插件配置就是它自己的 loader entry config——当前 profile 补丁文件里的 `model-parameters` 条目,例如:
62
62
 
63
63
  ```yaml
64
- model-parameters:
65
- enabled: true
66
- ttlDays: 7
67
- fillName: true
68
- fillContext: true
69
- fillMaxTokens: true
70
- fillReasoning: true
71
- fillInput: true
72
- providerMap: {}
73
- officialProviders:
74
- - deepseek
75
- - openai
76
- - anthropic
77
- - google
78
- - xai
64
+ - id: model-parameters
65
+ config:
66
+ enabled: true
67
+ ttlDays: 7
68
+ fillName: true
69
+ fillContext: true
70
+ fillMaxTokens: true
71
+ fillReasoning: true
72
+ fillInput: true
73
+ providerMap: {}
74
+ officialProviders:
75
+ - deepseek
76
+ - openai
77
+ - anthropic
78
+ - google
79
+ - xai
79
80
  ```
80
81
 
81
82
  ## 安装
package/lib/client.js CHANGED
@@ -153,7 +153,8 @@ window.__ModuleLoader__.load({
153
153
  * and the last fill report (filled / touched / unmatched).
154
154
  *
155
155
  * All reads/mutations go through the plugin's fenced API; config persists into
156
- * the `model-parameters` settings namespace (settings.yaml).
156
+ * the `model-parameters` profile entry — the plugin's own loader entry config,
157
+ * written through the host settings service.
157
158
  */
158
159
  function useLocaleRevision(ctx) {
159
160
  const subscribe = (0, react.useCallback)((onChange) => {
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useCallback","useSyncExternalStore","useState"],"sources":["../../../src/client/locales.ts","../../../src/client/api.ts","../../../src/client/settings-section.tsx","../../../src/client/index.tsx"],"sourcesContent":["/**\n * Locale dictionaries for @fonlan/dsh-model-parameters settings page.\n * zh is the key source of truth; en must carry the exact same key set.\n */\nexport const LOCALE_NS = 'model-parameters'\n\nexport const zh = {\n settingsTitle: '模型参数补全',\n sectionSub: '补全字段、目录刷新、Provider 映射与补全报告。',\n master: '启用自动补全',\n masterDesc: '关闭后不再自动补全任何字段(已补全的值保留)。',\n fields: '补全字段',\n fillName: '显示名称',\n fillNameDesc: '模型条目缺 name 时写入目录中的显示名称。',\n fillContext: '最大上下文',\n fillContextDesc: '缺 contextWindow 时写入目录的上下文上限。',\n fillMaxTokens: '最大输出',\n fillMaxTokensDesc: '缺 maxTokens 时写入目录的输出上限。',\n fillReasoning: '推理强度',\n fillReasoningDesc: '缺 reasoningEfforts 时按目录 effort 档位写入 identity 字典(如 {high:\"high\", max:\"max\"})。',\n fillInput: '输入模态',\n fillInputDesc: '缺 input 时写入目录中的 text/image 模态。',\n ttl: '目录更新周期(天)',\n ttlDesc: '缓存的 models.dev 目录超过该天数后,在下次补全时自动刷新。',\n providerMap: 'Provider 映射',\n providerMapDesc: 'dsh provider → models.dev provider 的匹配覆盖(可选)。',\n addMapping: '添加映射',\n removeMapping: '移除',\n catalog: '模型目录',\n catalogEntries: '目录条目',\n catalogProviders: '覆盖 Provider',\n catalogFetched: '上次更新',\n catalogFresh: '新鲜',\n catalogStale: '已过期',\n catalogNever: '尚未获取',\n refresh: '立即更新目录并补全',\n refreshBusy: '更新中…',\n reconcileNow: '立即补全一次',\n report: '最近一次补全',\n reportNever: '尚未执行',\n reportFilled: '补全字段',\n reportTouched: '涉及 Provider',\n reportUnmatched: '未匹配模型',\n reportEmpty: '全部匹配,无未匹配项',\n reportAt: '时间',\n error: '加载失败',\n saving: '保存中…',\n officialProviders: '官方厂商优先级',\n officialProvidersDesc: '同一模型 id 在多处出现时,优先采用这些 models.dev provider 的条目。',\n} as const\n\nexport const en: Record<keyof typeof zh, string> = {\n settingsTitle: 'Model Parameters',\n sectionSub: 'Field toggles, catalog refresh, provider mapping and the fill report.',\n master: 'Enable auto-fill',\n masterDesc: 'When off, no fields are filled (already-filled values are kept).',\n fields: 'Fields to fill',\n fillName: 'Display name',\n fillNameDesc: 'Write the catalog display name when a model entry lacks name.',\n fillContext: 'Context window',\n fillContextDesc: 'Write the catalog context limit when contextWindow is missing.',\n fillMaxTokens: 'Max output',\n fillMaxTokensDesc: 'Write the catalog output limit when maxTokens is missing.',\n fillReasoning: 'Reasoning efforts',\n fillReasoningDesc: 'Write an identity reasoningEfforts dict (e.g. {high:\"high\", max:\"max\"}) from catalog effort levels when missing.',\n fillInput: 'Input modalities',\n fillInputDesc: 'Write text/image modalities from the catalog when input is missing.',\n ttl: 'Catalog refresh interval (days)',\n ttlDesc: 'The cached models.dev catalog is refreshed on the next fill once older than this many days.',\n providerMap: 'Provider mapping',\n providerMapDesc: 'Optional dsh provider → models.dev provider matching overrides.',\n addMapping: 'Add mapping',\n removeMapping: 'Remove',\n catalog: 'Model catalog',\n catalogEntries: 'Catalog entries',\n catalogProviders: 'Providers covered',\n catalogFetched: 'Last updated',\n catalogFresh: 'fresh',\n catalogStale: 'stale',\n catalogNever: 'not fetched yet',\n refresh: 'Update catalog & fill now',\n refreshBusy: 'Updating…',\n reconcileNow: 'Fill now',\n report: 'Last fill',\n reportNever: 'never run',\n reportFilled: 'fields filled',\n reportTouched: 'providers touched',\n reportUnmatched: 'unmatched models',\n reportEmpty: 'all matched, nothing unmatched',\n reportAt: 'time',\n error: 'Failed to load',\n saving: 'Saving…',\n officialProviders: 'Official provider priority',\n officialProvidersDesc: 'When one model id appears under several providers, prefer entries from these models.dev providers.',\n}\n","/** Client transport for the model-parameters settings API. */\nimport type { ModelParametersConfig } from '../shared/config.js'\n\nexport interface CatalogView {\n entries: number\n providers: number\n fetchedAt: number\n fresh: boolean\n ttlDays: number\n}\n\nexport interface FillReportView {\n at: number\n filled: number\n touched: number\n unmatched: string[]\n}\n\nexport interface PluginState {\n config: ModelParametersConfig\n catalog: CatalogView\n report: FillReportView | null\n}\n\nexport class ModelParametersApiError 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-parameters/api/${method}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch (error) {\n throw new ModelParametersApiError('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 ModelParametersApiError(\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<PluginState>('state'),\n refresh: () => call<PluginState>('refresh'),\n reconcile: () => call<PluginState>('reconcile'),\n setConfig: (patch: Partial<ModelParametersConfig>) =>\n call<PluginState>('set-config', { patch }),\n}\n","/**\n * The Model Parameters settings page (设置 → 侧栏「模型参数补全」).\n *\n * Registers into the `settings.section` slot, so the page owns one entry in\n * the settings sidebar and renders its content in the panel's content column.\n * Content: master + per-field toggles, catalog TTL, provider→models.dev\n * mapping overrides, a manual refresh/fill action, the catalog freshness line,\n * and the last fill report (filled / touched / unmatched).\n *\n * All reads/mutations go through the plugin's fenced API; config persists into\n * the `model-parameters` settings namespace (settings.yaml).\n */\nimport { useCallback, useEffect, useState, useSyncExternalStore } from 'react'\nimport type { Context } from '@deepseek-ai/cordis'\ntype ClientContext = Context\nimport type { Translate } from '@deepseek-ai/dsh-client-ui-slots'\nimport { LOCALE_NS } from './locales'\nimport { api, type PluginState } from './api'\nimport type { ModelParametersConfig } from '../shared/config.js'\nimport './settings-section.css'\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\nfunction formatTime(epochMs: number): string {\n try {\n return new Date(epochMs).toLocaleString()\n } catch {\n return String(epochMs)\n }\n}\n\nexport function makeSettingsSection(ctx: ClientContext): () => JSX.Element {\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 ModelParametersSettingsSection(): JSX.Element {\n useLocaleRevision(ctx)\n const [state, setState] = useState<PluginState | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(true)\n const [busy, setBusy] = useState(false)\n const [saving, setSaving] = useState(false)\n const [mappingRows, setMappingRows] = useState<Array<{ from: string; to: string }>>([])\n const [newFrom, setNewFrom] = useState('')\n const [newTo, setNewTo] = useState('')\n\n const load = useCallback(async () => {\n setLoading(true)\n try {\n const next = await api.state()\n setState(next)\n setMappingRows(Object.entries(next.config.providerMap).map(([from, to]) => ({ from, to })))\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 patchConfig = useCallback(async (patch: Partial<ModelParametersConfig>) => {\n setSaving(true)\n try {\n const next = await api.setConfig(patch)\n setState(next)\n setMappingRows(Object.entries(next.config.providerMap).map(([from, to]) => ({ from, to })))\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setSaving(false)\n }\n }, [])\n\n const refresh = useCallback(async () => {\n setBusy(true)\n try {\n setState(await api.refresh())\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setBusy(false)\n }\n }, [])\n\n const reconcileNow = useCallback(async () => {\n setBusy(true)\n try {\n setState(await api.reconcile())\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setBusy(false)\n }\n }, [])\n\n const applyMappings = useCallback(async () => {\n const providerMap: Record<string, string> = {}\n for (const row of mappingRows) {\n const from = row.from.trim()\n const to = row.to.trim()\n if (from.length > 0 && to.length > 0) providerMap[from] = to\n }\n await patchConfig({ providerMap })\n }, [mappingRows, patchConfig])\n\n return (\n <div className=\"mp-page\">\n <header className=\"mp-page-head\">\n <h3 className=\"mp-page-title\">{t('settingsTitle')}</h3>\n <p className=\"mp-page-sub\">{t('sectionSub')}</p>\n </header>\n {error !== null && state === null ? (\n <p className=\"mp-error\">{t('error')}: {error}</p>\n ) : loading || state === null ? (\n <p className=\"mp-muted\">{t('saving')}</p>\n ) : (\n <ModelParametersBody\n t={t}\n state={state}\n error={error}\n saving={saving}\n busy={busy}\n mappingRows={mappingRows}\n newFrom={newFrom}\n newTo={newTo}\n setNewFrom={setNewFrom}\n setNewTo={setNewTo}\n setMappingRows={setMappingRows}\n onPatchConfig={patchConfig}\n onRefresh={refresh}\n onReconcile={reconcileNow}\n onApplyMappings={applyMappings}\n />\n )}\n </div>\n )\n }\n}\n\n/** The page body: divider-separated control groups. */\nfunction ModelParametersBody(props: {\n t: Translate\n state: PluginState\n error: string | null\n saving: boolean\n busy: boolean\n mappingRows: Array<{ from: string; to: string }>\n newFrom: string\n newTo: string\n setNewFrom: (value: string) => void\n setNewTo: (value: string) => void\n setMappingRows: (rows: Array<{ from: string; to: string }>) => void\n onPatchConfig: (patch: Partial<ModelParametersConfig>) => Promise<void>\n onRefresh: () => Promise<void>\n onReconcile: () => Promise<void>\n onApplyMappings: () => Promise<void>\n}): JSX.Element {\n const {\n t, state, error, saving, busy,\n mappingRows, newFrom, newTo,\n setNewFrom, setNewTo, setMappingRows,\n onPatchConfig, onRefresh, onReconcile, onApplyMappings,\n } = props\n\n const config = state.config\n const catalog = state.catalog\n const report = state.report\n\n return (\n <div className=\"mp-body\">\n <section className=\"mp-group\">\n <h4 className=\"mp-group-title\">{t('fields')}</h4>\n <label className=\"mp-toggle\">\n <input\n type=\"checkbox\"\n checked={config.enabled}\n disabled={saving}\n onChange={(event) => void onPatchConfig({ enabled: event.target.checked })}\n />\n <span className=\"mp-toggle-body\">\n <span className=\"mp-toggle-label\">{t('master')}</span>\n <span className=\"mp-toggle-desc\">{t('masterDesc')}</span>\n </span>\n </label>\n {([\n ['fillName', t('fillName'), t('fillNameDesc')],\n ['fillContext', t('fillContext'), t('fillContextDesc')],\n ['fillMaxTokens', t('fillMaxTokens'), t('fillMaxTokensDesc')],\n ['fillReasoning', t('fillReasoning'), t('fillReasoningDesc')],\n ['fillInput', t('fillInput'), t('fillInputDesc')],\n ] as const).map(([key, label, desc]) => (\n <label className=\"mp-toggle mp-toggle-sub\" key={key}>\n <input\n type=\"checkbox\"\n checked={config[key]}\n disabled={saving || !config.enabled}\n onChange={(event) => void onPatchConfig({ [key]: event.target.checked })}\n />\n <span className=\"mp-toggle-body\">\n <span className=\"mp-toggle-label\">{label}</span>\n <span className=\"mp-toggle-desc\">{desc}</span>\n </span>\n </label>\n ))}\n </section>\n\n <hr className=\"mp-divider\" />\n\n <section className=\"mp-group\">\n <h4 className=\"mp-group-title\">{t('catalog')}</h4>\n <div className=\"mp-meta\">\n <span>{t('catalogEntries')}: <strong>{catalog.entries.toLocaleString()}</strong></span>\n <span>{t('catalogProviders')}: <strong>{catalog.providers.toLocaleString()}</strong></span>\n <span>\n {t('catalogFetched')}:{' '}\n <strong>{catalog.fetchedAt > 0 ? formatTime(catalog.fetchedAt) : t('catalogNever')}</strong>{' '}\n {catalog.fetchedAt > 0 && (\n <span className={catalog.fresh ? 'mp-badge mp-badge-ok' : 'mp-badge mp-badge-warn'}>\n {catalog.fresh ? t('catalogFresh') : t('catalogStale')}\n </span>\n )}\n </span>\n <label className=\"mp-ttl\">\n {t('ttl')}\n <input\n className=\"mp-input mp-input-narrow\"\n type=\"number\"\n min={1}\n max={90}\n value={config.ttlDays}\n disabled={saving}\n onChange={(event) => {\n const days = Number(event.target.value)\n if (Number.isFinite(days) && days >= 1 && days <= 90) void onPatchConfig({ ttlDays: Math.round(days) })\n }}\n />\n </label>\n </div>\n <div className=\"mp-actions\">\n <button className=\"mp-button\" disabled={busy} onClick={() => void onRefresh()}>\n {busy ? t('refreshBusy') : t('refresh')}\n </button>\n <button className=\"mp-button mp-button-ghost\" disabled={busy} onClick={() => void onReconcile()}>\n {t('reconcileNow')}\n </button>\n </div>\n </section>\n\n <hr className=\"mp-divider\" />\n\n <section className=\"mp-group\">\n <h4 className=\"mp-group-title\">{t('providerMap')}</h4>\n <p className=\"mp-desc\">{t('providerMapDesc')}</p>\n {mappingRows.map((row, index) => (\n <div className=\"mp-map-row\" key={index}>\n <input\n className=\"mp-input\"\n value={row.from}\n disabled={saving}\n onChange={(event) => {\n const next = [...mappingRows]\n next[index] = { ...row, from: event.target.value }\n setMappingRows(next)\n }}\n />\n <span className=\"mp-map-arrow\">→</span>\n <input\n className=\"mp-input\"\n value={row.to}\n disabled={saving}\n onChange={(event) => {\n const next = [...mappingRows]\n next[index] = { ...row, to: event.target.value }\n setMappingRows(next)\n }}\n />\n <button\n className=\"mp-button mp-button-ghost\"\n disabled={saving}\n onClick={() => setMappingRows(mappingRows.filter((_, i) => i !== index))}\n >\n {t('removeMapping')}\n </button>\n </div>\n ))}\n <div className=\"mp-map-row\">\n <input\n className=\"mp-input\"\n placeholder=\"dsh provider\"\n value={newFrom}\n onChange={(event) => setNewFrom(event.target.value)}\n />\n <span className=\"mp-map-arrow\">→</span>\n <input\n className=\"mp-input\"\n placeholder=\"models.dev provider\"\n value={newTo}\n onChange={(event) => setNewTo(event.target.value)}\n />\n <button\n className=\"mp-button mp-button-ghost\"\n disabled={newFrom.trim().length === 0 || newTo.trim().length === 0}\n onClick={() => {\n setMappingRows([...mappingRows, { from: newFrom.trim(), to: newTo.trim() }])\n setNewFrom('')\n setNewTo('')\n }}\n >\n {t('addMapping')}\n </button>\n </div>\n <div className=\"mp-actions\">\n <button className=\"mp-button\" disabled={saving} onClick={() => void onApplyMappings()}>\n {saving ? t('saving') : '✓ ' + t('providerMap')}\n </button>\n </div>\n </section>\n\n <hr className=\"mp-divider\" />\n\n <section className=\"mp-group\">\n <h4 className=\"mp-group-title\">{t('report')}</h4>\n {report === null ? (\n <p className=\"mp-muted\">{t('reportNever')}</p>\n ) : (\n <ul className=\"mp-report\">\n <li>{t('reportAt')}: {formatTime(report.at)}</li>\n <li>{t('reportFilled')}: <strong>{report.filled}</strong></li>\n <li>{t('reportTouched')}: <strong>{report.touched}</strong></li>\n <li>\n {t('reportUnmatched')}:{' '}\n {report.unmatched.length === 0\n ? t('reportEmpty')\n : <code className=\"mp-code\">{report.unmatched.join(', ')}</code>}\n </li>\n </ul>\n )}\n </section>\n\n {error !== null && <p className=\"mp-error\">{error}</p>}\n </div>\n )\n}\n","/**\n * @fonlan/dsh-model-parameters client half: the plugin's own Settings section.\n *\n * Registers into the settings.section slot, so the page owns one entry in the\n * settings sidebar and renders in the panel's content column. The page itself\n * lives in ./settings-section.\n *\n * Deps resolved from the browser module table: react only. The slots service\n * is reached through the cordis context; types come from devDependencies and\n * are erased at build time.\n */\nimport type { Context } from '@deepseek-ai/cordis'\ntype ClientContext = Context\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport { LOCALE_NS, zh, en } from './locales'\nimport { makeSettingsSection } from './settings-section'\n\n/** The settings sidebar entry this page owns (must stay stable). */\nconst SECTION_ID = 'model-parameters'\n\n/** The slots service face (subset of @deepseek-ai/dsh-client-ui-slots). */\ninterface SlotEntry {\n name: string\n id?: string\n order?: number\n label?: () => string\n locale?: string\n}\n\ninterface Slots {\n inject(name: string, register: () => unknown): unknown\n register(def: SlotEntry, component: unknown): unknown\n}\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 ctx.effect(() => {\n const off = ctx.locale.register(LOCALE_NS, { zh, en })\n return () => off()\n }, 'model-parameters: dictionaries')\n\n const t = ctx.locale.bind(LOCALE_NS) as unknown as (key: string) => string\n const Section = makeSettingsSection(ctx)\n const slots = (ctx as unknown as { slots: Slots }).slots\n slots.inject('settings.section', () =>\n slots.register({\n name: 'settings.section',\n id: SECTION_ID,\n order: 320,\n label: () => t('settingsTitle'),\n locale: LOCALE_NS,\n }, Section),\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useCallback","useSyncExternalStore","useState"],"sources":["../../../src/client/locales.ts","../../../src/client/api.ts","../../../src/client/settings-section.tsx","../../../src/client/index.tsx"],"sourcesContent":["/**\n * Locale dictionaries for @fonlan/dsh-model-parameters settings page.\n * zh is the key source of truth; en must carry the exact same key set.\n */\nexport const LOCALE_NS = 'model-parameters'\n\nexport const zh = {\n settingsTitle: '模型参数补全',\n sectionSub: '补全字段、目录刷新、Provider 映射与补全报告。',\n master: '启用自动补全',\n masterDesc: '关闭后不再自动补全任何字段(已补全的值保留)。',\n fields: '补全字段',\n fillName: '显示名称',\n fillNameDesc: '模型条目缺 name 时写入目录中的显示名称。',\n fillContext: '最大上下文',\n fillContextDesc: '缺 contextWindow 时写入目录的上下文上限。',\n fillMaxTokens: '最大输出',\n fillMaxTokensDesc: '缺 maxTokens 时写入目录的输出上限。',\n fillReasoning: '推理强度',\n fillReasoningDesc: '缺 reasoningEfforts 时按目录 effort 档位写入 identity 字典(如 {high:\"high\", max:\"max\"})。',\n fillInput: '输入模态',\n fillInputDesc: '缺 input 时写入目录中的 text/image 模态。',\n ttl: '目录更新周期(天)',\n ttlDesc: '缓存的 models.dev 目录超过该天数后,在下次补全时自动刷新。',\n providerMap: 'Provider 映射',\n providerMapDesc: 'dsh provider → models.dev provider 的匹配覆盖(可选)。',\n addMapping: '添加映射',\n removeMapping: '移除',\n catalog: '模型目录',\n catalogEntries: '目录条目',\n catalogProviders: '覆盖 Provider',\n catalogFetched: '上次更新',\n catalogFresh: '新鲜',\n catalogStale: '已过期',\n catalogNever: '尚未获取',\n refresh: '立即更新目录并补全',\n refreshBusy: '更新中…',\n reconcileNow: '立即补全一次',\n report: '最近一次补全',\n reportNever: '尚未执行',\n reportFilled: '补全字段',\n reportTouched: '涉及 Provider',\n reportUnmatched: '未匹配模型',\n reportEmpty: '全部匹配,无未匹配项',\n reportAt: '时间',\n error: '加载失败',\n saving: '保存中…',\n officialProviders: '官方厂商优先级',\n officialProvidersDesc: '同一模型 id 在多处出现时,优先采用这些 models.dev provider 的条目。',\n} as const\n\nexport const en: Record<keyof typeof zh, string> = {\n settingsTitle: 'Model Parameters',\n sectionSub: 'Field toggles, catalog refresh, provider mapping and the fill report.',\n master: 'Enable auto-fill',\n masterDesc: 'When off, no fields are filled (already-filled values are kept).',\n fields: 'Fields to fill',\n fillName: 'Display name',\n fillNameDesc: 'Write the catalog display name when a model entry lacks name.',\n fillContext: 'Context window',\n fillContextDesc: 'Write the catalog context limit when contextWindow is missing.',\n fillMaxTokens: 'Max output',\n fillMaxTokensDesc: 'Write the catalog output limit when maxTokens is missing.',\n fillReasoning: 'Reasoning efforts',\n fillReasoningDesc: 'Write an identity reasoningEfforts dict (e.g. {high:\"high\", max:\"max\"}) from catalog effort levels when missing.',\n fillInput: 'Input modalities',\n fillInputDesc: 'Write text/image modalities from the catalog when input is missing.',\n ttl: 'Catalog refresh interval (days)',\n ttlDesc: 'The cached models.dev catalog is refreshed on the next fill once older than this many days.',\n providerMap: 'Provider mapping',\n providerMapDesc: 'Optional dsh provider → models.dev provider matching overrides.',\n addMapping: 'Add mapping',\n removeMapping: 'Remove',\n catalog: 'Model catalog',\n catalogEntries: 'Catalog entries',\n catalogProviders: 'Providers covered',\n catalogFetched: 'Last updated',\n catalogFresh: 'fresh',\n catalogStale: 'stale',\n catalogNever: 'not fetched yet',\n refresh: 'Update catalog & fill now',\n refreshBusy: 'Updating…',\n reconcileNow: 'Fill now',\n report: 'Last fill',\n reportNever: 'never run',\n reportFilled: 'fields filled',\n reportTouched: 'providers touched',\n reportUnmatched: 'unmatched models',\n reportEmpty: 'all matched, nothing unmatched',\n reportAt: 'time',\n error: 'Failed to load',\n saving: 'Saving…',\n officialProviders: 'Official provider priority',\n officialProvidersDesc: 'When one model id appears under several providers, prefer entries from these models.dev providers.',\n}\n","/** Client transport for the model-parameters settings API. */\nimport type { ModelParametersConfig } from '../shared/config.js'\n\nexport interface CatalogView {\n entries: number\n providers: number\n fetchedAt: number\n fresh: boolean\n ttlDays: number\n}\n\nexport interface FillReportView {\n at: number\n filled: number\n touched: number\n unmatched: string[]\n}\n\nexport interface PluginState {\n config: ModelParametersConfig\n catalog: CatalogView\n report: FillReportView | null\n}\n\nexport class ModelParametersApiError 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-parameters/api/${method}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n } catch (error) {\n throw new ModelParametersApiError('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 ModelParametersApiError(\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<PluginState>('state'),\n refresh: () => call<PluginState>('refresh'),\n reconcile: () => call<PluginState>('reconcile'),\n setConfig: (patch: Partial<ModelParametersConfig>) =>\n call<PluginState>('set-config', { patch }),\n}\n","/**\n * The Model Parameters settings page (设置 → 侧栏「模型参数补全」).\n *\n * Registers into the `settings.section` slot, so the page owns one entry in\n * the settings sidebar and renders its content in the panel's content column.\n * Content: master + per-field toggles, catalog TTL, provider→models.dev\n * mapping overrides, a manual refresh/fill action, the catalog freshness line,\n * and the last fill report (filled / touched / unmatched).\n *\n * All reads/mutations go through the plugin's fenced API; config persists into\n * the `model-parameters` profile entry — the plugin's own loader entry config,\n * written through the host settings service.\n */\nimport { useCallback, useEffect, useState, useSyncExternalStore } from 'react'\nimport type { Context } from '@deepseek-ai/cordis'\ntype ClientContext = Context\nimport type { Translate } from '@deepseek-ai/dsh-client-ui-slots'\nimport { LOCALE_NS } from './locales'\nimport { api, type PluginState } from './api'\nimport type { ModelParametersConfig } from '../shared/config.js'\nimport './settings-section.css'\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\nfunction formatTime(epochMs: number): string {\n try {\n return new Date(epochMs).toLocaleString()\n } catch {\n return String(epochMs)\n }\n}\n\nexport function makeSettingsSection(ctx: ClientContext): () => JSX.Element {\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 ModelParametersSettingsSection(): JSX.Element {\n useLocaleRevision(ctx)\n const [state, setState] = useState<PluginState | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(true)\n const [busy, setBusy] = useState(false)\n const [saving, setSaving] = useState(false)\n const [mappingRows, setMappingRows] = useState<Array<{ from: string; to: string }>>([])\n const [newFrom, setNewFrom] = useState('')\n const [newTo, setNewTo] = useState('')\n\n const load = useCallback(async () => {\n setLoading(true)\n try {\n const next = await api.state()\n setState(next)\n setMappingRows(Object.entries(next.config.providerMap).map(([from, to]) => ({ from, to })))\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 patchConfig = useCallback(async (patch: Partial<ModelParametersConfig>) => {\n setSaving(true)\n try {\n const next = await api.setConfig(patch)\n setState(next)\n setMappingRows(Object.entries(next.config.providerMap).map(([from, to]) => ({ from, to })))\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setSaving(false)\n }\n }, [])\n\n const refresh = useCallback(async () => {\n setBusy(true)\n try {\n setState(await api.refresh())\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setBusy(false)\n }\n }, [])\n\n const reconcileNow = useCallback(async () => {\n setBusy(true)\n try {\n setState(await api.reconcile())\n setError(null)\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause))\n } finally {\n setBusy(false)\n }\n }, [])\n\n const applyMappings = useCallback(async () => {\n const providerMap: Record<string, string> = {}\n for (const row of mappingRows) {\n const from = row.from.trim()\n const to = row.to.trim()\n if (from.length > 0 && to.length > 0) providerMap[from] = to\n }\n await patchConfig({ providerMap })\n }, [mappingRows, patchConfig])\n\n return (\n <div className=\"mp-page\">\n <header className=\"mp-page-head\">\n <h3 className=\"mp-page-title\">{t('settingsTitle')}</h3>\n <p className=\"mp-page-sub\">{t('sectionSub')}</p>\n </header>\n {error !== null && state === null ? (\n <p className=\"mp-error\">{t('error')}: {error}</p>\n ) : loading || state === null ? (\n <p className=\"mp-muted\">{t('saving')}</p>\n ) : (\n <ModelParametersBody\n t={t}\n state={state}\n error={error}\n saving={saving}\n busy={busy}\n mappingRows={mappingRows}\n newFrom={newFrom}\n newTo={newTo}\n setNewFrom={setNewFrom}\n setNewTo={setNewTo}\n setMappingRows={setMappingRows}\n onPatchConfig={patchConfig}\n onRefresh={refresh}\n onReconcile={reconcileNow}\n onApplyMappings={applyMappings}\n />\n )}\n </div>\n )\n }\n}\n\n/** The page body: divider-separated control groups. */\nfunction ModelParametersBody(props: {\n t: Translate\n state: PluginState\n error: string | null\n saving: boolean\n busy: boolean\n mappingRows: Array<{ from: string; to: string }>\n newFrom: string\n newTo: string\n setNewFrom: (value: string) => void\n setNewTo: (value: string) => void\n setMappingRows: (rows: Array<{ from: string; to: string }>) => void\n onPatchConfig: (patch: Partial<ModelParametersConfig>) => Promise<void>\n onRefresh: () => Promise<void>\n onReconcile: () => Promise<void>\n onApplyMappings: () => Promise<void>\n}): JSX.Element {\n const {\n t, state, error, saving, busy,\n mappingRows, newFrom, newTo,\n setNewFrom, setNewTo, setMappingRows,\n onPatchConfig, onRefresh, onReconcile, onApplyMappings,\n } = props\n\n const config = state.config\n const catalog = state.catalog\n const report = state.report\n\n return (\n <div className=\"mp-body\">\n <section className=\"mp-group\">\n <h4 className=\"mp-group-title\">{t('fields')}</h4>\n <label className=\"mp-toggle\">\n <input\n type=\"checkbox\"\n checked={config.enabled}\n disabled={saving}\n onChange={(event) => void onPatchConfig({ enabled: event.target.checked })}\n />\n <span className=\"mp-toggle-body\">\n <span className=\"mp-toggle-label\">{t('master')}</span>\n <span className=\"mp-toggle-desc\">{t('masterDesc')}</span>\n </span>\n </label>\n {([\n ['fillName', t('fillName'), t('fillNameDesc')],\n ['fillContext', t('fillContext'), t('fillContextDesc')],\n ['fillMaxTokens', t('fillMaxTokens'), t('fillMaxTokensDesc')],\n ['fillReasoning', t('fillReasoning'), t('fillReasoningDesc')],\n ['fillInput', t('fillInput'), t('fillInputDesc')],\n ] as const).map(([key, label, desc]) => (\n <label className=\"mp-toggle mp-toggle-sub\" key={key}>\n <input\n type=\"checkbox\"\n checked={config[key]}\n disabled={saving || !config.enabled}\n onChange={(event) => void onPatchConfig({ [key]: event.target.checked })}\n />\n <span className=\"mp-toggle-body\">\n <span className=\"mp-toggle-label\">{label}</span>\n <span className=\"mp-toggle-desc\">{desc}</span>\n </span>\n </label>\n ))}\n </section>\n\n <hr className=\"mp-divider\" />\n\n <section className=\"mp-group\">\n <h4 className=\"mp-group-title\">{t('catalog')}</h4>\n <div className=\"mp-meta\">\n <span>{t('catalogEntries')}: <strong>{catalog.entries.toLocaleString()}</strong></span>\n <span>{t('catalogProviders')}: <strong>{catalog.providers.toLocaleString()}</strong></span>\n <span>\n {t('catalogFetched')}:{' '}\n <strong>{catalog.fetchedAt > 0 ? formatTime(catalog.fetchedAt) : t('catalogNever')}</strong>{' '}\n {catalog.fetchedAt > 0 && (\n <span className={catalog.fresh ? 'mp-badge mp-badge-ok' : 'mp-badge mp-badge-warn'}>\n {catalog.fresh ? t('catalogFresh') : t('catalogStale')}\n </span>\n )}\n </span>\n <label className=\"mp-ttl\">\n {t('ttl')}\n <input\n className=\"mp-input mp-input-narrow\"\n type=\"number\"\n min={1}\n max={90}\n value={config.ttlDays}\n disabled={saving}\n onChange={(event) => {\n const days = Number(event.target.value)\n if (Number.isFinite(days) && days >= 1 && days <= 90) void onPatchConfig({ ttlDays: Math.round(days) })\n }}\n />\n </label>\n </div>\n <div className=\"mp-actions\">\n <button className=\"mp-button\" disabled={busy} onClick={() => void onRefresh()}>\n {busy ? t('refreshBusy') : t('refresh')}\n </button>\n <button className=\"mp-button mp-button-ghost\" disabled={busy} onClick={() => void onReconcile()}>\n {t('reconcileNow')}\n </button>\n </div>\n </section>\n\n <hr className=\"mp-divider\" />\n\n <section className=\"mp-group\">\n <h4 className=\"mp-group-title\">{t('providerMap')}</h4>\n <p className=\"mp-desc\">{t('providerMapDesc')}</p>\n {mappingRows.map((row, index) => (\n <div className=\"mp-map-row\" key={index}>\n <input\n className=\"mp-input\"\n value={row.from}\n disabled={saving}\n onChange={(event) => {\n const next = [...mappingRows]\n next[index] = { ...row, from: event.target.value }\n setMappingRows(next)\n }}\n />\n <span className=\"mp-map-arrow\">→</span>\n <input\n className=\"mp-input\"\n value={row.to}\n disabled={saving}\n onChange={(event) => {\n const next = [...mappingRows]\n next[index] = { ...row, to: event.target.value }\n setMappingRows(next)\n }}\n />\n <button\n className=\"mp-button mp-button-ghost\"\n disabled={saving}\n onClick={() => setMappingRows(mappingRows.filter((_, i) => i !== index))}\n >\n {t('removeMapping')}\n </button>\n </div>\n ))}\n <div className=\"mp-map-row\">\n <input\n className=\"mp-input\"\n placeholder=\"dsh provider\"\n value={newFrom}\n onChange={(event) => setNewFrom(event.target.value)}\n />\n <span className=\"mp-map-arrow\">→</span>\n <input\n className=\"mp-input\"\n placeholder=\"models.dev provider\"\n value={newTo}\n onChange={(event) => setNewTo(event.target.value)}\n />\n <button\n className=\"mp-button mp-button-ghost\"\n disabled={newFrom.trim().length === 0 || newTo.trim().length === 0}\n onClick={() => {\n setMappingRows([...mappingRows, { from: newFrom.trim(), to: newTo.trim() }])\n setNewFrom('')\n setNewTo('')\n }}\n >\n {t('addMapping')}\n </button>\n </div>\n <div className=\"mp-actions\">\n <button className=\"mp-button\" disabled={saving} onClick={() => void onApplyMappings()}>\n {saving ? t('saving') : '✓ ' + t('providerMap')}\n </button>\n </div>\n </section>\n\n <hr className=\"mp-divider\" />\n\n <section className=\"mp-group\">\n <h4 className=\"mp-group-title\">{t('report')}</h4>\n {report === null ? (\n <p className=\"mp-muted\">{t('reportNever')}</p>\n ) : (\n <ul className=\"mp-report\">\n <li>{t('reportAt')}: {formatTime(report.at)}</li>\n <li>{t('reportFilled')}: <strong>{report.filled}</strong></li>\n <li>{t('reportTouched')}: <strong>{report.touched}</strong></li>\n <li>\n {t('reportUnmatched')}:{' '}\n {report.unmatched.length === 0\n ? t('reportEmpty')\n : <code className=\"mp-code\">{report.unmatched.join(', ')}</code>}\n </li>\n </ul>\n )}\n </section>\n\n {error !== null && <p className=\"mp-error\">{error}</p>}\n </div>\n )\n}\n","/**\n * @fonlan/dsh-model-parameters client half: the plugin's own Settings section.\n *\n * Registers into the settings.section slot, so the page owns one entry in the\n * settings sidebar and renders in the panel's content column. The page itself\n * lives in ./settings-section.\n *\n * Deps resolved from the browser module table: react only. The slots service\n * is reached through the cordis context; types come from devDependencies and\n * are erased at build time.\n */\nimport type { Context } from '@deepseek-ai/cordis'\ntype ClientContext = Context\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport { LOCALE_NS, zh, en } from './locales'\nimport { makeSettingsSection } from './settings-section'\n\n/** The settings sidebar entry this page owns (must stay stable). */\nconst SECTION_ID = 'model-parameters'\n\n/** The slots service face (subset of @deepseek-ai/dsh-client-ui-slots). */\ninterface SlotEntry {\n name: string\n id?: string\n order?: number\n label?: () => string\n locale?: string\n}\n\ninterface Slots {\n inject(name: string, register: () => unknown): unknown\n register(def: SlotEntry, component: unknown): unknown\n}\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 ctx.effect(() => {\n const off = ctx.locale.register(LOCALE_NS, { zh, en })\n return () => off()\n }, 'model-parameters: dictionaries')\n\n const t = ctx.locale.bind(LOCALE_NS) as unknown as (key: string) => string\n const Section = makeSettingsSection(ctx)\n const slots = (ctx as unknown as { slots: Slots }).slots\n slots.inject('settings.section', () =>\n slots.register({\n name: 'settings.section',\n id: SECTION_ID,\n order: 320,\n label: () => t('settingsTitle'),\n locale: LOCALE_NS,\n }, Section),\n )\n}\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -6,13 +6,16 @@ import { join } from "node:path";
6
6
  /**
7
7
  * Plugin configuration and shared constants for @fonlan/dsh-model-parameters.
8
8
  *
9
- * The plugin owns the `model-parameters` settings namespace (stored in
10
- * settings.yaml) and OBSERVES the `llm-pi-ai` namespace: whenever provider
11
- * models are written there with missing capability fields (which is the norm
12
- * for a freshly synced gateway), it patches the missing fields from the cached
13
- * models.dev catalog.
9
+ * The plugin's own settings ARE its loader entry config (DSH >= 0.1.7): the
10
+ * profile entry whose id is `model-parameters` (see cordis.patch.yml) is
11
+ * validated with {@link Config}, the resolved document is handed to
12
+ * `apply(ctx, config)`, and the settings page is derived from that schema. The
13
+ * plugin OBSERVES the `llm-pi-ai` namespace: whenever provider models are
14
+ * written there with missing capability fields (which is the norm for a freshly
15
+ * synced gateway), it patches the missing fields from the cached models.dev
16
+ * catalog.
14
17
  */
15
- /** This plugin's own settings namespace. */
18
+ /** This plugin's own settings namespace: its profile entry id. */
16
19
  const PLUGIN_NS = "model-parameters";
17
20
  /** The namespace whose models this plugin completes: pi-ai provider profiles. */
18
21
  const TARGET_NS = "llm-pi-ai";
@@ -56,16 +59,17 @@ const REASONING_LEVELS = [
56
59
  "xhigh",
57
60
  "max"
58
61
  ];
62
+ /** The plugin's `Config` schema, exported for the loader and the settings page. */
59
63
  const Config = z.object({
60
- enabled: z.boolean().default(true),
61
- ttlDays: z.number().min(1).max(90).default(7),
62
- fillName: z.boolean().default(true),
63
- fillContext: z.boolean().default(true),
64
- fillMaxTokens: z.boolean().default(true),
65
- fillReasoning: z.boolean().default(true),
66
- fillInput: z.boolean().default(true),
67
- providerMap: z.dict(z.string()).default({}),
68
- officialProviders: z.array(z.string()).default([...DEFAULT_OFFICIAL_PROVIDERS])
64
+ enabled: z.boolean().default(true).volatile(),
65
+ ttlDays: z.number().min(1).max(90).default(7).volatile(),
66
+ fillName: z.boolean().default(true).volatile(),
67
+ fillContext: z.boolean().default(true).volatile(),
68
+ fillMaxTokens: z.boolean().default(true).volatile(),
69
+ fillReasoning: z.boolean().default(true).volatile(),
70
+ fillInput: z.boolean().default(true).volatile(),
71
+ providerMap: z.dict(z.string()).default({}).volatile(),
72
+ officialProviders: z.array(z.string()).default([...DEFAULT_OFFICIAL_PROVIDERS]).volatile()
69
73
  });
70
74
  const DEFAULT_CONFIG = {
71
75
  enabled: true,
@@ -78,6 +82,44 @@ const DEFAULT_CONFIG = {
78
82
  providerMap: {},
79
83
  officialProviders: [...DEFAULT_OFFICIAL_PROVIDERS]
80
84
  };
85
+ /**
86
+ * Read a config node that may be a plain value or a live (`.volatile()`) node.
87
+ *
88
+ * A plugin whose Config declares volatile fields receives a reactive config
89
+ * whose fields expose `get()`, while hand-written config objects and tests pass
90
+ * plain values. Both shapes are supported here so the same code path serves the
91
+ * settings page, `cordis.patch.yml`, and the unit tests.
92
+ */
93
+ function liveValue(node, fallback) {
94
+ if (node === void 0 || node === null) return fallback;
95
+ const getter = node.get;
96
+ if (typeof getter === "function") {
97
+ const value = getter.call(node);
98
+ return value === void 0 ? fallback : value;
99
+ }
100
+ return node;
101
+ }
102
+ /**
103
+ * Project the loader entry config into the plain document shape, per field
104
+ * falling back to {@link DEFAULT_CONFIG}. The config is re-read on EVERY call,
105
+ * so settings-page edits (which the loader swaps into the live cells in place)
106
+ * take effect immediately. Container fields are copied so callers can never
107
+ * mutate the live document through a returned reference.
108
+ */
109
+ function readConfig(config) {
110
+ const section = config ?? {};
111
+ return {
112
+ enabled: liveValue(section.enabled, DEFAULT_CONFIG.enabled),
113
+ ttlDays: liveValue(section.ttlDays, DEFAULT_CONFIG.ttlDays),
114
+ fillName: liveValue(section.fillName, DEFAULT_CONFIG.fillName),
115
+ fillContext: liveValue(section.fillContext, DEFAULT_CONFIG.fillContext),
116
+ fillMaxTokens: liveValue(section.fillMaxTokens, DEFAULT_CONFIG.fillMaxTokens),
117
+ fillReasoning: liveValue(section.fillReasoning, DEFAULT_CONFIG.fillReasoning),
118
+ fillInput: liveValue(section.fillInput, DEFAULT_CONFIG.fillInput),
119
+ providerMap: { ...liveValue(section.providerMap, DEFAULT_CONFIG.providerMap) },
120
+ officialProviders: [...liveValue(section.officialProviders, DEFAULT_CONFIG.officialProviders)]
121
+ };
122
+ }
81
123
  //#endregion
82
124
  //#region src/server/catalog.ts
83
125
  /**
@@ -309,6 +351,7 @@ const TARGET = TARGET_NS;
309
351
  const OWN = PLUGIN_NS;
310
352
  var ModelParametersService = class {
311
353
  ctx;
354
+ entryConfig;
312
355
  /**
313
356
  * The settings service value captured from the inject callback. The plugin
314
357
  * itself declares `inject: []` (settings is optional — headless profiles
@@ -317,42 +360,45 @@ var ModelParametersService = class {
317
360
  * a sibling fiber. The injected child ctx itself is only valid while its
318
361
  * fiber is active — the provider can be reloaded (and every dependent
319
362
  * fiber torn down) while an async reconcile is in flight — so all reads
320
- * and writes go through the captured service value, which is a plain
321
- * object and never walks the fiber chain.
363
+ * and writes of the `llm-pi-ai` namespace go through the captured service
364
+ * value, which is a plain object and never walks the fiber chain.
322
365
  */
323
366
  settings;
324
- scope;
325
367
  lastReport = null;
326
368
  catalog;
327
369
  reconciling = null;
328
- constructor(ctx) {
370
+ /**
371
+ * @param ctx the plugin context.
372
+ * @param entryConfig this plugin's own live entry config (the profile entry
373
+ * `model-parameters`): the resolved `Config` document, whose `.volatile()`
374
+ * fields are live cells that the loader swaps in place on every settings
375
+ * write. Read through {@link readConfig} on each access, never cached.
376
+ */
377
+ constructor(ctx, entryConfig) {
329
378
  this.ctx = ctx;
379
+ this.entryConfig = entryConfig;
330
380
  this.catalog = new CatalogManager(ctx.logger);
331
381
  }
332
382
  start() {
333
383
  this.ctx.inject(["settings"], (sctx) => {
334
384
  this.settings = sctx.settings;
335
- this.scope = this.settings.register(OWN, Config);
336
385
  this.reconcile();
337
386
  });
338
- this.ctx.on("settings/updated", (ns) => {
387
+ this.ctx.on("settings/document-updated", (ns) => {
339
388
  if (ns !== TARGET) return;
340
389
  this.reconcile();
341
390
  });
342
391
  }
343
392
  stop() {
344
- this.scope = void 0;
345
393
  this.settings = void 0;
346
394
  }
395
+ /**
396
+ * The plugin's current settings, projected from the live entry config.
397
+ * Works with no settings service at all (headless), which is why the plain
398
+ * {@link DEFAULT_CONFIG} fallback lives in {@link readConfig}.
399
+ */
347
400
  config() {
348
- if (this.scope !== void 0) try {
349
- return this.scope.get();
350
- } catch {}
351
- return {
352
- ...DEFAULT_CONFIG,
353
- providerMap: {},
354
- officialProviders: [...DEFAULT_CONFIG.officialProviders]
355
- };
401
+ return readConfig(this.entryConfig);
356
402
  }
357
403
  catalogSnapshot() {
358
404
  return this.catalog.snapshotOf();
@@ -365,14 +411,21 @@ var ModelParametersService = class {
365
411
  await this.catalog.refresh(this.config().ttlDays);
366
412
  await this.reconcile();
367
413
  }
368
- /** Update plugin config toggles from the settings page. */
414
+ /**
415
+ * Update the plugin's own settings from the settings page. `dsh-settings`
416
+ * addresses the plugin's loader entry by its id and refuses any path the
417
+ * `Config` schema did not mark volatile — every field here is volatile.
418
+ */
369
419
  async updateConfig(patch) {
370
- if (this.scope === void 0) throw new Error("model-parameters: settings service is not available in this profile");
371
- const current = this.config();
372
- await this.scope.update({
373
- ...current,
374
- ...patch
375
- });
420
+ const settings = this.settings;
421
+ if (settings === void 0) throw new Error("model-parameters: settings service is not available in this profile");
422
+ const { providerMap, ...rest } = patch;
423
+ if (Object.keys(rest).length > 0) await settings.update(OWN, rest);
424
+ if (providerMap !== void 0) await settings.mutate(OWN, [{
425
+ op: "set",
426
+ path: ["providerMap"],
427
+ value: providerMap
428
+ }]);
376
429
  }
377
430
  /**
378
431
  * Fill missing fields on every provider model from the catalog. Serialized:
@@ -622,8 +675,8 @@ function stateOf(service) {
622
675
  //#region src/index.ts
623
676
  const name = "@fonlan/dsh-model-parameters";
624
677
  const inject = [];
625
- function apply(ctx) {
626
- const service = new ModelParametersService(ctx);
678
+ function apply(ctx, config = {}) {
679
+ const service = new ModelParametersService(ctx, config);
627
680
  ctx.effect(() => {
628
681
  service.start();
629
682
  return () => service.stop();
@@ -6,15 +6,15 @@
6
6
  * tokens / reasoning efforts / input modalities (the norm for a freshly
7
7
  * synced gateway), it fills the missing fields from a cached models.dev
8
8
  * catalog (TTL-refreshed), and serves the fenced JSON API the web settings
9
- * page calls. The plugin's own toggles live in the `model-parameters`
10
- * settings namespace (settings.yaml).
9
+ * page calls. The plugin's own toggles are its loader entry config (the
10
+ * `model-parameters` profile entry, schema {@link Config}).
11
11
  */
12
12
  import type { Context } from '@deepseek-ai/cordis';
13
- import { Config } from './shared/config.js';
13
+ import { Config, type ModelParametersLiveConfig } from './shared/config.js';
14
14
  import { CatalogManager, acceptedInputModalities, identityReasoningEfforts, type CatalogEntry, type CatalogSnapshot } from './server/catalog.js';
15
15
  export declare const name = "@fonlan/dsh-model-parameters";
16
16
  export declare const inject: string[];
17
17
  export { Config };
18
18
  export { CatalogManager, acceptedInputModalities, identityReasoningEfforts };
19
19
  export type { CatalogEntry, CatalogSnapshot };
20
- export declare function apply(ctx: Context): void;
20
+ export declare function apply(ctx: Context, config?: ModelParametersLiveConfig): void;
@@ -1,19 +1,19 @@
1
1
  /**
2
- * The model-parameters service: owns the plugin settings namespace, watches
3
- * the `llm-pi-ai` namespace for provider model writes, reconciles missing
4
- * fields from the cached models.dev catalog, and tracks a fill report.
2
+ * The model-parameters service: completes missing model fields in the
3
+ * `llm-pi-ai` settings namespace from the cached models.dev catalog and tracks
4
+ * a fill report. Its own settings ARE this plugin's loader entry config (see
5
+ * shared/config.ts), so there is no per-plugin settings scope to own here.
5
6
  *
6
7
  * Trigger model:
7
8
  * - any committed `llm-pi-ai` settings change (settings UI apply, manual
8
- * settings.yaml edit published by the file provider, model-router-style
9
- * external writes) runs a reconcile;
9
+ * profile-patch edit, model-router-style external writes) runs a reconcile;
10
10
  * - plugin start runs a backfill reconcile over the existing configuration.
11
11
  * Reconcile is fill-only: it never overwrites a present field, and it writes
12
12
  * nothing when nothing is missing — so its own writes cannot re-trigger a
13
13
  * second patch (the next event sees the fields present and produces no ops).
14
14
  */
15
15
  import type { Context } from '@deepseek-ai/cordis';
16
- import { type ModelParametersConfig } from '../shared/config.js';
16
+ import { type ModelParametersConfig, type ModelParametersLiveConfig } from '../shared/config.js';
17
17
  import { type CatalogSnapshot } from './catalog.js';
18
18
  /** One reconcile run's outcome, surfaced to the settings page. */
19
19
  export interface FillReport {
@@ -27,6 +27,7 @@ export interface FillReport {
27
27
  }
28
28
  export declare class ModelParametersService {
29
29
  private readonly ctx;
30
+ private readonly entryConfig;
30
31
  /**
31
32
  * The settings service value captured from the inject callback. The plugin
32
33
  * itself declares `inject: []` (settings is optional — headless profiles
@@ -35,23 +36,38 @@ export declare class ModelParametersService {
35
36
  * a sibling fiber. The injected child ctx itself is only valid while its
36
37
  * fiber is active — the provider can be reloaded (and every dependent
37
38
  * fiber torn down) while an async reconcile is in flight — so all reads
38
- * and writes go through the captured service value, which is a plain
39
- * object and never walks the fiber chain.
39
+ * and writes of the `llm-pi-ai` namespace go through the captured service
40
+ * value, which is a plain object and never walks the fiber chain.
40
41
  */
41
42
  private settings;
42
- private scope;
43
43
  private lastReport;
44
44
  private readonly catalog;
45
45
  private reconciling;
46
- constructor(ctx: Context);
46
+ /**
47
+ * @param ctx the plugin context.
48
+ * @param entryConfig this plugin's own live entry config (the profile entry
49
+ * `model-parameters`): the resolved `Config` document, whose `.volatile()`
50
+ * fields are live cells that the loader swaps in place on every settings
51
+ * write. Read through {@link readConfig} on each access, never cached.
52
+ */
53
+ constructor(ctx: Context, entryConfig: ModelParametersLiveConfig | undefined);
47
54
  start(): void;
48
55
  stop(): void;
56
+ /**
57
+ * The plugin's current settings, projected from the live entry config.
58
+ * Works with no settings service at all (headless), which is why the plain
59
+ * {@link DEFAULT_CONFIG} fallback lives in {@link readConfig}.
60
+ */
49
61
  config(): ModelParametersConfig;
50
62
  catalogSnapshot(): CatalogSnapshot;
51
63
  report(): FillReport | null;
52
64
  /** Force a catalog refresh, then reconcile and report. */
53
65
  refreshAndReconcile(): Promise<void>;
54
- /** Update plugin config toggles from the settings page. */
66
+ /**
67
+ * Update the plugin's own settings from the settings page. `dsh-settings`
68
+ * addresses the plugin's loader entry by its id and refuses any path the
69
+ * `Config` schema did not mark volatile — every field here is volatile.
70
+ */
55
71
  updateConfig(patch: Partial<ModelParametersConfig>): Promise<void>;
56
72
  /**
57
73
  * Fill missing fields on every provider model from the catalog. Serialized:
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * Plugin configuration and shared constants for @fonlan/dsh-model-parameters.
3
3
  *
4
- * The plugin owns the `model-parameters` settings namespace (stored in
5
- * settings.yaml) and OBSERVES the `llm-pi-ai` namespace: whenever provider
6
- * models are written there with missing capability fields (which is the norm
7
- * for a freshly synced gateway), it patches the missing fields from the cached
8
- * models.dev catalog.
4
+ * The plugin's own settings ARE its loader entry config (DSH >= 0.1.7): the
5
+ * profile entry whose id is `model-parameters` (see cordis.patch.yml) is
6
+ * validated with {@link Config}, the resolved document is handed to
7
+ * `apply(ctx, config)`, and the settings page is derived from that schema. The
8
+ * plugin OBSERVES the `llm-pi-ai` namespace: whenever provider models are
9
+ * written there with missing capability fields (which is the norm for a freshly
10
+ * synced gateway), it patches the missing fields from the cached models.dev
11
+ * catalog.
9
12
  */
10
13
  import z from '@deepseek-ai/schemastery';
11
- /** This plugin's own settings namespace. */
14
+ /** This plugin's own settings namespace: its profile entry id. */
12
15
  export declare const PLUGIN_NS = "model-parameters";
13
16
  /** The namespace whose models this plugin completes: pi-ai provider profiles. */
14
17
  export declare const TARGET_NS = "llm-pi-ai";
@@ -28,7 +31,7 @@ export declare const ACCEPTED_INPUT_MODALITIES: readonly string[];
28
31
  * this set are written.
29
32
  */
30
33
  export declare const REASONING_LEVELS: readonly string[];
31
- /** Plugin configuration shape. */
34
+ /** Plugin configuration shape: the PLAIN document, with no live handles in it. */
32
35
  export interface ModelParametersConfig {
33
36
  /** Master switch: when off, no fills happen (listener is inert). */
34
37
  enabled: boolean;
@@ -49,5 +52,84 @@ export interface ModelParametersConfig {
49
52
  /** models.dev provider ids treated as first-party vendors for tie-breaking. */
50
53
  officialProviders: string[];
51
54
  }
52
- export declare const Config: z<ModelParametersConfig>;
55
+ /**
56
+ * Recursively readonly value a live handle returns — cosmokit's
57
+ * `VolatileSnapshot<T>`, spelled locally for the same reason as
58
+ * {@link LiveSettingsField}.
59
+ */
60
+ export type LiveSettingsSnapshot<T> = T extends object ? {
61
+ readonly [K in keyof T]: LiveSettingsSnapshot<T[K]>;
62
+ } : T;
63
+ /**
64
+ * One `volatile()` config field as the loader hands it to `apply()`: a live
65
+ * handle, not the value it currently holds. Spelled structurally rather than as
66
+ * cosmokit's `Volatile<T>` on purpose — cosmokit is a transitive dependency
67
+ * here, and importing it would put a second schemastery/cosmokit pair into the
68
+ * emitted declaration.
69
+ */
70
+ export interface LiveSettingsField<T> {
71
+ get(): LiveSettingsSnapshot<T>;
72
+ }
73
+ /** A hand-written entry config (profile patch / test fixture): every field is optional. */
74
+ export type ModelParametersConfigInput = {
75
+ enabled?: boolean | null;
76
+ ttlDays?: number | null;
77
+ fillName?: boolean | null;
78
+ fillContext?: boolean | null;
79
+ fillMaxTokens?: boolean | null;
80
+ fillReasoning?: boolean | null;
81
+ fillInput?: boolean | null;
82
+ providerMap?: Record<string, string> | null;
83
+ officialProviders?: string[] | null;
84
+ };
85
+ /** The resolved entry config `apply()` receives: every field is a live handle. */
86
+ export type ResolvedModelParametersConfig = {
87
+ enabled: LiveSettingsField<boolean>;
88
+ ttlDays: LiveSettingsField<number>;
89
+ fillName: LiveSettingsField<boolean>;
90
+ fillContext: LiveSettingsField<boolean>;
91
+ fillMaxTokens: LiveSettingsField<boolean>;
92
+ fillReasoning: LiveSettingsField<boolean>;
93
+ fillInput: LiveSettingsField<boolean>;
94
+ providerMap: LiveSettingsField<Record<string, string>>;
95
+ officialProviders: LiveSettingsField<string[]>;
96
+ };
97
+ /**
98
+ * Entry config schema. Deliberately NOT annotated `z<ModelParametersConfig>`:
99
+ * schemastery >= 3.18.3 carries a third `Mode` type parameter, so a
100
+ * `.volatile()` field's OUTPUT type is `Volatile<T>` (a live `{ get() }` cell),
101
+ * and that annotation no longer compiles. The plain document shape stays
102
+ * {@link ModelParametersConfig} and is what {@link readConfig} projects to.
103
+ *
104
+ * The annotation below names the type through the package's own default export
105
+ * (`z<...>`), with locally declared type arguments: the bare inferred type of a
106
+ * volatile schema resolves through a schemastery copy this package does not
107
+ * import, so `tsc -p tsconfig.build.json` (`declaration: true`) could not name
108
+ * it (TS2742) — the emitted declaration stays portable this way.
109
+ *
110
+ * `.volatile()` is REQUIRED on every field the settings page may write, not
111
+ * cosmetic: `dsh-settings` derives `volatileForm(schema)` from this schema, and
112
+ * with no volatile field at all it SKIPS this entry in `describe()` (the
113
+ * settings page disappears) and REJECTS every write as a non-volatile path.
114
+ * The profile entry id (`model-parameters`, see cordis.patch.yml) is the
115
+ * namespace those writes address.
116
+ */
117
+ export type ModelParametersConfigSchema = z<ModelParametersConfigInput, ResolvedModelParametersConfig>;
118
+ /** The plugin's `Config` schema, exported for the loader and the settings page. */
119
+ export declare const Config: ModelParametersConfigSchema;
53
120
  export declare const DEFAULT_CONFIG: ModelParametersConfig;
121
+ /**
122
+ * The entry config as `apply()` receives it. Every field may arrive either as a
123
+ * live `Volatile` cell or as a plain value (a hand-built config object, a
124
+ * profile patch written before the volatile marker existed, or a host that
125
+ * resolves the document without the wrapper).
126
+ */
127
+ export type ModelParametersLiveConfig = Partial<Record<keyof ModelParametersConfig, unknown>>;
128
+ /**
129
+ * Project the loader entry config into the plain document shape, per field
130
+ * falling back to {@link DEFAULT_CONFIG}. The config is re-read on EVERY call,
131
+ * so settings-page edits (which the loader swaps into the live cells in place)
132
+ * take effect immediately. Container fields are copied so callers can never
133
+ * mutate the live document through a returned reference.
134
+ */
135
+ export declare function readConfig(config: ModelParametersLiveConfig | undefined): ModelParametersConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonlan/dsh-model-parameters",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "DSH plugin: when a provider's model sync returns only ids, automatically fill display name, context window, max output tokens, reasoning efforts, and input modalities from the models.dev catalog (cached, TTL-refreshed), and surface a settings page with toggles, refresh, and a fill report.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -69,13 +69,13 @@
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@deepseek-ai/cordis": "^4.0.1",
72
- "@deepseek-ai/schemastery": "^3.18.2",
73
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
74
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
75
- "@deepseek-ai/dsh-llm-pi-ai": "^0.1.2-alpha.2",
76
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
77
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
78
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2",
72
+ "@deepseek-ai/schemastery": "^3.18.4",
73
+ "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
74
+ "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
75
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
76
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
77
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
78
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
79
79
  "react": "^18.2.0",
80
80
  "react-dom": "^18.2.0"
81
81
  },
@@ -86,13 +86,13 @@
86
86
  "tsdown": "^0.22.14",
87
87
  "typescript": "^5.9.0",
88
88
  "@deepseek-ai/cordis": "^4.0.1",
89
- "@deepseek-ai/schemastery": "^3.18.2",
90
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
91
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
92
- "@deepseek-ai/dsh-llm-pi-ai": "^0.1.2-alpha.2",
93
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
94
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
95
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2"
89
+ "@deepseek-ai/schemastery": "^3.18.4",
90
+ "@deepseek-ai/dsh-settings": "0.2.0-rc.1",
91
+ "@deepseek-ai/dsh-host-webserver": "0.2.0-rc.1",
92
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.0-rc.1",
93
+ "@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
94
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
95
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.1"
96
96
  },
97
97
  "packageManager": "pnpm@11.21.0"
98
98
  }
@@ -8,7 +8,8 @@
8
8
  * and the last fill report (filled / touched / unmatched).
9
9
  *
10
10
  * All reads/mutations go through the plugin's fenced API; config persists into
11
- * the `model-parameters` settings namespace (settings.yaml).
11
+ * the `model-parameters` profile entry — the plugin's own loader entry config,
12
+ * written through the host settings service.
12
13
  */
13
14
  import { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
14
15
  import type { Context } from '@deepseek-ai/cordis'
package/src/index.ts CHANGED
@@ -6,11 +6,11 @@
6
6
  * tokens / reasoning efforts / input modalities (the norm for a freshly
7
7
  * synced gateway), it fills the missing fields from a cached models.dev
8
8
  * catalog (TTL-refreshed), and serves the fenced JSON API the web settings
9
- * page calls. The plugin's own toggles live in the `model-parameters`
10
- * settings namespace (settings.yaml).
9
+ * page calls. The plugin's own toggles are its loader entry config (the
10
+ * `model-parameters` profile entry, schema {@link Config}).
11
11
  */
12
12
  import type { Context } from '@deepseek-ai/cordis'
13
- import { Config } from './shared/config.js'
13
+ import { Config, type ModelParametersLiveConfig } from './shared/config.js'
14
14
  import { ModelParametersService } from './server/service.js'
15
15
  import { registerApiRoutes } from './server/rpc.js'
16
16
  import {
@@ -32,8 +32,8 @@ export { Config }
32
32
  export { CatalogManager, acceptedInputModalities, identityReasoningEfforts }
33
33
  export type { CatalogEntry, CatalogSnapshot }
34
34
 
35
- export function apply(ctx: Context): void {
36
- const service = new ModelParametersService(ctx)
35
+ export function apply(ctx: Context, config: ModelParametersLiveConfig = {}): void {
36
+ const service = new ModelParametersService(ctx, config)
37
37
 
38
38
  ctx.effect(() => {
39
39
  service.start()
@@ -1,26 +1,26 @@
1
1
  /**
2
- * The model-parameters service: owns the plugin settings namespace, watches
3
- * the `llm-pi-ai` namespace for provider model writes, reconciles missing
4
- * fields from the cached models.dev catalog, and tracks a fill report.
2
+ * The model-parameters service: completes missing model fields in the
3
+ * `llm-pi-ai` settings namespace from the cached models.dev catalog and tracks
4
+ * a fill report. Its own settings ARE this plugin's loader entry config (see
5
+ * shared/config.ts), so there is no per-plugin settings scope to own here.
5
6
  *
6
7
  * Trigger model:
7
8
  * - any committed `llm-pi-ai` settings change (settings UI apply, manual
8
- * settings.yaml edit published by the file provider, model-router-style
9
- * external writes) runs a reconcile;
9
+ * profile-patch edit, model-router-style external writes) runs a reconcile;
10
10
  * - plugin start runs a backfill reconcile over the existing configuration.
11
11
  * Reconcile is fill-only: it never overwrites a present field, and it writes
12
12
  * nothing when nothing is missing — so its own writes cannot re-trigger a
13
13
  * second patch (the next event sees the fields present and produces no ops).
14
14
  */
15
15
  import type { Context } from '@deepseek-ai/cordis'
16
- import type { SettingsScope, SettingsPathOp } from '@deepseek-ai/dsh-settings'
16
+ import type { SettingsPathOp } from '@deepseek-ai/dsh-settings'
17
17
  import type { PiAiModelProfile } from '@deepseek-ai/dsh-llm-pi-ai'
18
18
  import {
19
- Config,
20
- DEFAULT_CONFIG,
21
19
  PLUGIN_NS,
22
20
  TARGET_NS,
21
+ readConfig,
23
22
  type ModelParametersConfig,
23
+ type ModelParametersLiveConfig,
24
24
  } from '../shared/config.js'
25
25
  import {
26
26
  CatalogManager,
@@ -53,56 +53,66 @@ export class ModelParametersService {
53
53
  * a sibling fiber. The injected child ctx itself is only valid while its
54
54
  * fiber is active — the provider can be reloaded (and every dependent
55
55
  * fiber torn down) while an async reconcile is in flight — so all reads
56
- * and writes go through the captured service value, which is a plain
57
- * object and never walks the fiber chain.
56
+ * and writes of the `llm-pi-ai` namespace go through the captured service
57
+ * value, which is a plain object and never walks the fiber chain.
58
58
  */
59
59
  private settings: Context['settings'] | undefined
60
- private scope: SettingsScope<ModelParametersConfig> | undefined
61
60
  private lastReport: FillReport | null = null
62
61
  private readonly catalog: CatalogManager
63
62
  private reconciling: Promise<void> | null = null
64
63
 
65
- constructor(private readonly ctx: Context) {
64
+ /**
65
+ * @param ctx the plugin context.
66
+ * @param entryConfig this plugin's own live entry config (the profile entry
67
+ * `model-parameters`): the resolved `Config` document, whose `.volatile()`
68
+ * fields are live cells that the loader swaps in place on every settings
69
+ * write. Read through {@link readConfig} on each access, never cached.
70
+ */
71
+ constructor(
72
+ private readonly ctx: Context,
73
+ private readonly entryConfig: ModelParametersLiveConfig | undefined,
74
+ ) {
66
75
  this.catalog = new CatalogManager(ctx.logger)
67
76
  }
68
77
 
69
78
  start(): void {
70
- // Own settings namespace (config toggles, TTL, provider map). The inject
71
- // callback fires asynchronously once the settings service is available, so
72
- // the first-run backfill reconcile lives INSIDE it — reconcile needs the
79
+ // The plugin's own settings ARE this entry config, so nothing is
80
+ // registered here: `config()` projects the live entry config and writes
81
+ // go through the settings service (see updateConfig). The inject callback
82
+ // fires asynchronously once the settings service is available, so the
83
+ // first-run backfill reconcile lives INSIDE it — reconcile needs the
73
84
  // settings service (to read the target namespace and persist fills).
74
85
  this.ctx.inject(['settings'], (sctx) => {
75
86
  this.settings = sctx.settings
76
- this.scope = this.settings.register(OWN, Config) as SettingsScope<ModelParametersConfig>
77
87
  // First-run backfill: ensure a fresh-enough catalog, then fill existing
78
88
  // models. Never blocks plugin startup.
79
89
  void this.reconcile()
80
90
  })
81
91
 
82
92
  // Watch the target namespace: any committed provider-model change may
83
- // leave fields missing -> reconcile.
84
- this.ctx.on('settings/updated', (ns) => {
93
+ // leave fields missing -> reconcile. `settings/document-updated` is the
94
+ // committed-change signal (dsh-settings bumps a form revision after the
95
+ // profile patch was written); it also fires for this plugin's own entry,
96
+ // which is filtered out here so a toggle write never triggers a reconcile.
97
+ this.ctx.on('settings/document-updated', (ns) => {
85
98
  if (ns !== TARGET) return
86
99
  void this.reconcile()
87
100
  })
88
101
  }
89
102
 
90
103
  stop(): void {
91
- this.scope = undefined
92
104
  this.settings = undefined
93
105
  }
94
106
 
95
107
  // ── reads for the settings page ────────────────────────────────────────────
96
108
 
109
+ /**
110
+ * The plugin's current settings, projected from the live entry config.
111
+ * Works with no settings service at all (headless), which is why the plain
112
+ * {@link DEFAULT_CONFIG} fallback lives in {@link readConfig}.
113
+ */
97
114
  config(): ModelParametersConfig {
98
- if (this.scope !== undefined) {
99
- try {
100
- return this.scope.get()
101
- } catch {
102
- // fall through to defaults
103
- }
104
- }
105
- return { ...DEFAULT_CONFIG, providerMap: {}, officialProviders: [...DEFAULT_CONFIG.officialProviders] }
115
+ return readConfig(this.entryConfig)
106
116
  }
107
117
 
108
118
  catalogSnapshot(): CatalogSnapshot {
@@ -119,13 +129,29 @@ export class ModelParametersService {
119
129
  await this.reconcile()
120
130
  }
121
131
 
122
- /** Update plugin config toggles from the settings page. */
132
+ /**
133
+ * Update the plugin's own settings from the settings page. `dsh-settings`
134
+ * addresses the plugin's loader entry by its id and refuses any path the
135
+ * `Config` schema did not mark volatile — every field here is volatile.
136
+ */
123
137
  async updateConfig(patch: Partial<ModelParametersConfig>): Promise<void> {
124
- if (this.scope === undefined) {
138
+ const settings = this.settings
139
+ if (settings === undefined) {
125
140
  throw new Error('model-parameters: settings service is not available in this profile')
126
141
  }
127
- const current = this.config()
128
- await this.scope.update({ ...current, ...patch })
142
+ // `update` MERGES the patch into the entry's stored section — the
143
+ // "merge a partial into the current config" contract the page has always
144
+ // used. `providerMap` is the one plain OBJECT the page re-sends whole (a
145
+ // removed mapping row must disappear), and a deep merge would resurrect
146
+ // the removed key, so that field rides a path `set` op instead: `mutate`
147
+ // delegates to the same `write()` and replaces the value at the path
148
+ // wholesale. Arrays (`officialProviders`) are replaced by the merge
149
+ // itself, since the merge only recurses into plain objects.
150
+ const { providerMap, ...rest } = patch
151
+ if (Object.keys(rest).length > 0) await settings.update(OWN, rest)
152
+ if (providerMap !== undefined) {
153
+ await settings.mutate(OWN, [{ op: 'set', path: ['providerMap'], value: providerMap }])
154
+ }
129
155
  }
130
156
 
131
157
  // ── reconcile ──────────────────────────────────────────────────────────────
@@ -158,7 +184,7 @@ export class ModelParametersService {
158
184
  // Operate on the RAW user section (descriptor.user), not the resolved
159
185
  // value: the resolved section is schema-normalized (pi-ai's loose object
160
186
  // resolution adds empty `input`/`compat` defaults), and persisting that
161
- // shape would leak schema-default noise into settings.yaml. The raw
187
+ // shape would leak schema-default noise into the profile patch. The raw
162
188
  // section is exactly what the user document stores.
163
189
  const descriptor = settings.describe({ redactSecrets: false }).find((candidate) => candidate.ns === TARGET)
164
190
  const section = descriptor?.user as { providers?: Record<string, { models?: PiAiModelProfile[] }> } | undefined
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Plugin configuration and shared constants for @fonlan/dsh-model-parameters.
3
3
  *
4
- * The plugin owns the `model-parameters` settings namespace (stored in
5
- * settings.yaml) and OBSERVES the `llm-pi-ai` namespace: whenever provider
6
- * models are written there with missing capability fields (which is the norm
7
- * for a freshly synced gateway), it patches the missing fields from the cached
8
- * models.dev catalog.
4
+ * The plugin's own settings ARE its loader entry config (DSH >= 0.1.7): the
5
+ * profile entry whose id is `model-parameters` (see cordis.patch.yml) is
6
+ * validated with {@link Config}, the resolved document is handed to
7
+ * `apply(ctx, config)`, and the settings page is derived from that schema. The
8
+ * plugin OBSERVES the `llm-pi-ai` namespace: whenever provider models are
9
+ * written there with missing capability fields (which is the norm for a freshly
10
+ * synced gateway), it patches the missing fields from the cached models.dev
11
+ * catalog.
9
12
  */
10
13
  import z from '@deepseek-ai/schemastery'
11
14
 
12
- /** This plugin's own settings namespace. */
15
+ /** This plugin's own settings namespace: its profile entry id. */
13
16
  export const PLUGIN_NS = 'model-parameters'
14
17
 
15
18
  /** The namespace whose models this plugin completes: pi-ai provider profiles. */
@@ -59,7 +62,7 @@ export const REASONING_LEVELS: readonly string[] = [
59
62
  'max',
60
63
  ]
61
64
 
62
- /** Plugin configuration shape. */
65
+ /** Plugin configuration shape: the PLAIN document, with no live handles in it. */
63
66
  export interface ModelParametersConfig {
64
67
  /** Master switch: when off, no fills happen (listener is inert). */
65
68
  enabled: boolean
@@ -81,16 +84,88 @@ export interface ModelParametersConfig {
81
84
  officialProviders: string[]
82
85
  }
83
86
 
84
- export const Config: z<ModelParametersConfig> = z.object({
85
- enabled: z.boolean().default(true),
86
- ttlDays: z.number().min(1).max(90).default(7),
87
- fillName: z.boolean().default(true),
88
- fillContext: z.boolean().default(true),
89
- fillMaxTokens: z.boolean().default(true),
90
- fillReasoning: z.boolean().default(true),
91
- fillInput: z.boolean().default(true),
92
- providerMap: z.dict(z.string()).default({}),
93
- officialProviders: z.array(z.string()).default([...DEFAULT_OFFICIAL_PROVIDERS]),
87
+ /**
88
+ * Recursively readonly value a live handle returns — cosmokit's
89
+ * `VolatileSnapshot<T>`, spelled locally for the same reason as
90
+ * {@link LiveSettingsField}.
91
+ */
92
+ export type LiveSettingsSnapshot<T> = T extends object
93
+ ? { readonly [K in keyof T]: LiveSettingsSnapshot<T[K]> }
94
+ : T
95
+
96
+ /**
97
+ * One `volatile()` config field as the loader hands it to `apply()`: a live
98
+ * handle, not the value it currently holds. Spelled structurally rather than as
99
+ * cosmokit's `Volatile<T>` on purpose — cosmokit is a transitive dependency
100
+ * here, and importing it would put a second schemastery/cosmokit pair into the
101
+ * emitted declaration.
102
+ */
103
+ export interface LiveSettingsField<T> {
104
+ get(): LiveSettingsSnapshot<T>
105
+ }
106
+
107
+ /** A hand-written entry config (profile patch / test fixture): every field is optional. */
108
+ export type ModelParametersConfigInput = {
109
+ enabled?: boolean | null
110
+ ttlDays?: number | null
111
+ fillName?: boolean | null
112
+ fillContext?: boolean | null
113
+ fillMaxTokens?: boolean | null
114
+ fillReasoning?: boolean | null
115
+ fillInput?: boolean | null
116
+ providerMap?: Record<string, string> | null
117
+ officialProviders?: string[] | null
118
+ }
119
+
120
+ /** The resolved entry config `apply()` receives: every field is a live handle. */
121
+ export type ResolvedModelParametersConfig = {
122
+ enabled: LiveSettingsField<boolean>
123
+ ttlDays: LiveSettingsField<number>
124
+ fillName: LiveSettingsField<boolean>
125
+ fillContext: LiveSettingsField<boolean>
126
+ fillMaxTokens: LiveSettingsField<boolean>
127
+ fillReasoning: LiveSettingsField<boolean>
128
+ fillInput: LiveSettingsField<boolean>
129
+ providerMap: LiveSettingsField<Record<string, string>>
130
+ officialProviders: LiveSettingsField<string[]>
131
+ }
132
+
133
+ /**
134
+ * Entry config schema. Deliberately NOT annotated `z<ModelParametersConfig>`:
135
+ * schemastery >= 3.18.3 carries a third `Mode` type parameter, so a
136
+ * `.volatile()` field's OUTPUT type is `Volatile<T>` (a live `{ get() }` cell),
137
+ * and that annotation no longer compiles. The plain document shape stays
138
+ * {@link ModelParametersConfig} and is what {@link readConfig} projects to.
139
+ *
140
+ * The annotation below names the type through the package's own default export
141
+ * (`z<...>`), with locally declared type arguments: the bare inferred type of a
142
+ * volatile schema resolves through a schemastery copy this package does not
143
+ * import, so `tsc -p tsconfig.build.json` (`declaration: true`) could not name
144
+ * it (TS2742) — the emitted declaration stays portable this way.
145
+ *
146
+ * `.volatile()` is REQUIRED on every field the settings page may write, not
147
+ * cosmetic: `dsh-settings` derives `volatileForm(schema)` from this schema, and
148
+ * with no volatile field at all it SKIPS this entry in `describe()` (the
149
+ * settings page disappears) and REJECTS every write as a non-volatile path.
150
+ * The profile entry id (`model-parameters`, see cordis.patch.yml) is the
151
+ * namespace those writes address.
152
+ */
153
+ export type ModelParametersConfigSchema = z<
154
+ ModelParametersConfigInput,
155
+ ResolvedModelParametersConfig
156
+ >
157
+
158
+ /** The plugin's `Config` schema, exported for the loader and the settings page. */
159
+ export const Config: ModelParametersConfigSchema = z.object({
160
+ enabled: z.boolean().default(true).volatile(),
161
+ ttlDays: z.number().min(1).max(90).default(7).volatile(),
162
+ fillName: z.boolean().default(true).volatile(),
163
+ fillContext: z.boolean().default(true).volatile(),
164
+ fillMaxTokens: z.boolean().default(true).volatile(),
165
+ fillReasoning: z.boolean().default(true).volatile(),
166
+ fillInput: z.boolean().default(true).volatile(),
167
+ providerMap: z.dict(z.string()).default({}).volatile(),
168
+ officialProviders: z.array(z.string()).default([...DEFAULT_OFFICIAL_PROVIDERS]).volatile(),
94
169
  })
95
170
 
96
171
  export const DEFAULT_CONFIG: ModelParametersConfig = {
@@ -104,3 +179,51 @@ export const DEFAULT_CONFIG: ModelParametersConfig = {
104
179
  providerMap: {},
105
180
  officialProviders: [...DEFAULT_OFFICIAL_PROVIDERS],
106
181
  }
182
+
183
+ /**
184
+ * The entry config as `apply()` receives it. Every field may arrive either as a
185
+ * live `Volatile` cell or as a plain value (a hand-built config object, a
186
+ * profile patch written before the volatile marker existed, or a host that
187
+ * resolves the document without the wrapper).
188
+ */
189
+ export type ModelParametersLiveConfig = Partial<Record<keyof ModelParametersConfig, unknown>>
190
+
191
+ /**
192
+ * Read a config node that may be a plain value or a live (`.volatile()`) node.
193
+ *
194
+ * A plugin whose Config declares volatile fields receives a reactive config
195
+ * whose fields expose `get()`, while hand-written config objects and tests pass
196
+ * plain values. Both shapes are supported here so the same code path serves the
197
+ * settings page, `cordis.patch.yml`, and the unit tests.
198
+ */
199
+ function liveValue<T>(node: unknown, fallback: T): T {
200
+ if (node === undefined || node === null) return fallback
201
+ const getter = (node as { get?: unknown }).get
202
+ if (typeof getter === 'function') {
203
+ const value = (getter as () => unknown).call(node)
204
+ return value === undefined ? fallback : (value as T)
205
+ }
206
+ return node as T
207
+ }
208
+
209
+ /**
210
+ * Project the loader entry config into the plain document shape, per field
211
+ * falling back to {@link DEFAULT_CONFIG}. The config is re-read on EVERY call,
212
+ * so settings-page edits (which the loader swaps into the live cells in place)
213
+ * take effect immediately. Container fields are copied so callers can never
214
+ * mutate the live document through a returned reference.
215
+ */
216
+ export function readConfig(config: ModelParametersLiveConfig | undefined): ModelParametersConfig {
217
+ const section = config ?? {}
218
+ return {
219
+ enabled: liveValue(section.enabled, DEFAULT_CONFIG.enabled),
220
+ ttlDays: liveValue(section.ttlDays, DEFAULT_CONFIG.ttlDays),
221
+ fillName: liveValue(section.fillName, DEFAULT_CONFIG.fillName),
222
+ fillContext: liveValue(section.fillContext, DEFAULT_CONFIG.fillContext),
223
+ fillMaxTokens: liveValue(section.fillMaxTokens, DEFAULT_CONFIG.fillMaxTokens),
224
+ fillReasoning: liveValue(section.fillReasoning, DEFAULT_CONFIG.fillReasoning),
225
+ fillInput: liveValue(section.fillInput, DEFAULT_CONFIG.fillInput),
226
+ providerMap: { ...liveValue<Record<string, string>>(section.providerMap, DEFAULT_CONFIG.providerMap) },
227
+ officialProviders: [...liveValue<readonly string[]>(section.officialProviders, DEFAULT_CONFIG.officialProviders)],
228
+ }
229
+ }