dsh-plugin-local-infer 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/docs/zh/README.md +10 -2
- package/dsh/client.js +669 -191
- package/package.json +6 -1
- package/packages/core/lib/adapter.d.ts +44 -2
- package/packages/core/lib/deps.d.ts +4 -3
- package/packages/core/lib/index.d.ts +1 -1
- package/packages/core/lib/registrar.d.ts +5 -5
- package/packages/core/lib/registrar.js +3 -3
- package/packages/core/package.json +1 -1
- package/packages/providers/lib/acquire.d.ts +5 -3
- package/packages/providers/lib/acquire.js +30 -9
- package/packages/providers/lib/adapters.d.ts +19 -209
- package/packages/providers/lib/adapters.js +13 -886
- package/packages/providers/lib/deps.d.ts +24 -2
- package/packages/providers/lib/deps.js +23 -4
- package/packages/providers/lib/engines/hyperqwenAdapter.d.ts +38 -0
- package/packages/providers/lib/engines/hyperqwenAdapter.js +105 -0
- package/packages/providers/lib/{infernix.js → engines/infernix.js} +1 -1
- package/packages/providers/lib/engines/infernixAdapter.d.ts +53 -0
- package/packages/providers/lib/engines/infernixAdapter.js +202 -0
- package/packages/providers/lib/engines/llamacppAdapter.d.ts +51 -0
- package/packages/providers/lib/engines/llamacppAdapter.js +232 -0
- package/packages/providers/lib/{ninfer.js → engines/ninfer.js} +1 -1
- package/packages/providers/lib/engines/ninferAdapter.d.ts +82 -0
- package/packages/providers/lib/engines/ninferAdapter.js +324 -0
- package/packages/providers/lib/engines/strata.d.ts +53 -0
- package/packages/providers/lib/engines/strata.js +197 -0
- package/packages/providers/lib/engines/strataAdapter.d.ts +98 -0
- package/packages/providers/lib/engines/strataAdapter.js +427 -0
- package/packages/providers/lib/engines.d.ts +5 -4
- package/packages/providers/lib/engines.js +10 -10
- package/packages/providers/lib/guidelines.js +1 -1
- package/packages/providers/lib/index.d.ts +14 -9
- package/packages/providers/lib/index.js +17 -7
- package/packages/providers/lib/model-defaults.d.ts +5 -4
- package/packages/providers/lib/model-defaults.js +22 -4
- package/packages/providers/lib/platform.js +7 -1
- package/packages/providers/package.json +1 -1
- package/packages/service/lib/index.d.ts +13 -4
- package/packages/service/lib/index.js +79 -5
- package/packages/service/lib/types.d.ts +5 -4
- package/packages/service/package.json +1 -1
- package/packages/tool/package.json +1 -1
- package/packages/ui/lib/config.d.ts +79 -0
- package/packages/ui/lib/config.js +235 -0
- package/packages/ui/lib/deps.d.ts +3 -3
- package/packages/ui/lib/ops/engine.d.ts +108 -0
- package/packages/ui/lib/ops/engine.js +493 -0
- package/packages/ui/lib/ops/hf.d.ts +27 -0
- package/packages/ui/lib/ops/hf.js +60 -0
- package/packages/ui/lib/ops/logs.d.ts +23 -0
- package/packages/ui/lib/ops/logs.js +58 -0
- package/packages/ui/lib/ops/model.d.ts +79 -0
- package/packages/ui/lib/ops/model.js +130 -0
- package/packages/ui/lib/request.d.ts +106 -0
- package/packages/ui/lib/request.js +80 -0
- package/packages/ui/lib/route.d.ts +88 -420
- package/packages/ui/lib/route.js +111 -1843
- package/packages/ui/lib/routes/chat-template.d.ts +14 -0
- package/packages/ui/lib/routes/chat-template.js +37 -0
- package/packages/ui/lib/routes/deps.d.ts +16 -0
- package/packages/ui/lib/routes/deps.js +27 -0
- package/packages/ui/lib/routes/download.d.ts +16 -0
- package/packages/ui/lib/routes/download.js +48 -0
- package/packages/ui/lib/routes/get.d.ts +18 -0
- package/packages/ui/lib/routes/get.js +57 -0
- package/packages/ui/lib/routes/install.d.ts +16 -0
- package/packages/ui/lib/routes/install.js +36 -0
- package/packages/ui/lib/routes/launch.d.ts +18 -0
- package/packages/ui/lib/routes/launch.js +50 -0
- package/packages/ui/lib/routes/provider-delete.d.ts +15 -0
- package/packages/ui/lib/routes/provider-delete.js +37 -0
- package/packages/ui/lib/routes/register.d.ts +16 -0
- package/packages/ui/lib/routes/register.js +28 -0
- package/packages/ui/lib/routes/save.d.ts +18 -0
- package/packages/ui/lib/routes/save.js +139 -0
- package/packages/ui/lib/routes/stop.d.ts +16 -0
- package/packages/ui/lib/routes/stop.js +29 -0
- package/packages/ui/lib/routes/summary.d.ts +15 -0
- package/packages/ui/lib/routes/summary.js +26 -0
- package/packages/ui/lib/routes/unregister.d.ts +14 -0
- package/packages/ui/lib/routes/unregister.js +26 -0
- package/packages/ui/lib/viewmodels/deps.d.ts +31 -0
- package/packages/ui/lib/viewmodels/deps.js +89 -0
- package/packages/ui/lib/viewmodels/depsHelpers.d.ts +28 -0
- package/packages/ui/lib/viewmodels/depsHelpers.js +33 -0
- package/packages/ui/lib/viewmodels/summary.d.ts +140 -0
- package/packages/ui/lib/viewmodels/summary.js +225 -0
- package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +30 -0
- package/packages/ui/lib/viewmodels/summaryHelpers.js +42 -0
- package/packages/ui/package.json +1 -1
- package/packages/providers/lib/strata.d.ts +0 -15
- package/packages/providers/lib/strata.js +0 -14
- /package/packages/providers/lib/{hyperqwen.d.ts → engines/hyperqwen.d.ts} +0 -0
- /package/packages/providers/lib/{hyperqwen.js → engines/hyperqwen.js} +0 -0
- /package/packages/providers/lib/{infernix.d.ts → engines/infernix.d.ts} +0 -0
- /package/packages/providers/lib/{llamacpp.d.ts → engines/llamacpp.d.ts} +0 -0
- /package/packages/providers/lib/{llamacpp.js → engines/llamacpp.js} +0 -0
- /package/packages/providers/lib/{ninfer.d.ts → engines/ninfer.d.ts} +0 -0
package/packages/ui/lib/route.js
CHANGED
|
@@ -12,517 +12,31 @@
|
|
|
12
12
|
* ~/.dsh/local-infer/config.json, shared with the CLI, so the browser never
|
|
13
13
|
* sees a secret it did not store. The routes are held by an effect, so they
|
|
14
14
|
* go when the plugin does.
|
|
15
|
+
*
|
|
16
|
+
* This module holds the HTTP plumbing every route builds on: the RouteError
|
|
17
|
+
* the domain operations throw with their own status, the RouteResponder the
|
|
18
|
+
* handlers answer through, and defineRoute, the template method that guards
|
|
19
|
+
* the method (405), reads the bounded body (413), and maps the handler's
|
|
20
|
+
* answer to a status. The request parsing lives in ./request.ts, the stored-
|
|
21
|
+
* config persistence in ./config.ts, the view models in ./viewmodels/, the
|
|
22
|
+
* operations in ./ops/, and each route definition in its own file under
|
|
23
|
+
* ./routes/. registerLocalInferRoute wires them onto the web server.
|
|
15
24
|
* @module dsh-plugin-local-infer/ui/route
|
|
16
25
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* repo reference (an owner/name pair) rather than a local path: it carries a
|
|
32
|
-
* slash and does not start with one. Only repo references are auto-downloaded
|
|
33
|
-
* into the model directory; a local path is used in place.
|
|
34
|
-
*/
|
|
35
|
-
function isHfRepoReference(value) {
|
|
36
|
-
return value !== '' && !value.startsWith('/') && value.includes('/');
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* chatTemplateLabel resolves the display label for a chat template repo: the
|
|
40
|
-
* preset's label when the repo is a shipped preset, else the repo itself.
|
|
41
|
-
*/
|
|
42
|
-
function chatTemplateLabel(repo) {
|
|
43
|
-
const preset = CHAT_TEMPLATE_OPTIONS.find((option) => option.value === repo);
|
|
44
|
-
return preset?.label ?? repo;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* resolveModelRegistryId maps the card's model value onto the VramModel
|
|
48
|
-
* registry key the estimator prices. The card's Model field carries either a
|
|
49
|
-
* registry key (a default-model selection) or an artifact file name (a
|
|
50
|
-
* directory-scan selection, e.g. a .ninfer file); the default list maps the
|
|
51
|
-
* file name to the registry key the estimator knows, so a scanned model prices
|
|
52
|
-
* a real VramModel rather than reading as not-measured. A value that is
|
|
53
|
-
* already a registry key passes through, and a missing value falls back to
|
|
54
|
-
* the engine's first default model.
|
|
55
|
-
*/
|
|
56
|
-
function resolveModelRegistryId(engineId, model) {
|
|
57
|
-
if (typeof model === 'string' && model !== '') {
|
|
58
|
-
const entry = defaultModelsForEngine(engineId).find((candidate) => candidate.file === model);
|
|
59
|
-
return entry?.modelId ?? model;
|
|
60
|
-
}
|
|
61
|
-
return defaultModelsForEngine(engineId)[0]?.modelId ?? engineId;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* The config file the route owns. It sits under the dsh home so a web user and
|
|
65
|
-
* a CLI user share one file, the way modsearch shares ~/.modsearch.
|
|
66
|
-
*/
|
|
67
|
-
export function localInferConfigPath() {
|
|
68
|
-
return join(homedir(), '.dsh', 'local-infer', 'config.json');
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* readConfig reads the stored engine, config, trust gate, and per-engine
|
|
72
|
-
* platform configs. A missing or broken file reads as the scaffold default
|
|
73
|
-
* (NInfer, empty config, gate off, no platform configs) so the card always has
|
|
74
|
-
* a form to draw. A platform entry that is not an object (or whose fields are
|
|
75
|
-
* not strings) is dropped, so a by-hand edit never poisons the deploy step.
|
|
76
|
-
*/
|
|
77
|
-
export function readConfig() {
|
|
78
|
-
const path = localInferConfigPath();
|
|
79
|
-
if (!existsSync(path)) {
|
|
80
|
-
return {
|
|
81
|
-
engine: 'ninfer',
|
|
82
|
-
config: {},
|
|
83
|
-
allowRunDeps: false,
|
|
84
|
-
platform: {},
|
|
85
|
-
showHelp: true,
|
|
86
|
-
providers: [],
|
|
87
|
-
logPath: DEFAULT_LOG_PATH_TEMPLATE,
|
|
88
|
-
lastLogs: '',
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
93
|
-
return {
|
|
94
|
-
engine: typeof parsed.engine === 'string' ? parsed.engine : 'ninfer',
|
|
95
|
-
config: parsed.config && typeof parsed.config === 'object' ? parsed.config : {},
|
|
96
|
-
allowRunDeps: typeof parsed.allowRunDeps === 'boolean' ? parsed.allowRunDeps : false,
|
|
97
|
-
platform: parsePlatformConfigs(parsed.platform),
|
|
98
|
-
// A file that predates the key (or carries a non-boolean) reads as
|
|
99
|
-
// descriptions visible, the default the card draws on first open.
|
|
100
|
-
showHelp: typeof parsed.showHelp === 'boolean' ? parsed.showHelp : true,
|
|
101
|
-
providers: parseProviders(parsed.providers),
|
|
102
|
-
// A file that predates the key reads as the plugin's default log dir,
|
|
103
|
-
// and the last-run logs start empty until a launch fills them.
|
|
104
|
-
logPath: typeof parsed.logPath === 'string' && parsed.logPath !== ''
|
|
105
|
-
? parsed.logPath
|
|
106
|
-
: DEFAULT_LOG_PATH_TEMPLATE,
|
|
107
|
-
lastLogs: typeof parsed.lastLogs === 'string' ? parsed.lastLogs : '',
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
return {
|
|
112
|
-
engine: 'ninfer',
|
|
113
|
-
config: {},
|
|
114
|
-
allowRunDeps: false,
|
|
115
|
-
platform: {},
|
|
116
|
-
showHelp: true,
|
|
117
|
-
providers: [],
|
|
118
|
-
logPath: DEFAULT_LOG_PATH_TEMPLATE,
|
|
119
|
-
lastLogs: '',
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* parseProviders reads the stored provider list, keeping only entries with a
|
|
125
|
-
* non-blank string id. A non-object entry or an entry with no id is dropped,
|
|
126
|
-
* so a by-hand edit never poisons the card's provider list. The per-engine
|
|
127
|
-
* platform config is read through the same string-field rule the platform map
|
|
128
|
-
* uses, so a provider never carries a platform field it cannot price.
|
|
129
|
-
*/
|
|
130
|
-
function parseProviders(raw) {
|
|
131
|
-
if (raw === null || typeof raw !== 'object' || !Array.isArray(raw)) {
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
|
-
const providers = [];
|
|
135
|
-
for (const entry of raw) {
|
|
136
|
-
if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
|
|
137
|
-
continue;
|
|
138
|
-
const fields = entry;
|
|
139
|
-
if (typeof fields.id !== 'string' || fields.id.trim() === '')
|
|
140
|
-
continue;
|
|
141
|
-
const engine = typeof fields.engine === 'string' ? fields.engine : 'ninfer';
|
|
142
|
-
const config = fields.config && typeof fields.config === 'object' ? fields.config : {};
|
|
143
|
-
const platform = parseStoredPlatformConfig(fields.platform) ?? {};
|
|
144
|
-
const provider = { id: fields.id, engine, config, platform };
|
|
145
|
-
const launch = parseLaunchRecord(fields.launch);
|
|
146
|
-
if (launch !== undefined) {
|
|
147
|
-
provider.launch = launch;
|
|
148
|
-
}
|
|
149
|
-
providers.push(provider);
|
|
150
|
-
}
|
|
151
|
-
return providers;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* parseLaunchRecord reads one stored launch record, keeping only the fields
|
|
155
|
-
* the re-link rule prices (the engine pid, the start time, the argv, the
|
|
156
|
-
* parent and previous pids, and the engine/model/port the detector ties the
|
|
157
|
-
* engine to). A record that is not an object, or that names no integer pid
|
|
158
|
-
* or engine id, is dropped, so a by-hand edit never poisons the re-link.
|
|
159
|
-
*/
|
|
160
|
-
function parseLaunchRecord(raw) {
|
|
161
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
|
-
const fields = raw;
|
|
165
|
-
if (typeof fields.pid !== 'number' || !Number.isInteger(fields.pid)) {
|
|
166
|
-
return undefined;
|
|
167
|
-
}
|
|
168
|
-
if (typeof fields.startedAt !== 'string' || fields.startedAt === '') {
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
if (typeof fields.engineId !== 'string' || fields.engineId.trim() === '') {
|
|
172
|
-
return undefined;
|
|
173
|
-
}
|
|
174
|
-
const record = {
|
|
175
|
-
pid: fields.pid,
|
|
176
|
-
startedAt: fields.startedAt,
|
|
177
|
-
argv: Array.isArray(fields.argv)
|
|
178
|
-
? fields.argv.filter((part) => typeof part === 'string')
|
|
179
|
-
: [],
|
|
180
|
-
previousPids: Array.isArray(fields.previousPids)
|
|
181
|
-
? fields.previousPids.filter((part) => typeof part === 'number')
|
|
182
|
-
: [],
|
|
183
|
-
engineId: fields.engineId,
|
|
184
|
-
};
|
|
185
|
-
if (typeof fields.parentPid === 'number') {
|
|
186
|
-
record.parentPid = fields.parentPid;
|
|
187
|
-
}
|
|
188
|
-
if (typeof fields.modelId === 'string' && fields.modelId !== '') {
|
|
189
|
-
record.modelId = fields.modelId;
|
|
190
|
-
}
|
|
191
|
-
if (typeof fields.port === 'number') {
|
|
192
|
-
record.port = fields.port;
|
|
193
|
-
}
|
|
194
|
-
return record;
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* newProviderId mints the stable id a new provider entry carries: a
|
|
198
|
-
* timestamp base plus a random tail, so two Saves in the same tick never
|
|
199
|
-
* collide. The card edits and deletes against this id.
|
|
200
|
-
*/
|
|
201
|
-
function newProviderId() {
|
|
202
|
-
return `provider-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* writeConfig persists the engine, config, trust gate, per-engine platform
|
|
206
|
-
* configs, and the help toggle, creating the directory tree on first write.
|
|
207
|
-
*/
|
|
208
|
-
export function writeConfig(engine, config, allowRunDeps, platform, showHelp, providers, logPath, lastLogs) {
|
|
209
|
-
const path = localInferConfigPath();
|
|
210
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
211
|
-
writeFileSync(path, JSON.stringify({
|
|
212
|
-
engine,
|
|
213
|
-
config,
|
|
214
|
-
allowRunDeps,
|
|
215
|
-
platform: platform ?? {},
|
|
216
|
-
showHelp: showHelp ?? true,
|
|
217
|
-
providers: providers ?? [],
|
|
218
|
-
logPath: logPath ?? DEFAULT_LOG_PATH_TEMPLATE,
|
|
219
|
-
lastLogs: lastLogs ?? '',
|
|
220
|
-
}, null, 2));
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* parsePlatformConfigs reads the stored platform map, keeping only entries
|
|
224
|
-
* whose fields are strings. A non-object map or a non-object entry reads as
|
|
225
|
-
* absent, so the deploy step never sees a platform config it cannot price.
|
|
226
|
-
*/
|
|
227
|
-
function parsePlatformConfigs(raw) {
|
|
228
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
229
|
-
return {};
|
|
230
|
-
}
|
|
231
|
-
const platform = {};
|
|
232
|
-
for (const [engineId, entry] of Object.entries(raw)) {
|
|
233
|
-
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
const stored = {};
|
|
237
|
-
const fields = entry;
|
|
238
|
-
if (typeof fields.engineDirectory === 'string') {
|
|
239
|
-
stored.engineDirectory = fields.engineDirectory;
|
|
240
|
-
}
|
|
241
|
-
if (typeof fields.engineFork === 'string') {
|
|
242
|
-
stored.engineFork = fields.engineFork;
|
|
243
|
-
}
|
|
244
|
-
if (typeof fields.buildCommands === 'string') {
|
|
245
|
-
stored.buildCommands = fields.buildCommands;
|
|
246
|
-
}
|
|
247
|
-
if (typeof fields.dockerTag === 'string') {
|
|
248
|
-
stored.dockerTag = fields.dockerTag;
|
|
249
|
-
}
|
|
250
|
-
if (stored.engineDirectory !== undefined ||
|
|
251
|
-
stored.engineFork !== undefined ||
|
|
252
|
-
stored.buildCommands !== undefined ||
|
|
253
|
-
stored.dockerTag !== undefined) {
|
|
254
|
-
platform[engineId] = stored;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return platform;
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* parseStoredPlatformConfig reads one engine's platform config (the launch
|
|
261
|
-
* request's `platform` key), keeping only string fields. A non-object or an
|
|
262
|
-
* object with no string fields reads as absent, so the launch keeps the stored
|
|
263
|
-
* entry the save route synced.
|
|
264
|
-
*/
|
|
265
|
-
function parseStoredPlatformConfig(raw) {
|
|
266
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
267
|
-
return undefined;
|
|
268
|
-
}
|
|
269
|
-
const fields = raw;
|
|
270
|
-
const stored = {};
|
|
271
|
-
if (typeof fields.engineDirectory === 'string') {
|
|
272
|
-
stored.engineDirectory = fields.engineDirectory;
|
|
273
|
-
}
|
|
274
|
-
if (typeof fields.engineFork === 'string') {
|
|
275
|
-
stored.engineFork = fields.engineFork;
|
|
276
|
-
}
|
|
277
|
-
if (typeof fields.buildCommands === 'string') {
|
|
278
|
-
stored.buildCommands = fields.buildCommands;
|
|
279
|
-
}
|
|
280
|
-
if (typeof fields.dockerTag === 'string') {
|
|
281
|
-
stored.dockerTag = fields.dockerTag;
|
|
282
|
-
}
|
|
283
|
-
if (stored.engineDirectory === undefined &&
|
|
284
|
-
stored.engineFork === undefined &&
|
|
285
|
-
stored.buildCommands === undefined &&
|
|
286
|
-
stored.dockerTag === undefined) {
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
return stored;
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* relinkStoredProviders prices every stored provider launch record against the
|
|
293
|
-
* live process table through the service's re-link rule and returns the
|
|
294
|
-
* provider list the card should read: an adopted engine is re-marked plugin-
|
|
295
|
-
* launched and tied to its model by the service, and a liveness watcher starts
|
|
296
|
-
* on its port so a later pkill settles the stored state through the death
|
|
297
|
-
* observer without another summary read; a dead engine's record is cleared on
|
|
298
|
-
* disk here (the previous-pid list with it), so a recycled pid cannot re-link
|
|
299
|
-
* to a config whose engine is down. The route calls it on mount and the
|
|
300
|
-
* summary calls it per read, making re-attach independent of the card opening.
|
|
301
|
-
*/
|
|
302
|
-
export async function relinkStoredProviders(localInfer, stored) {
|
|
303
|
-
const recorded = stored.providers.filter((provider) => provider.launch !== undefined);
|
|
304
|
-
if (recorded.length === 0)
|
|
305
|
-
return stored.providers;
|
|
306
|
-
const outcomes = await localInfer.reLinkProviders(recorded.map((provider) => ({ id: provider.id, record: provider.launch })));
|
|
307
|
-
if (!recorded.some((provider) => outcomes[provider.id] === 'dead')) {
|
|
308
|
-
return stored.providers;
|
|
309
|
-
}
|
|
310
|
-
const providers = stored.providers.map((provider) => {
|
|
311
|
-
if (provider.launch === undefined || outcomes[provider.id] !== 'dead') {
|
|
312
|
-
return provider;
|
|
313
|
-
}
|
|
314
|
-
// Rebuild the dead entry without the launch key, so the record (including
|
|
315
|
-
// the previous-pid list) is gone from the file, not just ignored.
|
|
316
|
-
return {
|
|
317
|
-
id: provider.id,
|
|
318
|
-
engine: provider.engine,
|
|
319
|
-
config: provider.config,
|
|
320
|
-
platform: provider.platform,
|
|
321
|
-
};
|
|
322
|
-
});
|
|
323
|
-
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath, stored.lastLogs);
|
|
324
|
-
return providers;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* buildSummary assembles the card's read model for one engine and config: the
|
|
328
|
-
* static engine and model lists, the detected hardware, the form model the
|
|
329
|
-
* selected engine's parameter surface renders to, the meter model the
|
|
330
|
-
* service's estimate prices, and the warning model the service's guideline
|
|
331
|
-
* engine fires. The estimate is the single source the meter and the verdict
|
|
332
|
-
* banner agree on, and the guideline results are the single source the red
|
|
333
|
-
* banner and the inline warnings agree on.
|
|
334
|
-
*/
|
|
335
|
-
export async function buildSummary(localInfer, request) {
|
|
336
|
-
const stored = readConfig();
|
|
337
|
-
const engineId = request?.engine ?? stored.engine;
|
|
338
|
-
// ENGINES is a known non-empty constant, so the fallback is always defined.
|
|
339
|
-
const engine = ENGINES.find((entry) => entry.id === engineId) ?? ENGINES[0];
|
|
340
|
-
// The Model Directory field defaults to the plugin's model download
|
|
341
|
-
// directory (~/.dsh/models, created when absent), so the card always
|
|
342
|
-
// seeds a real path the deploy step can target. A stored or requested
|
|
343
|
-
// directory that is a non-blank string wins over the default.
|
|
344
|
-
const modelDirectory = localInfer.defaultModelDirectory();
|
|
345
|
-
const rawConfig = request?.config ?? stored.config;
|
|
346
|
-
const config = typeof rawConfig.modelDirectory === 'string' && rawConfig.modelDirectory.trim() !== ''
|
|
347
|
-
? rawConfig
|
|
348
|
-
: { ...rawConfig, modelDirectory };
|
|
349
|
-
const modelDir = typeof config.modelDirectory === 'string' ? config.modelDirectory : modelDirectory;
|
|
350
|
-
const hardware = await localInfer.detectHardware();
|
|
351
|
-
// The model value the card carries maps onto the VramModel registry key the
|
|
352
|
-
// estimator prices: a default-model selection is already a registry key, a
|
|
353
|
-
// directory-scan selection is an artifact file name the default list maps
|
|
354
|
-
// to its registry key, and a missing value falls back to the engine's first
|
|
355
|
-
// default model. Pricing the resolved key is what keeps the meter from
|
|
356
|
-
// reading as not-measured (an empty chart) on a scanned model.
|
|
357
|
-
const modelId = resolveModelRegistryId(engine.id, config.model);
|
|
358
|
-
const pricedConfig = { ...config, model: modelId };
|
|
359
|
-
// Resolve the starting config over the registered presets and the engine's
|
|
360
|
-
// parameter surface, so the estimate and the model-aware guidelines price a
|
|
361
|
-
// sane budget (the preset's kvDtype and max-context) rather than a bare
|
|
362
|
-
// config that names no KV format. The resolved config carries the explicit
|
|
363
|
-
// values the user set over the preset defaults, and the engine's first
|
|
364
|
-
// default model when the user named none. The engine id rides into both
|
|
365
|
-
// entry points: the estimate's OS-reserve resolution runs the plugin-
|
|
366
|
-
// launched check against it, and the model-aware guidelines price the same
|
|
367
|
-
// usable budget the meter does.
|
|
368
|
-
const defaults = await localInfer.resolveDefaults(engine.id, modelId, hardware, pricedConfig);
|
|
369
|
-
const estimate = await localInfer.estimate(defaults.config, hardware, engine.id);
|
|
370
|
-
const guidelineResults = await localInfer.guidelines(defaults.config, hardware, engine.id);
|
|
371
|
-
// The directory scan lists the model files the selected engine can serve
|
|
372
|
-
// straight from the model directory, so the card's Model dropdown offers
|
|
373
|
-
// them beside the defaults. A missing directory reads as an empty list.
|
|
374
|
-
const modelFiles = modelEntriesForEngine(localInfer.listModelDirectory(modelDir), engine.id);
|
|
375
|
-
// The model status checks the selected model against the model directory
|
|
376
|
-
// and probes the hf CLI, so the card shows its Download row (or the URL
|
|
377
|
-
// row) only while the model is absent.
|
|
378
|
-
const entry = defaultModelFor(engine.id, modelId);
|
|
379
|
-
let modelStatus = null;
|
|
380
|
-
if (entry !== undefined) {
|
|
381
|
-
const present = existsSync(modelArtifactPath(entry, modelDir));
|
|
382
|
-
const hfCli = (await localInfer.hfCliAvailable()) !== null;
|
|
383
|
-
modelStatus = {
|
|
384
|
-
modelId: entry.modelId,
|
|
385
|
-
label: entry.label,
|
|
386
|
-
url: entry.url,
|
|
387
|
-
present,
|
|
388
|
-
hfCli,
|
|
389
|
-
downloadCommand: hfDownloadCommand(entry, modelDir),
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
// The chat-template status checks the selected template against the model
|
|
393
|
-
// directory and probes the hf CLI, so the card auto-downloads an absent
|
|
394
|
-
// template (and the System dependencies box shows the download command when
|
|
395
|
-
// the CLI is missing). Only HF repo references are downloadable; a local
|
|
396
|
-
// path or an empty template reads as null.
|
|
397
|
-
const chatTemplateValue = typeof config.chatTemplate === 'string' ? config.chatTemplate : '';
|
|
398
|
-
let chatTemplateStatus = null;
|
|
399
|
-
if (isHfRepoReference(chatTemplateValue)) {
|
|
400
|
-
const present = existsSync(join(modelDir, CHAT_TEMPLATE_FILE));
|
|
401
|
-
const hfCli = (await localInfer.hfCliAvailable()) !== null;
|
|
402
|
-
chatTemplateStatus = {
|
|
403
|
-
repo: chatTemplateValue,
|
|
404
|
-
label: chatTemplateLabel(chatTemplateValue),
|
|
405
|
-
present,
|
|
406
|
-
hfCli,
|
|
407
|
-
downloadCommand: hfChatTemplateCommand(chatTemplateValue, modelDir),
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
// The restart re-link: a provider entry whose launch record names a still-
|
|
411
|
-
// running engine (the DSH that launched it died, and the engine re-parented
|
|
412
|
-
// to init) is re-adopted as plugin-launched, so its status dot reads as
|
|
413
|
-
// running for its model and the OS-reserve resolver keeps the default. A
|
|
414
|
-
// record whose engine is dead is cleared (the previous-pid list with it),
|
|
415
|
-
// so a recycled pid cannot re-link to a config whose engine is down. The
|
|
416
|
-
// adopted engines also get a liveness watcher, so a later pkill settles the
|
|
417
|
-
// stored state without waiting for another summary read.
|
|
418
|
-
const linkedProviders = await relinkStoredProviders(localInfer, stored);
|
|
419
|
-
// The provider list the card renders as collapsed entries: each saved
|
|
420
|
-
// config's engine and model feed the detector, which resolves the running-
|
|
421
|
-
// state its status dot paints. A provider the detector cannot tie to a
|
|
422
|
-
// confirmed model reads as unsure, so the dot never overstates a live
|
|
423
|
-
// engine. The config rides into the detector so the signature prices the
|
|
424
|
-
// provider's own build mode and port, not the adapter's default-port guess.
|
|
425
|
-
const providerRefs = linkedProviders.map((provider) => {
|
|
426
|
-
const model = typeof provider.config.model === 'string' ? provider.config.model : undefined;
|
|
427
|
-
// exactOptionalPropertyTypes: include the model key only when it is a
|
|
428
|
-
// string, so a config that names no model never writes `undefined`.
|
|
429
|
-
return model !== undefined
|
|
430
|
-
? { id: provider.id, engine: provider.engine, model, config: provider.config }
|
|
431
|
-
: { id: provider.id, engine: provider.engine, config: provider.config };
|
|
432
|
-
});
|
|
433
|
-
const providerStatuses = providerRefs.length > 0 ? await localInfer.providerStatuses(providerRefs) : {};
|
|
434
|
-
const providers = linkedProviders.map((provider) => ({
|
|
435
|
-
id: provider.id,
|
|
436
|
-
engine: provider.engine,
|
|
437
|
-
model: typeof provider.config.model === 'string' ? provider.config.model : '',
|
|
438
|
-
status: providerStatuses[provider.id] ?? 'unsure',
|
|
439
|
-
config: provider.config,
|
|
440
|
-
platform: provider.platform,
|
|
441
|
-
}));
|
|
442
|
-
// The "unsure" banner: shown when no provider is confirmed running but at
|
|
443
|
-
// least one reads as unsure (a foreign engine the detector saw but could
|
|
444
|
-
// not tie to a model, or a busy card). The banner offers bash kill commands
|
|
445
|
-
// for the engine processes the detector saw holding VRAM, so the user can
|
|
446
|
-
// stop a provider launched in another session.
|
|
447
|
-
const anyRunning = providers.some((p) => p.status === 'running');
|
|
448
|
-
const anyUnsure = providers.some((p) => p.status === 'unsure');
|
|
449
|
-
const unsureBanner = !anyRunning && anyUnsure
|
|
450
|
-
? {
|
|
451
|
-
message: 'Unable to link to a provider (it may have been launched in another session). \n To stop, run:',
|
|
452
|
-
killCommands: await localInfer.unsureBannerKillCommands(),
|
|
453
|
-
}
|
|
454
|
-
: null;
|
|
455
|
-
// The registered flag tells the card whether the current config's provider
|
|
456
|
-
// and model are already in the DSH settings, so the Remove button shows
|
|
457
|
-
// only when there is something to remove. A profile without the settings
|
|
458
|
-
// seam (a host that predates the typert remote) or a host without the
|
|
459
|
-
// engine adapter registered reads as unregistered.
|
|
460
|
-
let registered = false;
|
|
461
|
-
try {
|
|
462
|
-
const regAdapter = resolveAdapter(localInfer, engine.id);
|
|
463
|
-
const regPlan = regAdapter.renderLaunch(config, hardware);
|
|
464
|
-
const regRow = resolveModelRow(regAdapter, regPlan, config);
|
|
465
|
-
const regEntry = buildModelEntry(regPlan, regRow);
|
|
466
|
-
registered = await localInfer.modelIsRegistered(regEntry.providerId, regRow.id);
|
|
467
|
-
}
|
|
468
|
-
catch {
|
|
469
|
-
// A missing adapter, a missing settings seam, or a refused read all
|
|
470
|
-
// read as unregistered, which hides the Remove button (a no-op removal).
|
|
471
|
-
}
|
|
472
|
-
return {
|
|
473
|
-
engines: ENGINES.map((entry) => {
|
|
474
|
-
// The platform defaults ride the engine entry so the card's Engine
|
|
475
|
-
// platform section seeds the build-command and docker-tag placeholders
|
|
476
|
-
// from the same data the deploy step consumes.
|
|
477
|
-
const defaults = enginePlatformDefaults(entry.id);
|
|
478
|
-
return {
|
|
479
|
-
id: entry.id,
|
|
480
|
-
label: entry.label,
|
|
481
|
-
supports: entry.supports,
|
|
482
|
-
...(defaults.defaultBuildCommand !== undefined
|
|
483
|
-
? { defaultBuildCommand: defaults.defaultBuildCommand }
|
|
484
|
-
: {}),
|
|
485
|
-
...(defaults.defaultDockerTag !== undefined
|
|
486
|
-
? { defaultDockerTag: defaults.defaultDockerTag }
|
|
487
|
-
: {}),
|
|
488
|
-
};
|
|
489
|
-
}),
|
|
490
|
-
models: localInfer.registeredVramModels().map((model) => ({ id: model.id, source: model.source })),
|
|
491
|
-
hardware,
|
|
492
|
-
engineId: engine.id,
|
|
493
|
-
// The hardware profile prices the form's hardware-dependent controls (the
|
|
494
|
-
// host-KV slider cap), so the render model and the meter agree on the
|
|
495
|
-
// machine they run on.
|
|
496
|
-
formModel: renderForm(engine.parameters, config, hardware),
|
|
497
|
-
meterModel: renderMeter(estimate.breakdown, estimate.verdict, hardware.gpu?.vramGiB ?? 0, estimate.source, estimate.reserveSource,
|
|
498
|
-
// The "Unknown Engine(s)" readout shows the VRAM the detector's foreign
|
|
499
|
-
// engine programs are holding: the OS-reserve segment is the GPU's used
|
|
500
|
-
// VRAM when a foreign engine is detected, so the readout names the
|
|
501
|
-
// footprint a provider launched in another session is eating off the
|
|
502
|
-
// card. Absent when no provider reads as unsure.
|
|
503
|
-
anyUnsure ? { bytes: estimate.breakdown.reserve } : null),
|
|
504
|
-
verdict: estimate.verdict,
|
|
505
|
-
warningsModel: renderWarnings(guidelineResults),
|
|
506
|
-
config,
|
|
507
|
-
platformConfigs: stored.platform,
|
|
508
|
-
defaults: { config: defaults.config, provenance: defaults.provenance },
|
|
509
|
-
modelDirectory,
|
|
510
|
-
modelDefaults: defaultModelsForEngine(engine.id).map((entry) => ({
|
|
511
|
-
id: entry.modelId,
|
|
512
|
-
label: entry.label,
|
|
513
|
-
url: entry.url,
|
|
514
|
-
})),
|
|
515
|
-
modelStatus,
|
|
516
|
-
chatTemplateStatus,
|
|
517
|
-
modelFiles,
|
|
518
|
-
showHelp: stored.showHelp,
|
|
519
|
-
providers,
|
|
520
|
-
logPath: stored.logPath,
|
|
521
|
-
lastLogs: stored.lastLogs,
|
|
522
|
-
unsureBanner,
|
|
523
|
-
registered,
|
|
524
|
-
};
|
|
525
|
-
}
|
|
26
|
+
import { readConfig } from "./config.js";
|
|
27
|
+
import { handleEngineDeath, relinkStoredProviders, syncPlatformConfigs, } from "./ops/engine.js";
|
|
28
|
+
import { chatTemplateRoute } from "./routes/chat-template.js";
|
|
29
|
+
import { depsRoute } from "./routes/deps.js";
|
|
30
|
+
import { downloadRoute } from "./routes/download.js";
|
|
31
|
+
import { getRoute } from "./routes/get.js";
|
|
32
|
+
import { installRoute } from "./routes/install.js";
|
|
33
|
+
import { launchRoute } from "./routes/launch.js";
|
|
34
|
+
import { providerDeleteRoute } from "./routes/provider-delete.js";
|
|
35
|
+
import { registerRoute } from "./routes/register.js";
|
|
36
|
+
import { saveRoute } from "./routes/save.js";
|
|
37
|
+
import { stopRoute } from "./routes/stop.js";
|
|
38
|
+
import { summaryRoute } from "./routes/summary.js";
|
|
39
|
+
import { unregisterRoute } from "./routes/unregister.js";
|
|
526
40
|
/**
|
|
527
41
|
* RouteError is a route failure that carries its own HTTP status, so the
|
|
528
42
|
* handlers map the domain answer (a gate that is off, an unsupported build
|
|
@@ -537,1348 +51,122 @@ export class RouteError extends Error {
|
|
|
537
51
|
}
|
|
538
52
|
}
|
|
539
53
|
/**
|
|
540
|
-
* The
|
|
541
|
-
*
|
|
542
|
-
* launch step reads, so the route keeps it in sync with the stored config:
|
|
543
|
-
* on route registration, after every save, and when a launch request carries
|
|
544
|
-
* a platform override. A disposer is tracked per engine, so an engine whose
|
|
545
|
-
* stored config is cleared is unregistered rather than left stale.
|
|
546
|
-
*/
|
|
547
|
-
const platformDisposers = new Map();
|
|
548
|
-
/**
|
|
549
|
-
* setPlatformConfig registers one engine's platform config with the service,
|
|
550
|
-
* replacing any stored entry. A config with no fields is read as "no platform
|
|
551
|
-
* config", which unregisters the engine so the deploy step falls back to the
|
|
552
|
-
* adapter's default source and cache directory.
|
|
553
|
-
*/
|
|
554
|
-
function setPlatformConfig(localInfer, engineId, config) {
|
|
555
|
-
platformDisposers.get(engineId)?.();
|
|
556
|
-
platformDisposers.delete(engineId);
|
|
557
|
-
if (config === undefined) {
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
if (config.engineDirectory === undefined &&
|
|
561
|
-
config.engineFork === undefined &&
|
|
562
|
-
config.buildCommands === undefined &&
|
|
563
|
-
config.dockerTag === undefined) {
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
const platform = { engineId, ...config };
|
|
567
|
-
platformDisposers.set(engineId, localInfer.registerEnginePlatformConfig(platform));
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* syncPlatformConfigs re-syncs the service registry with the stored platform
|
|
571
|
-
* map: it registers (or re-registers) every engine that carries a config, and
|
|
572
|
-
* unregisters every engine that no longer does, so a launch always reads the
|
|
573
|
-
* platform config the user last saved.
|
|
574
|
-
*/
|
|
575
|
-
function syncPlatformConfigs(localInfer, stored) {
|
|
576
|
-
const wanted = stored.platform;
|
|
577
|
-
for (const engineId of [...platformDisposers.keys()]) {
|
|
578
|
-
if (wanted[engineId] === undefined) {
|
|
579
|
-
setPlatformConfig(localInfer, engineId, undefined);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
for (const [engineId, config] of Object.entries(wanted)) {
|
|
583
|
-
setPlatformConfig(localInfer, engineId, config);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
|
-
* resolveBuildMode picks the deploy mode the dep plan prices: the config's
|
|
588
|
-
* buildMode when the selected engine supports it, else the engine's default
|
|
589
|
-
* (local when the engine can build locally, docker when it cannot). The
|
|
590
|
-
* docker-not-installed guideline reads this same value, so the panel and the
|
|
591
|
-
* warning never disagree about the mode.
|
|
592
|
-
*/
|
|
593
|
-
export function resolveBuildMode(engine, config) {
|
|
594
|
-
const defaultMode = engine.supports.local ? 'local' : 'docker';
|
|
595
|
-
const requested = typeof config.buildMode === 'string' ? config.buildMode : undefined;
|
|
596
|
-
if ((requested === 'docker' || requested === 'local') && engine.supports[requested]) {
|
|
597
|
-
return requested;
|
|
598
|
-
}
|
|
599
|
-
return defaultMode;
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* buildDeps assembles the deps panel read model for one engine and config:
|
|
603
|
-
* the detected hardware (with its package manager), the dep plan the service
|
|
604
|
-
* resolves, the per-dep statuses, the copy/paste install string, and the
|
|
605
|
-
* Play/Run trust gate the stored config carries.
|
|
54
|
+
* The request body size cap the POST routes enforce: a config payload is a
|
|
55
|
+
* few kilobytes at most, so a body past 64 KiB is a client bug, not a config.
|
|
606
56
|
*/
|
|
607
|
-
|
|
608
|
-
const stored = readConfig();
|
|
609
|
-
const engineId = request?.engine ?? stored.engine;
|
|
610
|
-
const config = request?.config ?? stored.config;
|
|
611
|
-
// ENGINES is a known non-empty constant, so the fallback is always defined.
|
|
612
|
-
const engine = ENGINES.find((entry) => entry.id === engineId) ?? ENGINES[0];
|
|
613
|
-
const buildMode = resolveBuildMode(engine, config);
|
|
614
|
-
const hardware = await localInfer.detectHardware();
|
|
615
|
-
const deps = await localInfer.deps(engine.id, buildMode, hardware);
|
|
616
|
-
const checks = await checkDeps(deps, hardware);
|
|
617
|
-
// The chat-template row: when a selected template is an HF repo reference
|
|
618
|
-
// and its file is not in the model directory, the System dependencies box
|
|
619
|
-
// shows the download command (the auto-download's fallback when the CLI is
|
|
620
|
-
// missing or the fetch failed). A present template, a local path, or no
|
|
621
|
-
// template leaves the row off.
|
|
622
|
-
const modelDirectory = localInfer.defaultModelDirectory();
|
|
623
|
-
const rawModelDir = config.modelDirectory;
|
|
624
|
-
const modelDir = typeof rawModelDir === 'string' && rawModelDir.trim() !== '' ? rawModelDir : modelDirectory;
|
|
625
|
-
const chatTemplateValue = typeof config.chatTemplate === 'string' ? config.chatTemplate : '';
|
|
626
|
-
let chatTemplate;
|
|
627
|
-
if (isHfRepoReference(chatTemplateValue) && !existsSync(join(modelDir, CHAT_TEMPLATE_FILE))) {
|
|
628
|
-
chatTemplate = {
|
|
629
|
-
repo: chatTemplateValue,
|
|
630
|
-
present: false,
|
|
631
|
-
hfCli: (await localInfer.hfCliAvailable()) !== null,
|
|
632
|
-
downloadCommand: hfChatTemplateCommand(chatTemplateValue, modelDir),
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
// exactOptionalPropertyTypes: the chat-template row only lands while a
|
|
636
|
-
// selected template is missing, so the key is included conditionally.
|
|
637
|
-
const depsInput = {
|
|
638
|
-
engineId: engine.id,
|
|
639
|
-
buildMode,
|
|
640
|
-
packageManager: hardware.packageManager ?? null,
|
|
641
|
-
checks,
|
|
642
|
-
installString: renderInstallString(deps, hardware),
|
|
643
|
-
runAllowed: stored.allowRunDeps,
|
|
644
|
-
};
|
|
645
|
-
if (chatTemplate !== undefined) {
|
|
646
|
-
depsInput.chatTemplate = chatTemplate;
|
|
647
|
-
}
|
|
648
|
-
return renderDeps(depsInput);
|
|
649
|
-
}
|
|
650
|
-
/**
|
|
651
|
-
* runInstallFor is the Play/Run path: it refuses with a 409 while the
|
|
652
|
-
* trust gate is off (the copy/paste path is always the fallback), runs the
|
|
653
|
-
* install through the service, and refreshes the deps model from the
|
|
654
|
-
* re-check, so the "Get" gate opens only after a re-check passes.
|
|
655
|
-
*/
|
|
656
|
-
export async function runInstallFor(localInfer, request) {
|
|
657
|
-
if (readConfig().allowRunDeps !== true) {
|
|
658
|
-
throw new RouteError(409, 'one-click install is not allowed; copy the install string instead');
|
|
659
|
-
}
|
|
660
|
-
const model = await buildDeps(localInfer, request);
|
|
661
|
-
const hardware = await localInfer.detectHardware();
|
|
662
|
-
const result = await localInfer.runInstall(model.engineId, model.buildMode, hardware);
|
|
663
|
-
const deps = model.deps.map((dep) => {
|
|
664
|
-
const entry = result.results.find((entry) => entry.id === dep.id);
|
|
665
|
-
return entry === undefined ? dep : { ...dep, status: entry.status };
|
|
666
|
-
});
|
|
667
|
-
return {
|
|
668
|
-
result,
|
|
669
|
-
deps: { ...model, deps, allSatisfied: result.ok },
|
|
670
|
-
};
|
|
671
|
-
}
|
|
57
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
672
58
|
/**
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
* runs the download through the service.
|
|
59
|
+
* readJsonBody collects a bounded JSON request body: chunks up to the cap,
|
|
60
|
+
* then parses. A body past the cap destroys the request and throws a 413
|
|
61
|
+
* RouteError carrying the route's own message, so the answer names the route
|
|
62
|
+
* that rejected the body.
|
|
678
63
|
*/
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const storedDir = stored.config.modelDirectory;
|
|
688
|
-
const directory = typeof request.directory === 'string' && request.directory.trim() !== ''
|
|
689
|
-
? request.directory
|
|
690
|
-
: typeof storedDir === 'string' && storedDir.trim() !== ''
|
|
691
|
-
? storedDir
|
|
692
|
-
: localInfer.defaultModelDirectory();
|
|
693
|
-
return localInfer.downloadModel(entry, directory);
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* chatTemplateDownloadFor runs the hf CLI download for the request's chat
|
|
697
|
-
* template: it resolves the template repo reference from the request or the
|
|
698
|
-
* stored config's chatTemplate (a 400 when neither names an HF repo
|
|
699
|
-
* reference), resolves the directory against the request, the stored config,
|
|
700
|
-
* and the plugin's default model directory in that order, and runs the
|
|
701
|
-
* download through the service.
|
|
702
|
-
*/
|
|
703
|
-
export async function chatTemplateDownloadFor(localInfer, request) {
|
|
704
|
-
const stored = readConfig();
|
|
705
|
-
let template = typeof request.template === 'string' ? request.template : '';
|
|
706
|
-
if (template.trim() === '') {
|
|
707
|
-
template = typeof stored.config.chatTemplate === 'string' ? stored.config.chatTemplate : '';
|
|
708
|
-
}
|
|
709
|
-
if (!isHfRepoReference(template)) {
|
|
710
|
-
throw new RouteError(400, `the chat template must be an HF repo reference, got "${template || '(none)'}"`);
|
|
711
|
-
}
|
|
712
|
-
const storedDir = stored.config.modelDirectory;
|
|
713
|
-
const directory = typeof request.directory === 'string' && request.directory.trim() !== ''
|
|
714
|
-
? request.directory
|
|
715
|
-
: typeof storedDir === 'string' && storedDir.trim() !== ''
|
|
716
|
-
? storedDir
|
|
717
|
-
: localInfer.defaultModelDirectory();
|
|
718
|
-
return localInfer.downloadChatTemplate(template, directory);
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* resolveAdapter picks the registered engine adapter an engine id routes to. A
|
|
722
|
-
* card that names an engine no adapter serves gets a 404, not a blank panel.
|
|
723
|
-
*/
|
|
724
|
-
function resolveAdapter(localInfer, engineId) {
|
|
725
|
-
const adapter = localInfer.registeredEngines().find((engine) => engine.id === engineId);
|
|
726
|
-
if (adapter === undefined) {
|
|
727
|
-
throw new RouteError(404, `no engine adapter is registered for "${engineId}"`);
|
|
728
|
-
}
|
|
729
|
-
return adapter;
|
|
730
|
-
}
|
|
731
|
-
/**
|
|
732
|
-
* launchCommandNote renders the plan's child-process launch line for the
|
|
733
|
-
* card's Logs section: the core formatter's command under a [local-infer]
|
|
734
|
-
* launch prefix, newline-terminated. A plan that names no command (a bare
|
|
735
|
-
* docker plan with no argv) has no note, and the run logs stay as they are.
|
|
736
|
-
*/
|
|
737
|
-
function launchCommandNote(plan) {
|
|
738
|
-
const command = formatLaunchCommand(plan);
|
|
739
|
-
if (command === undefined)
|
|
740
|
-
return undefined;
|
|
741
|
-
return `[local-infer] launch: ${command}\n`;
|
|
742
|
-
}
|
|
743
|
-
/**
|
|
744
|
-
* applyComposeArgs seeds a compose plan's compose arguments from the stored
|
|
745
|
-
* platform config (the card's docker-tag field for a compose engine, e.g.
|
|
746
|
-
* `--profile single`), so the up line the deploy runs and the down line the
|
|
747
|
-
* stop runs both select the profile the user named. A compose plan with no
|
|
748
|
-
* stored docker-tag keeps the engine's default profile; a non-compose plan is
|
|
749
|
-
* untouched.
|
|
750
|
-
*/
|
|
751
|
-
function applyComposeArgs(plan, localInfer, engineId) {
|
|
752
|
-
if (plan.kind !== 'compose')
|
|
753
|
-
return;
|
|
754
|
-
const platform = localInfer.enginePlatformConfig(engineId);
|
|
755
|
-
if (platform?.dockerTag !== undefined) {
|
|
756
|
-
plan.composeArgs = platform.dockerTag;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* persistStoredLogs overwrites the stored last-run logs (the card's Logs
|
|
761
|
-
* section shows them after a reload) and leaves every other stored field as
|
|
762
|
-
* the launch reads it.
|
|
763
|
-
*/
|
|
764
|
-
function persistStoredLogs(logs) {
|
|
765
|
-
const stored = readConfig();
|
|
766
|
-
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, stored.providers, stored.logPath, logs);
|
|
767
|
-
}
|
|
768
|
-
/**
|
|
769
|
-
* launchEngine renders the plan the registered adapter produces for the config
|
|
770
|
-
* and the detected hardware, then runs the service's launch entry point
|
|
771
|
-
* (deploy, then the health poll). The detail the launch returns names the
|
|
772
|
-
* broken step when the engine does not come up. The launch line (the engine
|
|
773
|
-
* binary and its arguments) leads the run logs the card's Logs section and
|
|
774
|
-
* the log file carry, so a broken setup diagnoses against the exact child
|
|
775
|
-
* process the launch started. The launch step persists the
|
|
776
|
-
* launch record (the engine pid, start time, launch arguments, and the DSH
|
|
777
|
-
* process pid that launched it) on the request's provider entry, so a DSH
|
|
778
|
-
* restart can re-link a surviving engine to its config.
|
|
779
|
-
*/
|
|
780
|
-
export async function launchEngine(localInfer, request, onProgress) {
|
|
781
|
-
const stored = readConfig();
|
|
782
|
-
const engineId = request.engine ?? stored.engine;
|
|
783
|
-
const config = request.config ?? stored.config;
|
|
784
|
-
const adapter = resolveAdapter(localInfer, engineId);
|
|
785
|
-
const hardware = await localInfer.detectHardware();
|
|
786
|
-
// Register the request's platform override (the card's unsaved Engine
|
|
787
|
-
// Directory / Engine Fork edits) so the service's launch resolves it against
|
|
788
|
-
// the adapter's deploy step. An absent platform keeps the stored entry,
|
|
789
|
-
// which the save route and route registration already synced.
|
|
790
|
-
if (request.platform !== undefined) {
|
|
791
|
-
setPlatformConfig(localInfer, engineId, request.platform);
|
|
792
|
-
}
|
|
793
|
-
const plan = adapter.renderLaunch(config, hardware);
|
|
794
|
-
applyComposeArgs(plan, localInfer, engineId);
|
|
795
|
-
const commandNote = launchCommandNote(plan);
|
|
796
|
-
if (commandNote !== undefined) {
|
|
797
|
-
// Persist before the deploy starts: a slow or hung launch keeps the
|
|
798
|
-
// command readable in the Logs section across a card reload, where the
|
|
799
|
-
// engine output has not landed yet.
|
|
800
|
-
persistStoredLogs(commandNote);
|
|
801
|
-
}
|
|
802
|
-
// The prereqs phase: the system-dependency install the local build needs. It
|
|
803
|
-
// runs only while the Play/Run trust gate is on, the same rule the Get step
|
|
804
|
-
// applies; a failed install aborts the launch before the deploy starts. The
|
|
805
|
-
// card's button reads "Prereqs..." for this phase, so the phase is reported
|
|
806
|
-
// only while an install actually runs (the gate on), not when the deps are
|
|
807
|
-
// already satisfied and the phase is skipped.
|
|
808
|
-
if (stored.allowRunDeps === true) {
|
|
809
|
-
onProgress?.('prereqs');
|
|
810
|
-
const buildMode = plan.kind === 'docker' ? 'docker' : 'local';
|
|
811
|
-
const install = await localInfer.runInstall(engineId, buildMode, hardware);
|
|
812
|
-
if (!install.ok) {
|
|
813
|
-
const result = {
|
|
814
|
-
plan,
|
|
815
|
-
healthy: false,
|
|
816
|
-
detail: 'a system dependency install failed',
|
|
817
|
-
};
|
|
818
|
-
persistLaunchLogs(result, engineId, config);
|
|
819
|
-
persistLaunchRecord(result, engineId, config, request.providerId);
|
|
820
|
-
return result;
|
|
64
|
+
async function readJsonBody(req, tooLargeMessage) {
|
|
65
|
+
const chunks = [];
|
|
66
|
+
let total = 0;
|
|
67
|
+
for await (const chunk of req) {
|
|
68
|
+
total += chunk.length;
|
|
69
|
+
if (total > MAX_BODY_BYTES) {
|
|
70
|
+
req.destroy();
|
|
71
|
+
throw new RouteError(413, tooLargeMessage);
|
|
821
72
|
}
|
|
73
|
+
chunks.push(chunk);
|
|
822
74
|
}
|
|
823
|
-
|
|
824
|
-
// reads them to decide whether a rebuild's missing-binary symlink into
|
|
825
|
-
// /usr/local/bin runs (on) or is written to the card's Logs section (off),
|
|
826
|
-
// and whether the git sync (fetch, rebase, rebuild) runs at all.
|
|
827
|
-
const result = await localInfer.launch(plan, {
|
|
828
|
-
...(onProgress !== undefined ? { onProgress } : {}),
|
|
829
|
-
allowRunDeps: stored.allowRunDeps,
|
|
830
|
-
...(request.autoRebuild !== undefined ? { autoRebuild: request.autoRebuild } : {}),
|
|
831
|
-
});
|
|
832
|
-
// Order the run logs: the build output (the rebuild decision, the build
|
|
833
|
-
// command, and its output) leads, the launch command follows it (after a
|
|
834
|
-
// successful build, or a direct launch with no build), and the engine's
|
|
835
|
-
// runtime output comes last. A deploy that failed before the engine printed
|
|
836
|
-
// anything still reports the command it tried to run.
|
|
837
|
-
const buildLogs = result.buildLogs ?? '';
|
|
838
|
-
const engineLogs = result.logs ?? '';
|
|
839
|
-
result.logs = buildLogs + (commandNote ?? '') + engineLogs;
|
|
840
|
-
if (result.buildLogs !== undefined) {
|
|
841
|
-
delete result.buildLogs;
|
|
842
|
-
}
|
|
843
|
-
persistLaunchLogs(result, engineId, config);
|
|
844
|
-
persistLaunchRecord(result, engineId, config, request.providerId);
|
|
845
|
-
return result;
|
|
846
|
-
}
|
|
847
|
-
/**
|
|
848
|
-
* findLaunchTarget resolves the stored provider entry a launch or stop
|
|
849
|
-
* targets: the entry the request names by id, or (an id the card does not
|
|
850
|
-
* send, a launch of the active config) the stored entry with the same engine
|
|
851
|
-
* and model. A launch with no resolvable entry persists no record, which
|
|
852
|
-
* reads as "no provider config to re-link" after a restart.
|
|
853
|
-
*/
|
|
854
|
-
function findLaunchTarget(stored, engineId, providerId, config) {
|
|
855
|
-
if (providerId !== undefined) {
|
|
856
|
-
const byId = stored.providers.find((entry) => entry.id === providerId);
|
|
857
|
-
if (byId !== undefined)
|
|
858
|
-
return byId;
|
|
859
|
-
}
|
|
860
|
-
const model = typeof config.model === 'string' ? config.model : undefined;
|
|
861
|
-
if (model === undefined)
|
|
862
|
-
return undefined;
|
|
863
|
-
return stored.providers.find((entry) => entry.engine === engineId &&
|
|
864
|
-
(typeof entry.config.model === 'string' ? entry.config.model : undefined) === model);
|
|
865
|
-
}
|
|
866
|
-
/**
|
|
867
|
-
* persistLaunchRecord stores a launch's record on the target provider entry:
|
|
868
|
-
* the engine pid the deploy step started, the start time, the plan's argv,
|
|
869
|
-
* the DSH process pid the launch ran under (only when it identified as DSH),
|
|
870
|
-
* and the previous-pid list the re-link rule prices (the entry's own list
|
|
871
|
-
* plus the current DSH pid, capped by the core builder). A launch whose
|
|
872
|
-
* deploy started no tracked process (a compose up) persists nothing, and a
|
|
873
|
-
* launch with no resolvable entry persists nothing.
|
|
874
|
-
*/
|
|
875
|
-
function persistLaunchRecord(result, engineId, config, providerId) {
|
|
876
|
-
if (result.pid === undefined)
|
|
877
|
-
return;
|
|
878
|
-
const stored = readConfig();
|
|
879
|
-
const target = findLaunchTarget(stored, engineId, providerId, config);
|
|
880
|
-
if (target === undefined)
|
|
881
|
-
return;
|
|
882
|
-
const record = launchRecordFromPlan(result.plan, result.pid, result.parentPid, target.launch?.previousPids ?? []);
|
|
883
|
-
const providers = stored.providers.map((entry) => entry.id === target.id ? { ...entry, launch: record } : entry);
|
|
884
|
-
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath, stored.lastLogs);
|
|
885
|
-
}
|
|
886
|
-
/**
|
|
887
|
-
* clearLaunchRecord removes the target provider entry's launch record (the
|
|
888
|
-
* engine pid and the previous-pid list with it), so a stopped engine's pid
|
|
889
|
-
* cannot be re-linked to its config by a recycled pid. A stop with no
|
|
890
|
-
* resolvable entry, or an entry that carries no record, is a no-op.
|
|
891
|
-
*/
|
|
892
|
-
function clearLaunchRecord(engineId, config, providerId) {
|
|
893
|
-
const stored = readConfig();
|
|
894
|
-
const target = findLaunchTarget(stored, engineId, providerId, config);
|
|
895
|
-
if (target === undefined || target.launch === undefined)
|
|
896
|
-
return;
|
|
897
|
-
const providers = stored.providers.map((entry) => {
|
|
898
|
-
if (entry.id !== target.id)
|
|
899
|
-
return entry;
|
|
900
|
-
// Rebuild the entry without the launch key, so the record (including the
|
|
901
|
-
// previous-pid list) is gone from the file, not just ignored.
|
|
902
|
-
return { id: entry.id, engine: entry.engine, config: entry.config, platform: entry.platform };
|
|
903
|
-
});
|
|
904
|
-
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath, stored.lastLogs);
|
|
905
|
-
}
|
|
906
|
-
/**
|
|
907
|
-
* handleEngineDeath persists a provider entry's state when its engine process
|
|
908
|
-
* dies outside the plugin's stop path: a crash, a pkill of a plugin-launched
|
|
909
|
-
* child seen through the service's exit watch, or the poll-detected exit of a
|
|
910
|
-
* process adopted after a DSH restart. The entry whose launch record names the
|
|
911
|
-
* dead pid (or the dead engine-and-port, when the report carries no pid match)
|
|
912
|
-
* has its record cleared, so a restart never re-links a dead pid, and the exit
|
|
913
|
-
* line lands in the card's last-run logs beside the engine's own output. An
|
|
914
|
-
* event that matches no entry carrying a record (a stop the route already
|
|
915
|
-
* cleared, an engine the plugin never tracked) is a no-op, which keeps an
|
|
916
|
-
* expected stop quiet.
|
|
917
|
-
*/
|
|
918
|
-
export function handleEngineDeath(event) {
|
|
919
|
-
const stored = readConfig();
|
|
920
|
-
let matched = false;
|
|
921
|
-
const providers = stored.providers.map((entry) => {
|
|
922
|
-
const record = entry.launch;
|
|
923
|
-
if (record === undefined)
|
|
924
|
-
return entry;
|
|
925
|
-
const byPid = event.pid !== undefined && record.pid === event.pid;
|
|
926
|
-
const byEnginePort = event.port !== undefined && record.engineId === entry.engine && record.port === event.port;
|
|
927
|
-
if (!byPid && !byEnginePort)
|
|
928
|
-
return entry;
|
|
929
|
-
matched = true;
|
|
930
|
-
// Rebuild the entry without the launch key, so the record (including the
|
|
931
|
-
// previous-pid list) is gone from the file, not just ignored.
|
|
932
|
-
return { id: entry.id, engine: entry.engine, config: entry.config, platform: entry.platform };
|
|
933
|
-
});
|
|
934
|
-
if (!matched)
|
|
935
|
-
return;
|
|
936
|
-
const signalNote = event.signal !== null && event.signal !== undefined
|
|
937
|
-
? ` on signal ${event.signal}`
|
|
938
|
-
: ` with exit code ${event.code}`;
|
|
939
|
-
const note = `[local-infer] engine ${event.engineId}` +
|
|
940
|
-
`${event.port !== undefined ? ` (port ${event.port})` : ''}` +
|
|
941
|
-
` pid ${event.pid ?? 'unknown'} exited${signalNote}` +
|
|
942
|
-
`${event.detail !== undefined ? `: ${event.detail}` : ''}\n`;
|
|
943
|
-
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath,
|
|
944
|
-
// Keep the last-run tail bounded, the same rule the launch logs cap uses.
|
|
945
|
-
(stored.lastLogs + note).slice(-65536));
|
|
946
|
-
}
|
|
947
|
-
/**
|
|
948
|
-
* persistLaunchLogs stores a launch's captured output as the last run and
|
|
949
|
-
* writes it to the configured log file. The log file is only written when the
|
|
950
|
-
* run produced output and the log location is set; the last-run logs are always
|
|
951
|
-
* persisted (an empty string clears a stale run) so the card's Logs section
|
|
952
|
-
* shows the previous run after a reload.
|
|
953
|
-
*/
|
|
954
|
-
export function persistLaunchLogs(result, engineId, config) {
|
|
955
|
-
const stored = readConfig();
|
|
956
|
-
const logs = result.logs ?? '';
|
|
957
|
-
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, stored.providers, stored.logPath, logs);
|
|
958
|
-
if (logs === '') {
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
const modelId = typeof config.model === 'string' ? config.model : undefined;
|
|
962
|
-
const logPath = resolveLogPath(stored.logPath, engineId, modelId, new Date(), homedir());
|
|
963
|
-
try {
|
|
964
|
-
mkdirSync(dirname(logPath), { recursive: true });
|
|
965
|
-
writeFileSync(logPath, logs);
|
|
966
|
-
}
|
|
967
|
-
catch {
|
|
968
|
-
// A log file that cannot be written (a read-only dir, a bad path) must not
|
|
969
|
-
// fail the launch; the in-memory last-run logs still reach the card.
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* stopEngine renders the same plan and runs the service's stop entry point, so
|
|
974
|
-
* the stop step targets the exact container or process the launch started. The
|
|
975
|
-
* stop then clears the target entry's launch record (the engine pid and the
|
|
976
|
-
* previous-pid list with it), so a recycled pid cannot re-link an engine the
|
|
977
|
-
* user just stopped.
|
|
978
|
-
*/
|
|
979
|
-
export async function stopEngine(localInfer, request) {
|
|
980
|
-
const stored = readConfig();
|
|
981
|
-
const engineId = request.engine ?? stored.engine;
|
|
982
|
-
const config = request.config ?? stored.config;
|
|
983
|
-
const adapter = resolveAdapter(localInfer, engineId);
|
|
984
|
-
const hardware = await localInfer.detectHardware();
|
|
985
|
-
const plan = adapter.renderLaunch(config, hardware);
|
|
986
|
-
applyComposeArgs(plan, localInfer, engineId);
|
|
987
|
-
await localInfer.stop(plan);
|
|
988
|
-
clearLaunchRecord(engineId, config, request.providerId);
|
|
75
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
989
76
|
}
|
|
990
77
|
/**
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
* the
|
|
994
|
-
*
|
|
78
|
+
* RouteResponder is the answer side of a route: the JSON writer every route
|
|
79
|
+
* answers through, the stream openers the Get and launch routes use for live
|
|
80
|
+
* output, and the chunk writer a fake response may omit (the route then
|
|
81
|
+
* degrades to collecting only).
|
|
995
82
|
*/
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
83
|
+
export class RouteResponder {
|
|
84
|
+
res;
|
|
85
|
+
constructor(res) {
|
|
86
|
+
this.res = res;
|
|
1000
87
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
return join(modelDir, engineId);
|
|
1006
|
-
}
|
|
1007
|
-
/**
|
|
1008
|
-
* resolveModelDirectory picks the model directory a Get run downloads into:
|
|
1009
|
-
* the config's modelDirectory override when it names a path, else the
|
|
1010
|
-
* plugin's default model directory. The same rule the summary and deps routes
|
|
1011
|
-
* apply, so a Get and the card's Model Directory field agree on the target.
|
|
1012
|
-
*/
|
|
1013
|
-
function resolveModelDirectory(localInfer, config) {
|
|
1014
|
-
const rawModelDir = config.modelDirectory;
|
|
1015
|
-
return typeof rawModelDir === 'string' && rawModelDir.trim() !== ''
|
|
1016
|
-
? rawModelDir
|
|
1017
|
-
: localInfer.defaultModelDirectory();
|
|
1018
|
-
}
|
|
1019
|
-
/**
|
|
1020
|
-
* getEngine is the Get step: it syncs the engine checkout (clone the git
|
|
1021
|
-
* repo, or the fork when the platform entry provides one; fetch and rebase
|
|
1022
|
-
* a clean checkout; abort with a Logs alert when the checkout has local
|
|
1023
|
-
* changes), runs the system dependency plan, and runs the engine's build
|
|
1024
|
-
* steps. Every command and its output streams through log as it lands, so
|
|
1025
|
-
* the card appends it to the Logs section live. The dependency install only
|
|
1026
|
-
* runs while the Play/Run trust gate is on; with the gate off the deps are
|
|
1027
|
-
* re-checked and a missing one aborts the step with the copy/paste note.
|
|
1028
|
-
*/
|
|
1029
|
-
export async function getEngine(localInfer, request, log, host = nodeLifecycleHost) {
|
|
1030
|
-
const stored = readConfig();
|
|
1031
|
-
const engineId = request.engine ?? stored.engine;
|
|
1032
|
-
const config = request.config ?? stored.config;
|
|
1033
|
-
// ENGINES is a known non-empty constant, so the fallback is always defined.
|
|
1034
|
-
const engine = ENGINES.find((entry) => entry.id === engineId) ?? ENGINES[0];
|
|
1035
|
-
// Register the request's platform override the same way launchEngine does,
|
|
1036
|
-
// so the next launch reads the Engine Directory / Engine Fork the Get used.
|
|
1037
|
-
if (request.platform !== undefined) {
|
|
1038
|
-
setPlatformConfig(localInfer, engineId, request.platform);
|
|
88
|
+
/** Write a JSON answer: the status and content-type head, then the body. */
|
|
89
|
+
json(status, body) {
|
|
90
|
+
this.res.writeHead(status, { 'content-type': 'application/json' });
|
|
91
|
+
this.res.end(JSON.stringify(body));
|
|
1039
92
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
log(`get ${engineId} in ${directory}\n`);
|
|
1044
|
-
const sync = await syncEngineRepo(host, { engineId, directory, ...(fork !== undefined && fork.trim() !== '' ? { fork } : {}) }, log);
|
|
1045
|
-
if (!sync.ok) {
|
|
1046
|
-
// A dirty checkout already alerted in the sync step; any other failure
|
|
1047
|
-
// names its step here.
|
|
1048
|
-
if (sync.aborted !== true && sync.detail !== undefined) {
|
|
1049
|
-
log(`!! ${sync.detail}\n`);
|
|
1050
|
-
}
|
|
1051
|
-
return { ok: false, ...(sync.detail !== undefined ? { detail: sync.detail } : {}) };
|
|
93
|
+
/** Open a plain-text stream (the Get route's live log output). */
|
|
94
|
+
textStream() {
|
|
95
|
+
this.res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
|
|
1052
96
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (stored.allowRunDeps === true) {
|
|
1057
|
-
const result = await localInfer.runInstall(engineId, buildMode, hardware);
|
|
1058
|
-
for (const entry of result.results) {
|
|
1059
|
-
log(` ${entry.id}: ${entry.status}${entry.detail !== undefined ? ` (${entry.detail})` : ''}\n`);
|
|
1060
|
-
}
|
|
1061
|
-
if (!result.ok) {
|
|
1062
|
-
log('!! a system dependency install failed; copy the install string from the panel\n');
|
|
1063
|
-
return { ok: false, detail: 'a system dependency install failed' };
|
|
1064
|
-
}
|
|
97
|
+
/** Open an NDJSON stream (the launch route's progress events). */
|
|
98
|
+
ndjsonStream() {
|
|
99
|
+
this.res.writeHead(200, { 'content-type': 'application/x-ndjson; charset=utf-8' });
|
|
1065
100
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
for (const check of checks) {
|
|
1071
|
-
log(` ${check.dep.id}: ${check.status}\n`);
|
|
1072
|
-
if (check.status !== 'satisfied')
|
|
1073
|
-
missing = true;
|
|
1074
|
-
}
|
|
1075
|
-
if (missing) {
|
|
1076
|
-
log('!! a system dependency is missing and one-click install is off; copy the install string from the panel\n');
|
|
1077
|
-
return { ok: false, detail: 'a system dependency is missing' };
|
|
101
|
+
/** Stream one chunk; a fake response without write() collects nothing. */
|
|
102
|
+
write(chunk) {
|
|
103
|
+
if (typeof this.res.write === 'function') {
|
|
104
|
+
this.res.write(chunk);
|
|
1078
105
|
}
|
|
1079
106
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
log(`!! ${build.detail}\n`);
|
|
1084
|
-
}
|
|
1085
|
-
return { ok: false, ...(build.detail !== undefined ? { detail: build.detail } : {}) };
|
|
107
|
+
/** End the response, optionally with a final body chunk. */
|
|
108
|
+
end(body) {
|
|
109
|
+
this.res.end(body);
|
|
1086
110
|
}
|
|
1087
|
-
// The model artifacts a Get leaves ready to launch: the Infernix DFlash2
|
|
1088
|
-
// drafter (when a preset is selected) and the PLE n-gram volume (when the
|
|
1089
|
-
// model key names one), pulled through the hf CLI into the model directory.
|
|
1090
|
-
// A download failure aborts the Get, so a launch never reads a missing
|
|
1091
|
-
// drafter or table.
|
|
1092
|
-
if (engineId === 'infernix') {
|
|
1093
|
-
const modelDir = resolveModelDirectory(localInfer, config);
|
|
1094
|
-
for (const command of infernixGetDownloads(config, modelDir)) {
|
|
1095
|
-
log(`$ ${command}\n`);
|
|
1096
|
-
const outcome = host.runStream !== undefined
|
|
1097
|
-
? await host.runStream(command, (chunk) => log(chunk))
|
|
1098
|
-
: await host.run(command);
|
|
1099
|
-
if (!outcome.ok) {
|
|
1100
|
-
log(`!! ${command} failed: ${outcome.detail ?? 'unknown'}\n`);
|
|
1101
|
-
return { ok: false, detail: 'a model artifact download failed' };
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
log('get complete\n');
|
|
1106
|
-
return { ok: true };
|
|
1107
|
-
}
|
|
1108
|
-
/**
|
|
1109
|
-
* persistGetLogs appends one Get run's streamed output to the stored last-
|
|
1110
|
-
* run logs (the card's Logs section shows it after a reload) and writes no
|
|
1111
|
-
* log file: the launch step owns the per-run files. The tail rule matches
|
|
1112
|
-
* the launch logs' cap, so a long build cannot grow the config without
|
|
1113
|
-
* bound.
|
|
1114
|
-
*/
|
|
1115
|
-
function persistGetLogs(output) {
|
|
1116
|
-
if (output === '')
|
|
1117
|
-
return;
|
|
1118
|
-
const stored = readConfig();
|
|
1119
|
-
const joined = `${stored.lastLogs}${stored.lastLogs === '' ? '' : '\n'}${output}`;
|
|
1120
|
-
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, stored.providers, stored.logPath, joined.slice(-65536));
|
|
1121
|
-
}
|
|
1122
|
-
/**
|
|
1123
|
-
* resolveContextWindow picks the context window the registrar publishes: the
|
|
1124
|
-
* config's maxContext when it names one, else the native ceiling of the
|
|
1125
|
-
* VramModel the config resolves to (the named model, or the single
|
|
1126
|
-
* registered model), else the 32768 spec default. The DSH contextWindow must
|
|
1127
|
-
* not promise a context the engine cannot serve, so the ceiling is the
|
|
1128
|
-
* model's own, not an optimistic number.
|
|
1129
|
-
*/
|
|
1130
|
-
export function resolveContextWindow(config, models) {
|
|
1131
|
-
const configured = config.maxContext;
|
|
1132
|
-
if (typeof configured === 'number' && configured > 0) {
|
|
1133
|
-
return configured;
|
|
1134
|
-
}
|
|
1135
|
-
let model;
|
|
1136
|
-
if (typeof config.model === 'string') {
|
|
1137
|
-
model = models.find((entry) => entry.id === config.model);
|
|
1138
|
-
}
|
|
1139
|
-
else if (models.length === 1) {
|
|
1140
|
-
model = models[0];
|
|
1141
|
-
}
|
|
1142
|
-
return model?.nativeMaxContext ?? 32768;
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* resolveModelRow picks the model row the registrar publishes for an engine
|
|
1146
|
-
* and config: the adapter's modelRef (the id the engine serves on its
|
|
1147
|
-
* OpenAI endpoint) when it declares one, else the plan's model id, else the
|
|
1148
|
-
* engine id. The display name follows the same fall-through.
|
|
1149
|
-
*/
|
|
1150
|
-
function resolveModelRow(adapter, plan, config) {
|
|
1151
|
-
const ref = adapter.modelRef?.(config);
|
|
1152
|
-
const id = ref?.id ?? plan.modelId ?? adapter.id;
|
|
1153
|
-
return { id, name: ref?.name ?? id, contextWindow: 0 };
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* registerFor renders the plan through the registered adapter, resolves the
|
|
1157
|
-
* model row and the context window, and runs the service's register entry
|
|
1158
|
-
* point, which upserts the provider and model through the settings seam.
|
|
1159
|
-
* The result carries the entry the registrar wrote, so the card names the
|
|
1160
|
-
* model it published.
|
|
1161
|
-
*/
|
|
1162
|
-
export async function registerFor(localInfer, request) {
|
|
1163
|
-
const stored = readConfig();
|
|
1164
|
-
const engineId = request?.engine ?? stored.engine;
|
|
1165
|
-
const config = request?.config ?? stored.config;
|
|
1166
|
-
const adapter = resolveAdapter(localInfer, engineId);
|
|
1167
|
-
const hardware = await localInfer.detectHardware();
|
|
1168
|
-
const plan = adapter.renderLaunch(config, hardware);
|
|
1169
|
-
const row = resolveModelRow(adapter, plan, config);
|
|
1170
|
-
row.contextWindow = resolveContextWindow(config, localInfer.registeredVramModels());
|
|
1171
|
-
// The vision flag publishes the Image input type on the DSH model row, so a
|
|
1172
|
-
// vision engine advertises ['text', 'image'] and a text-only engine
|
|
1173
|
-
// ['text']. The registrar copies it into the entry it writes.
|
|
1174
|
-
row.input = config.vision === true ? ['text', 'image'] : ['text'];
|
|
1175
|
-
const options = request?.agentDefault === true ? { agentDefault: true } : undefined;
|
|
1176
|
-
return localInfer.registerModel(plan, row, options);
|
|
1177
|
-
}
|
|
1178
|
-
/**
|
|
1179
|
-
* syncRegisteredModel re-runs the registration for a saved config when the
|
|
1180
|
-
* model is already registered in the DSH settings, so the registered model's
|
|
1181
|
-
* input types (the vision flag) stay in step with the saved config. A model
|
|
1182
|
-
* that is not registered (a fresh save) is a no-op, and a profile without the
|
|
1183
|
-
* settings seam throws, which the save route catches and swallows so the save
|
|
1184
|
-
* itself never fails on a sync that cannot run.
|
|
1185
|
-
*/
|
|
1186
|
-
export async function syncRegisteredModel(localInfer, engine, config) {
|
|
1187
|
-
const adapter = resolveAdapter(localInfer, engine);
|
|
1188
|
-
const hardware = await localInfer.detectHardware();
|
|
1189
|
-
const plan = adapter.renderLaunch(config, hardware);
|
|
1190
|
-
const row = resolveModelRow(adapter, plan, config);
|
|
1191
|
-
const buildMode = plan.kind === 'docker' ? 'docker' : 'local';
|
|
1192
|
-
const providerId = providerIdFor(adapter.id, row.id, buildMode);
|
|
1193
|
-
if (await localInfer.modelIsRegistered(providerId, row.id)) {
|
|
1194
|
-
await registerFor(localInfer, { engine, config });
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
/**
|
|
1198
|
-
* unregisterFor runs the service's removeModel entry point for the engine's
|
|
1199
|
-
* provider and the model row the register step resolved, so the removal
|
|
1200
|
-
* targets the exact entry the registration wrote.
|
|
1201
|
-
*/
|
|
1202
|
-
export async function unregisterFor(localInfer, request) {
|
|
1203
|
-
const stored = readConfig();
|
|
1204
|
-
const engineId = request?.engine ?? stored.engine;
|
|
1205
|
-
const config = request?.config ?? stored.config;
|
|
1206
|
-
const adapter = resolveAdapter(localInfer, engineId);
|
|
1207
|
-
const hardware = await localInfer.detectHardware();
|
|
1208
|
-
const plan = adapter.renderLaunch(config, hardware);
|
|
1209
|
-
const row = resolveModelRow(adapter, plan, config);
|
|
1210
|
-
// The removal must target the exact entry the register step wrote, so the
|
|
1211
|
-
// provider id is computed through buildModelEntry (the same function the
|
|
1212
|
-
// register step uses) rather than a parallel providerIdFor call, which
|
|
1213
|
-
// could drift if plan.engineId and adapter.id ever diverge.
|
|
1214
|
-
const entry = buildModelEntry(plan, row);
|
|
1215
|
-
await localInfer.removeModel(entry.providerId, row.id);
|
|
1216
|
-
return { removed: true };
|
|
1217
111
|
}
|
|
1218
112
|
/**
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
* names. The optional getHost is the lifecycle host the Get route's git and
|
|
1225
|
-
* build steps run through; it defaults to the node host, and a test injects
|
|
1226
|
-
* a stub so the route never clones a real repo in CI.
|
|
113
|
+
* defineRoute is the template every route file builds on: it guards the
|
|
114
|
+
* method (405), reads the bounded body (413, POST only), and maps the
|
|
115
|
+
* handler's answer to a status: a RouteError carries its own status, any
|
|
116
|
+
* other failure answers with the spec's failStatus. The route files carry
|
|
117
|
+
* only the handler body.
|
|
1227
118
|
*/
|
|
1228
|
-
export function
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const configParam = url.searchParams.get('config');
|
|
1232
|
-
let config;
|
|
1233
|
-
if (configParam !== null) {
|
|
1234
|
-
try {
|
|
1235
|
-
config = JSON.parse(configParam);
|
|
1236
|
-
}
|
|
1237
|
-
catch {
|
|
1238
|
-
config = undefined;
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
// exactOptionalPropertyTypes: build the request object with only the keys
|
|
1242
|
-
// that are present, so an absent value never writes `undefined`.
|
|
1243
|
-
const request = {};
|
|
1244
|
-
if (engineParam !== null) {
|
|
1245
|
-
request.engine = engineParam;
|
|
1246
|
-
}
|
|
1247
|
-
if (config !== undefined) {
|
|
1248
|
-
request.config = config;
|
|
1249
|
-
}
|
|
1250
|
-
return buildSummary(localInfer, request);
|
|
1251
|
-
};
|
|
1252
|
-
const route = {
|
|
1253
|
-
name: 'local-infer-config',
|
|
1254
|
-
kind: 'exact',
|
|
1255
|
-
path: '/local-infer/summary',
|
|
1256
|
-
handler: async (req, res) => {
|
|
1257
|
-
const send = (status, body) => {
|
|
1258
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1259
|
-
res.end(JSON.stringify(body));
|
|
1260
|
-
};
|
|
1261
|
-
if (req.method === 'GET') {
|
|
1262
|
-
try {
|
|
1263
|
-
send(200, await summary(new URL(req.url, 'http://localhost')));
|
|
1264
|
-
}
|
|
1265
|
-
catch (error) {
|
|
1266
|
-
send(500, { error: String(error?.message ?? error) });
|
|
1267
|
-
}
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
res.writeHead(405).end();
|
|
1271
|
-
},
|
|
1272
|
-
};
|
|
1273
|
-
const save = {
|
|
1274
|
-
name: 'local-infer-save',
|
|
1275
|
-
kind: 'exact',
|
|
1276
|
-
path: '/local-infer/save',
|
|
1277
|
-
handler: async (req, res) => {
|
|
1278
|
-
const send = (status, body) => {
|
|
1279
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1280
|
-
res.end(JSON.stringify(body));
|
|
1281
|
-
};
|
|
1282
|
-
if (req.method !== 'POST') {
|
|
1283
|
-
res.writeHead(405).end();
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
try {
|
|
1287
|
-
const chunks = [];
|
|
1288
|
-
let total = 0;
|
|
1289
|
-
for await (const chunk of req) {
|
|
1290
|
-
total += chunk.length;
|
|
1291
|
-
if (total > 64 * 1024) {
|
|
1292
|
-
send(413, { error: 'config payload too large' });
|
|
1293
|
-
req.destroy();
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
chunks.push(chunk);
|
|
1297
|
-
}
|
|
1298
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1299
|
-
const engine = typeof body.engine === 'string' ? body.engine : 'ninfer';
|
|
1300
|
-
const config = body.config && typeof body.config === 'object' ? body.config : {};
|
|
1301
|
-
const allowRunDeps = typeof body.allowRunDeps === 'boolean' ? body.allowRunDeps : false;
|
|
1302
|
-
const platform = parsePlatformConfigs(body.platform);
|
|
1303
|
-
// A body that predates the key keeps the descriptions visible.
|
|
1304
|
-
const showHelp = typeof body.showHelp === 'boolean' ? body.showHelp : true;
|
|
1305
|
-
const providerId = typeof body.id === 'string' && body.id.trim() !== '' ? body.id : undefined;
|
|
1306
|
-
// A body that omits the log location (or sends a blank one) keeps the
|
|
1307
|
-
// stored template, so an unsaved edit never wipes a custom path.
|
|
1308
|
-
const logPath = typeof body.logPath === 'string' && body.logPath !== ''
|
|
1309
|
-
? body.logPath
|
|
1310
|
-
: readConfig().logPath;
|
|
1311
|
-
// Save upserts a provider entry: an id updates the stored entry in
|
|
1312
|
-
// place, an absent id mints a new id and prepends the entry, so a Save
|
|
1313
|
-
// always adds (or refreshes) a card entry at the top of the list. The
|
|
1314
|
-
// provider's platform config is the per-engine entry the deploy step
|
|
1315
|
-
// reads, so a provider never carries a field it cannot price. A save
|
|
1316
|
-
// that updates an entry in place carries the entry's launch record
|
|
1317
|
-
// over, so editing a running provider's config never drops the record
|
|
1318
|
-
// the restart re-link prices. A settings-only save skips the upsert:
|
|
1319
|
-
// it persists the global fields the toggle edits and keeps the stored
|
|
1320
|
-
// list intact, so the help and allow-run toggles never mint a
|
|
1321
|
-
// duplicate entry.
|
|
1322
|
-
const stored = readConfig();
|
|
1323
|
-
let providers;
|
|
1324
|
-
if (body.settingsOnly === true) {
|
|
1325
|
-
providers = stored.providers;
|
|
1326
|
-
}
|
|
1327
|
-
else {
|
|
1328
|
-
const provider = {
|
|
1329
|
-
id: providerId ?? newProviderId(),
|
|
1330
|
-
engine,
|
|
1331
|
-
config,
|
|
1332
|
-
platform: platform[engine] ?? {},
|
|
1333
|
-
};
|
|
1334
|
-
if (providerId !== undefined) {
|
|
1335
|
-
const existing = stored.providers.find((entry) => entry.id === providerId);
|
|
1336
|
-
if (existing?.launch !== undefined) {
|
|
1337
|
-
provider.launch = existing.launch;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
providers =
|
|
1341
|
-
providerId !== undefined
|
|
1342
|
-
? stored.providers.map((entry) => (entry.id === providerId ? provider : entry))
|
|
1343
|
-
: [provider, ...stored.providers];
|
|
1344
|
-
}
|
|
1345
|
-
writeConfig(engine, config, allowRunDeps, platform, showHelp, providers, logPath);
|
|
1346
|
-
// Re-sync the service registry with the saved platform map, so the next
|
|
1347
|
-
// launch reads the platform config the user just saved.
|
|
1348
|
-
syncPlatformConfigs(localInfer, {
|
|
1349
|
-
engine,
|
|
1350
|
-
config,
|
|
1351
|
-
allowRunDeps,
|
|
1352
|
-
platform,
|
|
1353
|
-
showHelp,
|
|
1354
|
-
providers,
|
|
1355
|
-
logPath,
|
|
1356
|
-
lastLogs: stored.lastLogs,
|
|
1357
|
-
});
|
|
1358
|
-
// A Save on an already-registered model re-runs the registration, so
|
|
1359
|
-
// the DSH model row's input types (the vision flag) stay in step with
|
|
1360
|
-
// the config the user just saved. Best-effort: a profile without the
|
|
1361
|
-
// settings seam, or an engine no adapter serves, leaves the save intact.
|
|
1362
|
-
try {
|
|
1363
|
-
await syncRegisteredModel(localInfer, engine, config);
|
|
1364
|
-
}
|
|
1365
|
-
catch {
|
|
1366
|
-
// A sync that cannot run is a no-op for the save.
|
|
1367
|
-
}
|
|
1368
|
-
send(200, await buildSummary(localInfer, { engine, config }));
|
|
1369
|
-
}
|
|
1370
|
-
catch (error) {
|
|
1371
|
-
send(400, { error: String(error?.message ?? error) });
|
|
1372
|
-
}
|
|
1373
|
-
},
|
|
1374
|
-
};
|
|
1375
|
-
const deleteProvider = {
|
|
1376
|
-
name: 'local-infer-provider-delete',
|
|
1377
|
-
kind: 'exact',
|
|
1378
|
-
path: '/local-infer/provider-delete',
|
|
1379
|
-
handler: async (req, res) => {
|
|
1380
|
-
const send = (status, body) => {
|
|
1381
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1382
|
-
res.end(JSON.stringify(body));
|
|
1383
|
-
};
|
|
1384
|
-
if (req.method !== 'POST') {
|
|
1385
|
-
res.writeHead(405).end();
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
try {
|
|
1389
|
-
const chunks = [];
|
|
1390
|
-
let total = 0;
|
|
1391
|
-
for await (const chunk of req) {
|
|
1392
|
-
total += chunk.length;
|
|
1393
|
-
if (total > 64 * 1024) {
|
|
1394
|
-
send(413, { error: 'payload too large' });
|
|
1395
|
-
req.destroy();
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
chunks.push(chunk);
|
|
1399
|
-
}
|
|
1400
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1401
|
-
const id = typeof body.id === 'string' ? body.id : undefined;
|
|
1402
|
-
if (id === undefined) {
|
|
1403
|
-
send(400, { error: 'the provider id is required' });
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
// Delete removes the stored provider entry by id, keeping the rest of
|
|
1407
|
-
// the list and the active config intact, then re-syncs the registry so
|
|
1408
|
-
// a launch never reads a platform config the user just removed.
|
|
1409
|
-
const stored = readConfig();
|
|
1410
|
-
const providers = stored.providers.filter((entry) => entry.id !== id);
|
|
1411
|
-
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath, stored.lastLogs);
|
|
1412
|
-
syncPlatformConfigs(localInfer, stored);
|
|
1413
|
-
send(200, await buildSummary(localInfer));
|
|
1414
|
-
}
|
|
1415
|
-
catch (error) {
|
|
1416
|
-
send(400, { error: String(error?.message ?? error) });
|
|
1417
|
-
}
|
|
1418
|
-
},
|
|
1419
|
-
};
|
|
1420
|
-
const deps = {
|
|
1421
|
-
name: 'local-infer-deps',
|
|
1422
|
-
kind: 'exact',
|
|
1423
|
-
path: '/local-infer/deps',
|
|
1424
|
-
handler: async (req, res) => {
|
|
1425
|
-
const send = (status, body) => {
|
|
1426
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1427
|
-
res.end(JSON.stringify(body));
|
|
1428
|
-
};
|
|
1429
|
-
if (req.method !== 'GET') {
|
|
1430
|
-
res.writeHead(405).end();
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
|
-
const url = new URL(req.url, 'http://localhost');
|
|
1434
|
-
const engineParam = url.searchParams.get('engine');
|
|
1435
|
-
const configParam = url.searchParams.get('config');
|
|
1436
|
-
let config;
|
|
1437
|
-
if (configParam !== null) {
|
|
1438
|
-
try {
|
|
1439
|
-
config = JSON.parse(configParam);
|
|
1440
|
-
}
|
|
1441
|
-
catch {
|
|
1442
|
-
config = undefined;
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
// exactOptionalPropertyTypes: build the request object with only the
|
|
1446
|
-
// keys that are present, so an absent value never writes `undefined`.
|
|
1447
|
-
const request = {};
|
|
1448
|
-
if (engineParam !== null) {
|
|
1449
|
-
request.engine = engineParam;
|
|
1450
|
-
}
|
|
1451
|
-
if (config !== undefined) {
|
|
1452
|
-
request.config = config;
|
|
1453
|
-
}
|
|
1454
|
-
try {
|
|
1455
|
-
send(200, await buildDeps(localInfer, request));
|
|
1456
|
-
}
|
|
1457
|
-
catch (error) {
|
|
1458
|
-
send(500, { error: String(error?.message ?? error) });
|
|
1459
|
-
}
|
|
1460
|
-
},
|
|
1461
|
-
};
|
|
1462
|
-
const install = {
|
|
1463
|
-
name: 'local-infer-install',
|
|
1464
|
-
kind: 'exact',
|
|
1465
|
-
path: '/local-infer/install',
|
|
1466
|
-
handler: async (req, res) => {
|
|
1467
|
-
const send = (status, body) => {
|
|
1468
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1469
|
-
res.end(JSON.stringify(body));
|
|
1470
|
-
};
|
|
1471
|
-
if (req.method !== 'POST') {
|
|
1472
|
-
res.writeHead(405).end();
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
try {
|
|
1476
|
-
const chunks = [];
|
|
1477
|
-
let total = 0;
|
|
1478
|
-
for await (const chunk of req) {
|
|
1479
|
-
total += chunk.length;
|
|
1480
|
-
if (total > 64 * 1024) {
|
|
1481
|
-
send(413, { error: 'install payload too large' });
|
|
1482
|
-
req.destroy();
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
chunks.push(chunk);
|
|
1486
|
-
}
|
|
1487
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1488
|
-
const request = {};
|
|
1489
|
-
if (typeof body.engine === 'string') {
|
|
1490
|
-
request.engine = body.engine;
|
|
1491
|
-
}
|
|
1492
|
-
if (body.config && typeof body.config === 'object') {
|
|
1493
|
-
request.config = body.config;
|
|
1494
|
-
}
|
|
1495
|
-
send(200, await runInstallFor(localInfer, request));
|
|
1496
|
-
}
|
|
1497
|
-
catch (error) {
|
|
1498
|
-
if (error instanceof RouteError) {
|
|
1499
|
-
send(error.status, { error: error.message });
|
|
1500
|
-
return;
|
|
1501
|
-
}
|
|
1502
|
-
send(500, { error: String(error?.message ?? error) });
|
|
1503
|
-
}
|
|
1504
|
-
},
|
|
1505
|
-
};
|
|
1506
|
-
const download = {
|
|
1507
|
-
name: 'local-infer-download',
|
|
1508
|
-
kind: 'exact',
|
|
1509
|
-
path: '/local-infer/download',
|
|
1510
|
-
handler: async (req, res) => {
|
|
1511
|
-
const send = (status, body) => {
|
|
1512
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1513
|
-
res.end(JSON.stringify(body));
|
|
1514
|
-
};
|
|
1515
|
-
if (req.method !== 'POST') {
|
|
1516
|
-
res.writeHead(405).end();
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
try {
|
|
1520
|
-
const chunks = [];
|
|
1521
|
-
let total = 0;
|
|
1522
|
-
for await (const chunk of req) {
|
|
1523
|
-
total += chunk.length;
|
|
1524
|
-
if (total > 64 * 1024) {
|
|
1525
|
-
send(413, { error: 'download payload too large' });
|
|
1526
|
-
req.destroy();
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
chunks.push(chunk);
|
|
1530
|
-
}
|
|
1531
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1532
|
-
const request = {};
|
|
1533
|
-
if (typeof body.engine === 'string') {
|
|
1534
|
-
request.engine = body.engine;
|
|
1535
|
-
}
|
|
1536
|
-
if (typeof body.model === 'string') {
|
|
1537
|
-
request.model = body.model;
|
|
1538
|
-
}
|
|
1539
|
-
if (typeof body.directory === 'string') {
|
|
1540
|
-
request.directory = body.directory;
|
|
1541
|
-
}
|
|
1542
|
-
send(200, await downloadFor(localInfer, request));
|
|
1543
|
-
}
|
|
1544
|
-
catch (error) {
|
|
1545
|
-
if (error instanceof RouteError) {
|
|
1546
|
-
send(error.status, { error: error.message });
|
|
1547
|
-
return;
|
|
1548
|
-
}
|
|
1549
|
-
send(500, { error: String(error?.message ?? error) });
|
|
1550
|
-
}
|
|
1551
|
-
},
|
|
1552
|
-
};
|
|
1553
|
-
// The chat-template download route: the card's auto-download (and the
|
|
1554
|
-
// System-dependencies fallback) pulls a selected template into the model
|
|
1555
|
-
// directory through this route, so a picked preset works out of the box.
|
|
1556
|
-
const chatTemplate = {
|
|
1557
|
-
name: 'local-infer-chat-template',
|
|
1558
|
-
kind: 'exact',
|
|
1559
|
-
path: '/local-infer/chat-template',
|
|
1560
|
-
handler: async (req, res) => {
|
|
1561
|
-
const send = (status, body) => {
|
|
1562
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1563
|
-
res.end(JSON.stringify(body));
|
|
1564
|
-
};
|
|
1565
|
-
if (req.method !== 'POST') {
|
|
1566
|
-
res.writeHead(405).end();
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
try {
|
|
1570
|
-
const chunks = [];
|
|
1571
|
-
let total = 0;
|
|
1572
|
-
for await (const chunk of req) {
|
|
1573
|
-
total += chunk.length;
|
|
1574
|
-
if (total > 64 * 1024) {
|
|
1575
|
-
send(413, { error: 'chat template payload too large' });
|
|
1576
|
-
req.destroy();
|
|
1577
|
-
return;
|
|
1578
|
-
}
|
|
1579
|
-
chunks.push(chunk);
|
|
1580
|
-
}
|
|
1581
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1582
|
-
const request = {};
|
|
1583
|
-
if (typeof body.engine === 'string') {
|
|
1584
|
-
request.engine = body.engine;
|
|
1585
|
-
}
|
|
1586
|
-
if (typeof body.template === 'string') {
|
|
1587
|
-
request.template = body.template;
|
|
1588
|
-
}
|
|
1589
|
-
if (typeof body.directory === 'string') {
|
|
1590
|
-
request.directory = body.directory;
|
|
1591
|
-
}
|
|
1592
|
-
send(200, await chatTemplateDownloadFor(localInfer, request));
|
|
1593
|
-
}
|
|
1594
|
-
catch (error) {
|
|
1595
|
-
if (error instanceof RouteError) {
|
|
1596
|
-
send(error.status, { error: error.message });
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
send(500, { error: String(error?.message ?? error) });
|
|
1600
|
-
}
|
|
1601
|
-
},
|
|
1602
|
-
};
|
|
1603
|
-
// The Get route: a POST whose answer streams as plain text, so the card
|
|
1604
|
-
// appends the git sync, the dependency run, and the build output to its
|
|
1605
|
-
// Logs section as it lands. The full run persists as the last-run logs.
|
|
1606
|
-
const get = {
|
|
1607
|
-
name: 'local-infer-get',
|
|
1608
|
-
kind: 'exact',
|
|
1609
|
-
path: '/local-infer/get',
|
|
1610
|
-
handler: async (req, res) => {
|
|
1611
|
-
const send = (status, body) => {
|
|
1612
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1613
|
-
res.end(JSON.stringify(body));
|
|
1614
|
-
};
|
|
1615
|
-
if (req.method !== 'POST') {
|
|
1616
|
-
res.writeHead(405).end();
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
try {
|
|
1620
|
-
const chunks = [];
|
|
1621
|
-
let total = 0;
|
|
1622
|
-
for await (const chunk of req) {
|
|
1623
|
-
total += chunk.length;
|
|
1624
|
-
if (total > 64 * 1024) {
|
|
1625
|
-
send(413, { error: 'get payload too large' });
|
|
1626
|
-
req.destroy();
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
chunks.push(chunk);
|
|
1630
|
-
}
|
|
1631
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1632
|
-
const request = {};
|
|
1633
|
-
if (typeof body.engine === 'string') {
|
|
1634
|
-
request.engine = body.engine;
|
|
1635
|
-
}
|
|
1636
|
-
if (body.config && typeof body.config === 'object') {
|
|
1637
|
-
request.config = body.config;
|
|
1638
|
-
}
|
|
1639
|
-
const platform = parseStoredPlatformConfig(body.platform);
|
|
1640
|
-
if (platform !== undefined) {
|
|
1641
|
-
request.platform = platform;
|
|
1642
|
-
}
|
|
1643
|
-
res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
|
|
1644
|
-
let collected = '';
|
|
1645
|
-
const log = (text) => {
|
|
1646
|
-
collected += text;
|
|
1647
|
-
if (typeof res.write === 'function') {
|
|
1648
|
-
res.write(text);
|
|
1649
|
-
}
|
|
1650
|
-
};
|
|
1651
|
-
try {
|
|
1652
|
-
await getEngine(localInfer, request, log, getHost);
|
|
1653
|
-
}
|
|
1654
|
-
catch (error) {
|
|
1655
|
-
log(`!! get failed: ${String(error?.message ?? error)}\n`);
|
|
1656
|
-
}
|
|
1657
|
-
persistGetLogs(collected);
|
|
1658
|
-
res.end();
|
|
1659
|
-
}
|
|
1660
|
-
catch (error) {
|
|
1661
|
-
send(400, { error: String(error?.message ?? error) });
|
|
1662
|
-
}
|
|
1663
|
-
},
|
|
1664
|
-
};
|
|
1665
|
-
// The launch and stop routes share a POST body parser: the engine, config,
|
|
1666
|
-
// and platform override the plan renders from, falling back to the stored
|
|
1667
|
-
// config per key. The stop route ignores the platform key. `providerId`
|
|
1668
|
-
// names the saved provider entry the launch record (or its stop's clear)
|
|
1669
|
-
// targets.
|
|
1670
|
-
const launchRequestFromBody = (body) => {
|
|
1671
|
-
const request = {};
|
|
1672
|
-
if (typeof body.engine === 'string') {
|
|
1673
|
-
request.engine = body.engine;
|
|
1674
|
-
}
|
|
1675
|
-
if (body.config && typeof body.config === 'object') {
|
|
1676
|
-
request.config = body.config;
|
|
1677
|
-
}
|
|
1678
|
-
const platform = parseStoredPlatformConfig(body.platform);
|
|
1679
|
-
if (platform !== undefined) {
|
|
1680
|
-
request.platform = platform;
|
|
1681
|
-
}
|
|
1682
|
-
if (typeof body.providerId === 'string' && body.providerId.trim() !== '') {
|
|
1683
|
-
request.providerId = body.providerId;
|
|
1684
|
-
}
|
|
1685
|
-
if (typeof body.autoRebuild === 'boolean') {
|
|
1686
|
-
request.autoRebuild = body.autoRebuild;
|
|
1687
|
-
}
|
|
1688
|
-
return request;
|
|
1689
|
-
};
|
|
1690
|
-
const launch = {
|
|
1691
|
-
name: 'local-infer-launch',
|
|
1692
|
-
kind: 'exact',
|
|
1693
|
-
path: '/local-infer/launch',
|
|
1694
|
-
handler: async (req, res) => {
|
|
1695
|
-
const send = (status, body) => {
|
|
1696
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1697
|
-
res.end(JSON.stringify(body));
|
|
1698
|
-
};
|
|
1699
|
-
if (req.method !== 'POST') {
|
|
1700
|
-
res.writeHead(405).end();
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
try {
|
|
1704
|
-
const chunks = [];
|
|
1705
|
-
let total = 0;
|
|
1706
|
-
for await (const chunk of req) {
|
|
1707
|
-
total += chunk.length;
|
|
1708
|
-
if (total > 64 * 1024) {
|
|
1709
|
-
send(413, { error: 'launch payload too large' });
|
|
1710
|
-
req.destroy();
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
chunks.push(chunk);
|
|
1714
|
-
}
|
|
1715
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1716
|
-
// The launch streams its progress as NDJSON: a step event as each phase
|
|
1717
|
-
// (prereqs, download, build, launch) begins, then a final result event.
|
|
1718
|
-
// The card's Launch control reads the step that is running and times
|
|
1719
|
-
// out only the launch (health-poll) step; the rebuild steps run as long
|
|
1720
|
-
// as they need to.
|
|
1721
|
-
res.writeHead(200, { 'content-type': 'application/x-ndjson; charset=utf-8' });
|
|
1722
|
-
const writeLine = (line) => {
|
|
1723
|
-
if (typeof res.write === 'function') {
|
|
1724
|
-
res.write(JSON.stringify(line) + '\n');
|
|
1725
|
-
}
|
|
1726
|
-
};
|
|
1727
|
-
const onProgress = (step) => {
|
|
1728
|
-
writeLine({ step });
|
|
1729
|
-
};
|
|
1730
|
-
try {
|
|
1731
|
-
const result = await launchEngine(localInfer, launchRequestFromBody(body), onProgress);
|
|
1732
|
-
writeLine({ result });
|
|
1733
|
-
}
|
|
1734
|
-
catch (error) {
|
|
1735
|
-
// A launch that breaks before a plan renders (an unknown engine, a
|
|
1736
|
-
// hardware probe failure) still settles the card's control to the red
|
|
1737
|
-
// Logs state through a result event, not a bare 500.
|
|
1738
|
-
const detail = error instanceof RouteError
|
|
1739
|
-
? error.message
|
|
1740
|
-
: String(error?.message ?? error);
|
|
1741
|
-
writeLine({ result: { healthy: false, detail } });
|
|
1742
|
-
}
|
|
1743
|
-
res.end();
|
|
1744
|
-
}
|
|
1745
|
-
catch (error) {
|
|
1746
|
-
if (error instanceof RouteError) {
|
|
1747
|
-
send(error.status, { error: error.message });
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
send(500, { error: String(error?.message ?? error) });
|
|
1751
|
-
}
|
|
1752
|
-
},
|
|
1753
|
-
};
|
|
1754
|
-
const stop = {
|
|
1755
|
-
name: 'local-infer-stop',
|
|
1756
|
-
kind: 'exact',
|
|
1757
|
-
path: '/local-infer/stop',
|
|
1758
|
-
handler: async (req, res) => {
|
|
1759
|
-
const send = (status, body) => {
|
|
1760
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1761
|
-
res.end(JSON.stringify(body));
|
|
1762
|
-
};
|
|
1763
|
-
if (req.method !== 'POST') {
|
|
1764
|
-
res.writeHead(405).end();
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
try {
|
|
1768
|
-
const chunks = [];
|
|
1769
|
-
let total = 0;
|
|
1770
|
-
for await (const chunk of req) {
|
|
1771
|
-
total += chunk.length;
|
|
1772
|
-
if (total > 64 * 1024) {
|
|
1773
|
-
send(413, { error: 'stop payload too large' });
|
|
1774
|
-
req.destroy();
|
|
1775
|
-
return;
|
|
1776
|
-
}
|
|
1777
|
-
chunks.push(chunk);
|
|
1778
|
-
}
|
|
1779
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1780
|
-
await stopEngine(localInfer, launchRequestFromBody(body));
|
|
1781
|
-
send(200, { stopped: true });
|
|
1782
|
-
}
|
|
1783
|
-
catch (error) {
|
|
1784
|
-
if (error instanceof RouteError) {
|
|
1785
|
-
send(error.status, { error: error.message });
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
send(500, { error: String(error?.message ?? error) });
|
|
1789
|
-
}
|
|
1790
|
-
},
|
|
1791
|
-
};
|
|
1792
|
-
// The register and unregister routes share a body parser: the engine and
|
|
1793
|
-
// config the plan renders from, plus agentDefault for the register route,
|
|
1794
|
-
// falling back to the stored config per key.
|
|
1795
|
-
const registerRequestFromBody = (body) => {
|
|
1796
|
-
const request = {};
|
|
1797
|
-
if (typeof body.engine === 'string') {
|
|
1798
|
-
request.engine = body.engine;
|
|
1799
|
-
}
|
|
1800
|
-
if (body.config && typeof body.config === 'object') {
|
|
1801
|
-
request.config = body.config;
|
|
1802
|
-
}
|
|
1803
|
-
if (typeof body.agentDefault === 'boolean') {
|
|
1804
|
-
request.agentDefault = body.agentDefault;
|
|
1805
|
-
}
|
|
1806
|
-
return request;
|
|
1807
|
-
};
|
|
1808
|
-
const register = {
|
|
1809
|
-
name: 'local-infer-register',
|
|
1810
|
-
kind: 'exact',
|
|
1811
|
-
path: '/local-infer/register',
|
|
1812
|
-
handler: async (req, res) => {
|
|
1813
|
-
const send = (status, body) => {
|
|
1814
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1815
|
-
res.end(JSON.stringify(body));
|
|
1816
|
-
};
|
|
1817
|
-
if (req.method !== 'POST') {
|
|
1818
|
-
res.writeHead(405).end();
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
try {
|
|
1822
|
-
const chunks = [];
|
|
1823
|
-
let total = 0;
|
|
1824
|
-
for await (const chunk of req) {
|
|
1825
|
-
total += chunk.length;
|
|
1826
|
-
if (total > 64 * 1024) {
|
|
1827
|
-
send(413, { error: 'register payload too large' });
|
|
1828
|
-
req.destroy();
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
chunks.push(chunk);
|
|
1832
|
-
}
|
|
1833
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1834
|
-
send(200, await registerFor(localInfer, registerRequestFromBody(body)));
|
|
1835
|
-
}
|
|
1836
|
-
catch (error) {
|
|
1837
|
-
if (error instanceof RouteError) {
|
|
1838
|
-
send(error.status, { error: error.message });
|
|
1839
|
-
return;
|
|
1840
|
-
}
|
|
1841
|
-
send(500, { error: String(error?.message ?? error) });
|
|
1842
|
-
}
|
|
1843
|
-
},
|
|
1844
|
-
};
|
|
1845
|
-
const unregister = {
|
|
1846
|
-
name: 'local-infer-unregister',
|
|
119
|
+
export function defineRoute(spec) {
|
|
120
|
+
return {
|
|
121
|
+
name: spec.name,
|
|
1847
122
|
kind: 'exact',
|
|
1848
|
-
path:
|
|
123
|
+
path: spec.path,
|
|
1849
124
|
handler: async (req, res) => {
|
|
1850
|
-
|
|
1851
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
1852
|
-
res.end(JSON.stringify(body));
|
|
1853
|
-
};
|
|
1854
|
-
if (req.method !== 'POST') {
|
|
125
|
+
if (req.method !== spec.method) {
|
|
1855
126
|
res.writeHead(405).end();
|
|
1856
127
|
return;
|
|
1857
128
|
}
|
|
129
|
+
const responder = new RouteResponder(res);
|
|
1858
130
|
try {
|
|
1859
|
-
const
|
|
1860
|
-
|
|
1861
|
-
for await (const chunk of req) {
|
|
1862
|
-
total += chunk.length;
|
|
1863
|
-
if (total > 64 * 1024) {
|
|
1864
|
-
send(413, { error: 'unregister payload too large' });
|
|
1865
|
-
req.destroy();
|
|
1866
|
-
return;
|
|
1867
|
-
}
|
|
1868
|
-
chunks.push(chunk);
|
|
1869
|
-
}
|
|
1870
|
-
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
1871
|
-
send(200, await unregisterFor(localInfer, registerRequestFromBody(body)));
|
|
131
|
+
const body = spec.method === 'POST' ? await readJsonBody(req, spec.tooLargeMessage) : {};
|
|
132
|
+
await spec.run({ url: new URL(req.url, 'http://localhost'), body }, responder);
|
|
1872
133
|
}
|
|
1873
134
|
catch (error) {
|
|
1874
135
|
if (error instanceof RouteError) {
|
|
1875
|
-
|
|
136
|
+
responder.json(error.status, { error: error.message });
|
|
1876
137
|
return;
|
|
1877
138
|
}
|
|
1878
|
-
|
|
139
|
+
responder.json(spec.failStatus, { error: String(error?.message ?? error) });
|
|
1879
140
|
}
|
|
1880
141
|
},
|
|
1881
142
|
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* registerLocalInferRoute registers the summary, save, provider-delete, deps,
|
|
146
|
+
* install, download, chat-template, get, launch, stop, register, and
|
|
147
|
+
* unregister routes on the web server and holds them by an effect, so a
|
|
148
|
+
* bundle switch off removes them before the next switch on re-registers. The
|
|
149
|
+
* scoped context carries the webServer; the localInfer service is passed in
|
|
150
|
+
* because the scope only injects what it names. The optional getHost is the
|
|
151
|
+
* lifecycle host the Get route's git and build steps run through; it
|
|
152
|
+
* defaults to the node host, and a test injects a stub so the route never
|
|
153
|
+
* clones a real repo in CI.
|
|
154
|
+
*/
|
|
155
|
+
export function registerLocalInferRoute(scope, localInfer, getHost) {
|
|
156
|
+
const routes = [
|
|
157
|
+
summaryRoute(localInfer),
|
|
158
|
+
saveRoute(localInfer),
|
|
159
|
+
providerDeleteRoute(localInfer),
|
|
160
|
+
depsRoute(localInfer),
|
|
161
|
+
installRoute(localInfer),
|
|
162
|
+
downloadRoute(localInfer),
|
|
163
|
+
chatTemplateRoute(localInfer),
|
|
164
|
+
getRoute(localInfer, getHost),
|
|
165
|
+
launchRoute(localInfer),
|
|
166
|
+
stopRoute(localInfer),
|
|
167
|
+
registerRoute(localInfer),
|
|
168
|
+
unregisterRoute(localInfer),
|
|
169
|
+
];
|
|
1882
170
|
scope.effect(() => {
|
|
1883
171
|
// Sync the service's platform-config registry with the stored config on
|
|
1884
172
|
// route registration, so a launch before the first save reads the
|
|
@@ -1894,32 +182,12 @@ export function registerLocalInferRoute(scope, localInfer, getHost) {
|
|
|
1894
182
|
// open the summary route, so a DSH that restarts re-attaches its engines
|
|
1895
183
|
// and a pkill after the restart still settles the stored state.
|
|
1896
184
|
void relinkStoredProviders(localInfer, readConfig()).catch(() => undefined);
|
|
1897
|
-
const
|
|
1898
|
-
const disposeSave = scope.webServer.register(save);
|
|
1899
|
-
const disposeDeleteProvider = scope.webServer.register(deleteProvider);
|
|
1900
|
-
const disposeDeps = scope.webServer.register(deps);
|
|
1901
|
-
const disposeInstall = scope.webServer.register(install);
|
|
1902
|
-
const disposeDownload = scope.webServer.register(download);
|
|
1903
|
-
const disposeChatTemplate = scope.webServer.register(chatTemplate);
|
|
1904
|
-
const disposeGet = scope.webServer.register(get);
|
|
1905
|
-
const disposeLaunch = scope.webServer.register(launch);
|
|
1906
|
-
const disposeStop = scope.webServer.register(stop);
|
|
1907
|
-
const disposeRegister = scope.webServer.register(register);
|
|
1908
|
-
const disposeUnregister = scope.webServer.register(unregister);
|
|
185
|
+
const disposers = routes.map((route) => scope.webServer.register(route));
|
|
1909
186
|
return () => {
|
|
1910
187
|
localInfer.setEngineDeathObserver(undefined);
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
disposeDeps();
|
|
1915
|
-
disposeInstall();
|
|
1916
|
-
disposeDownload();
|
|
1917
|
-
disposeChatTemplate();
|
|
1918
|
-
disposeGet();
|
|
1919
|
-
disposeLaunch();
|
|
1920
|
-
disposeStop();
|
|
1921
|
-
disposeRegister();
|
|
1922
|
-
disposeUnregister();
|
|
188
|
+
for (const dispose of disposers) {
|
|
189
|
+
dispose();
|
|
190
|
+
}
|
|
1923
191
|
};
|
|
1924
192
|
}, 'local-infer: settings routes');
|
|
1925
193
|
}
|