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.
Files changed (100) hide show
  1. package/README.md +10 -2
  2. package/docs/zh/README.md +10 -2
  3. package/dsh/client.js +669 -191
  4. package/package.json +6 -1
  5. package/packages/core/lib/adapter.d.ts +44 -2
  6. package/packages/core/lib/deps.d.ts +4 -3
  7. package/packages/core/lib/index.d.ts +1 -1
  8. package/packages/core/lib/registrar.d.ts +5 -5
  9. package/packages/core/lib/registrar.js +3 -3
  10. package/packages/core/package.json +1 -1
  11. package/packages/providers/lib/acquire.d.ts +5 -3
  12. package/packages/providers/lib/acquire.js +30 -9
  13. package/packages/providers/lib/adapters.d.ts +19 -209
  14. package/packages/providers/lib/adapters.js +13 -886
  15. package/packages/providers/lib/deps.d.ts +24 -2
  16. package/packages/providers/lib/deps.js +23 -4
  17. package/packages/providers/lib/engines/hyperqwenAdapter.d.ts +38 -0
  18. package/packages/providers/lib/engines/hyperqwenAdapter.js +105 -0
  19. package/packages/providers/lib/{infernix.js → engines/infernix.js} +1 -1
  20. package/packages/providers/lib/engines/infernixAdapter.d.ts +53 -0
  21. package/packages/providers/lib/engines/infernixAdapter.js +202 -0
  22. package/packages/providers/lib/engines/llamacppAdapter.d.ts +51 -0
  23. package/packages/providers/lib/engines/llamacppAdapter.js +232 -0
  24. package/packages/providers/lib/{ninfer.js → engines/ninfer.js} +1 -1
  25. package/packages/providers/lib/engines/ninferAdapter.d.ts +82 -0
  26. package/packages/providers/lib/engines/ninferAdapter.js +324 -0
  27. package/packages/providers/lib/engines/strata.d.ts +53 -0
  28. package/packages/providers/lib/engines/strata.js +197 -0
  29. package/packages/providers/lib/engines/strataAdapter.d.ts +98 -0
  30. package/packages/providers/lib/engines/strataAdapter.js +427 -0
  31. package/packages/providers/lib/engines.d.ts +5 -4
  32. package/packages/providers/lib/engines.js +10 -10
  33. package/packages/providers/lib/guidelines.js +1 -1
  34. package/packages/providers/lib/index.d.ts +14 -9
  35. package/packages/providers/lib/index.js +17 -7
  36. package/packages/providers/lib/model-defaults.d.ts +5 -4
  37. package/packages/providers/lib/model-defaults.js +22 -4
  38. package/packages/providers/lib/platform.js +7 -1
  39. package/packages/providers/package.json +1 -1
  40. package/packages/service/lib/index.d.ts +13 -4
  41. package/packages/service/lib/index.js +79 -5
  42. package/packages/service/lib/types.d.ts +5 -4
  43. package/packages/service/package.json +1 -1
  44. package/packages/tool/package.json +1 -1
  45. package/packages/ui/lib/config.d.ts +79 -0
  46. package/packages/ui/lib/config.js +235 -0
  47. package/packages/ui/lib/deps.d.ts +3 -3
  48. package/packages/ui/lib/ops/engine.d.ts +108 -0
  49. package/packages/ui/lib/ops/engine.js +493 -0
  50. package/packages/ui/lib/ops/hf.d.ts +27 -0
  51. package/packages/ui/lib/ops/hf.js +60 -0
  52. package/packages/ui/lib/ops/logs.d.ts +23 -0
  53. package/packages/ui/lib/ops/logs.js +58 -0
  54. package/packages/ui/lib/ops/model.d.ts +79 -0
  55. package/packages/ui/lib/ops/model.js +130 -0
  56. package/packages/ui/lib/request.d.ts +106 -0
  57. package/packages/ui/lib/request.js +80 -0
  58. package/packages/ui/lib/route.d.ts +88 -420
  59. package/packages/ui/lib/route.js +111 -1843
  60. package/packages/ui/lib/routes/chat-template.d.ts +14 -0
  61. package/packages/ui/lib/routes/chat-template.js +37 -0
  62. package/packages/ui/lib/routes/deps.d.ts +16 -0
  63. package/packages/ui/lib/routes/deps.js +27 -0
  64. package/packages/ui/lib/routes/download.d.ts +16 -0
  65. package/packages/ui/lib/routes/download.js +48 -0
  66. package/packages/ui/lib/routes/get.d.ts +18 -0
  67. package/packages/ui/lib/routes/get.js +57 -0
  68. package/packages/ui/lib/routes/install.d.ts +16 -0
  69. package/packages/ui/lib/routes/install.js +36 -0
  70. package/packages/ui/lib/routes/launch.d.ts +18 -0
  71. package/packages/ui/lib/routes/launch.js +50 -0
  72. package/packages/ui/lib/routes/provider-delete.d.ts +15 -0
  73. package/packages/ui/lib/routes/provider-delete.js +37 -0
  74. package/packages/ui/lib/routes/register.d.ts +16 -0
  75. package/packages/ui/lib/routes/register.js +28 -0
  76. package/packages/ui/lib/routes/save.d.ts +18 -0
  77. package/packages/ui/lib/routes/save.js +139 -0
  78. package/packages/ui/lib/routes/stop.d.ts +16 -0
  79. package/packages/ui/lib/routes/stop.js +29 -0
  80. package/packages/ui/lib/routes/summary.d.ts +15 -0
  81. package/packages/ui/lib/routes/summary.js +26 -0
  82. package/packages/ui/lib/routes/unregister.d.ts +14 -0
  83. package/packages/ui/lib/routes/unregister.js +26 -0
  84. package/packages/ui/lib/viewmodels/deps.d.ts +31 -0
  85. package/packages/ui/lib/viewmodels/deps.js +89 -0
  86. package/packages/ui/lib/viewmodels/depsHelpers.d.ts +28 -0
  87. package/packages/ui/lib/viewmodels/depsHelpers.js +33 -0
  88. package/packages/ui/lib/viewmodels/summary.d.ts +140 -0
  89. package/packages/ui/lib/viewmodels/summary.js +225 -0
  90. package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +30 -0
  91. package/packages/ui/lib/viewmodels/summaryHelpers.js +42 -0
  92. package/packages/ui/package.json +1 -1
  93. package/packages/providers/lib/strata.d.ts +0 -15
  94. package/packages/providers/lib/strata.js +0 -14
  95. /package/packages/providers/lib/{hyperqwen.d.ts → engines/hyperqwen.d.ts} +0 -0
  96. /package/packages/providers/lib/{hyperqwen.js → engines/hyperqwen.js} +0 -0
  97. /package/packages/providers/lib/{infernix.d.ts → engines/infernix.d.ts} +0 -0
  98. /package/packages/providers/lib/{llamacpp.d.ts → engines/llamacpp.d.ts} +0 -0
  99. /package/packages/providers/lib/{llamacpp.js → engines/llamacpp.js} +0 -0
  100. /package/packages/providers/lib/{ninfer.d.ts → engines/ninfer.d.ts} +0 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * The llama.cpp engine adapter. LlamaCppAdapter is the llama.cpp engine,
3
+ * both-capable like NInfer. `renderLaunch` renders a local `llama-server`
4
+ * process or a `docker run` of the ggml image from the same
5
+ * `LLAMACPP_PARAMETERS` surface; `deploy` builds the engine in the engine
6
+ * directory (or pulls the image), downloads the GGUF model into the model
7
+ * directory, and starts the engine; `healthCheck` polls the health URL;
8
+ * `stop` signals the tracked process and stops the named container.
9
+ * @module dsh-plugin-local-infer/providers/engines/llamacppAdapter
10
+ */
11
+ import { basename, dirname, join } from 'node:path';
12
+ import { EngineAdapter } from '../../../core/lib/index.js';
13
+ import { customArgFlags, flagsFromSpecs, nodeLifecycleHost, pollHealth, trackChild, } from "../adapters.js";
14
+ import { syncAndRebuildEngine } from "../acquire.js";
15
+ import { LLAMACPP_MODEL_ARTIFACTS, LLAMACPP_PARAMETERS, llamaCppModelKey, } from "./llamacpp.js";
16
+ /**
17
+ * The llama.cpp docker image the adapter pulls and runs. The engineFork
18
+ * platform config overrides the git source for a local build; the image stays
19
+ * the upstream ggml artifact.
20
+ */
21
+ export const LLAMACPP_DOCKER_IMAGE = 'ghcr.io/ggml-org/llama.cpp';
22
+ /**
23
+ * llamaCppArtifact resolves the artifact a model key deploys. A key no entry
24
+ * names falls back to the 27B artifact, so a bare config always renders a plan.
25
+ */
26
+ function llamaCppArtifact(modelKey) {
27
+ const artifact = modelKey !== undefined ? LLAMACPP_MODEL_ARTIFACTS[modelKey] : undefined;
28
+ return artifact ?? LLAMACPP_MODEL_ARTIFACTS['qwen3-27b'];
29
+ }
30
+ /**
31
+ * LlamaCppAdapter is the llama.cpp engine, both-capable like NInfer.
32
+ * `renderLaunch` renders a local `llama-server` process or a `docker run` of the
33
+ * ggml image from the same `LLAMACPP_PARAMETERS` surface; `deploy` builds the
34
+ * engine in the engine directory (or pulls the image), downloads the GGUF model
35
+ * into the model directory, and starts the engine; `healthCheck` polls the health
36
+ * URL; `stop` signals the tracked process and stops the named container.
37
+ */
38
+ export class LlamaCppAdapter extends EngineAdapter {
39
+ id = 'llamacpp';
40
+ supports = { docker: true, local: true };
41
+ host;
42
+ children = new Map();
43
+ constructor(host = nodeLifecycleHost) {
44
+ super();
45
+ this.host = host;
46
+ }
47
+ parameters() {
48
+ return LLAMACPP_PARAMETERS;
49
+ }
50
+ modelRef(config) {
51
+ // The engine serves the model under the artifact's advertised id, so the
52
+ // registrar publishes that alias and a chat turn routes to it.
53
+ const artifact = llamaCppArtifact(llamaCppModelKey(config));
54
+ return { id: artifact.modelId, name: artifact.modelId };
55
+ }
56
+ launchSignature(config = {}) {
57
+ // The local plan runs the llama-server binary; the docker plan's host
58
+ // process is docker run of the ggml image, so the image name is the
59
+ // command-line marker the reserve probe matches on.
60
+ const port = this.port(config);
61
+ if (this.buildMode(config) === 'docker') {
62
+ return { binary: LLAMACPP_DOCKER_IMAGE, port };
63
+ }
64
+ return { binary: 'llama-server', port };
65
+ }
66
+ renderLaunch(config, _hw) {
67
+ const port = this.port(config);
68
+ const modelId = llamaCppModelKey(config);
69
+ const artifact = llamaCppArtifact(modelId);
70
+ // A model key that names a local GGUF file (a directory-scanned model)
71
+ // overrides the artifact map's file, so a file the user has on disk
72
+ // launches as-is.
73
+ const file = modelId.endsWith('.gguf') ? modelId : artifact.file;
74
+ const modelDirectory = typeof config.modelDirectory === 'string' ? config.modelDirectory : '';
75
+ const flags = flagsFromSpecs(LLAMACPP_PARAMETERS, config);
76
+ if (this.buildMode(config) === 'docker') {
77
+ const modelPath = modelDirectory === '' ? file : join(modelDirectory, file);
78
+ const containerModelPath = `/models/${basename(file)}`;
79
+ const modelMount = modelDirectory === ''
80
+ ? `${file}:${containerModelPath}`
81
+ : `${dirname(modelPath)}:/models`;
82
+ const containerName = `llamacpp-${port}`;
83
+ const customArgs = customArgFlags(config);
84
+ return {
85
+ engineId: this.id,
86
+ kind: 'docker',
87
+ argv: [
88
+ 'docker',
89
+ 'run',
90
+ '--rm',
91
+ '--gpus',
92
+ 'all',
93
+ '--name',
94
+ containerName,
95
+ '-p',
96
+ `127.0.0.1:${port}:8080`,
97
+ '-v',
98
+ modelMount,
99
+ LLAMACPP_DOCKER_IMAGE,
100
+ '--model',
101
+ containerModelPath,
102
+ ...flags,
103
+ ...customArgs,
104
+ ],
105
+ env: {},
106
+ containerName,
107
+ modelId,
108
+ ...(modelDirectory === '' ? {} : { modelDirectory }),
109
+ port,
110
+ healthUrl: `http://127.0.0.1:${port}/health`,
111
+ };
112
+ }
113
+ const modelPath = modelDirectory === '' ? file : join(modelDirectory, file);
114
+ const customArgs = customArgFlags(config);
115
+ return {
116
+ engineId: this.id,
117
+ kind: 'process',
118
+ argv: [
119
+ 'llama-server',
120
+ '--model',
121
+ modelPath,
122
+ '--host',
123
+ '127.0.0.1',
124
+ '--port',
125
+ String(port),
126
+ ...flags,
127
+ ...customArgs,
128
+ ],
129
+ env: {},
130
+ modelId,
131
+ ...(modelDirectory === '' ? {} : { modelDirectory }),
132
+ port,
133
+ healthUrl: `http://127.0.0.1:${port}/health`,
134
+ };
135
+ }
136
+ async deploy(plan, platform, onProgress, options) {
137
+ if (plan.kind === 'process') {
138
+ const engineDir = platform?.engineDirectory;
139
+ let buildLogs = '';
140
+ // Auto Rebuild off (the card's checkbox): the git sync (fetch, rebase,
141
+ // rebuild) is skipped and the checkout launches as-is.
142
+ if (engineDir !== undefined && options?.autoRebuild !== false) {
143
+ // The git sync is the checkout download: the card's button reads
144
+ // "Downloading..." for it, not "Building...".
145
+ onProgress?.('download');
146
+ // The conditional rebuild fetches the checkout, prices it against the
147
+ // remote branch, and rebuilds only when it is behind (or a fresh
148
+ // clone). The decision, the build command, and its output are captured
149
+ // into buildLogs and threaded to the card's Logs section.
150
+ const sync = await syncAndRebuildEngine(this.host, {
151
+ engineId: this.id,
152
+ directory: engineDir,
153
+ ...(platform?.engineFork !== undefined ? { fork: platform.engineFork } : {}),
154
+ ...(plan.argv?.[0] !== undefined ? { binary: plan.argv[0] } : {}),
155
+ ...(options?.allowRunDeps !== undefined ? { allowRunDeps: options.allowRunDeps } : {}),
156
+ ...(platform?.buildCommands !== undefined
157
+ ? { buildCommands: platform.buildCommands }
158
+ : {}),
159
+ // Report the build phase the moment the build steps start, so the
160
+ // card's button reads "Building..." while the build is in flight.
161
+ onBuildStart: () => onProgress?.('build'),
162
+ }, () => { });
163
+ buildLogs = sync.logs;
164
+ if (!sync.ok) {
165
+ return { ok: false, detail: sync.detail ?? 'unknown', logs: buildLogs };
166
+ }
167
+ // The build output reads as a fresh Logs section: a "Building..."
168
+ // separator leads the build decision and output, the same line the
169
+ // card writes when the build step event lands.
170
+ if (sync.rebuilt && buildLogs !== '') {
171
+ buildLogs = `--- Building...\n${buildLogs}`;
172
+ }
173
+ }
174
+ const withLogs = (result) => buildLogs !== '' ? { ...result, logs: buildLogs } : result;
175
+ onProgress?.('download');
176
+ const download = await this.downloadModel(plan);
177
+ if (!download.ok)
178
+ return withLogs(download);
179
+ const handle = this.host.spawn(plan.argv ?? [], plan.env ?? {}, engineDir);
180
+ trackChild(this.children, this.planKey(plan), handle);
181
+ return withLogs({ ok: true, handle });
182
+ }
183
+ if (plan.kind === 'docker') {
184
+ // The image pull is the docker build step: the card's button reads
185
+ // "Building..." for it.
186
+ onProgress?.('build');
187
+ const pull = await this.host.run(`docker pull ${LLAMACPP_DOCKER_IMAGE}`);
188
+ if (!pull.ok) {
189
+ return { ok: false, detail: `docker pull failed: ${pull.detail ?? 'unknown'}` };
190
+ }
191
+ const handle = this.host.spawn(plan.argv ?? [], plan.env ?? {});
192
+ trackChild(this.children, this.planKey(plan), handle);
193
+ return { ok: true, handle };
194
+ }
195
+ // llama.cpp renders process and docker plans; a compose plan is a no-op.
196
+ return { ok: true };
197
+ }
198
+ async healthCheck(plan, options) {
199
+ return pollHealth(this.host, plan, options);
200
+ }
201
+ async stop(plan) {
202
+ if (plan.kind === 'docker' && plan.containerName !== undefined) {
203
+ await this.host.run(`docker stop ${plan.containerName}`);
204
+ }
205
+ const handle = this.children.get(this.planKey(plan));
206
+ if (handle !== undefined) {
207
+ handle.kill();
208
+ this.children.delete(this.planKey(plan));
209
+ }
210
+ }
211
+ buildMode(config) {
212
+ return config.buildMode === 'docker' ? 'docker' : 'local';
213
+ }
214
+ port(config) {
215
+ return typeof config.port === 'number' ? config.port : 8080;
216
+ }
217
+ planKey(plan) {
218
+ return `${plan.engineId}:${plan.port}`;
219
+ }
220
+ async downloadModel(plan) {
221
+ const artifact = plan.modelId !== undefined ? LLAMACPP_MODEL_ARTIFACTS[plan.modelId] : undefined;
222
+ if (artifact === undefined)
223
+ return { ok: true };
224
+ const target = plan.modelDirectory ?? '.';
225
+ const download = await this.host.run(`hf download ${artifact.repo} ${artifact.file} --local-dir ${target}`);
226
+ if (!download.ok) {
227
+ return { ok: false, detail: `model download failed: ${download.detail ?? 'unknown'}` };
228
+ }
229
+ return { ok: true };
230
+ }
231
+ }
232
+ //# sourceMappingURL=llamacppAdapter.js.map
@@ -216,7 +216,7 @@ export const NINFER_PARAMETERS = [
216
216
  {
217
217
  key: 'lmHeadDraft',
218
218
  label: 'LM-head draft',
219
- description: 'Drafts from the small proposal head instead of the full output head, cutting the per-draft projection cost. An MTP-backend feature, so the field unlocks only while MTP is selected.',
219
+ description: 'Drafts from the small proposal head instead of the full output head, cutting the per-draft projection cost.',
220
220
  form: { kind: 'boolean' },
221
221
  defaultValue: false,
222
222
  group: 'Speculation',
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The NInfer engine adapter. NInferModelArtifact is the per-model deploy data:
3
+ * the `.ninfer` artifact file, the Hugging Face repo the model download pulls
4
+ * it from, and the `--model-id` alias the server advertises. The data is the
5
+ * surface; adding a model is a data edit here, never an adapter rewire.
6
+ * NInferAdapter is the canonical both-capable engine. `renderLaunch` renders a
7
+ * local `ninfer-serve` process or a `docker run` of the ghcr image from the
8
+ * same spec surface; `deploy` builds the engine in the engine directory (or
9
+ * pulls the image), downloads the model into the model directory, and starts
10
+ * the engine; `healthCheck` polls the health URL; `stop` signals the tracked
11
+ * process and stops the named container.
12
+ * @module dsh-plugin-local-infer/providers/engines/ninferAdapter
13
+ */
14
+ import { EngineAdapter } from '../../../core/lib/index.js';
15
+ import type { Config, DeployOptions, DeployResult, EngineLaunchSignature, EnginePlatformConfig, HealthCheckOptions, HealthResult, HardwareProfile, LaunchPlan, LaunchProgress, LifecycleHost, ParameterSpec } from '../../../core/lib/index.js';
16
+ /**
17
+ * NInferModelArtifact is the per-model deploy data: the `.ninfer` artifact
18
+ * file, the Hugging Face repo the model download pulls it from, and the
19
+ * `--model-id` alias the server advertises. The data is the surface; adding a
20
+ * model is a data edit here, never an adapter rewire.
21
+ */
22
+ export interface NInferModelArtifact {
23
+ file: string;
24
+ repo: string;
25
+ modelId: string;
26
+ }
27
+ /**
28
+ * The NInfer model artifacts, keyed by the model registry id. The 27B entry
29
+ * is the default the DEFAULT_MODELS list names; the 35B-A3B entries are the
30
+ * Qwen3.8 measured artifact (`qwen3-35b`) and the Qwen3.6 default
31
+ * (`qwen36-35b`).
32
+ */
33
+ export declare const NINFER_MODEL_ARTIFACTS: Record<string, NInferModelArtifact>;
34
+ /**
35
+ * The NInfer docker image the adapter pulls and runs. The engineFork platform
36
+ * config overrides the git source for a local build; the image stays the
37
+ * upstream ghcr artifact.
38
+ */
39
+ export declare const NINFER_DOCKER_IMAGE = "ghcr.io/iamwavecut/ninfer-all";
40
+ /**
41
+ * NInferAdapter is the canonical both-capable engine. `renderLaunch` renders a
42
+ * local `ninfer-serve` process or a `docker run` of the ghcr image from the
43
+ * same spec surface; `deploy` builds the engine in the engine directory (or
44
+ * pulls the image), downloads the model into the model directory, and starts
45
+ * the engine; `healthCheck` polls the health URL; `stop` signals the tracked
46
+ * process and stops the named container.
47
+ */
48
+ export declare class NInferAdapter extends EngineAdapter {
49
+ readonly id = "ninfer";
50
+ readonly supports: {
51
+ docker: boolean;
52
+ local: boolean;
53
+ };
54
+ private readonly host;
55
+ private readonly children;
56
+ constructor(host?: LifecycleHost);
57
+ parameters(): readonly ParameterSpec[];
58
+ modelRef(config: Config): {
59
+ id: string;
60
+ name: string;
61
+ };
62
+ launchSignature(config?: Config): EngineLaunchSignature;
63
+ renderLaunch(config: Config, _hw: HardwareProfile): LaunchPlan;
64
+ deploy(plan: LaunchPlan, platform?: EnginePlatformConfig, onProgress?: LaunchProgress, options?: DeployOptions): Promise<DeployResult>;
65
+ healthCheck(plan: LaunchPlan, options?: HealthCheckOptions): Promise<HealthResult>;
66
+ stop(plan: LaunchPlan): Promise<void>;
67
+ private buildMode;
68
+ private port;
69
+ private planKey;
70
+ private downloadModel;
71
+ /**
72
+ * resolveChatTemplate rewrites a --chat-template flag that carries an HF repo
73
+ * reference into the local file the engine reads. A value with a slash that
74
+ * is not an absolute path (the combo control's known choices) is a repo the
75
+ * deploy step downloads into the model directory; an already-local path is
76
+ * left as-is. The download is best-effort: a template that is already on
77
+ * disk is used without a re-fetch, and a missing hf CLI leaves the flag
78
+ * untouched so the engine reports the missing template itself.
79
+ */
80
+ private resolveChatTemplate;
81
+ }
82
+ //# sourceMappingURL=ninferAdapter.d.ts.map
@@ -0,0 +1,324 @@
1
+ /**
2
+ * The NInfer engine adapter. NInferModelArtifact is the per-model deploy data:
3
+ * the `.ninfer` artifact file, the Hugging Face repo the model download pulls
4
+ * it from, and the `--model-id` alias the server advertises. The data is the
5
+ * surface; adding a model is a data edit here, never an adapter rewire.
6
+ * NInferAdapter is the canonical both-capable engine. `renderLaunch` renders a
7
+ * local `ninfer-serve` process or a `docker run` of the ghcr image from the
8
+ * same spec surface; `deploy` builds the engine in the engine directory (or
9
+ * pulls the image), downloads the model into the model directory, and starts
10
+ * the engine; `healthCheck` polls the health URL; `stop` signals the tracked
11
+ * process and stops the named container.
12
+ * @module dsh-plugin-local-infer/providers/engines/ninferAdapter
13
+ */
14
+ import { basename, dirname, join } from 'node:path';
15
+ import { EngineAdapter } from '../../../core/lib/index.js';
16
+ import { customArgFlags, flagsFromSpecs, nodeLifecycleHost, pollHealth, trackChild, } from "../adapters.js";
17
+ import { syncAndRebuildEngine } from "../acquire.js";
18
+ import { NINFER_PARAMETERS } from "./ninfer.js";
19
+ /**
20
+ * The NInfer model artifacts, keyed by the model registry id. The 27B entry
21
+ * is the default the DEFAULT_MODELS list names; the 35B-A3B entries are the
22
+ * Qwen3.8 measured artifact (`qwen3-35b`) and the Qwen3.6 default
23
+ * (`qwen36-35b`).
24
+ */
25
+ export const NINFER_MODEL_ARTIFACTS = {
26
+ 'qwen3-27b': {
27
+ file: 'qwen3_8_27b_nvfp4.ninfer',
28
+ repo: 'neroued/Qwen3.8-27B-nvfp4-NInfer',
29
+ modelId: 'Qwen3.8-27B',
30
+ },
31
+ // The Swift 1.5 ablated 27B the local launcher bootstraps: the nvfp4-full
32
+ // DFlash2 artifact the qwen-ninfer script serves. It reuses the 27B model id
33
+ // the engine advertises, so a chat turn routes to the same alias.
34
+ 'qwen3-27b-swift15': {
35
+ file: 'qwen3_8_27b_swift15abl_nvfp4full-dflash2.ninfer',
36
+ repo: 'kaushikvira/Qwen3.8-27B-swift15-uncensored-ajgazin-nvfp4full-dflash2-NInfer-v3',
37
+ modelId: 'Qwen3.8-27B',
38
+ },
39
+ 'qwen3-35b': {
40
+ file: 'qwen3_8_35b_a3b.ninfer',
41
+ repo: 'Neroued/Qwen3.8-35B-A3B-NInfer',
42
+ modelId: 'Qwen3.8-35B-A3B',
43
+ },
44
+ 'qwen36-35b': {
45
+ file: 'qwen3_6_35b_a3b.ninfer',
46
+ repo: 'neroued/Qwen3.6-35B-A3B-NInfer',
47
+ modelId: 'Qwen3.6-35B-A3B',
48
+ },
49
+ };
50
+ /**
51
+ * artifactFor resolves the artifact a model id deploys. An id no entry names
52
+ * falls back to the 27B artifact, so a bare config always renders a plan.
53
+ */
54
+ function artifactFor(modelId) {
55
+ const artifact = modelId !== undefined ? NINFER_MODEL_ARTIFACTS[modelId] : undefined;
56
+ return artifact ?? NINFER_MODEL_ARTIFACTS['qwen3-27b'];
57
+ }
58
+ /**
59
+ * The NInfer docker image the adapter pulls and runs. The engineFork platform
60
+ * config overrides the git source for a local build; the image stays the
61
+ * upstream ghcr artifact.
62
+ */
63
+ export const NINFER_DOCKER_IMAGE = 'ghcr.io/iamwavecut/ninfer-all';
64
+ /**
65
+ * NInferAdapter is the canonical both-capable engine. `renderLaunch` renders a
66
+ * local `ninfer-serve` process or a `docker run` of the ghcr image from the
67
+ * same spec surface; `deploy` builds the engine in the engine directory (or
68
+ * pulls the image), downloads the model into the model directory, and starts
69
+ * the engine; `healthCheck` polls the health URL; `stop` signals the tracked
70
+ * process and stops the named container.
71
+ */
72
+ export class NInferAdapter extends EngineAdapter {
73
+ id = 'ninfer';
74
+ supports = { docker: true, local: true };
75
+ host;
76
+ children = new Map();
77
+ constructor(host = nodeLifecycleHost) {
78
+ super();
79
+ this.host = host;
80
+ }
81
+ parameters() {
82
+ return NINFER_PARAMETERS;
83
+ }
84
+ modelRef(config) {
85
+ // The id the engine advertises on its OpenAI endpoint is the artifact's
86
+ // --model-id alias, not the VramModel registry key the config names, so
87
+ // the registrar publishes the alias and a chat turn routes to it.
88
+ const artifact = artifactFor(typeof config.model === 'string' ? config.model : 'qwen3-27b');
89
+ return { id: artifact.modelId, name: artifact.modelId };
90
+ }
91
+ launchSignature(config = {}) {
92
+ // The local plan runs the ninfer-serve binary; the docker plan's host
93
+ // process is docker run of the ghcr image, so the image name is the
94
+ // command-line marker the reserve probe matches on.
95
+ const port = this.port(config);
96
+ if (this.buildMode(config) === 'docker') {
97
+ return { binary: NINFER_DOCKER_IMAGE, port };
98
+ }
99
+ return { binary: 'ninfer-serve', port };
100
+ }
101
+ renderLaunch(config, _hw) {
102
+ const port = this.port(config);
103
+ const modelId = typeof config.model === 'string' ? config.model : 'qwen3-27b';
104
+ const artifact = artifactFor(modelId);
105
+ // A model key that names a local artifact file (a directory-scanned
106
+ // model) overrides the artifact map's file, so a file the user has on
107
+ // disk launches as-is.
108
+ const file = modelId.endsWith('.ninfer') ? modelId : artifact.file;
109
+ const modelDirectory = typeof config.modelDirectory === 'string' ? config.modelDirectory : '';
110
+ const flags = flagsFromSpecs(NINFER_PARAMETERS, config);
111
+ if (this.buildMode(config) === 'docker') {
112
+ const modelPath = modelDirectory === '' ? file : join(modelDirectory, file);
113
+ const containerModelPath = `/models/${basename(file)}`;
114
+ const modelMount = modelDirectory === ''
115
+ ? `${file}:${containerModelPath}`
116
+ : `${dirname(modelPath)}:/models`;
117
+ const containerName = `ninfer-${port}`;
118
+ const customArgs = customArgFlags(config);
119
+ return {
120
+ engineId: this.id,
121
+ kind: 'docker',
122
+ argv: [
123
+ 'docker',
124
+ 'run',
125
+ '--rm',
126
+ '--gpus',
127
+ 'all',
128
+ '--name',
129
+ containerName,
130
+ '-p',
131
+ `127.0.0.1:${port}:8080`,
132
+ '--ulimit',
133
+ 'memlock=-1',
134
+ '-v',
135
+ 'ninfer-cache:/cache',
136
+ '-v',
137
+ modelMount,
138
+ NINFER_DOCKER_IMAGE,
139
+ 'serve',
140
+ containerModelPath,
141
+ '--model-id',
142
+ artifact.modelId,
143
+ ...flags,
144
+ ...customArgs,
145
+ ],
146
+ env: {},
147
+ containerName,
148
+ modelId,
149
+ ...(modelDirectory === '' ? {} : { modelDirectory }),
150
+ port,
151
+ healthUrl: `http://127.0.0.1:${port}/health`,
152
+ };
153
+ }
154
+ const modelPath = modelDirectory === '' ? file : join(modelDirectory, file);
155
+ const customArgs = customArgFlags(config);
156
+ return {
157
+ engineId: this.id,
158
+ kind: 'process',
159
+ argv: [
160
+ 'ninfer-serve',
161
+ modelPath,
162
+ '--host',
163
+ '127.0.0.1',
164
+ '--port',
165
+ String(port),
166
+ '--model-id',
167
+ artifact.modelId,
168
+ ...flags,
169
+ ...customArgs,
170
+ ],
171
+ env: {},
172
+ modelId,
173
+ ...(modelDirectory === '' ? {} : { modelDirectory }),
174
+ port,
175
+ healthUrl: `http://127.0.0.1:${port}/health`,
176
+ };
177
+ }
178
+ async deploy(plan, platform, onProgress, options) {
179
+ if (plan.kind === 'process') {
180
+ const engineDir = platform?.engineDirectory;
181
+ let buildLogs = '';
182
+ // Auto Rebuild off (the card's checkbox): the git sync (fetch, rebase,
183
+ // rebuild) is skipped and the checkout launches as-is.
184
+ if (engineDir !== undefined && options?.autoRebuild !== false) {
185
+ // The git sync is the checkout download: the card's button reads
186
+ // "Downloading..." for it, not "Building...".
187
+ onProgress?.('download');
188
+ // The conditional rebuild fetches the checkout, prices it against the
189
+ // remote branch, and rebuilds only when it is behind (or a fresh
190
+ // clone). The decision, the build command, and its output are captured
191
+ // into buildLogs and threaded to the card's Logs section.
192
+ const sync = await syncAndRebuildEngine(this.host, {
193
+ engineId: this.id,
194
+ directory: engineDir,
195
+ ...(platform?.engineFork !== undefined ? { fork: platform.engineFork } : {}),
196
+ ...(plan.argv?.[0] !== undefined ? { binary: plan.argv[0] } : {}),
197
+ ...(options?.allowRunDeps !== undefined ? { allowRunDeps: options.allowRunDeps } : {}),
198
+ ...(platform?.buildCommands !== undefined
199
+ ? { buildCommands: platform.buildCommands }
200
+ : {}),
201
+ // Report the build phase the moment the build steps start, so the
202
+ // card's button reads "Building..." while the build is in flight.
203
+ onBuildStart: () => onProgress?.('build'),
204
+ }, () => { });
205
+ buildLogs = sync.logs;
206
+ if (!sync.ok) {
207
+ return { ok: false, detail: sync.detail ?? 'unknown', logs: buildLogs };
208
+ }
209
+ // The build output reads as a fresh Logs section: a "Building..."
210
+ // separator leads the build decision and output, the same line the
211
+ // card writes when the build step event lands.
212
+ if (sync.rebuilt && buildLogs !== '') {
213
+ buildLogs = `--- Building...\n${buildLogs}`;
214
+ }
215
+ }
216
+ const withLogs = (result) => buildLogs !== '' ? { ...result, logs: buildLogs } : result;
217
+ onProgress?.('download');
218
+ const download = await this.downloadModel(plan);
219
+ if (!download.ok)
220
+ return withLogs(download);
221
+ // A chat template the user picked as an HF repo reference (the combo
222
+ // control's known choices) is not a path the engine can read, so the
223
+ // deploy step downloads it into the model directory and rewrites the
224
+ // --chat-template flag to the local file before the spawn.
225
+ onProgress?.('download');
226
+ const templateResult = await this.resolveChatTemplate(plan);
227
+ if (!templateResult.ok)
228
+ return withLogs(templateResult);
229
+ const handle = this.host.spawn(plan.argv ?? [], plan.env ?? {}, engineDir);
230
+ trackChild(this.children, this.planKey(plan), handle);
231
+ return withLogs({ ok: true, handle });
232
+ }
233
+ if (plan.kind === 'docker') {
234
+ // The image pull is the docker build step: the card's button reads
235
+ // "Building..." for it.
236
+ onProgress?.('build');
237
+ const pull = await this.host.run(`docker pull ${NINFER_DOCKER_IMAGE}`);
238
+ if (!pull.ok) {
239
+ return { ok: false, detail: `docker pull failed: ${pull.detail ?? 'unknown'}` };
240
+ }
241
+ const handle = this.host.spawn(plan.argv ?? [], plan.env ?? {});
242
+ trackChild(this.children, this.planKey(plan), handle);
243
+ return { ok: true, handle };
244
+ }
245
+ // NInfer renders process and docker plans; a compose plan is a no-op.
246
+ return { ok: true };
247
+ }
248
+ async healthCheck(plan, options) {
249
+ return pollHealth(this.host, plan, options);
250
+ }
251
+ async stop(plan) {
252
+ if (plan.kind === 'docker' && plan.containerName !== undefined) {
253
+ await this.host.run(`docker stop ${plan.containerName}`);
254
+ }
255
+ const handle = this.children.get(this.planKey(plan));
256
+ if (handle !== undefined) {
257
+ handle.kill();
258
+ this.children.delete(this.planKey(plan));
259
+ }
260
+ }
261
+ buildMode(config) {
262
+ return config.buildMode === 'docker' ? 'docker' : 'local';
263
+ }
264
+ port(config) {
265
+ return typeof config.port === 'number' ? config.port : 18020;
266
+ }
267
+ planKey(plan) {
268
+ return `${plan.engineId}:${plan.port}`;
269
+ }
270
+ async downloadModel(plan) {
271
+ const artifact = plan.modelId !== undefined ? NINFER_MODEL_ARTIFACTS[plan.modelId] : undefined;
272
+ if (artifact === undefined)
273
+ return { ok: true };
274
+ const target = plan.modelDirectory ?? '.';
275
+ const download = await this.host.run(`hf download ${artifact.repo} ${artifact.file} --local-dir ${target}`);
276
+ if (!download.ok) {
277
+ return { ok: false, detail: `model download failed: ${download.detail ?? 'unknown'}` };
278
+ }
279
+ return { ok: true };
280
+ }
281
+ /**
282
+ * resolveChatTemplate rewrites a --chat-template flag that carries an HF repo
283
+ * reference into the local file the engine reads. A value with a slash that
284
+ * is not an absolute path (the combo control's known choices) is a repo the
285
+ * deploy step downloads into the model directory; an already-local path is
286
+ * left as-is. The download is best-effort: a template that is already on
287
+ * disk is used without a re-fetch, and a missing hf CLI leaves the flag
288
+ * untouched so the engine reports the missing template itself.
289
+ */
290
+ async resolveChatTemplate(plan) {
291
+ const argv = plan.argv;
292
+ if (argv === undefined)
293
+ return { ok: true };
294
+ const index = argv.indexOf('--chat-template');
295
+ if (index === -1)
296
+ return { ok: true };
297
+ const value = argv[index + 1];
298
+ if (value === undefined)
299
+ return { ok: true };
300
+ // An absolute path (a leading /) is already local; a value with no slash
301
+ // is a bare file name the engine resolves against its own directory.
302
+ if (value.startsWith('/') || !value.includes('/'))
303
+ return { ok: true };
304
+ // An HF repo reference: download the template file into the model
305
+ // directory and point the flag at the local copy.
306
+ const target = plan.modelDirectory ?? '.';
307
+ const localPath = `${target.endsWith('/') ? '' : `${target}/`}chat_template.jinja`;
308
+ // A template already on disk skips the re-fetch; the hf CLI is optional,
309
+ // so a download failure is reported but does not abort the launch.
310
+ const probe = await this.host.run(`[ -f ${localPath} ]`);
311
+ if (!probe.ok) {
312
+ const download = await this.host.run(`hf download ${value} chat_template.jinja --local-dir ${target}`);
313
+ if (!download.ok) {
314
+ return {
315
+ ok: false,
316
+ detail: `chat template download failed: ${download.detail ?? 'unknown'}`,
317
+ };
318
+ }
319
+ }
320
+ argv[index + 1] = localPath;
321
+ return { ok: true };
322
+ }
323
+ }
324
+ //# sourceMappingURL=ninferAdapter.js.map