@gehennawu/dsh-service 2.0.7 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,633 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { createMetadataCache, createSourceFetcher, loadPiEvidenceFromDir, querySmartFill } from './model-smart-fill.js'
5
+ import { modelSettingsFor, redactSensitive } from './model-workbench-settings.js'
6
+ import { loadPiAiCatalog, locateDshPackageJson, profileFingerprint, selfCheckModelsPatch, fetchModelsProbe, deriveKeyRefFor, MODEL_ID_RE, MODELS_ROUTE_RE, MODELS_KEY_REF_RE, WORKBENCH_PLAN_TTL_MS } from './model-workbench.js'
7
+ import { MODEL_FIELDS, ROUTE_FIELDS, PROTOCOLS, object, displayProfile, containsSensitive, sensitiveHeader, patchFields, schemaVocabulary, validateField } from './model-config-schema.js'
8
+
9
+ const clone = (value) => structuredClone(value)
10
+ const has = (value, key) => object(value) && Object.hasOwn(value, key)
11
+ const error = (code) => { throw new Error(code) }
12
+ const conflict = (failure) => failure?.code === 'SETTINGS_CONFLICT' || failure?.name === 'SettingsConflictError'
13
+ const builtin = (catalog, route) => catalog.channelIds.has(route)
14
+ const fingerprint = (access) => profileFingerprint({ baseline: access.fingerprint?.() ?? access.get('llm-pi-ai'), namespace: access.namespace, revision: access.revision })
15
+ const validRoute = (route) => typeof route === 'string' && route.length > 0 && route.length <= 128 && !/[\p{Cc}]/u.test(route) && !['__proto__', 'constructor', 'prototype'].includes(route)
16
+ const own = (value, key) => value !== null && typeof value === 'object' && Object.hasOwn(value, key) ? value[key] : undefined
17
+ const setOwn = (value, key, child) => { Object.defineProperty(value, key, { value: child, writable: true, enumerable: true, configurable: true }); return child }
18
+ const routeRaw = (access, route) => validRoute(route) ? own(access.get('llm-pi-ai')?.providers, route) : undefined
19
+ const validId = (id) => typeof id === 'string' && id.length > 0 && id.length <= 512 && MODEL_ID_RE.test(id)
20
+
21
+ // Only a bounded diagnostic summary crosses the RPC/persistence boundary.
22
+ export function modelTestFailure(failure, fallback = 'stream-failed') {
23
+ const code = typeof failure?.code === 'string' && /^[a-z0-9_-]{1,80}$/i.test(failure.code) ? failure.code : fallback
24
+ const message = typeof failure?.message === 'string' ? failure.message
25
+ .replace(/\b(?:Bearer\s+\S+|sk-[a-z0-9_-]+)\b/gi, '<REDACTED>')
26
+ .replace(/((?:api[-_]?key|authorization|cookie|token|secret|password)\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s,;}]+)/gi, '$1<REDACTED>')
27
+ .replace(/https?:\/\/[^\s"'<>]+/gi, '<URL>')
28
+ .replace(/[\r\n\p{Cc}]+/gu, ' ').slice(0, 600) : undefined
29
+ return { code, ...(message ? { message } : {}), ...(Number.isInteger(failure?.status) && failure.status >= 100 && failure.status <= 599 ? { status: failure.status } : {}) }
30
+ }
31
+
32
+ /** No product write path uses normalized catalog/display values as a baseline. */
33
+ export function createModelConfigRoutes({ ctx, workbenchStore, loadPiCatalog = loadPiAiCatalog, loadSubagentRoute, loadAgentDefaultModelSelection, smartFill, providerAvailability, notifyProviderAvailability = () => {}, uuid = randomUUID }) {
34
+ const sessions = new Map()
35
+ const plans = new Map()
36
+ const queries = new Map()
37
+ const metadataCache = createMetadataCache({ fetcher: createSourceFetcher() })
38
+ let piEvidence = null
39
+ const ensureEvidence = () => {
40
+ if (piEvidence === null) piEvidence = (async () => {
41
+ const entry = locateDshPackageJson()
42
+ if (!entry) return null
43
+ const path = String(entry).startsWith('file://') ? fileURLToPath(entry) : entry
44
+ return loadPiEvidenceFromDir(join(dirname(path), 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers', 'data'))
45
+ })().catch(() => { piEvidence = null; return null })
46
+ return piEvidence
47
+ }
48
+ let writes = Promise.resolve()
49
+ const serialize = (work) => {
50
+ const result = writes.then(work)
51
+ writes = result.then(() => undefined, () => undefined)
52
+ return result
53
+ }
54
+ const remember = (table, value) => {
55
+ const id = uuid()
56
+ table.set(id, { ...value, expires: Date.now() + WORKBENCH_PLAN_TTL_MS })
57
+ for (const [key, entry] of table) if (entry.expires < Date.now()) table.delete(key)
58
+ while (table.size > 128) table.delete(table.keys().next().value)
59
+ return id
60
+ }
61
+ const get = (table, id) => {
62
+ const value = table.get(id)
63
+ if (!value) error('unknown-plan')
64
+ if (Date.now() > value.expires) { table.delete(id); error('plan-expired') }
65
+ return value
66
+ }
67
+ const references = async (route, ids) => {
68
+ const rows = []
69
+ const current = loadAgentDefaultModelSelection?.()
70
+ const subagent = await loadSubagentRoute?.()
71
+ if (current?.provider === route && (!ids || ids.includes(current.model))) rows.push({ kind: 'agent-default-model', provider: route, model: current.model })
72
+ if (subagent?.mode === 'custom' && subagent.provider === route && (!ids || ids.includes(subagent.model))) rows.push({ kind: 'subagent-route', provider: route, model: subagent.model })
73
+ for (const fallback of subagent?.fallbacks ?? []) if (fallback.provider === route && (!ids || ids.includes(fallback.model))) rows.push({ kind: 'subagent-fallback', provider: route, model: fallback.model })
74
+ return rows
75
+ }
76
+ const storeLoad = async () => workbenchStore ? await workbenchStore.load() : {}
77
+ const latest = (session) => {
78
+ const access = modelSettingsFor(ctx)
79
+ if (fingerprint(access) !== session.baseline) error('conflict')
80
+ return access
81
+ }
82
+ const sessionDTO = (access, route, raw) => ({
83
+ route, profile: displayProfile(raw),
84
+ inherited: displayProfile(access.inherited?.()?.providers?.[route] ?? {}),
85
+ explicit: displayProfile(access.explicit?.()?.providers?.[route] ?? {}),
86
+ effective: displayProfile(access.effective?.()?.providers?.[route] ?? {}),
87
+ schema: schemaVocabulary(),
88
+ })
89
+ // A complete non-sensitive snapshot is a precondition, not a post-write promise.
90
+ const snapshotValue = (raw) => {
91
+ if (raw === undefined) return null
92
+ const next = clone(raw)
93
+ if (object(next.headers)) next.headers = Object.fromEntries(Object.entries(next.headers).filter(([name]) => !sensitiveHeader(name)))
94
+ if (containsSensitive(next)) error('sensitive-snapshot')
95
+ return next
96
+ }
97
+ const persistBefore = async (route, before, after, baseline) => {
98
+ if (!workbenchStore) error('snapshot-unavailable')
99
+ if (after === undefined && containsSensitive(before)) error('sensitive-snapshot')
100
+ const safeBefore = snapshotValue(before)
101
+ const safeAfter = snapshotValue(after)
102
+ const token = uuid()
103
+ const previous = clone((await storeLoad()).providers?.[route])
104
+ const previousRemoved = clone((await storeLoad()).removed?.[route])
105
+ await workbenchStore.mutate((state) => {
106
+ state.providers ??= {}; state.removed ??= {}
107
+ const snapshot = { kind: after === undefined ? 'removed' : 'transaction', token, before: safeBefore, after: safeAfter, expectedAfter: profileFingerprint(after ?? null), protectedHeaders: !!Object.keys(before?.headers ?? {}).some(sensitiveHeader), baseline, at: Date.now(), status: 'pending' }
108
+ if (after === undefined) { state.removed[route] = snapshot; delete state.providers[route] }
109
+ else { state.providers[route] = snapshot; delete state.removed[route] }
110
+ })
111
+ return { token, previous, previousRemoved }
112
+ }
113
+ const rollbackSnapshot = async (route, backup) => {
114
+ await workbenchStore.mutate((state) => {
115
+ if (state.providers?.[route]?.token !== backup.token && state.removed?.[route]?.token !== backup.token) return
116
+ state.providers ??= {}; state.removed ??= {}
117
+ if (backup.previous === undefined) delete state.providers[route]; else state.providers[route] = backup.previous
118
+ if (backup.previousRemoved === undefined) delete state.removed[route]; else state.removed[route] = backup.previousRemoved
119
+ })
120
+ }
121
+ const apply = async (session, next, { ops, deletedIds, renamedModelId, remove = false } = {}) => serialize(async () => {
122
+ const access = latest(session)
123
+ if (access.revisionTracked !== true) error('model-config-revision-unavailable')
124
+ const before = routeRaw(access, session.route)
125
+ if (remove && access.inheritedRoute(session.route)) error('model-config-inherited-route')
126
+ if (remove || deletedIds?.length) if ((await references(session.route, remove ? undefined : deletedIds)).length) error('removal-blocked')
127
+ if ((await storeLoad()).recovery?.[session.route]) error('recovery-pending')
128
+ if (renamedModelId && providerAvailability) {
129
+ await providerAvailability.loading
130
+ if (!providerAvailability.modelEnabled(session.route, renamedModelId)) error('disabled-model-rename')
131
+ }
132
+ const backup = await persistBefore(session.route, before, next, session.baseline)
133
+ try {
134
+ await access.mutate('llm-pi-ai', ops ?? [{ op: next === undefined ? 'unset' : 'set', path: ['providers', session.route], ...(next === undefined ? {} : { value: next }) }], access.revision)
135
+ } catch (failure) {
136
+ try { await rollbackSnapshot(session.route, backup) } catch (_) {}
137
+ if (conflict(failure)) error('conflict')
138
+ // Host errors may embed configuration; return only a stable non-sensitive code.
139
+ error('configuration-rejected')
140
+ }
141
+ let recoveryRecorded = true
142
+ try { await workbenchStore.mutate((state) => { const row = (next === undefined ? state.removed : state.providers)?.[session.route]; if (row?.token === backup.token) row.status = 'applied' }) } catch (_) { recoveryRecorded = false }
143
+ return { route: session.route, saved: true, undoable: true, recoveryRecorded, credentialValueRestored: false }
144
+ })
145
+ const route = (handle) => ({ feature: 'modelCatalog', handle: async (payload = {}) => {
146
+ try {
147
+ if (Object.hasOwn(payload, 'route') && !validRoute(payload.route)) error('bad-route')
148
+ return { ok: true, value: await handle(payload) }
149
+ } catch (failure) {
150
+ const code = conflict(failure) ? 'conflict' : /^[a-z][a-z0-9-]+$/.test(failure?.message ?? '') ? failure.message : 'operation-failed'
151
+ return { ok: false, error: code }
152
+ }
153
+ } })
154
+ const endpoints = {
155
+ 'models3-model-enabled': route(async ({ sessionId, modelId, enabled, expected }) => serialize(async () => {
156
+ if (!providerAvailability || !validId(modelId) || typeof enabled !== 'boolean' || typeof expected !== 'boolean') error('bad-payload')
157
+ const session = get(sessions, sessionId)
158
+ const access = latest(session)
159
+ if (!routeRaw(access, session.route)?.models?.some((model) => model.id === modelId)) error('bad-model-id')
160
+ const result = await providerAvailability.setModel(session.route, modelId, enabled, expected)
161
+ notifyProviderAvailability()
162
+ return result
163
+ })),
164
+ 'models3-enabled-preview': route(async ({ sessionId, enabled }) => {
165
+ if (!providerAvailability || typeof enabled !== 'boolean') error('bad-payload')
166
+ await providerAvailability.loading
167
+ const session = get(sessions, sessionId)
168
+ latest(session)
169
+ const previous = providerAvailability.enabled(session.route)
170
+ const uses = await references(session.route)
171
+ const planId = remember(plans, { kind: 'availability', session, enabled, previous })
172
+ return { planId, route: session.route, previous, enabled, references: uses, configurationRetained: true, credentialRetained: true }
173
+ }),
174
+ 'models3-enabled-commit': route(async ({ planId }) => {
175
+ const plan = get(plans, planId)
176
+ if (plan.kind !== 'availability') error('unknown-plan')
177
+ return serialize(async () => {
178
+ latest(plan.session)
179
+ const result = await providerAvailability.set(plan.session.route, plan.enabled, plan.previous)
180
+ plans.delete(planId)
181
+ notifyProviderAvailability()
182
+ return result
183
+ })
184
+ }),
185
+ 'models3-fill': route(async (payload) => {
186
+ if (!validId(payload.modelId)) error('bad-model-id')
187
+ if (payload.referenceModelId !== undefined && !validId(payload.referenceModelId)) error('bad-model-id')
188
+ if (payload.enabled === false) return { groups: [], sources: [], readonly: [], mode: 'off' }
189
+ const access = modelSettingsFor(ctx)
190
+ const raw = routeRaw(access, payload.route)
191
+ const catalog = await loadPiCatalog()
192
+ const records = [...catalog.byModelId.values()].flat().filter((entry) => entry.channel === payload.route)
193
+ const catalogApis = [...new Set(records.map((entry) => entry.api).filter(Boolean))]
194
+ const exactApi = (catalog.byModelId.get(payload.modelId) ?? []).find((entry) => entry.channel === payload.route)?.api
195
+ const api = raw?.api ?? payload.api ?? exactApi ?? (catalogApis.length === 1 ? catalogApis[0] : undefined)
196
+ const referenceModelId = payload.referenceModelId ?? payload.modelId
197
+ const target = { channel: '', api: '', modelId: referenceModelId, modelOnly: true, current: raw?.models?.find((x) => x.id === payload.modelId) ?? {} }
198
+ const result = smartFill ? await smartFill(target) : await querySmartFill({ mode: 'on', target, piIndex: await ensureEvidence(), cache: metadataCache, forceRefresh: true })
199
+ const groups = []; const readonly = []
200
+ for (const group of result.groups ?? []) {
201
+ const options = group.options ?? group.candidates ?? []
202
+ for (const option of options.filter((x) => x.readOnly === true || x.adoptable === false)) readonly.push({ field: group.field, value: option.value, source: option.sourceRef ?? option.source, kind: option.kind, evidence: option.evidence })
203
+ for (const row of group.inexpressible ?? []) readonly.push({ field: group.field, ...row })
204
+ const adopted = options.filter((x) => x.adoptable === true && x.readOnly !== true)
205
+ const keys = [...new Set(adopted.map((x) => x.key ?? null))]
206
+ for (const key of keys) {
207
+ const candidates = adopted.filter((x) => (x.key ?? null) === key).map((x) => ({ id: x.candidateId ?? x.id, value: x.value, source: redactSensitive(x.sourceRef ?? x.source), reference: x.evidence !== 'exact-channel-protocol', evidence: x.evidence }))
208
+ if (candidates.length) groups.push({ key: key === null ? group.field : 'compat.' + key, field: group.field, path: key === null ? [group.field] : ['compat', key], candidates })
209
+ }
210
+ }
211
+ const sourceCandidates = []
212
+ for (const group of groups) for (const candidate of group.candidates) {
213
+ const identity = profileFingerprint(candidate.source)
214
+ if (!sourceCandidates.some((x) => x.id === identity)) sourceCandidates.push({ id: identity, name: [candidate.source?.source, candidate.source?.channel, candidate.source?.api, candidate.source?.modelId].filter(Boolean).join(' / '), source: candidate.source })
215
+ }
216
+ const selectedSource = undefined
217
+ const sourceIdentity = profileFingerprint({ route: payload.route, api, modelId: payload.modelId, baseURL: raw?.baseURL ?? null })
218
+ const bindingNeedsReview = false
219
+ if (selectedSource && !sourceCandidates.some((x) => x.id === selectedSource)) error('unknown-source')
220
+ const queryId = remember(queries, { kind: 'source', route: payload.route, api, modelId: payload.modelId, referenceModelId, identity: sourceIdentity, sourceCandidates })
221
+ if (selectedSource) for (const group of groups) group.candidates = group.candidates.filter((x) => profileFingerprint(x.source) === selectedSource)
222
+ for (const group of groups) group.candidates = group.candidates.filter((x) => !containsSensitive(x.value))
223
+ return { reasoningSupported: result.groups?.find((group) => group.field === 'reasoningEfforts')?.supported === true, groups: groups.filter((x) => x.candidates.length), readonly: redactSensitive(readonly), candidates: sourceCandidates, queryId, selectedSource: selectedSource ?? '', bindingNeedsReview, sources: (result.sources ?? []).map((x) => ({ ...x, id: x.sourceId ?? x.id })), mode: 'on' }
224
+ }),
225
+ 'models3-bind-source': route(async ({ queryId, sourceId }) => {
226
+ const query = get(queries, queryId)
227
+ if (!workbenchStore) error('snapshot-unavailable')
228
+ if (sourceId !== '' && !query.sourceCandidates.some((x) => x.id === sourceId)) error('unknown-source')
229
+ const access = modelSettingsFor(ctx)
230
+ const raw = routeRaw(access, query.route)
231
+ if (profileFingerprint({ route: query.route, api: raw?.api ?? query.api, modelId: query.modelId, baseURL: raw?.baseURL ?? null }) !== query.identity) error('conflict')
232
+ await workbenchStore.mutate((state) => {
233
+ state.bindings ??= {}
234
+ const byRoute = own(state.bindings, query.route) ?? setOwn(state.bindings, query.route, {})
235
+ const byApi = own(byRoute, query.api) ?? setOwn(byRoute, query.api, {})
236
+ if (!sourceId) delete byApi[query.modelId]
237
+ else setOwn(byApi, query.modelId, { identity: query.identity, sourceId, referenceModelId: query.referenceModelId })
238
+ })
239
+ return { bound: !!sourceId, sourceId }
240
+ }),
241
+ 'models3-snapshot': route(async () => {
242
+ const access = modelSettingsFor(ctx)
243
+ const providers = access.get('llm-pi-ai')?.providers ?? {}
244
+ const catalog = await loadPiCatalog()
245
+ const state = await storeLoad()
246
+ if (providerAvailability) await providerAvailability.loading
247
+ const llm = ctx.get('llm')
248
+ let loaded = []; try { loaded = await llm?.listProviders?.() ?? [] } catch (_) {}
249
+ const configuredIds = new Set()
250
+ for (const id of Object.keys(providers)) {
251
+ if (validRoute(id)) configuredIds.add(id)
252
+ }
253
+ for (const id of catalog.channelIds) {
254
+ if (has(providers, id) || access.inheritedRoute?.(id)) configuredIds.add(id)
255
+ }
256
+ const routes = [...configuredIds].map((id) => ({
257
+ route: id,
258
+ displayName: providers[id]?.displayName ?? id,
259
+ source: builtin(catalog, id) ? 'builtin' : 'custom',
260
+ configured: has(providers, id) || access.inheritedRoute?.(id) === true,
261
+ modelCount: Array.isArray(providers[id]?.models) && providers[id].models.length ? providers[id].models.length : [...catalog.byModelId.values()].flat().filter((x) => x.channel === id).length,
262
+ enabled: providerAvailability ? providerAvailability.enabled(id) : state.disabledProviders?.[id] !== true,
263
+ runtimeLoaded: loaded.some((x) => x.id === id || x.provider === id),
264
+ lastValidation: state.validations?.[id],
265
+ undoable: !!state.providers?.[id] && state.providers[id].kind === 'transaction',
266
+ }))
267
+ const templates = [...catalog.channelIds].filter((id) => !configuredIds.has(id)).map((id) => {
268
+ const records = [...catalog.byModelId.entries()]
269
+ .flatMap(([modelId, recs]) => recs.filter((x) => x.channel === id).map((x) => ({
270
+ id: modelId,
271
+ name: x.name ?? modelId,
272
+ api: x.api ?? 'openai-completions',
273
+ contextWindow: x.contextWindow,
274
+ maxTokens: x.maxTokens,
275
+ })))
276
+ const defaultEntry = catalog.channels?.find((c) => c.id === id) || [...catalog.byModelId.values()].flat().find((x) => x.channel === id)
277
+ return {
278
+ id,
279
+ name: defaultEntry?.name ?? id,
280
+ api: defaultEntry?.api ?? 'openai-completions',
281
+ baseURL: defaultEntry?.baseUrl ?? '',
282
+ models: records,
283
+ }
284
+ })
285
+ return {
286
+ routes,
287
+ templates,
288
+ removedRoutes: Object.entries(state.removed ?? {}).filter(([, v]) => v.kind === 'removed' && v.before).map(([id, v]) => ({ route: id, displayName: v.before.displayName ?? id, at: v.at, taken: has(providers, id) })),
289
+ }
290
+ }),
291
+ 'models3-open': route(async ({ route: id }) => {
292
+ const access = modelSettingsFor(ctx)
293
+ const catalog = await loadPiCatalog()
294
+ const raw = routeRaw(access, id)
295
+ if (!raw && !builtin(catalog, id)) error('route-gone')
296
+ const defaultEntry = catalog.channels?.find((c) => c.id === id) || [...catalog.byModelId.values()].flat().find((x) => x.channel === id)
297
+ const effectiveRaw = raw ?? {
298
+ displayName: defaultEntry?.name ?? id,
299
+ api: defaultEntry?.api ?? 'openai-completions',
300
+ baseURL: defaultEntry?.baseUrl ?? '',
301
+ models: [],
302
+ }
303
+ const sessionId = remember(sessions, { route: id, baseline: fingerprint(access), readOnly: false, ownerEntry: access.namespace })
304
+ const dto = sessionDTO(access, id, effectiveRaw)
305
+ dto.builtin = builtin(catalog, id)
306
+ dto.resolvedApi = effectiveRaw.api ?? defaultEntry?.api ?? [...catalog.byModelId.values()].flat().find((entry) => entry.channel === id && entry.api)?.api ?? ''
307
+ dto.credentialState = 'unknown'
308
+ const credentialService = ctx.get('credentials')
309
+ try {
310
+ if (effectiveRaw.apiKeyEnv && typeof credentialService?.resolve === 'function') {
311
+ const hit = await credentialService.resolve(effectiveRaw.apiKeyEnv)
312
+ dto.credentialState = hit?.value ? 'configured' : 'missing'
313
+ } else if (typeof credentialService?.readRecord === 'function') {
314
+ const record = await credentialService.readRecord(`llm-pi-ai/${id}`)
315
+ if (record?.kind === 'api-key' && record.key) dto.credentialState = 'configured'
316
+ else if (record?.kind === 'grant') dto.credentialState = 'authorized'
317
+ }
318
+ } catch (_) { /* Missing status must not prevent configuration editing. */ }
319
+ if (builtin(catalog, id)) dto.catalogModels = [...catalog.byModelId.entries()].flatMap(([modelId, records]) => records.filter((x) => x.channel === id).map((x) => ({ id: modelId, api: x.api, ...displayProfile({ models: [{ id: modelId, ...x }] }).models[0] })))
320
+ dto.resolved = {
321
+ displayName: effectiveRaw.displayName ?? dto.effective.displayName ?? defaultEntry?.name ?? id,
322
+ api: dto.resolvedApi,
323
+ baseURL: effectiveRaw.baseURL ?? dto.effective.baseURL ?? defaultEntry?.baseUrl ?? '',
324
+ models: (effectiveRaw.models ?? []).map((model) => ({
325
+ ...(dto.catalogModels ?? []).find((entry) => entry.id === model.id && (!model.api || entry.api === model.api)),
326
+ ...(dto.effective.models ?? []).find((entry) => entry.id === model.id),
327
+ ...displayProfile({ models: [model] }).models[0],
328
+ })),
329
+ }
330
+ if (providerAvailability) await providerAvailability.loading
331
+ const modelAvailability = (effectiveRaw.models ?? []).map((model) => ({ id: model.id, enabled: providerAvailability ? providerAvailability.modelEnabled(id, model.id) : true }))
332
+ return { ...dto, modelAvailability, sessionId, readOnly: false, inheritedRoute: access.inheritedRoute(id), fingerprint: fingerprint(access) }
333
+ }),
334
+ 'models3-save-model': route(async (payload) => {
335
+ const session = get(sessions, payload.sessionId)
336
+ const access = latest(session)
337
+ const catalog = await loadPiCatalog()
338
+ const defaultEntry = catalog.channels?.find((c) => c.id === session.route) || [...catalog.byModelId.values()].flat().find((x) => x.channel === session.route)
339
+ const raw = routeRaw(access, session.route) ?? {
340
+ displayName: defaultEntry?.name ?? session.route,
341
+ api: defaultEntry?.api ?? 'openai-completions',
342
+ baseURL: defaultEntry?.baseUrl ?? '',
343
+ models: [],
344
+ }
345
+ const models = clone(raw.models ?? [])
346
+ const originalModelId = payload.add === true ? payload.modelId : payload.originalModelId ?? payload.modelId
347
+ if (!validId(originalModelId) || !validId(payload.modelId)) error('bad-model-id')
348
+ const position = models.findIndex((x) => x.id === originalModelId)
349
+ const renamed = payload.add !== true && originalModelId !== payload.modelId
350
+ if (renamed && models.some((model) => model.id === payload.modelId)) error('duplicate-model')
351
+ if (payload.add === true ? position !== -1 : position === -1) error(payload.add === true ? 'duplicate-model' : 'unknown-model')
352
+ const previous = position === -1 ? { id: payload.modelId } : models[position]
353
+ const next = patchFields(previous, payload.patch ?? {}, payload.unset ?? [], MODEL_FIELDS, raw.api)
354
+ next.id = payload.modelId
355
+ // Models are a replaced array in Profile composition, so deleting a field
356
+ // from that array does not magically re-expose an inherited model row.
357
+ const inheritedModel = access.inherited?.()?.providers?.[session.route]?.models?.find((row) => row.id === originalModelId)
358
+ for (const field of payload.unset ?? []) if (has(inheritedModel, field)) next[field] = clone(inheritedModel[field])
359
+ if (has(payload.patch, 'compat')) {
360
+ const offered = schemaVocabulary().compatGates[raw.api] ?? []
361
+ next.compat = { ...Object.fromEntries(Object.entries(previous.compat ?? {}).filter(([key]) => !offered.includes(key))), ...next.compat }
362
+ }
363
+ if (position === -1) models.push(next); else models[position] = next
364
+ if (!selfCheckModelsPatch(models).ok) error('schema-invalid')
365
+ const result = await apply(session, { ...(routeRaw(access, session.route) ?? {}), models }, { ops: [{ op: 'set', path: ['providers', session.route, 'models'], value: models }], ...(renamed ? { deletedIds: [originalModelId], renamedModelId: originalModelId } : {}) })
366
+ sessions.delete(payload.sessionId)
367
+ return result
368
+ }),
369
+ 'models3-add-models': route(async ({ sessionId, modelIds }) => {
370
+ const session = get(sessions, sessionId)
371
+ const access = latest(session)
372
+ const catalog = await loadPiCatalog()
373
+ const defaultEntry = catalog.channels?.find((c) => c.id === session.route) || [...catalog.byModelId.values()].flat().find((x) => x.channel === session.route)
374
+ const raw = routeRaw(access, session.route) ?? {
375
+ displayName: defaultEntry?.name ?? session.route,
376
+ api: defaultEntry?.api ?? 'openai-completions',
377
+ baseURL: defaultEntry?.baseUrl ?? '',
378
+ models: [],
379
+ }
380
+ if (!Array.isArray(modelIds) || !modelIds.length || modelIds.length > 200 || modelIds.some((id) => !validId(id)) || new Set(modelIds).size !== modelIds.length) error('bad-model-id')
381
+ const models = clone(raw.models ?? [])
382
+ if (modelIds.some((id) => models.some((x) => x.id === id))) error('duplicate-model')
383
+ for (const id of modelIds) models.push({ id })
384
+ if (!selfCheckModelsPatch(models).ok) error('schema-invalid')
385
+ const result = await apply(session, { ...(routeRaw(access, session.route) ?? {}), models }, { ops: [{ op: 'set', path: ['providers', session.route, 'models'], value: models }] })
386
+ sessions.delete(sessionId)
387
+ return result
388
+ }),
389
+ 'models3-save-route': route(async (payload) => {
390
+ const session = get(sessions, payload.sessionId)
391
+ const access = latest(session)
392
+ const catalog = await loadPiCatalog()
393
+ const defaultEntry = catalog.channels?.find((c) => c.id === session.route) || [...catalog.byModelId.values()].flat().find((x) => x.channel === session.route)
394
+ const raw = routeRaw(access, session.route) ?? {
395
+ displayName: defaultEntry?.name ?? session.route,
396
+ api: defaultEntry?.api ?? 'openai-completions',
397
+ baseURL: defaultEntry?.baseUrl ?? '',
398
+ models: [],
399
+ }
400
+ const next = patchFields(raw, payload.patch ?? {}, payload.unset ?? [], ROUTE_FIELDS.filter((x) => x !== 'apiKeyEnv'), payload.patch?.api ?? raw.api)
401
+ if (has(payload.patch, 'headers')) next.headers = { ...Object.fromEntries(Object.entries(raw.headers ?? {}).filter(([key]) => sensitiveHeader(key))), ...next.headers }
402
+ if ((payload.unset ?? []).includes('headers') && Object.keys(raw.headers ?? {}).some(sensitiveHeader)) error('sensitive-header')
403
+ // Preserve unknown nested keys; the client never owns their write baseline.
404
+ for (const key of ['compat', 'thinkingBudgets', 'retryPolicy']) if (has(payload.patch, key)) {
405
+ const known = key === 'compat' ? schemaVocabulary().compatGates[next.api] ?? [] : key === 'thinkingBudgets' ? ['minimal', 'low', 'medium', 'high'] : ['mode', 'maxRetries', 'retryableCodes', 'backoff']
406
+ next[key] = { ...Object.fromEntries(Object.entries(raw[key] ?? {}).filter(([name]) => !known.includes(name))), ...next[key] }
407
+ }
408
+ const ops = []
409
+ for (const key of payload.unset ?? []) ops.push({ op: 'unset', path: ['providers', session.route, key] })
410
+ for (const key of Object.keys(payload.patch ?? {})) ops.push({ op: 'set', path: ['providers', session.route, key], value: next[key] })
411
+ // Calculate unset's true inherited result for the restore fingerprint.
412
+ for (const key of payload.unset ?? []) { const value = access.inherited?.()?.providers?.[session.route]?.[key]; if (value !== undefined) next[key] = clone(value) }
413
+ const result = await apply(session, next, { ops })
414
+ sessions.delete(payload.sessionId)
415
+ return result
416
+ }),
417
+ 'models3-remove-preview': route(async (payload) => {
418
+ const session = get(sessions, payload.sessionId)
419
+ const access = latest(session)
420
+ const raw = routeRaw(access, session.route) ?? {
421
+ api: 'openai-completions',
422
+ baseURL: '',
423
+ models: [],
424
+ }
425
+ if (containsSensitive(raw)) error('sensitive-snapshot')
426
+ const ids = payload.modelIds
427
+ let next
428
+ if (ids !== undefined) {
429
+ if (!Array.isArray(ids) || !ids.length || ids.some((id) => !raw.models?.some((m) => m.id === id))) error('unknown-model')
430
+ const models = raw.models.filter((x) => !ids.includes(x.id))
431
+ if (!models.length) error('last-model-undeletable')
432
+ next = { ...raw, models }
433
+ }
434
+ const refs = await references(session.route, ids)
435
+ const blocked = refs.length > 0 || ids === undefined && access.inheritedRoute(session.route)
436
+ const planId = remember(plans, { ...session, kind: 'remove', next, deletedIds: ids, remove: ids === undefined })
437
+ return { planId, route: session.route, removeRoute: ids === undefined, references: refs, blocked, modelsRemoved: ids ?? raw.models?.map((x) => x.id) ?? [], credentialRetained: true, before: displayProfile(raw) }
438
+ }),
439
+ 'models3-remove-commit': route(async ({ planId }) => {
440
+ const plan = get(plans, planId)
441
+ if (plan.kind !== 'remove') error('unknown-plan')
442
+ const result = await apply(plan, plan.next, { deletedIds: plan.deletedIds, remove: plan.remove })
443
+ plans.delete(planId)
444
+ return result
445
+ }),
446
+ 'models3-restore-preview': route(async ({ route: id }) => {
447
+ const access = modelSettingsFor(ctx)
448
+ const state = await storeLoad()
449
+ const snapshot = state.removed?.[id] ?? state.providers?.[id]
450
+ if (!snapshot || !['removed', 'transaction'].includes(snapshot.kind)) error('nothing-to-restore')
451
+ const raw = routeRaw(access, id)
452
+ if (profileFingerprint(raw ?? null) !== snapshot.expectedAfter) error('restore-conflict')
453
+ const next = clone(snapshot.before ?? undefined)
454
+ if (snapshot.protectedHeaders && next) next.headers = { ...(next.headers ?? {}), ...Object.fromEntries(Object.entries(raw?.headers ?? {}).filter(([name]) => sensitiveHeader(name))) }
455
+ const planId = remember(plans, { kind: 'restore', route: id, baseline: fingerprint(access), next, restoreToken: snapshot.token, remove: snapshot.before === null })
456
+ return { planId, route: id, before: displayProfile(raw ?? {}), restore: displayProfile(snapshot.before ?? {}), removesRoute: snapshot.before === null, credentialValueRestored: false }
457
+ }),
458
+ 'models3-restore-commit': route(async ({ planId }) => {
459
+ const plan = get(plans, planId)
460
+ if (plan.kind !== 'restore') error('unknown-plan')
461
+ const state = await storeLoad()
462
+ if ((state.removed?.[plan.route] ?? state.providers?.[plan.route])?.token !== plan.restoreToken) error('restore-conflict')
463
+ const access = latest(plan)
464
+ const current = routeRaw(access, plan.route)
465
+ const removedIds = (current?.models ?? []).filter((row) => !(plan.next?.models ?? []).some((x) => x.id === row.id)).map((row) => row.id)
466
+ const result = await apply(plan, plan.next, { remove: plan.remove, deletedIds: removedIds })
467
+ plans.delete(planId)
468
+ return result
469
+ }),
470
+ 'models3-create-preview': route(async ({ route: id, profile, key }) => {
471
+ if (!MODELS_ROUTE_RE.test(id ?? '') || (id?.length ?? 0) > 64) error('bad-route')
472
+ const access = modelSettingsFor(ctx)
473
+ if (routeRaw(access, id)) error('route-taken')
474
+ if (!object(profile) || Object.keys(profile).some((key) => ![...ROUTE_FIELDS.filter((x) => x !== 'apiKeyEnv'), 'models'].includes(key))) error('unknown-field')
475
+ for (const [field, value] of Object.entries(profile)) if (field !== 'models') validateField(field, value, { api: profile.api })
476
+ if (!PROTOCOLS.includes(profile.api) || !profile.baseURL || !selfCheckModelsPatch(profile.models).ok) error('schema-invalid')
477
+ for (const row of profile.models) { if (Object.keys(row).some((x) => !['id', ...MODEL_FIELDS].includes(x))) error('unknown-field'); for (const [field, value] of Object.entries(row)) if (field !== 'id') validateField(field, value, { api: profile.api }) }
478
+ const credentialRef = typeof key === 'string' && key.trim() ? `${deriveKeyRefFor(id)}_${uuid().replaceAll('-', '').slice(0, 12)}` : null
479
+ const planId = remember(plans, { kind: 'create', route: id, baseline: fingerprint(access), next: clone(profile), key: typeof key === 'string' && key.trim() ? key.trim() : null, credentialRef })
480
+ return { planId, route: id, profile: displayProfile(profile), credentialStored: false }
481
+ }),
482
+ 'models3-create-commit': route(async ({ planId }) => {
483
+ const plan = get(plans, planId)
484
+ if (plan.kind !== 'create') error('unknown-plan')
485
+ latest(plan)
486
+ if (plan.key && plan.credentialRef) {
487
+ const service = ctx.get('credentials')
488
+ if (!service?.set) error('credential-rejected')
489
+ await service.set(plan.credentialRef, plan.key)
490
+ plan.next.apiKeyEnv = plan.credentialRef
491
+ }
492
+ const result = await apply(plan, plan.next)
493
+ plans.delete(planId)
494
+ return { ...result, credentialStored: !!plan.key }
495
+ }),
496
+ 'models3-discover': route(async ({ sessionId, baseURL, api, key }) => {
497
+ let routeName = ''
498
+ let targetURL = baseURL
499
+ let targetApi = api ?? 'openai-completions'
500
+ let targetKey = typeof key === 'string' && key.trim() ? key.trim() : undefined
501
+ if (sessionId) {
502
+ const session = get(sessions, sessionId)
503
+ const access = latest(session)
504
+ const raw = routeRaw(access, session.route)
505
+ if (!raw) error('route-gone')
506
+ routeName = session.route
507
+ const catalog = await loadPiCatalog()
508
+ const catalogModel = [...catalog.byModelId.values()].flat().find((entry) => entry.channel === session.route && entry.api)
509
+ const catalogChannel = catalog.channels?.find((entry) => entry.id === session.route)
510
+ targetURL = raw.baseURL ?? catalogChannel?.baseUrl ?? catalogModel?.baseUrl
511
+ targetApi = raw.api ?? catalogModel?.api
512
+ if (raw.apiKeyEnv) {
513
+ const credentials = ctx.get('credentials')
514
+ try { targetKey = (await credentials?.resolve(raw.apiKeyEnv))?.value } catch (_) { error('credential-missing') }
515
+ if (typeof targetKey !== 'string' || !targetKey) error('credential-missing')
516
+ }
517
+ } else if (!targetURL) {
518
+ error('unknown-plan')
519
+ }
520
+ if (!PROTOCOLS.includes(targetApi)) error('probe-protocol-unsupported')
521
+ let rows
522
+ try { rows = await fetchModelsProbe(targetURL, targetKey, targetApi) } catch (failure) {
523
+ const status = /^probe-status:(\d{3})$/.exec(failure?.message ?? '')?.[1]
524
+ if (status === '401') error(targetKey ? 'discovery-auth-failed' : 'credential-missing')
525
+ if (status === '403') error('discovery-forbidden')
526
+ if (status === '429') error('discovery-rate-limited')
527
+ if (status) error('discovery-http-' + status)
528
+ throw failure
529
+ }
530
+ return { route: routeName, usedCredential: !!targetKey, candidates: rows.map((x) => ({ id: x.id, name: x.name })), saved: false }
531
+ }),
532
+ 'models3-credential-preview': route(async ({ sessionId, shared = false }) => {
533
+ const session = get(sessions, sessionId)
534
+ const access = latest(session)
535
+ const raw = routeRaw(access, session.route)
536
+ if (!raw) error('route-gone')
537
+ const users = Object.entries(access.get('llm-pi-ai').providers ?? {}).filter(([, v]) => raw.apiKeyEnv && v.apiKeyEnv === raw.apiKeyEnv).map(([id]) => id)
538
+ if (shared && !MODELS_KEY_REF_RE.test(raw.apiKeyEnv ?? '')) error('shared-ref-unavailable')
539
+ // Default is a fresh owner-only reference: never overwrite or compensate a
540
+ // pre-existing credential. Shared replacement is a separate explicit intent.
541
+ const ref = shared && MODELS_KEY_REF_RE.test(raw.apiKeyEnv ?? '') ? raw.apiKeyEnv : `${deriveKeyRefFor(session.route)}_${uuid().replaceAll('-', '').slice(0, 12)}`
542
+ const service = ctx.get('credentials')
543
+ if (!service?.set || !service?.describe) error('credential-rejected')
544
+ const info = await service.describe(ref)
545
+ if (info?.writable === false) error('credential-rejected')
546
+ const planId = remember(plans, { ...session, kind: 'credential', credentialRef: ref, shared: shared === true, configured: info?.configured === true })
547
+ return { planId, ref, shared: shared === true, users, externalConsumersUnknown: true, configured: info?.configured === true }
548
+ }),
549
+ 'models3-credential-commit': route(async ({ planId, key }) => serialize(async () => {
550
+ const plan = get(plans, planId)
551
+ if (plan.kind !== 'credential' || !plan.credentialRef) error('unknown-plan')
552
+ if (typeof key !== 'string' || !key.trim() || key.length > 4096) error('bad-key')
553
+ let access = latest(plan)
554
+ if (access.revisionTracked !== true) error('model-config-revision-unavailable')
555
+ if (own((await storeLoad()).recovery, plan.route)) error('recovery-pending')
556
+ const service = ctx.get('credentials')
557
+ const info = await service?.describe?.(plan.credentialRef)
558
+ if (!plan.shared && info?.configured) error('credential-taken')
559
+ if (info?.writable === false) error('credential-rejected')
560
+ const raw = routeRaw(access, plan.route)
561
+ const next = { ...raw, apiKeyEnv: plan.credentialRef }
562
+ const backup = await persistBefore(plan.route, raw, next, plan.baseline)
563
+ try { access = latest(plan); access.assertOwner?.() } catch (failure) { try { await rollbackSnapshot(plan.route, backup) } catch (_) {}; throw failure }
564
+ try { await service.set(plan.credentialRef, key) } catch (_) { try { await rollbackSnapshot(plan.route, backup) } catch (_) {}; error('credential-rejected') }
565
+ plans.delete(planId)
566
+ try { await access.mutate('llm-pi-ai', [{ op: 'set', path: ['providers', plan.route, 'apiKeyEnv'], value: plan.credentialRef }], access.revision) } catch (_) {
567
+ try { await rollbackSnapshot(plan.route, backup) } catch (_) {}
568
+ // Never delete a credential during compensation. A concurrent consumer
569
+ // may have adopted even a newly allocated reference after its creation.
570
+ return { saved: false, credentialStored: true, ref: plan.credentialRef, shared: plan.shared, configurationFailed: true, undoable: false }
571
+ }
572
+ let recoveryRecorded = true
573
+ try { await workbenchStore.mutate((state) => { if (state.providers?.[plan.route]?.token === backup.token) state.providers[plan.route].status = 'applied' }) } catch (_) { recoveryRecorded = false }
574
+ return { saved: true, credentialStored: true, ref: plan.credentialRef, shared: plan.shared, undoable: true, recoveryRecorded }
575
+ })),
576
+ 'models3-test-preview': route(async ({ sessionId, modelId }) => {
577
+ const session = get(sessions, sessionId)
578
+ const access = latest(session)
579
+ const raw = routeRaw(access, session.route)
580
+ if (!raw?.models?.some((row) => row.id === modelId)) error('unknown-model')
581
+ const planId = remember(plans, { ...session, kind: 'test', testModelId: modelId })
582
+ return { planId, route: session.route, modelId, text: 'ping', system: 'Reply with OK.', maxTokens: 30000, timeoutMs: 120000, mayCharge: true, scope: 'basic-text-call' }
583
+ }),
584
+ 'models3-test-commit': route(async ({ planId }) => {
585
+ const plan = get(plans, planId)
586
+ if (plan.kind !== 'test' || !plan.testModelId) error('unknown-plan')
587
+ latest(plan); plans.delete(planId)
588
+ const llm = ctx.get('llm')
589
+ if (!llm?.stream) error('llm-service-missing')
590
+ const signal = AbortSignal.timeout(120000)
591
+ const began = Date.now()
592
+ let completed = false
593
+ let finishReason
594
+ let failureDetail
595
+ let stage = 'start'
596
+ const deadline = new Promise((_, reject) => signal.addEventListener('abort', () => reject(new Error('stream-timeout')), { once: true }))
597
+ let iterator
598
+ try {
599
+ const stream = await Promise.race([Promise.resolve(llm.stream({ provider: plan.route, model: plan.testModelId, system: 'Reply with OK.', messages: [{ role: 'user', content: [{ type: 'text', text: 'ping' }] }], maxTokens: 30000, signal })), deadline])
600
+ iterator = stream[Symbol.asyncIterator]()
601
+ stage = 'stream'
602
+ let visibleText = false
603
+ while (true) {
604
+ const step = await Promise.race([iterator.next(), deadline])
605
+ if (step.done) break
606
+ const chunk = step.value
607
+ if (signal.aborted || completed) error('stream-failed')
608
+ if (chunk?.type === 'text-delta' && typeof chunk.text === 'string' && chunk.text.length > 0 || chunk?.type === 'block-end' && chunk.block?.type === 'text' && typeof chunk.block.text === 'string' && chunk.block.text.length > 0) visibleText = true
609
+ if (chunk?.type === 'finish') {
610
+ finishReason = ['stop', 'error', 'aborted', 'max-tokens', 'tool-calls'].includes(chunk.reason?.kind) ? chunk.reason.kind : 'unknown'
611
+ if (finishReason !== 'stop' || !visibleText) {
612
+ failureDetail = modelTestFailure(chunk.reason?.failure, finishReason === 'stop' ? 'empty-output' : finishReason)
613
+ error('stream-failed')
614
+ }
615
+ completed = true
616
+ }
617
+ }
618
+ if (!completed) failureDetail = modelTestFailure(null, 'missing-finish')
619
+ } catch (failure) {
620
+ completed = false
621
+ failureDetail ??= modelTestFailure(failure, signal.aborted ? 'stream-timeout' : 'stream-failed')
622
+ try { Promise.resolve(iterator?.return?.()).catch(() => {}) } catch (_) {}
623
+ }
624
+ const result = { ok: completed, at: Date.now(), durationMs: Date.now() - began, modelId: plan.testModelId, scope: 'basic-text-call', ...(finishReason ? { finishReason } : {}), ...(!completed ? { failure: { stage, ...failureDetail } } : {}) }
625
+ if (!completed) {
626
+ try { ctx.logger?.warn?.('dsh-service: model test failed %s', JSON.stringify({ route: plan.route, ...result })) } catch (_) { /* Diagnostics cannot veto the test result. */ }
627
+ }
628
+ try { await workbenchStore?.mutate((state) => { state.validations ??= {}; state.validations[plan.route] = result }) } catch (_) {}
629
+ return result
630
+ }),
631
+ }
632
+ return endpoints
633
+ }