@fonlan/dsh-model-parameters 0.1.4 → 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 +26 -22
- package/README.md +20 -19
- package/lib/client.js +65 -98
- package/lib/client.js.map +1 -1
- package/lib/index.js +92 -39
- package/lib/types/index.d.ts +4 -4
- package/lib/types/server/service.d.ts +27 -11
- package/lib/types/shared/config.d.ts +90 -8
- package/package.json +15 -15
- package/src/client/index.tsx +20 -17
- package/src/client/locales.ts +2 -6
- package/src/client/{settings-card.css → settings-section.css} +24 -79
- package/src/client/{settings-card.tsx → settings-section.tsx} +41 -62
- package/src/index.ts +5 -5
- package/src/server/service.ts +58 -32
- package/src/shared/config.ts +140 -17
|
@@ -1,98 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Styles for the Model Parameters
|
|
2
|
+
* Styles for the Model Parameters settings page.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* field language (ValueField): border-l2 inputs on bg-layer-3, inverted
|
|
9
|
-
* primary buttons, divider-separated groups.
|
|
4
|
+
* The page occupies the settings panel's content column: a header (title plus
|
|
5
|
+
* a one-line summary) over divider-separated control groups. Inner controls
|
|
6
|
+
* follow the built-in settings language: border-l2 inputs on bg-layer-3,
|
|
7
|
+
* inverted primary buttons.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
|
-
/*
|
|
10
|
+
/* page layout */
|
|
13
11
|
|
|
14
|
-
.mp-
|
|
15
|
-
border: 1px solid var(--dsw-alias-border-l2, #2a3346);
|
|
16
|
-
background: var(--dsw-alias-bg-layer-3, #0d1017);
|
|
17
|
-
border-radius: 12px;
|
|
18
|
-
list-style: none;
|
|
19
|
-
transition: border-color .16s, background .16s;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.mp-card:hover {
|
|
23
|
-
border-color: var(--dsw-alias-label-dimmed, #4a5468);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.mp-card[data-open] {
|
|
27
|
-
background: var(--dsw-alias-bg-layer-2, #141a26);
|
|
28
|
-
border-color: var(--dsw-alias-label-dimmed, #4a5468);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.mp-card-head {
|
|
32
|
-
appearance: none;
|
|
33
|
-
width: 100%;
|
|
34
|
-
font: inherit;
|
|
35
|
-
color: inherit;
|
|
36
|
-
text-align: left;
|
|
37
|
-
cursor: pointer;
|
|
38
|
-
background: transparent;
|
|
39
|
-
border: 0;
|
|
40
|
-
border-radius: 12px;
|
|
41
|
-
align-items: center;
|
|
42
|
-
gap: 12px;
|
|
43
|
-
padding: 14px 16px;
|
|
12
|
+
.mp-page {
|
|
44
13
|
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
gap: 16px;
|
|
16
|
+
max-width: 720px;
|
|
45
17
|
}
|
|
46
18
|
|
|
47
|
-
.mp-
|
|
48
|
-
outline: 2px solid var(--dsw-alias-brand-primary, #4f8cff);
|
|
49
|
-
outline-offset: -2px;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.mp-card-headText {
|
|
53
|
-
flex-direction: column;
|
|
54
|
-
flex: 1;
|
|
55
|
-
gap: 4px;
|
|
56
|
-
min-width: 0;
|
|
19
|
+
.mp-page-head {
|
|
57
20
|
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 6px;
|
|
58
23
|
}
|
|
59
24
|
|
|
60
|
-
.mp-
|
|
25
|
+
.mp-page-title {
|
|
26
|
+
margin: 0;
|
|
61
27
|
color: var(--dsw-alias-label-primary, #e6e6e6);
|
|
62
28
|
font-size: 15px;
|
|
63
29
|
font-weight: 600;
|
|
64
30
|
line-height: 1.4;
|
|
65
31
|
}
|
|
66
32
|
|
|
67
|
-
.mp-
|
|
33
|
+
.mp-page-sub {
|
|
34
|
+
margin: 0;
|
|
68
35
|
color: var(--dsw-alias-label-tertiary, #8b93a7);
|
|
69
36
|
font-size: 13px;
|
|
70
37
|
line-height: 1.5;
|
|
71
38
|
}
|
|
72
39
|
|
|
73
|
-
|
|
74
|
-
color: var(--dsw-alias-label-tertiary, #8b93a7);
|
|
75
|
-
flex: none;
|
|
76
|
-
transition: transform .16s;
|
|
77
|
-
display: flex;
|
|
78
|
-
align-items: center;
|
|
79
|
-
justify-content: center;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.mp-card-chevron[data-open] {
|
|
83
|
-
transform: rotate(180deg);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.mp-card-body {
|
|
87
|
-
border-top: 1px solid var(--dsw-alias-border-l2, #2a3346);
|
|
88
|
-
margin: 0 16px;
|
|
89
|
-
padding: 12px 0 8px;
|
|
90
|
-
display: flex;
|
|
91
|
-
flex-direction: column;
|
|
92
|
-
gap: 12px;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/* ── body layout ────────────────────────────────────────────────────────── */
|
|
40
|
+
/* body layout */
|
|
96
41
|
|
|
97
42
|
.mp-body {
|
|
98
43
|
display: flex;
|
|
@@ -141,7 +86,7 @@
|
|
|
141
86
|
line-height: 1.6;
|
|
142
87
|
}
|
|
143
88
|
|
|
144
|
-
/*
|
|
89
|
+
/* toggles */
|
|
145
90
|
|
|
146
91
|
.mp-toggle {
|
|
147
92
|
display: flex;
|
|
@@ -177,7 +122,7 @@
|
|
|
177
122
|
line-height: 1.5;
|
|
178
123
|
}
|
|
179
124
|
|
|
180
|
-
/*
|
|
125
|
+
/* catalog meta / ttl */
|
|
181
126
|
|
|
182
127
|
.mp-meta {
|
|
183
128
|
display: flex;
|
|
@@ -198,7 +143,7 @@
|
|
|
198
143
|
line-height: 1.6;
|
|
199
144
|
}
|
|
200
145
|
|
|
201
|
-
/*
|
|
146
|
+
/* inputs */
|
|
202
147
|
|
|
203
148
|
.mp-input {
|
|
204
149
|
border: 1px solid var(--dsw-alias-border-l2, #2a3346);
|
|
@@ -226,7 +171,7 @@
|
|
|
226
171
|
width: 72px;
|
|
227
172
|
}
|
|
228
173
|
|
|
229
|
-
/*
|
|
174
|
+
/* mapping rows */
|
|
230
175
|
|
|
231
176
|
.mp-map-row {
|
|
232
177
|
display: flex;
|
|
@@ -244,7 +189,7 @@
|
|
|
244
189
|
flex: none;
|
|
245
190
|
}
|
|
246
191
|
|
|
247
|
-
/*
|
|
192
|
+
/* buttons */
|
|
248
193
|
|
|
249
194
|
.mp-actions {
|
|
250
195
|
display: flex;
|
|
@@ -291,7 +236,7 @@
|
|
|
291
236
|
border-color: var(--dsw-alias-label-dimmed, #4a5468);
|
|
292
237
|
}
|
|
293
238
|
|
|
294
|
-
/*
|
|
239
|
+
/* report / badges / code */
|
|
295
240
|
|
|
296
241
|
.mp-report {
|
|
297
242
|
margin: 0;
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Model Parameters
|
|
3
|
-
* provider→models.dev mapping overrides, a manual refresh/fill action, the
|
|
4
|
-
* catalog freshness line, and the last fill report (filled / touched /
|
|
5
|
-
* unmatched).
|
|
2
|
+
* The Model Parameters settings page (设置 → 侧栏「模型参数补全」).
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* that discloses the controls in place.
|
|
4
|
+
* Registers into the `settings.section` slot, so the page owns one entry in
|
|
5
|
+
* the settings sidebar and renders its content in the panel's content column.
|
|
6
|
+
* Content: master + per-field toggles, catalog TTL, provider→models.dev
|
|
7
|
+
* mapping overrides, a manual refresh/fill action, the catalog freshness line,
|
|
8
|
+
* and the last fill report (filled / touched / unmatched).
|
|
13
9
|
*
|
|
14
10
|
* All reads/mutations go through the plugin's fenced API; config persists into
|
|
15
|
-
* the `model-parameters`
|
|
11
|
+
* the `model-parameters` profile entry — the plugin's own loader entry config,
|
|
12
|
+
* written through the host settings service.
|
|
16
13
|
*/
|
|
17
14
|
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
|
|
18
15
|
import type { Context } from '@deepseek-ai/cordis'
|
|
@@ -21,7 +18,7 @@ import type { Translate } from '@deepseek-ai/dsh-client-ui-slots'
|
|
|
21
18
|
import { LOCALE_NS } from './locales'
|
|
22
19
|
import { api, type PluginState } from './api'
|
|
23
20
|
import type { ModelParametersConfig } from '../shared/config.js'
|
|
24
|
-
import './settings-
|
|
21
|
+
import './settings-section.css'
|
|
25
22
|
|
|
26
23
|
function useLocaleRevision(ctx: ClientContext): number {
|
|
27
24
|
const subscribe = useCallback(
|
|
@@ -52,7 +49,7 @@ function formatTime(epochMs: number): string {
|
|
|
52
49
|
}
|
|
53
50
|
}
|
|
54
51
|
|
|
55
|
-
export function
|
|
52
|
+
export function makeSettingsSection(ctx: ClientContext): () => JSX.Element {
|
|
56
53
|
const t: Translate = (() => {
|
|
57
54
|
try {
|
|
58
55
|
return ctx.locale.bind(LOCALE_NS) as unknown as Translate
|
|
@@ -61,10 +58,8 @@ export function makeSettingsCard(ctx: ClientContext): () => JSX.Element {
|
|
|
61
58
|
}
|
|
62
59
|
})()
|
|
63
60
|
|
|
64
|
-
return function
|
|
61
|
+
return function ModelParametersSettingsSection(): JSX.Element {
|
|
65
62
|
useLocaleRevision(ctx)
|
|
66
|
-
// Card-local disclosure: collapsed by default, like the built-in plugin cards.
|
|
67
|
-
const [open, setOpen] = useState(false)
|
|
68
63
|
const [state, setState] = useState<PluginState | null>(null)
|
|
69
64
|
const [error, setError] = useState<string | null>(null)
|
|
70
65
|
const [loading, setLoading] = useState(true)
|
|
@@ -140,58 +135,42 @@ export function makeSettingsCard(ctx: ClientContext): () => JSX.Element {
|
|
|
140
135
|
await patchConfig({ providerMap })
|
|
141
136
|
}, [mappingRows, patchConfig])
|
|
142
137
|
|
|
143
|
-
const title = t('settingsTitle')
|
|
144
|
-
|
|
145
138
|
return (
|
|
146
|
-
<
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
busy={busy}
|
|
174
|
-
mappingRows={mappingRows}
|
|
175
|
-
newFrom={newFrom}
|
|
176
|
-
newTo={newTo}
|
|
177
|
-
setNewFrom={setNewFrom}
|
|
178
|
-
setNewTo={setNewTo}
|
|
179
|
-
setMappingRows={setMappingRows}
|
|
180
|
-
onPatchConfig={patchConfig}
|
|
181
|
-
onRefresh={refresh}
|
|
182
|
-
onReconcile={reconcileNow}
|
|
183
|
-
onApplyMappings={applyMappings}
|
|
184
|
-
/>
|
|
185
|
-
)}
|
|
186
|
-
</div>
|
|
139
|
+
<div className="mp-page">
|
|
140
|
+
<header className="mp-page-head">
|
|
141
|
+
<h3 className="mp-page-title">{t('settingsTitle')}</h3>
|
|
142
|
+
<p className="mp-page-sub">{t('sectionSub')}</p>
|
|
143
|
+
</header>
|
|
144
|
+
{error !== null && state === null ? (
|
|
145
|
+
<p className="mp-error">{t('error')}: {error}</p>
|
|
146
|
+
) : loading || state === null ? (
|
|
147
|
+
<p className="mp-muted">{t('saving')}</p>
|
|
148
|
+
) : (
|
|
149
|
+
<ModelParametersBody
|
|
150
|
+
t={t}
|
|
151
|
+
state={state}
|
|
152
|
+
error={error}
|
|
153
|
+
saving={saving}
|
|
154
|
+
busy={busy}
|
|
155
|
+
mappingRows={mappingRows}
|
|
156
|
+
newFrom={newFrom}
|
|
157
|
+
newTo={newTo}
|
|
158
|
+
setNewFrom={setNewFrom}
|
|
159
|
+
setNewTo={setNewTo}
|
|
160
|
+
setMappingRows={setMappingRows}
|
|
161
|
+
onPatchConfig={patchConfig}
|
|
162
|
+
onRefresh={refresh}
|
|
163
|
+
onReconcile={reconcileNow}
|
|
164
|
+
onApplyMappings={applyMappings}
|
|
165
|
+
/>
|
|
187
166
|
)}
|
|
188
|
-
</
|
|
167
|
+
</div>
|
|
189
168
|
)
|
|
190
169
|
}
|
|
191
170
|
}
|
|
192
171
|
|
|
193
|
-
/** The
|
|
194
|
-
function
|
|
172
|
+
/** The page body: divider-separated control groups. */
|
|
173
|
+
function ModelParametersBody(props: {
|
|
195
174
|
t: Translate
|
|
196
175
|
state: PluginState
|
|
197
176
|
error: string | null
|
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
|
|
10
|
-
*
|
|
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()
|
package/src/server/service.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The model-parameters service:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
|
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
|