dsh-plugin-local-infer 0.1.0 → 0.1.2
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/LICENSE +7 -0
- package/README.md +8 -1
- package/cordis.patch.yml +6 -6
- package/docs/screenshot-20261014.png +0 -0
- package/docs/zh/README.md +100 -0
- package/dsh/client.js +644 -143
- package/dsh/host.js +1 -1
- package/package.json +14 -11
- package/packages/core/lib/adapter.d.ts +75 -4
- package/packages/core/lib/debug.d.ts +1 -1
- package/packages/core/lib/debug.js +1 -1
- package/packages/core/lib/index.d.ts +3 -2
- package/packages/core/lib/index.js +2 -1
- package/packages/core/lib/launch-command.d.ts +13 -0
- package/packages/core/lib/launch-command.js +64 -0
- package/packages/core/lib/models.d.ts +2 -1
- package/packages/core/lib/models.js +10 -1
- package/packages/core/lib/status.d.ts +9 -1
- package/packages/core/lib/status.js +6 -1
- package/packages/core/package.json +1 -1
- package/packages/providers/lib/acquire.d.ts +125 -0
- package/packages/providers/lib/acquire.js +325 -0
- package/packages/providers/lib/adapters.d.ts +43 -6
- package/packages/providers/lib/adapters.js +386 -58
- package/packages/providers/lib/debug.d.ts +1 -1
- package/packages/providers/lib/debug.js +1 -1
- package/packages/providers/lib/deps.d.ts +6 -4
- package/packages/providers/lib/deps.js +17 -3
- package/packages/providers/lib/engines.d.ts +6 -5
- package/packages/providers/lib/engines.js +12 -4
- package/packages/providers/lib/guidelines.d.ts +2 -2
- package/packages/providers/lib/guidelines.js +17 -1
- package/packages/providers/lib/hyperqwen.d.ts +2 -2
- package/packages/providers/lib/hyperqwen.js +1 -1
- package/packages/providers/lib/index.d.ts +10 -4
- package/packages/providers/lib/index.js +9 -5
- package/packages/providers/lib/infernix.d.ts +170 -0
- package/packages/providers/lib/infernix.js +671 -0
- package/packages/providers/lib/llamacpp.d.ts +2 -2
- package/packages/providers/lib/llamacpp.js +1 -1
- package/packages/providers/lib/model-defaults.d.ts +7 -5
- package/packages/providers/lib/model-defaults.js +28 -4
- package/packages/providers/lib/ninfer.d.ts +9 -2
- package/packages/providers/lib/ninfer.js +10 -3
- package/packages/providers/lib/platform.d.ts +31 -0
- package/packages/providers/lib/platform.js +58 -0
- package/packages/providers/lib/presets.d.ts +2 -2
- package/packages/providers/lib/presets.js +1 -1
- package/packages/providers/lib/strata.d.ts +2 -2
- package/packages/providers/lib/strata.js +1 -1
- package/packages/providers/lib/vram.d.ts +2 -2
- package/packages/providers/lib/vram.js +1 -1
- package/packages/providers/package.json +3 -3
- package/packages/service/lib/index.d.ts +63 -20
- package/packages/service/lib/index.js +279 -43
- package/packages/service/lib/types.d.ts +44 -2
- package/packages/service/package.json +2 -2
- package/packages/tool/lib/index.d.ts +1 -1
- package/packages/tool/lib/index.js +1 -1
- package/packages/tool/package.json +3 -3
- package/packages/ui/lib/deps.d.ts +2 -2
- package/packages/ui/lib/deps.js +1 -1
- package/packages/ui/lib/form.d.ts +2 -2
- package/packages/ui/lib/form.js +1 -1
- package/packages/ui/lib/index.d.ts +1 -1
- package/packages/ui/lib/index.js +1 -1
- package/packages/ui/lib/meter.d.ts +2 -2
- package/packages/ui/lib/meter.js +1 -1
- package/packages/ui/lib/route.d.ts +84 -13
- package/packages/ui/lib/route.js +496 -56
- package/packages/ui/lib/warnings.d.ts +2 -2
- package/packages/ui/lib/warnings.js +1 -1
- package/packages/ui/package.json +5 -5
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
* injectable `LifecycleHost`, so the tests stub the host and never run a real
|
|
6
6
|
* build, docker, or network in CI. The node implementation
|
|
7
7
|
* (`nodeLifecycleHost`) is what the service wires in on a live host.
|
|
8
|
-
* @module
|
|
8
|
+
* @module dsh-plugin-local-infer/providers/adapters
|
|
9
9
|
*/
|
|
10
10
|
import { exec, spawn as nodeSpawn } from 'node:child_process';
|
|
11
11
|
import { basename, dirname, join } from 'node:path';
|
|
12
|
-
import { EngineAdapter } from '../../core/lib/index.js';
|
|
12
|
+
import { EngineAdapter, formatLaunchCommand } from '../../core/lib/index.js';
|
|
13
|
+
import { syncAndRebuildEngine } from "./acquire.js";
|
|
13
14
|
import { NINFER_PARAMETERS } from "./ninfer.js";
|
|
14
15
|
import { HYPERQWEN_PARAMETERS } from "./hyperqwen.js";
|
|
15
16
|
import { LLAMACPP_MODEL_ARTIFACTS, LLAMACPP_PARAMETERS, llamaCppModelKey, } from "./llamacpp.js";
|
|
17
|
+
import { INFERNIX_MODEL_ARTIFACTS, INFERNIX_PARAMETERS, infernixArtifactFor, infernixDraftModelPath, infernixModelKey, } from "./infernix.js";
|
|
18
|
+
export { INFERNIX_MODEL_ARTIFACTS } from "./infernix.js";
|
|
16
19
|
import { STRATA_PARAMETERS } from "./strata.js";
|
|
17
20
|
/**
|
|
18
21
|
* The NInfer model artifacts, keyed by the model registry id. The 27B entry
|
|
@@ -169,6 +172,35 @@ export function createNodeLifecycleHost(debug) {
|
|
|
169
172
|
});
|
|
170
173
|
});
|
|
171
174
|
},
|
|
175
|
+
async runStream(command, onOutput) {
|
|
176
|
+
log?.(`run: ${command}`);
|
|
177
|
+
return new Promise(resolve => {
|
|
178
|
+
// No overall timeout: a full CUDA build legitimately runs long, and
|
|
179
|
+
// the card prices a stalled step with its own output-idle watchdog.
|
|
180
|
+
const child = nodeSpawn(command, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
181
|
+
let output = '';
|
|
182
|
+
const append = (chunk) => {
|
|
183
|
+
const text = chunk.toString('utf8');
|
|
184
|
+
onOutput(text);
|
|
185
|
+
output += text;
|
|
186
|
+
if (output.length > 2048) {
|
|
187
|
+
output = output.slice(-1024);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
child.stdout.on('data', append);
|
|
191
|
+
child.stderr.on('data', append);
|
|
192
|
+
child.on('error', error => {
|
|
193
|
+
resolve({ ok: false, detail: error.message });
|
|
194
|
+
});
|
|
195
|
+
child.on('close', code => {
|
|
196
|
+
if (code === 0) {
|
|
197
|
+
resolve({ ok: true });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
resolve({ ok: false, detail: `exit ${code}: ${output.trim().slice(-1024)}` });
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
},
|
|
172
204
|
spawn(argv, env, cwd) {
|
|
173
205
|
log?.(`spawn: ${argv.join(' ')}${cwd !== undefined ? ` (cwd: ${cwd})` : ''}`);
|
|
174
206
|
const child = nodeSpawn(argv[0] ?? '', argv.slice(1), {
|
|
@@ -190,6 +222,51 @@ export function createNodeLifecycleHost(debug) {
|
|
|
190
222
|
};
|
|
191
223
|
child.stdout?.on('data', append);
|
|
192
224
|
child.stderr?.on('data', append);
|
|
225
|
+
// An EventEmitter with no 'error' listener turns the event into a thrown
|
|
226
|
+
// uncaughtException, which takes the DSH host down with it. A spawn that
|
|
227
|
+
// cannot start (a missing engine binary, an ENOENT) and a stream that
|
|
228
|
+
// breaks on a crashing child both emit 'error' here, so every path gets
|
|
229
|
+
// a listener and the failure lands in the logs instead of the crash.
|
|
230
|
+
child.stdout?.on('error', (error) => {
|
|
231
|
+
logs = (logs + `stdout error: ${error.message}\n`).slice(-65536);
|
|
232
|
+
});
|
|
233
|
+
child.stderr?.on('error', (error) => {
|
|
234
|
+
logs = (logs + `stderr error: ${error.message}\n`).slice(-65536);
|
|
235
|
+
});
|
|
236
|
+
// The one-shot exit contract: 'error' names a child that never ran,
|
|
237
|
+
// 'exit' carries the code or signal, and 'close' follows once the pipes
|
|
238
|
+
// drain. The first settled source wins, so the handle's onExit listener
|
|
239
|
+
// (crash, pkill, or clean stop) fires exactly once with the detail.
|
|
240
|
+
const listeners = new Set();
|
|
241
|
+
let exitInfo = null;
|
|
242
|
+
const settle = (info) => {
|
|
243
|
+
if (exitInfo !== null)
|
|
244
|
+
return;
|
|
245
|
+
exitInfo = info;
|
|
246
|
+
for (const listener of listeners) {
|
|
247
|
+
try {
|
|
248
|
+
listener(info);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// A listener that throws must not re-raise into the child's event
|
|
252
|
+
// pipeline and take the host down the way an unlistened 'error' did.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
listeners.clear();
|
|
256
|
+
};
|
|
257
|
+
let exitSignal = null;
|
|
258
|
+
child.on('error', (error) => {
|
|
259
|
+
logs = (logs + `spawn error: ${error.message}\n`).slice(-65536);
|
|
260
|
+
// The error fires before the stdio drains; settle immediately so a
|
|
261
|
+
// launch of a missing binary reports rather than hangs the watcher.
|
|
262
|
+
settle({ code: null, signal: null, error: error.message });
|
|
263
|
+
});
|
|
264
|
+
child.on('exit', (_code, signal) => {
|
|
265
|
+
exitSignal = signal;
|
|
266
|
+
});
|
|
267
|
+
child.on('close', (code, signal) => {
|
|
268
|
+
settle({ code, signal: signal ?? exitSignal });
|
|
269
|
+
});
|
|
193
270
|
return {
|
|
194
271
|
pid: child.pid ?? -1,
|
|
195
272
|
kill: () => {
|
|
@@ -198,6 +275,16 @@ export function createNodeLifecycleHost(debug) {
|
|
|
198
275
|
get logs() {
|
|
199
276
|
return logs;
|
|
200
277
|
},
|
|
278
|
+
onExit: (listener) => {
|
|
279
|
+
// A listener registered after the child already ended fires
|
|
280
|
+
// immediately, so a late attach (the service wiring after deploy)
|
|
281
|
+
// never misses the crash that landed in between.
|
|
282
|
+
if (exitInfo !== null) {
|
|
283
|
+
listener(exitInfo);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
listeners.add(listener);
|
|
287
|
+
},
|
|
201
288
|
};
|
|
202
289
|
},
|
|
203
290
|
async fetch(url) {
|
|
@@ -219,6 +306,24 @@ export function createNodeLifecycleHost(debug) {
|
|
|
219
306
|
* in debug mode.
|
|
220
307
|
*/
|
|
221
308
|
export const nodeLifecycleHost = createNodeLifecycleHost();
|
|
309
|
+
/**
|
|
310
|
+
* trackChild stores a spawned handle in the adapter's children map and ties
|
|
311
|
+
* the entry's lifetime to the child's: when the child ends (a crash, a pkill,
|
|
312
|
+
* or the adapter's own stop kill) the entry is dropped, so a later stop never
|
|
313
|
+
* signals a pid a recycled process has taken over. A handle whose host cannot
|
|
314
|
+
* watch (onExit absent) stays tracked until stop removes it.
|
|
315
|
+
*/
|
|
316
|
+
function trackChild(children, key, handle) {
|
|
317
|
+
children.set(key, handle);
|
|
318
|
+
handle.onExit?.(() => {
|
|
319
|
+
// Compare identity before deleting: a newer launch that already replaced
|
|
320
|
+
// this key's entry keeps its handle, and only the ended child's entry
|
|
321
|
+
// goes.
|
|
322
|
+
if (children.get(key) === handle) {
|
|
323
|
+
children.delete(key);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
222
327
|
/**
|
|
223
328
|
* pollHealth polls a plan's health URL until a 200 or the attempt cap, the
|
|
224
329
|
* same loop the qwen-ninfer launcher runs. A poll interval of 0 skips the
|
|
@@ -368,53 +473,64 @@ export class NInferAdapter extends EngineAdapter {
|
|
|
368
473
|
healthUrl: `http://127.0.0.1:${port}/health`,
|
|
369
474
|
};
|
|
370
475
|
}
|
|
371
|
-
async deploy(plan, platform) {
|
|
476
|
+
async deploy(plan, platform, onProgress, options) {
|
|
372
477
|
if (plan.kind === 'process') {
|
|
373
478
|
const engineDir = platform?.engineDirectory;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
479
|
+
let buildLogs = '';
|
|
480
|
+
// Auto Rebuild off (the card's checkbox): the git sync (fetch, rebase,
|
|
481
|
+
// rebuild) is skipped and the checkout launches as-is.
|
|
482
|
+
if (engineDir !== undefined && options?.autoRebuild !== false) {
|
|
483
|
+
// The git sync is the checkout download: the card's button reads
|
|
484
|
+
// "Downloading..." for it, not "Building...".
|
|
485
|
+
onProgress?.('download');
|
|
486
|
+
// The conditional rebuild fetches the checkout, prices it against the
|
|
487
|
+
// remote branch, and rebuilds only when it is behind (or a fresh
|
|
488
|
+
// clone). The decision, the build command, and its output are captured
|
|
489
|
+
// into buildLogs and threaded to the card's Logs section.
|
|
490
|
+
const sync = await syncAndRebuildEngine(this.host, {
|
|
491
|
+
engineId: this.id,
|
|
492
|
+
directory: engineDir,
|
|
493
|
+
...(platform?.engineFork !== undefined ? { fork: platform.engineFork } : {}),
|
|
494
|
+
...(plan.argv?.[0] !== undefined ? { binary: plan.argv[0] } : {}),
|
|
495
|
+
...(options?.allowRunDeps !== undefined ? { allowRunDeps: options.allowRunDeps } : {}),
|
|
496
|
+
}, () => { });
|
|
497
|
+
buildLogs = sync.logs;
|
|
498
|
+
if (!sync.ok) {
|
|
499
|
+
return { ok: false, detail: sync.detail ?? 'unknown', logs: buildLogs };
|
|
390
500
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
|
|
501
|
+
// The build step reads as "Building..." only when a build actually
|
|
502
|
+
// ran (a fresh clone, or a rebase of a checkout that was behind).
|
|
503
|
+
if (sync.rebuilt) {
|
|
504
|
+
onProgress?.('build');
|
|
395
505
|
}
|
|
396
506
|
}
|
|
507
|
+
const withLogs = (result) => buildLogs !== '' ? { ...result, logs: buildLogs } : result;
|
|
508
|
+
onProgress?.('download');
|
|
397
509
|
const download = await this.downloadModel(plan);
|
|
398
510
|
if (!download.ok)
|
|
399
|
-
return download;
|
|
511
|
+
return withLogs(download);
|
|
400
512
|
// A chat template the user picked as an HF repo reference (the combo
|
|
401
513
|
// control's known choices) is not a path the engine can read, so the
|
|
402
514
|
// deploy step downloads it into the model directory and rewrites the
|
|
403
515
|
// --chat-template flag to the local file before the spawn.
|
|
516
|
+
onProgress?.('download');
|
|
404
517
|
const templateResult = await this.resolveChatTemplate(plan);
|
|
405
518
|
if (!templateResult.ok)
|
|
406
|
-
return templateResult;
|
|
519
|
+
return withLogs(templateResult);
|
|
407
520
|
const handle = this.host.spawn(plan.argv ?? [], plan.env ?? {}, engineDir);
|
|
408
|
-
this.children
|
|
409
|
-
return { ok: true, handle };
|
|
521
|
+
trackChild(this.children, this.planKey(plan), handle);
|
|
522
|
+
return withLogs({ ok: true, handle });
|
|
410
523
|
}
|
|
411
524
|
if (plan.kind === 'docker') {
|
|
525
|
+
// The image pull is the docker build step: the card's button reads
|
|
526
|
+
// "Building..." for it.
|
|
527
|
+
onProgress?.('build');
|
|
412
528
|
const pull = await this.host.run(`docker pull ${NINFER_DOCKER_IMAGE}`);
|
|
413
529
|
if (!pull.ok) {
|
|
414
530
|
return { ok: false, detail: `docker pull failed: ${pull.detail ?? 'unknown'}` };
|
|
415
531
|
}
|
|
416
532
|
const handle = this.host.spawn(plan.argv ?? [], plan.env ?? {});
|
|
417
|
-
this.children
|
|
533
|
+
trackChild(this.children, this.planKey(plan), handle);
|
|
418
534
|
return { ok: true, handle };
|
|
419
535
|
}
|
|
420
536
|
// NInfer renders process and docker plans; a compose plan is a no-op.
|
|
@@ -547,8 +663,23 @@ export class HyperQwenAdapter extends EngineAdapter {
|
|
|
547
663
|
healthUrl: `http://127.0.0.1:${port}/health`,
|
|
548
664
|
};
|
|
549
665
|
}
|
|
550
|
-
async deploy(plan) {
|
|
551
|
-
|
|
666
|
+
async deploy(plan, platform, onProgress, _options) {
|
|
667
|
+
// The card's docker-tag field carries the compose arguments (the profile
|
|
668
|
+
// the up step selects); the deploy step applies it to the plan so the up
|
|
669
|
+
// line and the card's Logs section agree.
|
|
670
|
+
if (platform?.dockerTag !== undefined) {
|
|
671
|
+
plan.composeArgs = platform.dockerTag;
|
|
672
|
+
}
|
|
673
|
+
// The core formatter is the single source of the compose up line: the
|
|
674
|
+
// card's Logs section prints the same string this run gets.
|
|
675
|
+
const command = formatLaunchCommand(plan);
|
|
676
|
+
if (command === undefined) {
|
|
677
|
+
return { ok: false, detail: 'compose plan carries no command' };
|
|
678
|
+
}
|
|
679
|
+
// The compose up is the docker build step: the card's button reads
|
|
680
|
+
// "Building..." for it.
|
|
681
|
+
onProgress?.('build');
|
|
682
|
+
const up = await this.host.run(command);
|
|
552
683
|
if (!up.ok) {
|
|
553
684
|
return { ok: false, detail: `compose up failed: ${up.detail ?? 'unknown'}` };
|
|
554
685
|
}
|
|
@@ -558,7 +689,14 @@ export class HyperQwenAdapter extends EngineAdapter {
|
|
|
558
689
|
return pollHealth(this.host, plan, options);
|
|
559
690
|
}
|
|
560
691
|
async stop(plan) {
|
|
561
|
-
|
|
692
|
+
// The down step brings down the containers the up step started, so it
|
|
693
|
+
// runs the same compose arguments (the profile the up selected); an
|
|
694
|
+
// absent composeArgs falls back to the engine's default profile.
|
|
695
|
+
const composeArgs = plan.composeArgs?.split(/\s+/).filter((token) => token !== '');
|
|
696
|
+
const args = composeArgs !== undefined && composeArgs.length > 0
|
|
697
|
+
? composeArgs.join(' ')
|
|
698
|
+
: `--profile ${plan.profile ?? 'single'}`;
|
|
699
|
+
await this.host.run(`docker compose -f ${plan.composeFile ?? 'docker-compose.yml'} ${args} down`);
|
|
562
700
|
}
|
|
563
701
|
port(config) {
|
|
564
702
|
return typeof config.port === 'number' ? config.port : 18020;
|
|
@@ -687,45 +825,57 @@ export class LlamaCppAdapter extends EngineAdapter {
|
|
|
687
825
|
healthUrl: `http://127.0.0.1:${port}/health`,
|
|
688
826
|
};
|
|
689
827
|
}
|
|
690
|
-
async deploy(plan, platform) {
|
|
828
|
+
async deploy(plan, platform, onProgress, options) {
|
|
691
829
|
if (plan.kind === 'process') {
|
|
692
830
|
const engineDir = platform?.engineDirectory;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
831
|
+
let buildLogs = '';
|
|
832
|
+
// Auto Rebuild off (the card's checkbox): the git sync (fetch, rebase,
|
|
833
|
+
// rebuild) is skipped and the checkout launches as-is.
|
|
834
|
+
if (engineDir !== undefined && options?.autoRebuild !== false) {
|
|
835
|
+
// The git sync is the checkout download: the card's button reads
|
|
836
|
+
// "Downloading..." for it, not "Building...".
|
|
837
|
+
onProgress?.('download');
|
|
838
|
+
// The conditional rebuild fetches the checkout, prices it against the
|
|
839
|
+
// remote branch, and rebuilds only when it is behind (or a fresh
|
|
840
|
+
// clone). The decision, the build command, and its output are captured
|
|
841
|
+
// into buildLogs and threaded to the card's Logs section.
|
|
842
|
+
const sync = await syncAndRebuildEngine(this.host, {
|
|
843
|
+
engineId: this.id,
|
|
844
|
+
directory: engineDir,
|
|
845
|
+
...(platform?.engineFork !== undefined ? { fork: platform.engineFork } : {}),
|
|
846
|
+
...(plan.argv?.[0] !== undefined ? { binary: plan.argv[0] } : {}),
|
|
847
|
+
...(options?.allowRunDeps !== undefined ? { allowRunDeps: options.allowRunDeps } : {}),
|
|
848
|
+
}, () => { });
|
|
849
|
+
buildLogs = sync.logs;
|
|
850
|
+
if (!sync.ok) {
|
|
851
|
+
return { ok: false, detail: sync.detail ?? 'unknown', logs: buildLogs };
|
|
707
852
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (
|
|
711
|
-
|
|
853
|
+
// The build step reads as "Building..." only when a build actually
|
|
854
|
+
// ran (a fresh clone, or a rebase of a checkout that was behind).
|
|
855
|
+
if (sync.rebuilt) {
|
|
856
|
+
onProgress?.('build');
|
|
712
857
|
}
|
|
713
858
|
}
|
|
859
|
+
const withLogs = (result) => buildLogs !== '' ? { ...result, logs: buildLogs } : result;
|
|
860
|
+
onProgress?.('download');
|
|
714
861
|
const download = await this.downloadModel(plan);
|
|
715
862
|
if (!download.ok)
|
|
716
|
-
return download;
|
|
863
|
+
return withLogs(download);
|
|
717
864
|
const handle = this.host.spawn(plan.argv ?? [], plan.env ?? {}, engineDir);
|
|
718
|
-
this.children
|
|
719
|
-
return { ok: true };
|
|
865
|
+
trackChild(this.children, this.planKey(plan), handle);
|
|
866
|
+
return withLogs({ ok: true, handle });
|
|
720
867
|
}
|
|
721
868
|
if (plan.kind === 'docker') {
|
|
869
|
+
// The image pull is the docker build step: the card's button reads
|
|
870
|
+
// "Building..." for it.
|
|
871
|
+
onProgress?.('build');
|
|
722
872
|
const pull = await this.host.run(`docker pull ${LLAMACPP_DOCKER_IMAGE}`);
|
|
723
873
|
if (!pull.ok) {
|
|
724
874
|
return { ok: false, detail: `docker pull failed: ${pull.detail ?? 'unknown'}` };
|
|
725
875
|
}
|
|
726
876
|
const handle = this.host.spawn(plan.argv ?? [], plan.env ?? {});
|
|
727
|
-
this.children
|
|
728
|
-
return { ok: true };
|
|
877
|
+
trackChild(this.children, this.planKey(plan), handle);
|
|
878
|
+
return { ok: true, handle };
|
|
729
879
|
}
|
|
730
880
|
// llama.cpp renders process and docker plans; a compose plan is a no-op.
|
|
731
881
|
return { ok: true };
|
|
@@ -826,14 +976,17 @@ export class StrataAdapter extends EngineAdapter {
|
|
|
826
976
|
healthUrl: `http://127.0.0.1:${port}/health`,
|
|
827
977
|
};
|
|
828
978
|
}
|
|
829
|
-
async deploy(plan) {
|
|
979
|
+
async deploy(plan, _platform, onProgress, _options) {
|
|
980
|
+
// The image pull is the docker build step: the card's button reads
|
|
981
|
+
// "Building..." for it.
|
|
982
|
+
onProgress?.('build');
|
|
830
983
|
const pull = await this.host.run(`docker pull ${STRATA_DOCKER_IMAGE}`);
|
|
831
984
|
if (!pull.ok) {
|
|
832
985
|
return { ok: false, detail: `docker pull failed: ${pull.detail ?? 'unknown'}` };
|
|
833
986
|
}
|
|
834
987
|
const handle = this.host.spawn(plan.argv ?? [], plan.env ?? {});
|
|
835
|
-
this.children
|
|
836
|
-
return { ok: true };
|
|
988
|
+
trackChild(this.children, this.planKey(plan), handle);
|
|
989
|
+
return { ok: true, handle };
|
|
837
990
|
}
|
|
838
991
|
async healthCheck(plan, options) {
|
|
839
992
|
return pollHealth(this.host, plan, options);
|
|
@@ -855,4 +1008,179 @@ export class StrataAdapter extends EngineAdapter {
|
|
|
855
1008
|
return `${plan.engineId}:${plan.port}`;
|
|
856
1009
|
}
|
|
857
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* InfernixAdapter is the local-only engine for Infernix, the C++/CUDA fork
|
|
1013
|
+
* of NInfer (https://github.com/wallawalla47/infernix) tuned for Qwen models
|
|
1014
|
+
* on a single RTX 50-series GPU. `renderLaunch` renders a local
|
|
1015
|
+
* `infernix-serve` process from the `INFERNIX_PARAMETERS` surface (the
|
|
1016
|
+
* Infernix-only fields, from `--ngram-volume` to the quality routes, spell
|
|
1017
|
+
* onto the argv only for this engine); `deploy` builds the engine in the
|
|
1018
|
+
* engine directory (the engine's own cmake preset), downloads the model
|
|
1019
|
+
* artifact (and the n-gram volume, for the multi-file Flash-Next repo) into
|
|
1020
|
+
* the model directory, and starts the engine; `healthCheck` polls the health
|
|
1021
|
+
* URL; `stop` signals the tracked process. The engine ships no docker image,
|
|
1022
|
+
* so the adapter supports the local build mode only.
|
|
1023
|
+
*/
|
|
1024
|
+
export class InfernixAdapter extends EngineAdapter {
|
|
1025
|
+
id = 'infernix';
|
|
1026
|
+
supports = { docker: false, local: true };
|
|
1027
|
+
host;
|
|
1028
|
+
children = new Map();
|
|
1029
|
+
constructor(host = nodeLifecycleHost) {
|
|
1030
|
+
super();
|
|
1031
|
+
this.host = host;
|
|
1032
|
+
}
|
|
1033
|
+
parameters() {
|
|
1034
|
+
return INFERNIX_PARAMETERS;
|
|
1035
|
+
}
|
|
1036
|
+
modelRef(config) {
|
|
1037
|
+
// The engine serves the model under the artifact's advertised id, so the
|
|
1038
|
+
// registrar publishes that alias and a chat turn routes to it. A model
|
|
1039
|
+
// key that names a local artifact file (a directory-scanned model)
|
|
1040
|
+
// reuses the 27B alias, the same fallback the NInfer adapter applies.
|
|
1041
|
+
const artifact = infernixArtifactFor(infernixModelKey(config));
|
|
1042
|
+
return { id: artifact.modelId, name: artifact.modelId };
|
|
1043
|
+
}
|
|
1044
|
+
launchSignature(config = {}) {
|
|
1045
|
+
// The local plan runs the infernix-serve binary (the .exe on Windows),
|
|
1046
|
+
// so the binary name is the command-line marker the reserve probe
|
|
1047
|
+
// matches on, and the port the plan binds rides along.
|
|
1048
|
+
return { binary: 'infernix-serve', port: this.port(config) };
|
|
1049
|
+
}
|
|
1050
|
+
renderLaunch(config, _hw) {
|
|
1051
|
+
const port = this.port(config);
|
|
1052
|
+
const modelKey = infernixModelKey(config);
|
|
1053
|
+
const artifact = infernixArtifactFor(modelKey);
|
|
1054
|
+
// A model key that names a local artifact file (a directory-scanned
|
|
1055
|
+
// model, a .ninfer or .infernix name) overrides the artifact map's file,
|
|
1056
|
+
// so a file the user has on disk launches as-is.
|
|
1057
|
+
const file = modelKey.endsWith('.ninfer') || modelKey.endsWith('.infernix')
|
|
1058
|
+
? modelKey
|
|
1059
|
+
: artifact.file;
|
|
1060
|
+
const modelDirectory = typeof config.modelDirectory === 'string' ? config.modelDirectory : '';
|
|
1061
|
+
const modelPath = modelDirectory === '' ? file : join(modelDirectory, file);
|
|
1062
|
+
// The engine binary is the .exe on a Windows host, where the published
|
|
1063
|
+
// release zip ships the server and its DLLs.
|
|
1064
|
+
const binary = process.platform === 'win32' ? 'infernix-serve.exe' : 'infernix-serve';
|
|
1065
|
+
const flags = flagsFromSpecs(INFERNIX_PARAMETERS, config);
|
|
1066
|
+
// The dflash2 backend always loads a drafter through --draft-model, so the
|
|
1067
|
+
// flag is spelled here from the resolved path (the selected DFlash2 preset,
|
|
1068
|
+
// or the Custom Draft model field) rather than by a spec toArgv. A config
|
|
1069
|
+
// that names no drafter path is the misconfiguration the dflash2
|
|
1070
|
+
// guideline names, so the flag is spelled only when a path resolves.
|
|
1071
|
+
if (config.spec === 'dflash2') {
|
|
1072
|
+
const draftPath = infernixDraftModelPath(config);
|
|
1073
|
+
if (draftPath !== null) {
|
|
1074
|
+
flags.push('--draft-model', draftPath);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const customArgs = customArgFlags(config);
|
|
1078
|
+
return {
|
|
1079
|
+
engineId: this.id,
|
|
1080
|
+
kind: 'process',
|
|
1081
|
+
argv: [
|
|
1082
|
+
binary,
|
|
1083
|
+
modelPath,
|
|
1084
|
+
'--host',
|
|
1085
|
+
'127.0.0.1',
|
|
1086
|
+
'--port',
|
|
1087
|
+
String(port),
|
|
1088
|
+
'--model-id',
|
|
1089
|
+
artifact.modelId,
|
|
1090
|
+
...flags,
|
|
1091
|
+
...customArgs,
|
|
1092
|
+
],
|
|
1093
|
+
env: {},
|
|
1094
|
+
modelId: modelKey,
|
|
1095
|
+
...(modelDirectory === '' ? {} : { modelDirectory }),
|
|
1096
|
+
port,
|
|
1097
|
+
healthUrl: `http://127.0.0.1:${port}/health`,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
async deploy(plan, platform, onProgress, options) {
|
|
1101
|
+
if (plan.kind !== 'process') {
|
|
1102
|
+
// Infernix renders process plans only; a docker or compose plan is a
|
|
1103
|
+
// no-op.
|
|
1104
|
+
return { ok: true };
|
|
1105
|
+
}
|
|
1106
|
+
const engineDir = platform?.engineDirectory;
|
|
1107
|
+
let buildLogs = '';
|
|
1108
|
+
// Auto Rebuild off (the card's checkbox): the git sync (fetch, rebase,
|
|
1109
|
+
// rebuild) is skipped and the checkout launches as-is.
|
|
1110
|
+
if (engineDir !== undefined && options?.autoRebuild !== false) {
|
|
1111
|
+
// The git sync is the checkout download: the card's button reads
|
|
1112
|
+
// "Downloading..." for it, not "Building...".
|
|
1113
|
+
onProgress?.('download');
|
|
1114
|
+
// The conditional rebuild fetches the checkout, prices it against the
|
|
1115
|
+
// remote branch, and rebuilds only when it is behind (or a fresh
|
|
1116
|
+
// clone). The engine's own quick start (the cmake preset, then the
|
|
1117
|
+
// preset's build) rides the acquisition-data build steps. The decision,
|
|
1118
|
+
// the build command, and its output are captured into buildLogs and
|
|
1119
|
+
// threaded to the card's Logs section.
|
|
1120
|
+
const sync = await syncAndRebuildEngine(this.host, {
|
|
1121
|
+
engineId: this.id,
|
|
1122
|
+
directory: engineDir,
|
|
1123
|
+
...(platform?.engineFork !== undefined ? { fork: platform.engineFork } : {}),
|
|
1124
|
+
...(plan.argv?.[0] !== undefined ? { binary: plan.argv[0] } : {}),
|
|
1125
|
+
...(options?.allowRunDeps !== undefined ? { allowRunDeps: options.allowRunDeps } : {}),
|
|
1126
|
+
}, () => { });
|
|
1127
|
+
buildLogs = sync.logs;
|
|
1128
|
+
if (!sync.ok) {
|
|
1129
|
+
return { ok: false, detail: sync.detail ?? 'unknown', logs: buildLogs };
|
|
1130
|
+
}
|
|
1131
|
+
// The build step reads as "Building..." only when a build actually
|
|
1132
|
+
// ran (a fresh clone, or a rebase of a checkout that was behind).
|
|
1133
|
+
if (sync.rebuilt) {
|
|
1134
|
+
onProgress?.('build');
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const withLogs = (result) => buildLogs !== '' ? { ...result, logs: buildLogs } : result;
|
|
1138
|
+
onProgress?.('download');
|
|
1139
|
+
const download = await this.downloadModel(plan);
|
|
1140
|
+
if (!download.ok)
|
|
1141
|
+
return withLogs(download);
|
|
1142
|
+
const handle = this.host.spawn(plan.argv ?? [], plan.env ?? {}, engineDir);
|
|
1143
|
+
this.children.set(this.planKey(plan), handle);
|
|
1144
|
+
return withLogs({ ok: true, handle });
|
|
1145
|
+
}
|
|
1146
|
+
async healthCheck(plan, options) {
|
|
1147
|
+
return pollHealth(this.host, plan, options);
|
|
1148
|
+
}
|
|
1149
|
+
async stop(plan) {
|
|
1150
|
+
const handle = this.children.get(this.planKey(plan));
|
|
1151
|
+
if (handle !== undefined) {
|
|
1152
|
+
handle.kill();
|
|
1153
|
+
this.children.delete(this.planKey(plan));
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
port(config) {
|
|
1157
|
+
// 8080 is the infernix-serve CLI default, so the form seeds it and the
|
|
1158
|
+
// adapter falls back to it when a config names no port.
|
|
1159
|
+
return typeof config.port === 'number' ? config.port : 8080;
|
|
1160
|
+
}
|
|
1161
|
+
planKey(plan) {
|
|
1162
|
+
return `${plan.engineId}:${plan.port}`;
|
|
1163
|
+
}
|
|
1164
|
+
async downloadModel(plan) {
|
|
1165
|
+
const artifact = plan.modelId !== undefined ? INFERNIX_MODEL_ARTIFACTS[plan.modelId] : undefined;
|
|
1166
|
+
// A model key that names a local artifact file carries no artifact entry,
|
|
1167
|
+
// so the file the user has on disk launches as-is without a download.
|
|
1168
|
+
if (artifact === undefined)
|
|
1169
|
+
return { ok: true };
|
|
1170
|
+
const target = plan.modelDirectory ?? '.';
|
|
1171
|
+
// A multi-file artifact (the Flash-Next shards) downloads the whole repo,
|
|
1172
|
+
// which carries the n-gram volume beside the shards, so the
|
|
1173
|
+
// --ngram-volume flag points at a file the same download left on disk.
|
|
1174
|
+
// A single-file artifact downloads the file alone.
|
|
1175
|
+
const repo = artifact.repo;
|
|
1176
|
+
const files = artifact.files ?? [];
|
|
1177
|
+
const download = files.length > 1
|
|
1178
|
+
? await this.host.run(`hf download ${repo} --local-dir ${target}`)
|
|
1179
|
+
: await this.host.run(`hf download ${repo} ${artifact.file} --local-dir ${target}`);
|
|
1180
|
+
if (!download.ok) {
|
|
1181
|
+
return { ok: false, detail: `model download failed: ${download.detail ?? 'unknown'}` };
|
|
1182
|
+
}
|
|
1183
|
+
return { ok: true };
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
858
1186
|
//# sourceMappingURL=adapters.js.map
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the two ways DSH enables debug mode (the HARNESS_LOG_LEVEL environment
|
|
4
4
|
* variable and the DSH logger level config) into a single DebugLogger the
|
|
5
5
|
* engine adapters use to capture the full build-to-launch command sequence.
|
|
6
|
-
* @module
|
|
6
|
+
* @module dsh-plugin-local-infer/providers/debug
|
|
7
7
|
*/
|
|
8
8
|
import { Context } from '@deepseek-ai/cordis';
|
|
9
9
|
import { type DebugLogger } from '../../core/lib/index.js';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the two ways DSH enables debug mode (the HARNESS_LOG_LEVEL environment
|
|
4
4
|
* variable and the DSH logger level config) into a single DebugLogger the
|
|
5
5
|
* engine adapters use to capture the full build-to-launch command sequence.
|
|
6
|
-
* @module
|
|
6
|
+
* @module dsh-plugin-local-infer/providers/debug
|
|
7
7
|
*/
|
|
8
8
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The dnf and brew blocks are the plan's "flagged when untested" sets: they
|
|
11
11
|
* name the right packages but have not been run on a live host.
|
|
12
|
-
* @module
|
|
12
|
+
* @module dsh-plugin-local-infer/providers/deps
|
|
13
13
|
*/
|
|
14
|
-
import type { EngineDepPlan, HardwareProfile } from '
|
|
14
|
+
import type { EngineDepPlan, HardwareProfile } from 'dsh-plugin-local-infer/core';
|
|
15
15
|
/**
|
|
16
16
|
* The named host deps every engine plan composes. The install blocks are the
|
|
17
17
|
* copy/paste commands the user runs; envAfter carries the per-manager export
|
|
@@ -217,8 +217,10 @@ export declare const SYSTEM_DEPS: {
|
|
|
217
217
|
* The per-engine dep plans, keyed by engine id. The local NInfer list is the
|
|
218
218
|
* qwen-ninfer header's toolchain plus the CUDA toolkit and the hf CLI; the
|
|
219
219
|
* docker lists are the container runtime plus the GPU toolkit plus the hf
|
|
220
|
-
* CLI.
|
|
221
|
-
*
|
|
220
|
+
* CLI. Infernix builds through the engine's own cmake preset, so its local
|
|
221
|
+
* list is the same toolchain as NInfer's, and its docker list stays empty
|
|
222
|
+
* because the engine ships no container image. llama.cpp and Strata carry
|
|
223
|
+
* the specified-not-verified lists until their adapters land in Phase 5.
|
|
222
224
|
*/
|
|
223
225
|
export declare const ENGINE_DEP_PLANS: Record<string, EngineDepPlan>;
|
|
224
226
|
//# sourceMappingURL=deps.d.ts.map
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The dnf and brew blocks are the plan's "flagged when untested" sets: they
|
|
11
11
|
* name the right packages but have not been run on a live host.
|
|
12
|
-
* @module
|
|
12
|
+
* @module dsh-plugin-local-infer/providers/deps
|
|
13
13
|
*/
|
|
14
14
|
import { execFile } from 'node:child_process';
|
|
15
15
|
/**
|
|
@@ -189,8 +189,10 @@ export const SYSTEM_DEPS = {
|
|
|
189
189
|
* The per-engine dep plans, keyed by engine id. The local NInfer list is the
|
|
190
190
|
* qwen-ninfer header's toolchain plus the CUDA toolkit and the hf CLI; the
|
|
191
191
|
* docker lists are the container runtime plus the GPU toolkit plus the hf
|
|
192
|
-
* CLI.
|
|
193
|
-
*
|
|
192
|
+
* CLI. Infernix builds through the engine's own cmake preset, so its local
|
|
193
|
+
* list is the same toolchain as NInfer's, and its docker list stays empty
|
|
194
|
+
* because the engine ships no container image. llama.cpp and Strata carry
|
|
195
|
+
* the specified-not-verified lists until their adapters land in Phase 5.
|
|
194
196
|
*/
|
|
195
197
|
export const ENGINE_DEP_PLANS = {
|
|
196
198
|
ninfer: {
|
|
@@ -213,6 +215,18 @@ export const ENGINE_DEP_PLANS = {
|
|
|
213
215
|
local: [SYSTEM_DEPS.cmake, SYSTEM_DEPS.hfCli],
|
|
214
216
|
docker: [SYSTEM_DEPS.docker, SYSTEM_DEPS.hfCli],
|
|
215
217
|
},
|
|
218
|
+
infernix: {
|
|
219
|
+
local: [
|
|
220
|
+
SYSTEM_DEPS.cmake,
|
|
221
|
+
SYSTEM_DEPS.ninja,
|
|
222
|
+
SYSTEM_DEPS.pkgConfig,
|
|
223
|
+
SYSTEM_DEPS.ffmpeg,
|
|
224
|
+
SYSTEM_DEPS.curl,
|
|
225
|
+
SYSTEM_DEPS.cudaToolkit,
|
|
226
|
+
SYSTEM_DEPS.hfCli,
|
|
227
|
+
],
|
|
228
|
+
docker: [],
|
|
229
|
+
},
|
|
216
230
|
strata: {
|
|
217
231
|
local: [],
|
|
218
232
|
docker: [SYSTEM_DEPS.docker, SYSTEM_DEPS.hfCli],
|