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
@@ -0,0 +1,671 @@
1
+ /**
2
+ * Infernix parameter surface. The declarative `ParameterSpec` data for the
3
+ * Infernix engine (https://github.com/wallawalla47/infernix), a C++/CUDA
4
+ * fork of NInfer tuned for Qwen models on a single RTX 50-series GPU,
5
+ * sourced from the project's `docs/cli.md` and `docs/serving.md`. The
6
+ * spelling is an `infernix-serve` CLI flag the `InfernixAdapter` passes onto
7
+ * the process.
8
+ *
9
+ * The surface carries the flags the NInfer surface shares (KV format,
10
+ * context, speculation, sampling) plus the Infernix-only fields: the
11
+ * `--ngram-volume` PLE table for Qwen3.8-Flash-Next, the `--draft-model`
12
+ * DFlash2 companion drafter, the n-gram copy proposal set, the offloaded-
13
+ * expert memory tiers, and the quality routes. Those fields live only on
14
+ * this surface, so the form renderer shows them only while the Infernix
15
+ * engine is selected, and the adapters of the other engines never spell
16
+ * them. Adding a flag here is a data edit, never a core rewire.
17
+ * @module dsh-plugin-local-infer/providers/infernix
18
+ */
19
+ import { join } from 'node:path';
20
+ import { CHAT_TEMPLATE_OPTIONS, LM_HEAD_DRAFT_GATE_MESSAGE } from "./ninfer.js";
21
+ /**
22
+ * The KV cache quantization formats Infernix serves, in the order the form
23
+ * dropdown lists them. The set differs from NInfer's: the 2-bit and 4-bit-
24
+ * K vector-quantized tiers (vq2, k4v2) replace the KVarN rk tiers, and both
25
+ * hold three to four times the context of INT8 in the same VRAM.
26
+ */
27
+ export const INFERNIX_KV_DTYPE_OPTIONS = [
28
+ { value: 'bf16', label: 'bf16', note: 'full precision; the widest KV pool' },
29
+ { value: 'int8', label: 'int8', note: 'half the bytes of bf16; the everyday choice' },
30
+ { value: 'fp8', label: 'fp8', note: 'half the bytes of bf16; the long-context tier' },
31
+ { value: 'nvfp4', label: 'nvfp4', note: 'NVFP4; the smallest KV tier, needs SM 89 or newer' },
32
+ { value: 'k8v4', label: 'k8v4', note: '8-bit keys, 4-bit values; between int8 and vq2' },
33
+ { value: 'vq2', label: 'vq2', note: '2-bit vector-quantized K and V; the longest context tier' },
34
+ { value: 'k4v2', label: 'k4v2', note: '4-bit K with 2-bit V; between k8v4 and vq2' },
35
+ ];
36
+ /**
37
+ * The speculative-decoding backends. `none` loads no speculative backend at
38
+ * all, which is the smallest resident profile.
39
+ */
40
+ export const INFERNIX_SPEC_OPTIONS = [
41
+ { value: 'none', label: 'none', note: 'no speculative backend; the smallest resident profile' },
42
+ { value: 'mtp', label: 'mtp', note: 'multi-token-prediction head; the 27B and 35B-A3B choice' },
43
+ { value: 'dflash', label: 'dflash', note: 'DFlash; the 35B-A3B companion-weight backend' },
44
+ {
45
+ value: 'dflash2',
46
+ label: 'dflash2',
47
+ note: 'DFlash2; the 27B and Flash-Next backend, the Flash-Next drafter rides --draft-model',
48
+ },
49
+ ];
50
+ /**
51
+ * The PLE n-gram table residency modes for Qwen3.8-Flash-Next. `auto` takes
52
+ * pinned RAM only when the RAM ledger keeps every expert in RAM beside it.
53
+ */
54
+ export const INFERNIX_NGRAM_RESIDENCY_OPTIONS = [
55
+ {
56
+ value: 'auto',
57
+ label: 'auto',
58
+ note: 'reads the table into RAM when the ledger allows, else from the volume',
59
+ },
60
+ { value: 'ram', label: 'ram', note: 'reads the whole table into pinned host RAM at startup' },
61
+ { value: 'volume', label: 'volume', note: 'reads rows on demand from the n-gram volume' },
62
+ ];
63
+ /**
64
+ * The reasoning-loop guard modes. The guard ends a reply, or closes the
65
+ * thinking, when the open thinking keeps repeating whole passages.
66
+ */
67
+ export const INFERNIX_REASONING_LOOP_OPTIONS = [
68
+ { value: 'off', label: 'off', note: 'no guard; the engine default' },
69
+ { value: 'stop', label: 'stop', note: 'ends the reply when the thinking repeats' },
70
+ { value: 'conclude', label: 'conclude', note: 'closes the thinking and lets the model answer' },
71
+ ];
72
+ /**
73
+ * The gate message the form shows while the speculation fields are greyed
74
+ * out. It names the fix (pick a backend) and the backends that unlock the
75
+ * fields, so a first-time user knows the next click, not just the constraint.
76
+ */
77
+ export const SPEC_GATE_MESSAGE = 'Pick a speculation backend first (mtp, dflash, or dflash2)';
78
+ /**
79
+ * The gate message the form shows while the dflash2-only fields are greyed
80
+ * out. It names the fix (pick dflash2) and the reason, so a first-time user
81
+ * knows the next click, not just the constraint.
82
+ */
83
+ export const DFLASH2_GATE_MESSAGE = 'Pick the dflash2 speculation backend first; the DFlash2 drafter loads with dflash2';
84
+ /**
85
+ * The Infernix model artifacts, keyed by the config model key. The 27B
86
+ * NVIDIA NVFP4 entry is the default the DEFAULT_MODELS list names; an id no
87
+ * entry names falls back to it, so a bare config always renders a plan. The
88
+ * Flash-Next entry is the recommended Dense8 conversion: its download pulls
89
+ * the whole repo, which carries the FP8 n-gram volume beside the shards, so
90
+ * the `--ngram-volume` flag points at a file the same download left on disk.
91
+ */
92
+ export const INFERNIX_MODEL_ARTIFACTS = {
93
+ 'qwen3-27b': {
94
+ file: 'qwen3_8_27b_nvfp4-nvidia.ninfer',
95
+ repo: 'Wallawalla47/Qwen3.8-27B-NVIDIA-NVFP4-NInferV3',
96
+ modelId: 'Qwen3.8-27B',
97
+ },
98
+ 'qwen3-27b-uncensored': {
99
+ file: 'qwen3_8_27b_nvfp4-uncensored.ninfer',
100
+ repo: 'Wallawalla47/Qwen3.8-27B-Uncensored-NVFP4-NInferV3',
101
+ modelId: 'Qwen3.8-27B',
102
+ },
103
+ 'qwen38-flash-next': {
104
+ file: 'Qwen3.8-Flash-Next-NVIDIA-NVFP4-Dense8-Infernix-00001-of-00003.infernix',
105
+ files: [
106
+ 'Qwen3.8-Flash-Next-NVIDIA-NVFP4-Dense8-Infernix-00001-of-00003.infernix',
107
+ 'Qwen3.8-Flash-Next-NVIDIA-NVFP4-Dense8-Infernix-00002-of-00003.infernix',
108
+ 'Qwen3.8-Flash-Next-NVIDIA-NVFP4-Dense8-Infernix-00003-of-00003.infernix',
109
+ ],
110
+ repo: 'Wallawalla47/Qwen3.8-Flash-Next-NVIDIA-NVFP4-Dense8-Infernix',
111
+ modelId: 'Qwen3.8-Flash-Next',
112
+ },
113
+ 'qwen38-flash-next-dflash2': {
114
+ file: 'Qwen3.8-Flash-Next-DFlash2-Infernix.infernix',
115
+ repo: 'Wallawalla47/Qwen3.8-Flash-Next-DFlash2-Infernix',
116
+ modelId: 'Qwen3.8-Flash-Next-DFlash2',
117
+ },
118
+ 'qwen36-35b': {
119
+ file: 'qwen3_6_35b_a3b.ninfer',
120
+ repo: 'neroued/Qwen3.6-35B-A3B-NInfer',
121
+ modelId: 'Qwen3.6-35B-A3B',
122
+ },
123
+ };
124
+ /**
125
+ * The common DFlash2 drafters the Infernix DFlash2 model dropdown offers, in
126
+ * dropdown order. The Flash-Next DFlash2 is Inco AI's drafter (the 776 MB
127
+ * `.infernix` artifact) that the Infernix README recommends beside any
128
+ * Flash-Next conversion. The list is the single source the dropdown, the
129
+ * `--draft-model` path resolver, and the Get download step read.
130
+ */
131
+ export const INFERNIX_DFLASH2_MODELS = [
132
+ {
133
+ file: 'Qwen3.8-Flash-Next-DFlash2-Infernix.infernix',
134
+ repo: 'Wallawalla47/Qwen3.8-Flash-Next-DFlash2-Infernix',
135
+ label: 'Qwen3.8-Flash-Next DFlash2',
136
+ },
137
+ ];
138
+ /**
139
+ * The dropdown value the DFlash2 model field's Custom entry carries. A
140
+ * selection of this value (or no selection) makes the effective `--draft-model`
141
+ * path fall through to the Draft model path field, so a drafter no list entry
142
+ * names still loads.
143
+ */
144
+ export const DFLASH2_CUSTOM_VALUE = 'custom';
145
+ /**
146
+ * dflash2PresetOptions renders the DFlash2 model dropdown options: one entry
147
+ * per common drafter (the label, the repo as the note) plus the Custom entry
148
+ * last. The option value is the drafter's artifact file, so the selection
149
+ * resolves back to the model data by file name.
150
+ */
151
+ export function dflash2PresetOptions() {
152
+ return [
153
+ ...INFERNIX_DFLASH2_MODELS.map((model) => ({
154
+ value: model.file,
155
+ label: model.label,
156
+ note: model.repo,
157
+ })),
158
+ {
159
+ value: DFLASH2_CUSTOM_VALUE,
160
+ label: 'Custom',
161
+ note: 'use the Draft model path field',
162
+ },
163
+ ];
164
+ }
165
+ /**
166
+ * The PLE n-gram volumes, keyed by the config model key. The Flash-Next entry
167
+ * is the FP8 volume (the recommended one), which ships beside the Dense8 model
168
+ * shards in the same repo, so a Get of the Flash-Next model pulls the table
169
+ * into the model directory.
170
+ */
171
+ export const INFERNIX_NGRAM_VOLUMES = {
172
+ 'qwen38-flash-next': {
173
+ file: 'Qwen3.8-Flash-Next-NVIDIA-NVFP4-Infernix.ngram',
174
+ repo: 'Wallawalla47/Qwen3.8-Flash-Next-NVIDIA-NVFP4-Dense8-Infernix',
175
+ },
176
+ };
177
+ /**
178
+ * infernixDraftModelPath resolves the effective `--draft-model` path a config
179
+ * carries: the selected DFlash2 preset's artifact (the model directory plus the
180
+ * drafter file, the layout the hf CLI download leaves) when a preset is
181
+ * selected, else the Custom Draft model path field. Returns null when neither
182
+ * names a path, which is the misconfiguration the dflash2 guideline names.
183
+ */
184
+ export function infernixDraftModelPath(config) {
185
+ const preset = typeof config.draftModelPreset === 'string' ? config.draftModelPreset : '';
186
+ if (preset !== '' && preset !== DFLASH2_CUSTOM_VALUE) {
187
+ const model = INFERNIX_DFLASH2_MODELS.find((entry) => entry.file === preset);
188
+ const modelDir = typeof config.modelDirectory === 'string' && config.modelDirectory !== ''
189
+ ? config.modelDirectory
190
+ : '';
191
+ if (model !== undefined && modelDir !== '') {
192
+ return join(modelDir, model.file, model.file);
193
+ }
194
+ }
195
+ const custom = typeof config.draftModel === 'string' ? config.draftModel : '';
196
+ return custom !== '' ? custom : null;
197
+ }
198
+ /**
199
+ * infernixGetDownloads renders the hf CLI download commands a Get run pulls
200
+ * for a config: the selected DFlash2 drafter (into the model directory, the
201
+ * layout the `--draft-model` path expects) when a preset is selected, and the
202
+ * model key's PLE n-gram volume (into the model directory) when the key names
203
+ * one. The commands are the single source the Get route streams, so a Get
204
+ * leaves the model directory ready to launch.
205
+ */
206
+ export function infernixGetDownloads(config, modelDir) {
207
+ const commands = [];
208
+ const preset = typeof config.draftModelPreset === 'string' ? config.draftModelPreset : '';
209
+ if (preset !== '' && preset !== DFLASH2_CUSTOM_VALUE) {
210
+ const model = INFERNIX_DFLASH2_MODELS.find((entry) => entry.file === preset);
211
+ if (model !== undefined) {
212
+ commands.push(`hf download ${model.repo} ${model.file} --local-dir ${join(modelDir, model.file)}`);
213
+ }
214
+ }
215
+ const volume = INFERNIX_NGRAM_VOLUMES[infernixModelKey(config)];
216
+ if (volume !== undefined) {
217
+ commands.push(`hf download ${volume.repo} ${volume.file} --local-dir ${modelDir}`);
218
+ }
219
+ return commands;
220
+ }
221
+ /**
222
+ * The Infernix `ParameterSpec` surface, in form order. The `group` field is
223
+ * the form section; specs without a group render under `General`. The
224
+ * Infernix-only fields (ngramVolume, draftModel, the n-gram proposal set,
225
+ * the expert memory tiers, the quality routes) appear only on this surface,
226
+ * so the form shows them only while the Infernix engine is selected.
227
+ */
228
+ export const INFERNIX_PARAMETERS = [
229
+ {
230
+ key: 'port',
231
+ label: 'Port',
232
+ description: 'The loopback port the engine serves its OpenAI endpoint and health check on. The registrar publishes it as the model base URL.',
233
+ // The port is a typed number, not a range strip: a 1..65535 drag slider is
234
+ // noise, so `slider: false` renders the plain number entry only.
235
+ form: { kind: 'number', min: 1, max: 65535, step: 1, slider: false },
236
+ // 8080 is the port the infernix-serve CLI binds by default, so the form
237
+ // seeds the value the adapter's own fallback uses when a config names no
238
+ // port.
239
+ defaultValue: 8080,
240
+ group: 'Engine',
241
+ // The port contributes no flag of its own: renderLaunch spells the local
242
+ // `--port` from this value, and the health URL derives from it too.
243
+ },
244
+ {
245
+ key: 'kvDtype',
246
+ label: 'KV cache format',
247
+ description: 'Quantizes the KV cache. Smaller formats fit a longer context in the same VRAM at a small quality cost.',
248
+ form: { kind: 'enum', options: [...INFERNIX_KV_DTYPE_OPTIONS] },
249
+ defaultValue: 'int8',
250
+ group: 'Memory',
251
+ hardwareConstraint: (value, hw) => {
252
+ // nvfp4 KV runs on SM 89 (Ada) and newer; a 30-series card (sm86) cannot
253
+ // serve it, so the warning names the fix before the engine starts.
254
+ if (value === 'nvfp4' && hw.gpu && hw.gpu.sm < 89) {
255
+ return {
256
+ ok: false,
257
+ severity: 'error',
258
+ message: `nvfp4 KV needs SM 89 or newer; this card reports SM ${hw.gpu.sm}. Pick k8v4 or vq2.`,
259
+ };
260
+ }
261
+ return { ok: true };
262
+ },
263
+ toArgv: (value) => ['--kv-dtype', String(value)],
264
+ },
265
+ {
266
+ key: 'maxContext',
267
+ label: 'Max context',
268
+ description: 'The longest conversation the engine accepts. Raising it reserves more VRAM for the KV cache.',
269
+ form: { kind: 'number', min: 256, max: 262144, step: 256, unit: 'tokens' },
270
+ defaultValue: 32768,
271
+ group: 'Memory',
272
+ toArgv: (value) => ['--max-context', String(value)],
273
+ },
274
+ {
275
+ key: 'kvCapacity',
276
+ label: 'KV capacity',
277
+ description: 'The shared KV pool across concurrent requests. Leave unset to let the engine maximize the pool from the remaining GPU memory.',
278
+ form: {
279
+ kind: 'number',
280
+ min: 256,
281
+ step: 256,
282
+ unit: 'tokens',
283
+ // The range strip caps at the live max-context value (the pool cannot
284
+ // outgrow the context it serves), re-priced on every form render so a
285
+ // max-context edit moves the cap without touching the current value.
286
+ // The number entry stays uncapped, so a typed value is never hard-limited.
287
+ sliderMax: (_hw, config) => (typeof config.maxContext === 'number' ? config.maxContext : 0),
288
+ },
289
+ group: 'Memory',
290
+ toArgv: (value) => ['--kv-capacity', String(value)],
291
+ },
292
+ {
293
+ key: 'hostContextMib',
294
+ label: 'Host context MiB',
295
+ description: 'Pinned host RAM for the hybrid prefix cache: the slab pool the KV blocks and state snapshots share. Retains inactive conversations when device memory is under pressure.',
296
+ form: {
297
+ kind: 'number',
298
+ min: 0,
299
+ step: 1024,
300
+ unit: 'MiB',
301
+ // The range strip caps at half the host RAM (the pool cannot outgrow the
302
+ // machine it pins), while the number entry stays uncapped so a typed
303
+ // value is never hard-limited by the slider.
304
+ sliderMax: (hw) => Math.max(0, Math.floor((hw.hostRamGiB ?? 0) * 0.5 * 1024)),
305
+ },
306
+ group: 'Memory',
307
+ // 16384 MiB is the engine default the form seeds; a value of 0 keeps the
308
+ // cache on the device only, which the engine still accepts.
309
+ defaultValue: 16384,
310
+ toArgv: (value) => ['--host-context-mib', String(value)],
311
+ },
312
+ {
313
+ key: 'mediaLiveMib',
314
+ label: 'Media live MiB',
315
+ description: 'Pinned host RAM reserved for live media (vision) decode. Retains the in-flight image and video buffers outside the GPU workspace.',
316
+ form: { kind: 'number', min: 0, step: 1024, unit: 'MiB' },
317
+ group: 'Memory',
318
+ toArgv: (value) => ['--media-live-mib', String(value)],
319
+ },
320
+ {
321
+ key: 'ramHeadroomMib',
322
+ label: 'RAM headroom MiB',
323
+ description: 'Host RAM the pinned weights and caches leave free for the rest of the machine. The startup refuses a pin that would take it.',
324
+ form: { kind: 'number', min: 0, step: 256, unit: 'MiB' },
325
+ defaultValue: 8192,
326
+ group: 'Memory',
327
+ toArgv: (value) => ['--ram-headroom-mib', String(value)],
328
+ },
329
+ {
330
+ key: 'vramHeadroomMib',
331
+ label: 'VRAM headroom MiB',
332
+ description: 'VRAM the startup sizing leaves free for the display and other programs. Leave unset for the engine automatic headroom.',
333
+ form: { kind: 'number', min: 0, step: 256, unit: 'MiB' },
334
+ group: 'Memory',
335
+ // The engine defaults to auto, so a bare value spells no flag.
336
+ toArgv: (value) => ['--vram-headroom-mib', String(value)],
337
+ },
338
+ {
339
+ key: 'expertRamMib',
340
+ label: 'Expert RAM MiB',
341
+ description: 'Host RAM for the routed experts of offloaded models. Less than the full set keeps that many in RAM and reads the rest from the artifact on an SSD tier.',
342
+ form: { kind: 'number', min: 0, step: 1024, unit: 'MiB' },
343
+ group: 'Memory',
344
+ // The engine defaults to auto (what the RAM ledger leaves), so a bare
345
+ // value spells no flag.
346
+ toArgv: (value) => ['--expert-ram-mib', String(value)],
347
+ },
348
+ {
349
+ key: 'expertPinMib',
350
+ label: 'Expert pin MiB',
351
+ description: 'How much of the expert RAM the GPU page-locks. The rest stays unpinned and the CPU serves it, which the page-locking limit on the OS bounds.',
352
+ form: { kind: 'number', min: 0, step: 1024, unit: 'MiB' },
353
+ group: 'Memory',
354
+ // The engine defaults to auto (what the OS lets the GPU page-lock), so a
355
+ // bare value spells no flag.
356
+ toArgv: (value) => ['--expert-pin-mib', String(value)],
357
+ },
358
+ {
359
+ key: 'ngramResidency',
360
+ label: 'N-gram residency',
361
+ description: 'Chooses where the PLE n-gram table of Qwen3.8-Flash-Next lives: pinned RAM, the volume, or the engine automatic rule.',
362
+ form: { kind: 'enum', options: [...INFERNIX_NGRAM_RESIDENCY_OPTIONS] },
363
+ defaultValue: 'auto',
364
+ group: 'Memory',
365
+ // auto is the engine default, so the bare value spells no flag.
366
+ toArgv: (value) => (value === 'auto' ? [] : ['--ngram-residency', String(value)]),
367
+ },
368
+ {
369
+ key: 'maxConcurrency',
370
+ label: 'Max concurrency',
371
+ description: 'How many requests decode at once. More lanes share the KV pool and cut per-request throughput.',
372
+ form: { kind: 'number', min: 1, max: 8, step: 1, unit: 'lanes' },
373
+ defaultValue: 1,
374
+ group: 'Concurrency',
375
+ toArgv: (value) => ['--max-concurrency', String(value)],
376
+ },
377
+ {
378
+ key: 'maxPendingRequests',
379
+ label: 'Max pending requests',
380
+ description: 'How many extra requests may wait for an admission slot. Raises the queue for bursty agent traffic at the cost of longer waits.',
381
+ form: { kind: 'number', min: 0, step: 1, unit: 'requests' },
382
+ group: 'Concurrency',
383
+ toArgv: (value) => ['--max-pending-requests', String(value)],
384
+ },
385
+ {
386
+ key: 'spec',
387
+ label: 'Speculation backend',
388
+ description: 'The speculative-decoding backend. Speeds decode when drafts are accepted; adds draft weights and CUDA graphs to resident memory.',
389
+ form: { kind: 'enum', options: [...INFERNIX_SPEC_OPTIONS] },
390
+ defaultValue: 'none',
391
+ group: 'Speculation',
392
+ toArgv: (value) => (value === 'none' ? [] : ['--spec', String(value)]),
393
+ },
394
+ {
395
+ key: 'draftTokens',
396
+ label: 'Draft tokens',
397
+ description: 'How many draft tokens a speculative round verifies. More drafts speed decode but cost more per round; auto verifies the count the drafter probabilities make fastest.',
398
+ form: { kind: 'number', min: 1, max: 15, step: 1, unit: 'tokens' },
399
+ defaultValue: 3,
400
+ group: 'Speculation',
401
+ requires: (config) => config.spec && config.spec !== 'none' ? null : SPEC_GATE_MESSAGE,
402
+ toArgv: (value) => ['--draft-tokens', String(value)],
403
+ },
404
+ {
405
+ key: 'lmHeadDraft',
406
+ label: 'LM-head draft',
407
+ 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.',
408
+ form: { kind: 'boolean' },
409
+ defaultValue: false,
410
+ group: 'Speculation',
411
+ requires: (config) => config.spec === 'mtp' ? null : LM_HEAD_DRAFT_GATE_MESSAGE,
412
+ toArgv: (value) => (value === true ? ['--lm-head-draft'] : []),
413
+ },
414
+ {
415
+ key: 'draftModelPreset',
416
+ label: 'DFlash2 model',
417
+ description: 'The DFlash2 drafter to download into the model directory. Pick a common model, or Custom to use the Draft model path. A Get run pulls the selected drafter through the hf CLI.',
418
+ form: { kind: 'enum', options: [...dflash2PresetOptions()] },
419
+ // The first common drafter is the selection a fresh config carries, so a
420
+ // switch to dflash2 always has a drafter path to load.
421
+ defaultValue: INFERNIX_DFLASH2_MODELS[0]?.file,
422
+ group: 'Speculation',
423
+ // The drafter only loads with the dflash2 backend, so the field stays
424
+ // greyed out until the user picks it.
425
+ requires: (config) => config.spec === 'dflash2' ? null : DFLASH2_GATE_MESSAGE,
426
+ // No toArgv: the adapter resolves the effective --draft-model path from
427
+ // this selection (or the Custom Draft model field) and spells the flag.
428
+ },
429
+ {
430
+ key: 'draftModel',
431
+ label: 'Draft model',
432
+ description: 'The DFlash2 drafter companion artifact for Qwen3.8-Flash-Next. Downloads separately from the model, and the engine loads it beside the main artifact. Used while the DFlash2 model dropdown is Custom.',
433
+ form: { kind: 'path' },
434
+ group: 'Speculation',
435
+ // The drafter only loads with the dflash2 backend, and the Custom path
436
+ // field is only live while the DFlash2 model dropdown is Custom (or
437
+ // cleared), so a selected preset never leaves a stale custom path behind.
438
+ requires: (config) => {
439
+ if (config.spec !== 'dflash2')
440
+ return DFLASH2_GATE_MESSAGE;
441
+ const preset = typeof config.draftModelPreset === 'string' ? config.draftModelPreset : '';
442
+ if (preset !== '' && preset !== DFLASH2_CUSTOM_VALUE) {
443
+ return 'A DFlash2 model is selected; switch to Custom to use a custom Draft model path';
444
+ }
445
+ return null;
446
+ },
447
+ // No toArgv: the adapter resolves the effective --draft-model path and
448
+ // spells the flag, so the preset and the custom path share one source.
449
+ },
450
+ {
451
+ key: 'ngramDraftTokens',
452
+ label: 'N-gram draft tokens',
453
+ description: 'Verified n-gram copy proposals per round beside the spec drafter. Speeds decode on repetitive text; 0 disables the proposals.',
454
+ form: { kind: 'number', min: 0, max: 63, step: 1, unit: 'proposals' },
455
+ group: 'Speculation',
456
+ toArgv: (value) => (value === 0 ? [] : ['--ngram-draft-tokens', String(value)]),
457
+ },
458
+ {
459
+ key: 'ngramMinMatch',
460
+ label: 'N-gram min match',
461
+ description: 'The minimum matched tokens an n-gram copy proposal must reach. Raises it to verify longer copies at a lower proposal rate.',
462
+ form: { kind: 'number', min: 4, max: 64, step: 1, unit: 'tokens' },
463
+ defaultValue: 12,
464
+ group: 'Speculation',
465
+ // The minimum match only prices a proposal the user enabled, so the
466
+ // field stays greyed out while n-gram drafting is off.
467
+ requires: (config) => typeof config.ngramDraftTokens === 'number' && config.ngramDraftTokens > 0
468
+ ? null
469
+ : 'Enable n-gram copy proposals first (n-gram draft tokens above 0)',
470
+ toArgv: (value) => ['--ngram-min-match', String(value)],
471
+ },
472
+ {
473
+ key: 'ngramVolume',
474
+ label: 'N-gram volume',
475
+ description: 'The PLE n-gram table file for Qwen3.8-Flash-Next, kept apart from the model shards and read from an NVMe drive. The recommended FP8 table downloads with the Dense8 model repo.',
476
+ form: { kind: 'path' },
477
+ group: 'Artifacts',
478
+ toArgv: (value) => (value ? ['--ngram-volume', String(value)] : []),
479
+ },
480
+ {
481
+ key: 'expertState',
482
+ label: 'Expert state',
483
+ description: 'The saved expert-cache state file. The engine seeds the VRAM expert cache from it at startup and rewrites it on stop, so warm experts survive a restart.',
484
+ form: { kind: 'path' },
485
+ group: 'Artifacts',
486
+ // The engine defaults to the artifact-named state file, so a bare value
487
+ // spells no flag.
488
+ toArgv: (value) => (value ? ['--expert-state', String(value)] : []),
489
+ },
490
+ {
491
+ key: 'prefixCacheFile',
492
+ label: 'Prefix cache file',
493
+ description: 'The file the hybrid prefix cache saves to on stop and reloads on start, so cached KV blocks survive a restart.',
494
+ form: { kind: 'path' },
495
+ group: 'Artifacts',
496
+ toArgv: (value) => (value ? ['--prefix-cache-file', String(value)] : []),
497
+ },
498
+ {
499
+ key: 'prefillChunk',
500
+ label: 'Prefill chunk',
501
+ description: 'The prompt-chunk size for prefill, in multiples of 128. The engine rounds it to whole attention waves. The form seeds 2048; a lower value trades prefill speed for smaller per-chunk memory.',
502
+ form: {
503
+ kind: 'number',
504
+ min: 128,
505
+ step: 128,
506
+ unit: 'tokens',
507
+ // The range strip caps at the live max-context value (a chunk cannot
508
+ // outgrow the context it serves), re-priced on every form render. The
509
+ // number entry stays uncapped, so a typed value is never hard-limited.
510
+ sliderMax: (_hw, config) => (typeof config.maxContext === 'number' ? config.maxContext : 0),
511
+ },
512
+ // 2048 tokens is the engine default the form seeds.
513
+ defaultValue: 2048,
514
+ group: 'Engine',
515
+ toArgv: (value) => ['--prefill-chunk', String(value)],
516
+ },
517
+ {
518
+ key: 'vision',
519
+ label: 'Vision input',
520
+ description: 'Accepts image and video input. Adds the vision tower and its encode workspace to resident memory.',
521
+ form: { kind: 'boolean' },
522
+ defaultValue: false,
523
+ group: 'Engine',
524
+ toArgv: (value) => (value === true ? ['--vision'] : []),
525
+ },
526
+ {
527
+ key: 'noCudaGraph',
528
+ label: 'Disable CUDA graphs',
529
+ description: 'Turns off CUDA Graph decode. Costs decode speed, which helps a debug build that cannot instantiate graphs.',
530
+ form: { kind: 'boolean' },
531
+ defaultValue: false,
532
+ group: 'Engine',
533
+ toArgv: (value) => (value === true ? ['--no-cuda-graph'] : []),
534
+ },
535
+ {
536
+ key: 'reproducible',
537
+ label: 'Reproducible route',
538
+ description: 'Runs the bit-exact reproducible route: request-invariant output from release to release, at the cost of the fast paths.',
539
+ form: { kind: 'boolean' },
540
+ defaultValue: false,
541
+ group: 'Engine',
542
+ toArgv: (value) => (value === true ? ['--reproducible'] : []),
543
+ },
544
+ {
545
+ key: 'lossless',
546
+ label: 'Lossless route',
547
+ description: 'Computes the model forward pass exactly, rounded once to its dtype. The output depends on the request and artifact alone, at the slowest speed.',
548
+ form: { kind: 'boolean' },
549
+ defaultValue: false,
550
+ group: 'Engine',
551
+ toArgv: (value) => (value === true ? ['--lossless'] : []),
552
+ },
553
+ {
554
+ key: 'ropeYarnFactor',
555
+ label: 'YaRN factor',
556
+ description: 'Extends the context ceiling by the YaRN scaling factor, from the native limit up to 4x. Long-context extrapolation, not a quality guarantee.',
557
+ form: { kind: 'number', min: 1, max: 4, step: 0.05 },
558
+ defaultValue: 1,
559
+ group: 'Engine',
560
+ // 1 leaves the native RoPE unchanged, so the bare value spells no flag.
561
+ toArgv: (value) => (value === 1 ? [] : ['--rope-yarn-factor', String(value)]),
562
+ },
563
+ {
564
+ key: 'chatTemplate',
565
+ label: 'Chat template',
566
+ description: 'Overrides the artifact built-in Jinja chat template. Pick a preset from the dropdown, or choose Custom and type a local path or HF repo reference. Changes take effect after a restart.',
567
+ form: { kind: 'combo', options: [...CHAT_TEMPLATE_OPTIONS] },
568
+ group: 'Engine',
569
+ toArgv: (value) => (value ? ['--chat-template', String(value)] : []),
570
+ },
571
+ {
572
+ key: 'temperature',
573
+ label: 'Temperature',
574
+ description: 'Sampling randomness. 0 decodes deterministically; higher values draw from a wider token distribution.',
575
+ form: { kind: 'number', min: 0, max: 2, step: 0.05 },
576
+ defaultValue: 0.9,
577
+ group: 'Sampling',
578
+ toArgv: (value) => ['--temperature', String(value)],
579
+ },
580
+ {
581
+ key: 'minP',
582
+ label: 'Min-p',
583
+ description: 'Drops tokens whose probability falls under this fraction of the top token, trimming the sampling tail.',
584
+ form: { kind: 'number', min: 0, max: 1, step: 0.01 },
585
+ defaultValue: 0.05,
586
+ group: 'Sampling',
587
+ toArgv: (value) => ['--min-p', String(value)],
588
+ },
589
+ {
590
+ key: 'defaultMaxTokens',
591
+ label: 'Default max tokens',
592
+ description: 'Caps the response length when a request names no max. Raises it for long answers; the engine still honours a per-request cap.',
593
+ form: {
594
+ kind: 'number',
595
+ // A response under 1024 tokens is too short to be useful, so the floor
596
+ // is 1024, not 1.
597
+ min: 1024,
598
+ step: 1,
599
+ unit: 'tokens',
600
+ // The range strip caps at 12.5% of the live max-context value (the
601
+ // default response cannot outgrow the quarter of context it is priced
602
+ // against), re-priced on every form render. The number entry stays
603
+ // uncapped, so a typed value is never hard-limited by the slider.
604
+ sliderMax: (_hw, config) => {
605
+ const ctx = typeof config.maxContext === 'number' ? config.maxContext : 0;
606
+ return Math.max(0, Math.floor(ctx * 0.125));
607
+ },
608
+ },
609
+ group: 'Sampling',
610
+ toArgv: (value) => ['--default-max-tokens', String(value)],
611
+ },
612
+ {
613
+ key: 'defaultThinkingBudget',
614
+ label: 'Default thinking budget',
615
+ description: 'Reserves reasoning tokens for a thinking model when a request names no budget. Higher budgets spend more time reasoning per turn.',
616
+ form: { kind: 'number', min: 0, step: 1, unit: 'tokens' },
617
+ group: 'Sampling',
618
+ toArgv: (value) => ['--default-thinking-budget', String(value)],
619
+ },
620
+ {
621
+ key: 'preserveThinking',
622
+ label: 'Preserve thinking',
623
+ description: 'Keeps the reasoning trace in the context across turns instead of dropping it, so a multi-turn conversation builds on its own prior reasoning.',
624
+ form: { kind: 'boolean' },
625
+ defaultValue: false,
626
+ group: 'Sampling',
627
+ toArgv: (value) => (value === true ? ['--preserve-thinking'] : []),
628
+ },
629
+ {
630
+ key: 'tolerantToolCalls',
631
+ label: 'Tolerant tool calls',
632
+ description: 'Recovers tool calls the model truncates or malforms: a suffix after a complete call, a cut closing tag, or a call stranded in unclosed thinking.',
633
+ form: { kind: 'boolean' },
634
+ defaultValue: false,
635
+ group: 'Sampling',
636
+ toArgv: (value) => (value === true ? ['--tolerant-tool-calls'] : []),
637
+ },
638
+ {
639
+ key: 'reasoningLoop',
640
+ label: 'Reasoning loop guard',
641
+ description: 'Ends the reply, or closes the thinking, when the open thinking keeps repeating whole passages. Guards long thinking runs that stall.',
642
+ form: { kind: 'enum', options: [...INFERNIX_REASONING_LOOP_OPTIONS] },
643
+ defaultValue: 'off',
644
+ group: 'Sampling',
645
+ // off is the engine default, so the bare value spells no flag.
646
+ toArgv: (value) => (value === 'off' ? [] : ['--reasoning-loop', String(value)]),
647
+ },
648
+ ];
649
+ /**
650
+ * The Infernix model key a config carries: the string model value, falling
651
+ * back to the 27B artifact key so a bare config always renders a plan. A
652
+ * value that names a local artifact file (a directory-scanned model, a
653
+ * `.ninfer` or `.infernix` name) passes through unchanged.
654
+ */
655
+ export function infernixModelKey(config) {
656
+ return typeof config.model === 'string' && config.model !== '' ? config.model : 'qwen3-27b';
657
+ }
658
+ /**
659
+ * infernixArtifactFor resolves the artifact a model key deploys. A key that
660
+ * names a local artifact file passes through the artifact map's 27B entry
661
+ * for the model id, and an id no entry names falls back to the 27B entry, so
662
+ * a bare config always renders a plan.
663
+ */
664
+ export function infernixArtifactFor(modelKey) {
665
+ if (modelKey.endsWith('.ninfer') || modelKey.endsWith('.infernix')) {
666
+ return INFERNIX_MODEL_ARTIFACTS['qwen3-27b'];
667
+ }
668
+ const artifact = INFERNIX_MODEL_ARTIFACTS[modelKey];
669
+ return artifact ?? INFERNIX_MODEL_ARTIFACTS['qwen3-27b'];
670
+ }
671
+ //# sourceMappingURL=infernix.js.map