@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.
- package/README.en.md +58 -10
- package/README.md +58 -10
- package/client.js +1 -1
- package/index.js +127 -7
- package/model-config-routes.js +633 -0
- package/model-config-schema.js +132 -0
- package/model-smart-fill.js +2206 -0
- package/model-workbench-routes.js +1740 -0
- package/model-workbench-settings.js +389 -0
- package/model-workbench.js +1618 -0
- package/package.json +10 -2
- package/provider-availability.js +84 -0
- package/subagent-routes.js +7 -0
|
@@ -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
|
+
}
|