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.
Files changed (73) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +8 -1
  3. package/cordis.patch.yml +6 -6
  4. package/docs/screenshot-20261014.png +0 -0
  5. package/docs/zh/README.md +100 -0
  6. package/dsh/client.js +644 -143
  7. package/dsh/host.js +1 -1
  8. package/package.json +14 -11
  9. package/packages/core/lib/adapter.d.ts +75 -4
  10. package/packages/core/lib/debug.d.ts +1 -1
  11. package/packages/core/lib/debug.js +1 -1
  12. package/packages/core/lib/index.d.ts +3 -2
  13. package/packages/core/lib/index.js +2 -1
  14. package/packages/core/lib/launch-command.d.ts +13 -0
  15. package/packages/core/lib/launch-command.js +64 -0
  16. package/packages/core/lib/models.d.ts +2 -1
  17. package/packages/core/lib/models.js +10 -1
  18. package/packages/core/lib/status.d.ts +9 -1
  19. package/packages/core/lib/status.js +6 -1
  20. package/packages/core/package.json +1 -1
  21. package/packages/providers/lib/acquire.d.ts +125 -0
  22. package/packages/providers/lib/acquire.js +325 -0
  23. package/packages/providers/lib/adapters.d.ts +43 -6
  24. package/packages/providers/lib/adapters.js +386 -58
  25. package/packages/providers/lib/debug.d.ts +1 -1
  26. package/packages/providers/lib/debug.js +1 -1
  27. package/packages/providers/lib/deps.d.ts +6 -4
  28. package/packages/providers/lib/deps.js +17 -3
  29. package/packages/providers/lib/engines.d.ts +6 -5
  30. package/packages/providers/lib/engines.js +12 -4
  31. package/packages/providers/lib/guidelines.d.ts +2 -2
  32. package/packages/providers/lib/guidelines.js +17 -1
  33. package/packages/providers/lib/hyperqwen.d.ts +2 -2
  34. package/packages/providers/lib/hyperqwen.js +1 -1
  35. package/packages/providers/lib/index.d.ts +10 -4
  36. package/packages/providers/lib/index.js +9 -5
  37. package/packages/providers/lib/infernix.d.ts +170 -0
  38. package/packages/providers/lib/infernix.js +671 -0
  39. package/packages/providers/lib/llamacpp.d.ts +2 -2
  40. package/packages/providers/lib/llamacpp.js +1 -1
  41. package/packages/providers/lib/model-defaults.d.ts +7 -5
  42. package/packages/providers/lib/model-defaults.js +28 -4
  43. package/packages/providers/lib/ninfer.d.ts +9 -2
  44. package/packages/providers/lib/ninfer.js +10 -3
  45. package/packages/providers/lib/platform.d.ts +31 -0
  46. package/packages/providers/lib/platform.js +58 -0
  47. package/packages/providers/lib/presets.d.ts +2 -2
  48. package/packages/providers/lib/presets.js +1 -1
  49. package/packages/providers/lib/strata.d.ts +2 -2
  50. package/packages/providers/lib/strata.js +1 -1
  51. package/packages/providers/lib/vram.d.ts +2 -2
  52. package/packages/providers/lib/vram.js +1 -1
  53. package/packages/providers/package.json +3 -3
  54. package/packages/service/lib/index.d.ts +63 -20
  55. package/packages/service/lib/index.js +279 -43
  56. package/packages/service/lib/types.d.ts +44 -2
  57. package/packages/service/package.json +2 -2
  58. package/packages/tool/lib/index.d.ts +1 -1
  59. package/packages/tool/lib/index.js +1 -1
  60. package/packages/tool/package.json +3 -3
  61. package/packages/ui/lib/deps.d.ts +2 -2
  62. package/packages/ui/lib/deps.js +1 -1
  63. package/packages/ui/lib/form.d.ts +2 -2
  64. package/packages/ui/lib/form.js +1 -1
  65. package/packages/ui/lib/index.d.ts +1 -1
  66. package/packages/ui/lib/index.js +1 -1
  67. package/packages/ui/lib/meter.d.ts +2 -2
  68. package/packages/ui/lib/meter.js +1 -1
  69. package/packages/ui/lib/route.d.ts +84 -13
  70. package/packages/ui/lib/route.js +496 -56
  71. package/packages/ui/lib/warnings.d.ts +2 -2
  72. package/packages/ui/lib/warnings.js +1 -1
  73. 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 @dsh/dsh-plugin-local-infer/providers/adapters
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
- if (engineDir !== undefined) {
375
- const fork = platform?.engineFork;
376
- if (fork !== undefined) {
377
- // Mirror the qwen-ninfer launcher: an existing checkout (a .git under
378
- // the engine directory) is updated in place with a best-effort fetch
379
- // and a hard reset to the fork's master, then built as-is; a missing
380
- // checkout is a fresh clone. The fetch is best-effort (a `; true`
381
- // pins the branch to success) so an offline host still builds the
382
- // checkout it has; only a missing checkout that cannot be cloned
383
- // fails the step.
384
- const sync = await this.host.run(`if [ -d ${engineDir}/.git ]; then ` +
385
- `git -C ${engineDir} fetch ${fork} 2>/dev/null && git -C ${engineDir} reset --hard ${fork}/master 2>/dev/null; true; ` +
386
- `else git clone ${fork} ${engineDir}; fi`);
387
- if (!sync.ok) {
388
- return { ok: false, detail: `git sync failed: ${sync.detail ?? 'unknown'}` };
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
- const build = await this.host.run(`cmake -S ${engineDir} -B ${engineDir}/build -G Ninja -DCMAKE_BUILD_TYPE=Release && ` +
392
- `cmake --build ${engineDir}/build -j`);
393
- if (!build.ok) {
394
- return { ok: false, detail: `build failed: ${build.detail ?? 'unknown'}` };
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.set(this.planKey(plan), handle);
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.set(this.planKey(plan), handle);
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
- const up = await this.host.run(`docker compose -f ${plan.composeFile} --profile ${plan.profile} up --pull always -d`);
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
- await this.host.run(`docker compose -f ${plan.composeFile} --profile ${plan.profile} down`);
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
- if (engineDir !== undefined) {
694
- const fork = platform?.engineFork;
695
- if (fork !== undefined) {
696
- // Mirror the qwen-ninfer launcher: an existing checkout is updated in
697
- // place with a best-effort fetch and a hard reset to the fork's
698
- // master, then built as-is; a missing checkout is a fresh clone. The
699
- // fetch is best-effort (a `; true` pins the branch to success) so an
700
- // offline host still builds the checkout it has.
701
- const sync = await this.host.run(`if [ -d ${engineDir}/.git ]; then ` +
702
- `git -C ${engineDir} fetch ${fork} 2>/dev/null && git -C ${engineDir} reset --hard ${fork}/master 2>/dev/null; true; ` +
703
- `else git clone ${fork} ${engineDir}; fi`);
704
- if (!sync.ok) {
705
- return { ok: false, detail: `git sync failed: ${sync.detail ?? 'unknown'}` };
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
- const build = await this.host.run(`cmake -S ${engineDir} -B ${engineDir}/build -G Ninja -DCMAKE_BUILD_TYPE=Release && ` +
709
- `cmake --build ${engineDir}/build -j`);
710
- if (!build.ok) {
711
- return { ok: false, detail: `build failed: ${build.detail ?? 'unknown'}` };
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.set(this.planKey(plan), handle);
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.set(this.planKey(plan), handle);
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.set(this.planKey(plan), handle);
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 @dsh/dsh-plugin-local-infer/providers/debug
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 @dsh/dsh-plugin-local-infer/providers/debug
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 @dsh/dsh-plugin-local-infer/providers/deps
12
+ * @module dsh-plugin-local-infer/providers/deps
13
13
  */
14
- import type { EngineDepPlan, HardwareProfile } from '@dsh/dsh-plugin-local-infer/core';
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. llama.cpp and Strata carry the specified-not-verified lists until
221
- * their adapters land in Phase 5.
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 @dsh/dsh-plugin-local-infer/providers/deps
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. llama.cpp and Strata carry the specified-not-verified lists until
193
- * their adapters land in Phase 5.
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],