dsh-plugin-local-infer 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +8 -1
- package/cordis.patch.yml +6 -6
- package/docs/screenshot-20261014.png +0 -0
- package/docs/zh/README.md +100 -0
- package/dsh/client.js +644 -143
- package/dsh/host.js +1 -1
- package/package.json +14 -11
- package/packages/core/lib/adapter.d.ts +75 -4
- package/packages/core/lib/debug.d.ts +1 -1
- package/packages/core/lib/debug.js +1 -1
- package/packages/core/lib/index.d.ts +3 -2
- package/packages/core/lib/index.js +2 -1
- package/packages/core/lib/launch-command.d.ts +13 -0
- package/packages/core/lib/launch-command.js +64 -0
- package/packages/core/lib/models.d.ts +2 -1
- package/packages/core/lib/models.js +10 -1
- package/packages/core/lib/status.d.ts +9 -1
- package/packages/core/lib/status.js +6 -1
- package/packages/core/package.json +1 -1
- package/packages/providers/lib/acquire.d.ts +125 -0
- package/packages/providers/lib/acquire.js +325 -0
- package/packages/providers/lib/adapters.d.ts +43 -6
- package/packages/providers/lib/adapters.js +386 -58
- package/packages/providers/lib/debug.d.ts +1 -1
- package/packages/providers/lib/debug.js +1 -1
- package/packages/providers/lib/deps.d.ts +6 -4
- package/packages/providers/lib/deps.js +17 -3
- package/packages/providers/lib/engines.d.ts +6 -5
- package/packages/providers/lib/engines.js +12 -4
- package/packages/providers/lib/guidelines.d.ts +2 -2
- package/packages/providers/lib/guidelines.js +17 -1
- package/packages/providers/lib/hyperqwen.d.ts +2 -2
- package/packages/providers/lib/hyperqwen.js +1 -1
- package/packages/providers/lib/index.d.ts +10 -4
- package/packages/providers/lib/index.js +9 -5
- package/packages/providers/lib/infernix.d.ts +170 -0
- package/packages/providers/lib/infernix.js +671 -0
- package/packages/providers/lib/llamacpp.d.ts +2 -2
- package/packages/providers/lib/llamacpp.js +1 -1
- package/packages/providers/lib/model-defaults.d.ts +7 -5
- package/packages/providers/lib/model-defaults.js +28 -4
- package/packages/providers/lib/ninfer.d.ts +9 -2
- package/packages/providers/lib/ninfer.js +10 -3
- package/packages/providers/lib/platform.d.ts +31 -0
- package/packages/providers/lib/platform.js +58 -0
- package/packages/providers/lib/presets.d.ts +2 -2
- package/packages/providers/lib/presets.js +1 -1
- package/packages/providers/lib/strata.d.ts +2 -2
- package/packages/providers/lib/strata.js +1 -1
- package/packages/providers/lib/vram.d.ts +2 -2
- package/packages/providers/lib/vram.js +1 -1
- package/packages/providers/package.json +3 -3
- package/packages/service/lib/index.d.ts +63 -20
- package/packages/service/lib/index.js +279 -43
- package/packages/service/lib/types.d.ts +44 -2
- package/packages/service/package.json +2 -2
- package/packages/tool/lib/index.d.ts +1 -1
- package/packages/tool/lib/index.js +1 -1
- package/packages/tool/package.json +3 -3
- package/packages/ui/lib/deps.d.ts +2 -2
- package/packages/ui/lib/deps.js +1 -1
- package/packages/ui/lib/form.d.ts +2 -2
- package/packages/ui/lib/form.js +1 -1
- package/packages/ui/lib/index.d.ts +1 -1
- package/packages/ui/lib/index.js +1 -1
- package/packages/ui/lib/meter.d.ts +2 -2
- package/packages/ui/lib/meter.js +1 -1
- package/packages/ui/lib/route.d.ts +84 -13
- package/packages/ui/lib/route.js +496 -56
- package/packages/ui/lib/warnings.d.ts +2 -2
- package/packages/ui/lib/warnings.js +1 -1
- package/packages/ui/package.json +5 -5
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Phase 6 over the dsh settings seam, and `resolveDefaults` and
|
|
16
16
|
* `fetchPreset` by Phase 7 over the core defaults resolver and the
|
|
17
17
|
* mod-search bridge.
|
|
18
|
-
* @module
|
|
18
|
+
* @module dsh-plugin-local-infer/service
|
|
19
19
|
*/
|
|
20
20
|
import { exec, execFile, spawn } from 'node:child_process';
|
|
21
21
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -46,6 +46,14 @@ export class LocalInferError extends Error {
|
|
|
46
46
|
* "not available" instead.
|
|
47
47
|
*/
|
|
48
48
|
const PROBE_TIMEOUT_MS = 5000;
|
|
49
|
+
/**
|
|
50
|
+
* The adopted-process poll interval. A re-adopted engine (a surviving process
|
|
51
|
+
* with no ChildProcess after a DSH restart) is watched for death through a
|
|
52
|
+
* liveness probe on this cadence: fast enough that a pkill settles the
|
|
53
|
+
* provider's state within seconds, slow enough that a signal-0 send per
|
|
54
|
+
* watched engine is free.
|
|
55
|
+
*/
|
|
56
|
+
const PROCESS_WATCH_MS = 5000;
|
|
49
57
|
/**
|
|
50
58
|
* `ctx.localInfer`. Holds the engine, preset, guideline, and VramModel
|
|
51
59
|
* reservations; the registries are read per call so a provider that unloads
|
|
@@ -72,6 +80,25 @@ export class LocalInfer extends Service {
|
|
|
72
80
|
// restart, so a fresh start tracks only the launches it made itself. See
|
|
73
81
|
// PLAN.md for the PID-cache note: a full PID cache is a future refinement.
|
|
74
82
|
launchedModels = new Map();
|
|
83
|
+
// The live ports tracked per launched engine (launches and adopted re-links
|
|
84
|
+
// both register). The death handler clears the engine's plugin-launched flag
|
|
85
|
+
// only when its last live port dies, so one provider's engine stopping does
|
|
86
|
+
// not un-flag a second provider's still-running engine on another port.
|
|
87
|
+
launchedPorts = new Map();
|
|
88
|
+
// The poll watchers for processes adopted from stored launch records: a
|
|
89
|
+
// re-adopted engine has no ChildProcess to listen on after a DSH restart, so
|
|
90
|
+
// a liveness poll stands in for the exit event. Keyed by port; the timers
|
|
91
|
+
// are unref'd so a watched engine never keeps the host or a test alive.
|
|
92
|
+
engineWatchers = new Map();
|
|
93
|
+
// The pids whose death this process already watches through a live child
|
|
94
|
+
// handle. A summary re-link that adopts this DSH's own launch record must
|
|
95
|
+
// not stack a poll watcher on top of the handle's close event.
|
|
96
|
+
handledPids = new Set();
|
|
97
|
+
// The route-registered hook for engine process deaths (a crash, a pkill, or
|
|
98
|
+
// a poll-detected death of an adopted process). The observer persists the
|
|
99
|
+
// provider entry's cleared state; a headless profile registers none and the
|
|
100
|
+
// service still settles its own live state.
|
|
101
|
+
deathObserver;
|
|
75
102
|
constructor(ctx) {
|
|
76
103
|
super(ctx, 'localInfer');
|
|
77
104
|
}
|
|
@@ -267,47 +294,65 @@ export class LocalInfer extends Service {
|
|
|
267
294
|
}
|
|
268
295
|
/**
|
|
269
296
|
* providerStatuses resolves the running-state the card paints each saved
|
|
270
|
-
* provider's status dot from. It
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* busy
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
297
|
+
* provider's status dot from. It prices each provider's own launch signature
|
|
298
|
+
* through the registered adapter (the build mode's binary, the port the
|
|
299
|
+
* provider's config binds, the env keys), so an engine the plugin launched
|
|
300
|
+
* on a non-default port matches its config instead of reading forever as a
|
|
301
|
+
* process no signature covers, and ties the matched port to the model the
|
|
302
|
+
* plugin launched it with (the launched-models map). The GPU-usage reading
|
|
303
|
+
* feeds the busy-card rule. The answer is the core resolver's: a busy card
|
|
304
|
+
* or no engine running reads as unsure for every provider, an engine the
|
|
305
|
+
* detector saw but could not tie to a model reads as unsure for its configs
|
|
306
|
+
* and stopped for the rest, and a confirmed engine-plus-model reads as
|
|
307
|
+
* running. The options inject the probe results, so a test prices a busy
|
|
308
|
+
* card or a foreign engine without a live nvidia-smi or ps.
|
|
281
309
|
*/
|
|
282
310
|
async providerStatuses(providers, options) {
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
311
|
+
const signatures = providers.map((provider) => {
|
|
312
|
+
const adapter = this.engineRegistry.get(provider.engine);
|
|
313
|
+
return adapter?.launchSignature?.(provider.config ?? {});
|
|
314
|
+
});
|
|
315
|
+
const markers = [];
|
|
316
|
+
for (const signature of signatures) {
|
|
317
|
+
if (signature !== undefined && !markers.includes(signature.binary)) {
|
|
318
|
+
markers.push(signature.binary);
|
|
288
319
|
}
|
|
289
320
|
}
|
|
290
|
-
const markers = refs.map((ref) => ref.signature.binary);
|
|
291
321
|
const processes = options?.processes !== undefined ? options.processes : await probeProcesses(markers);
|
|
292
322
|
const running = [];
|
|
293
|
-
|
|
294
|
-
|
|
323
|
+
const pricedSignatures = new Set();
|
|
324
|
+
for (const [index, provider] of providers.entries()) {
|
|
325
|
+
const signature = signatures[index];
|
|
326
|
+
if (signature === undefined)
|
|
327
|
+
continue;
|
|
328
|
+
// Two providers sharing one (binary, port) signature price one probe:
|
|
329
|
+
// the first provider's running entry answers for both.
|
|
330
|
+
const signatureKey = `${signature.binary}|${signature.port ?? ''}`;
|
|
331
|
+
if (pricedSignatures.has(signatureKey))
|
|
332
|
+
continue;
|
|
333
|
+
pricedSignatures.add(signatureKey);
|
|
334
|
+
const match = processes.find((line) => matchLaunchSignature(line, signature));
|
|
295
335
|
if (match === undefined)
|
|
296
336
|
continue;
|
|
297
|
-
const entry = {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const launched = this.launchedModels.get(ref.signature.port);
|
|
303
|
-
if (launched !== undefined && launched.engineId === ref.engineId) {
|
|
337
|
+
const entry = { engineId: provider.engine };
|
|
338
|
+
if (signature.port !== undefined) {
|
|
339
|
+
entry.port = signature.port;
|
|
340
|
+
const launched = this.launchedModels.get(signature.port);
|
|
341
|
+
if (launched !== undefined && launched.engineId === provider.engine) {
|
|
304
342
|
entry.modelId = launched.modelId;
|
|
305
343
|
}
|
|
306
344
|
}
|
|
307
345
|
running.push(entry);
|
|
308
346
|
}
|
|
309
347
|
const gpuUsage = options?.gpuUsage !== undefined ? options.gpuUsage : await probeGpuUsage();
|
|
310
|
-
|
|
348
|
+
// The provider's resolved port rides into the resolver so two configs of
|
|
349
|
+
// one engine on different ports confirm only against the engine running
|
|
350
|
+
// on their own port.
|
|
351
|
+
const priced = providers.map((provider, index) => {
|
|
352
|
+
const port = signatures[index]?.port;
|
|
353
|
+
return port !== undefined ? { ...provider, port } : provider;
|
|
354
|
+
});
|
|
355
|
+
return computeProviderStatuses({ providers: priced, running, gpuBusy: isGpuBusy(gpuUsage) });
|
|
311
356
|
}
|
|
312
357
|
/**
|
|
313
358
|
* unsureBannerKillCommands returns the bash kill commands for the engine
|
|
@@ -411,13 +456,34 @@ export class LocalInfer extends Service {
|
|
|
411
456
|
throw new LocalInferError(`no engine adapter is registered for "${plan.engineId}"`, 'LOCAL_INFER_INVALID_ENGINE');
|
|
412
457
|
}
|
|
413
458
|
const platform = this.platformConfigRegistry.get(plan.engineId);
|
|
414
|
-
|
|
459
|
+
// The deploy step reports its download and build sub-steps through the
|
|
460
|
+
// progress sink, so the card's button reads the phase that is running.
|
|
461
|
+
const onProgress = options?.onProgress;
|
|
462
|
+
// The trust gate and the auto-rebuild flag ride into the deploy step,
|
|
463
|
+
// which reads them to decide whether a rebuild's missing-binary symlink
|
|
464
|
+
// runs (gate on) or is written to the card's Logs section (off), and
|
|
465
|
+
// whether the git sync (fetch, rebase, rebuild) runs at all.
|
|
466
|
+
// exactOptionalPropertyTypes: only pass the option object when a key is
|
|
467
|
+
// present.
|
|
468
|
+
const hasDeployOptions = options?.allowRunDeps !== undefined || options?.autoRebuild !== undefined;
|
|
469
|
+
const deployOptions = hasDeployOptions
|
|
470
|
+
? {
|
|
471
|
+
...(options?.allowRunDeps !== undefined ? { allowRunDeps: options.allowRunDeps } : {}),
|
|
472
|
+
...(options?.autoRebuild !== undefined ? { autoRebuild: options.autoRebuild } : {}),
|
|
473
|
+
}
|
|
474
|
+
: undefined;
|
|
475
|
+
const deployResult = await adapter.deploy(plan, platform, onProgress, deployOptions);
|
|
415
476
|
if (!deployResult.ok) {
|
|
416
477
|
// exactOptionalPropertyTypes: only write the detail key when it is present.
|
|
417
478
|
const result = { plan, healthy: false };
|
|
418
479
|
if (deployResult.detail !== undefined) {
|
|
419
480
|
result.detail = deployResult.detail;
|
|
420
481
|
}
|
|
482
|
+
// A failed deploy still carries the build decision and output it captured,
|
|
483
|
+
// so the card's Logs section shows the step that broke.
|
|
484
|
+
if (deployResult.logs !== undefined && deployResult.logs !== '') {
|
|
485
|
+
result.logs = deployResult.logs;
|
|
486
|
+
}
|
|
421
487
|
return result;
|
|
422
488
|
}
|
|
423
489
|
// A successful deploy means the engine process is running, so the
|
|
@@ -425,12 +491,31 @@ export class LocalInfer extends Service {
|
|
|
425
491
|
// already priced by the estimator, and pricing the GPU's used VRAM on
|
|
426
492
|
// top would double-count.
|
|
427
493
|
this.launchedEngines.add(plan.engineId);
|
|
494
|
+
if (plan.port !== undefined) {
|
|
495
|
+
// A fresh launch on a port replaces any adopted-process watcher left
|
|
496
|
+
// over from the engine this launch displaced on that port.
|
|
497
|
+
this.stopEngineWatcher(plan.port);
|
|
498
|
+
const ports = this.launchedPorts.get(plan.engineId) ?? new Set();
|
|
499
|
+
ports.add(plan.port);
|
|
500
|
+
this.launchedPorts.set(plan.engineId, ports);
|
|
501
|
+
}
|
|
428
502
|
// The provider-status detector ties a running engine's port to the model
|
|
429
503
|
// the plugin launched it with, so a live engine the plugin started reads
|
|
430
504
|
// as that model (green) rather than "engine running, model unknown".
|
|
431
505
|
if (plan.modelId !== undefined && plan.port !== undefined) {
|
|
432
506
|
this.launchedModels.set(plan.port, { engineId: plan.engineId, modelId: plan.modelId });
|
|
433
507
|
}
|
|
508
|
+
// The exit watch attaches before the health poll, so an engine that
|
|
509
|
+
// crashes mid-startup (or is pkill'd during the poll) settles the live
|
|
510
|
+
// state and reaches the death observer instead of leaving the port tied
|
|
511
|
+
// to a model that is no longer serving.
|
|
512
|
+
if (deployResult.handle !== undefined) {
|
|
513
|
+
this.watchLaunchedHandle(deployResult.handle, plan);
|
|
514
|
+
}
|
|
515
|
+
// The health poll is the launch phase: the rebuild (download, build) is
|
|
516
|
+
// done and the engine process is up, so the card's button reads
|
|
517
|
+
// "Launching..." while it waits for the engine to report ready.
|
|
518
|
+
onProgress?.('launch');
|
|
434
519
|
const health = await adapter.healthCheck(plan, options);
|
|
435
520
|
const launchResult = { plan, healthy: health.healthy };
|
|
436
521
|
if (health.detail !== undefined) {
|
|
@@ -449,10 +534,16 @@ export class LocalInfer extends Service {
|
|
|
449
534
|
}
|
|
450
535
|
}
|
|
451
536
|
// Read the engine's captured output after the health poll, so the result
|
|
452
|
-
// carries the log lines the engine printed while it came up. The
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
537
|
+
// carries the log lines the engine printed while it came up. The deploy
|
|
538
|
+
// step's own captured output (the rebuild decision, the build command, and
|
|
539
|
+
// its output) is kept separate in buildLogs, so the route orders the launch
|
|
540
|
+
// command after a build (or a direct launch) and before the engine output.
|
|
541
|
+
const buildLogs = deployResult.logs ?? '';
|
|
542
|
+
const handleLogs = deployResult.handle?.logs ?? '';
|
|
543
|
+
if (buildLogs !== '') {
|
|
544
|
+
launchResult.buildLogs = buildLogs;
|
|
545
|
+
}
|
|
546
|
+
if (handleLogs !== '') {
|
|
456
547
|
launchResult.logs = handleLogs;
|
|
457
548
|
}
|
|
458
549
|
return launchResult;
|
|
@@ -489,16 +580,138 @@ export class LocalInfer extends Service {
|
|
|
489
580
|
// resolver keeps its 3 GiB default and the provider-status detector
|
|
490
581
|
// ties its port to the model the record names.
|
|
491
582
|
this.launchedEngines.add(entry.record.engineId);
|
|
492
|
-
if (entry.record.port !== undefined
|
|
493
|
-
this.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
583
|
+
if (entry.record.port !== undefined) {
|
|
584
|
+
const ports = this.launchedPorts.get(entry.record.engineId) ?? new Set();
|
|
585
|
+
ports.add(entry.record.port);
|
|
586
|
+
this.launchedPorts.set(entry.record.engineId, ports);
|
|
587
|
+
if (entry.record.modelId !== undefined) {
|
|
588
|
+
this.launchedModels.set(entry.record.port, {
|
|
589
|
+
engineId: entry.record.engineId,
|
|
590
|
+
modelId: entry.record.modelId,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
// Re-attach the exit watch the restart dropped: the adopted child
|
|
594
|
+
// has no ChildProcess to listen on, so the poll stands in and the
|
|
595
|
+
// death settles the state the same way a live child's close event
|
|
596
|
+
// does. A repeat summary call keeps the existing watcher.
|
|
597
|
+
this.watchAdoptedEngine(entry.record, processAlive);
|
|
497
598
|
}
|
|
498
599
|
}
|
|
499
600
|
}
|
|
500
601
|
return outcomes;
|
|
501
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* setEngineDeathObserver registers the route's hook for engine process
|
|
605
|
+
* deaths: a crash or pkill seen through a launched handle's exit event, or a
|
|
606
|
+
* poll-detected death of a process adopted from a stored launch record. The
|
|
607
|
+
* observer persists the provider entry's cleared state so the card reads the
|
|
608
|
+
* provider stopped instead of carrying a stale record. Passing undefined
|
|
609
|
+
* clears it, which the route's effect does on dispose.
|
|
610
|
+
*/
|
|
611
|
+
setEngineDeathObserver(observer) {
|
|
612
|
+
this.deathObserver = observer;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* watchLaunchedHandle ties a launched child's one-shot exit to the live
|
|
616
|
+
* state: the host's close/error event (a crash, a pkill, or the stop kill)
|
|
617
|
+
* reports through observeEngineDeath. A handle whose host cannot watch (no
|
|
618
|
+
* onExit, a stub) is left to the re-link rule to price after a restart.
|
|
619
|
+
*/
|
|
620
|
+
watchLaunchedHandle(handle, plan) {
|
|
621
|
+
if (typeof handle.onExit !== 'function')
|
|
622
|
+
return;
|
|
623
|
+
if (handle.pid > 0) {
|
|
624
|
+
this.handledPids.add(handle.pid);
|
|
625
|
+
}
|
|
626
|
+
handle.onExit((exit) => {
|
|
627
|
+
this.handledPids.delete(handle.pid);
|
|
628
|
+
this.observeEngineDeath({
|
|
629
|
+
engineId: plan.engineId,
|
|
630
|
+
...(plan.port !== undefined ? { port: plan.port } : {}),
|
|
631
|
+
...(plan.modelId !== undefined ? { modelId: plan.modelId } : {}),
|
|
632
|
+
...(handle.pid > 0 ? { pid: handle.pid } : {}),
|
|
633
|
+
code: exit.code,
|
|
634
|
+
signal: exit.signal,
|
|
635
|
+
...(exit.error !== undefined ? { detail: exit.error } : {}),
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* watchAdoptedEngine re-attaches the exit watch for a process this DSH did
|
|
641
|
+
* not spawn: a surviving engine adopted from a stored launch record has no
|
|
642
|
+
* ChildProcess to listen on after a restart, so an unref'd poll on the
|
|
643
|
+
* liveness probe stands in for the exit event. When the pid dies the watcher
|
|
644
|
+
* stops itself and reports through observeEngineDeath, so the provider's
|
|
645
|
+
* state settles without waiting for the next summary re-link to price the
|
|
646
|
+
* record dead.
|
|
647
|
+
*/
|
|
648
|
+
watchAdoptedEngine(record, processAlive) {
|
|
649
|
+
if (record.port === undefined)
|
|
650
|
+
return;
|
|
651
|
+
if (this.engineWatchers.has(record.port))
|
|
652
|
+
return;
|
|
653
|
+
// A launch this DSH made itself already has the child handle's close
|
|
654
|
+
// event; a poll watcher would only double-report the same death.
|
|
655
|
+
if (this.handledPids.has(record.pid))
|
|
656
|
+
return;
|
|
657
|
+
const port = record.port;
|
|
658
|
+
const timer = setInterval(() => {
|
|
659
|
+
if (processAlive(record.pid))
|
|
660
|
+
return;
|
|
661
|
+
this.observeEngineDeath({
|
|
662
|
+
engineId: record.engineId,
|
|
663
|
+
port,
|
|
664
|
+
...(record.modelId !== undefined ? { modelId: record.modelId } : {}),
|
|
665
|
+
pid: record.pid,
|
|
666
|
+
code: null,
|
|
667
|
+
signal: null,
|
|
668
|
+
detail: 'the adopted engine process exited outside this DSH session',
|
|
669
|
+
});
|
|
670
|
+
}, PROCESS_WATCH_MS);
|
|
671
|
+
timer.unref();
|
|
672
|
+
this.engineWatchers.set(port, { pid: record.pid, timer });
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* stopEngineWatcher cancels one port's adopted-process poll, if it has one.
|
|
676
|
+
*/
|
|
677
|
+
stopEngineWatcher(port) {
|
|
678
|
+
const watcher = this.engineWatchers.get(port);
|
|
679
|
+
if (watcher === undefined)
|
|
680
|
+
return;
|
|
681
|
+
clearInterval(watcher.timer);
|
|
682
|
+
this.engineWatchers.delete(port);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* observeEngineDeath settles one engine process's death against the live
|
|
686
|
+
* state: the port-to-model tie drops when the dead engine still owns the
|
|
687
|
+
* port, the port leaves the engine's live ledger, the plugin-launched flag
|
|
688
|
+
* clears with the engine's last live port, the adopted-process watcher for
|
|
689
|
+
* the port stops, and the registered death observer (the route's persistence
|
|
690
|
+
* hook) receives the event. The launch record itself is the route's: the
|
|
691
|
+
* observer clears the provider entry's record so the card reads the provider
|
|
692
|
+
* stopped, not stale.
|
|
693
|
+
*/
|
|
694
|
+
observeEngineDeath(event) {
|
|
695
|
+
if (event.port !== undefined) {
|
|
696
|
+
const tracked = this.launchedModels.get(event.port);
|
|
697
|
+
if (tracked !== undefined && tracked.engineId === event.engineId) {
|
|
698
|
+
this.launchedModels.delete(event.port);
|
|
699
|
+
}
|
|
700
|
+
this.stopEngineWatcher(event.port);
|
|
701
|
+
const ports = this.launchedPorts.get(event.engineId);
|
|
702
|
+
if (ports !== undefined) {
|
|
703
|
+
ports.delete(event.port);
|
|
704
|
+
if (ports.size === 0) {
|
|
705
|
+
this.launchedPorts.delete(event.engineId);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const remaining = event.port !== undefined ? this.launchedPorts.get(event.engineId) : undefined;
|
|
710
|
+
if (remaining === undefined || remaining.size === 0) {
|
|
711
|
+
this.launchedEngines.delete(event.engineId);
|
|
712
|
+
}
|
|
713
|
+
this.deathObserver?.(event);
|
|
714
|
+
}
|
|
502
715
|
/**
|
|
503
716
|
* stop signals the engine a launch started: it resolves the adapter by the
|
|
504
717
|
* plan's engineId and runs the adapter's stop step (kill the tracked process,
|
|
@@ -510,14 +723,31 @@ export class LocalInfer extends Service {
|
|
|
510
723
|
throw new LocalInferError(`no engine adapter is registered for "${plan.engineId}"`, 'LOCAL_INFER_INVALID_ENGINE');
|
|
511
724
|
}
|
|
512
725
|
await adapter.stop(plan);
|
|
513
|
-
// The stop step signalled the tracked process or container, so the
|
|
514
|
-
// engine is no longer plugin-launched and the OS-reserve resolver may
|
|
515
|
-
// price the card's live usage for it again.
|
|
516
|
-
this.launchedEngines.delete(plan.engineId);
|
|
517
726
|
// Clear the port-to-model entry too, so the provider-status detector no
|
|
518
|
-
// longer ties this port to the model the plugin launched it with
|
|
727
|
+
// longer ties this port to the model the plugin launched it with, and
|
|
728
|
+
// cancel any adopted-process watcher standing on the port.
|
|
519
729
|
if (plan.port !== undefined) {
|
|
520
730
|
this.launchedModels.delete(plan.port);
|
|
731
|
+
this.stopEngineWatcher(plan.port);
|
|
732
|
+
const ports = this.launchedPorts.get(plan.engineId);
|
|
733
|
+
if (ports !== undefined) {
|
|
734
|
+
ports.delete(plan.port);
|
|
735
|
+
if (ports.size === 0) {
|
|
736
|
+
this.launchedPorts.delete(plan.engineId);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// The engine is no longer plugin-launched only when it has no other
|
|
740
|
+
// live port: a second provider's engine on another port keeps the
|
|
741
|
+
// OS-reserve default.
|
|
742
|
+
const remaining = this.launchedPorts.get(plan.engineId);
|
|
743
|
+
if (remaining === undefined || remaining.size === 0) {
|
|
744
|
+
this.launchedEngines.delete(plan.engineId);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
// A plan with no port (a compose profile the stop step signalled down)
|
|
749
|
+
// clears the flag outright.
|
|
750
|
+
this.launchedEngines.delete(plan.engineId);
|
|
521
751
|
}
|
|
522
752
|
}
|
|
523
753
|
/**
|
|
@@ -1025,6 +1255,12 @@ const shellRunCommand = (command) => new Promise(resolve => {
|
|
|
1025
1255
|
};
|
|
1026
1256
|
child.stdout.on('data', append);
|
|
1027
1257
|
child.stderr.on('data', append);
|
|
1258
|
+
// Without a listener, a child that fails to start (a missing shell) emits
|
|
1259
|
+
// an unlistened 'error' and throws it as an uncaughtException through the
|
|
1260
|
+
// host. Read it as a failed command instead.
|
|
1261
|
+
child.on('error', (error) => {
|
|
1262
|
+
resolve({ ok: false, detail: error.message });
|
|
1263
|
+
});
|
|
1028
1264
|
child.on('close', code => {
|
|
1029
1265
|
if (code === 0) {
|
|
1030
1266
|
resolve({ ok: true });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CommandOutcome, Config, DefaultModel, ModelDirectoryEntry, EngineAdapter, EngineDepPlan, EnginePlatformConfig, Guideline, GuidelineResult, GpuUsageProbe, HardwareProfile, InstallResult, LaunchPlan, ModelRow, Preset, PresetSource, ProcessLine, ProviderLaunchRecord, ProviderRef, ProviderStatus, ReLinkOutcome, RegisterResult, ReserveResolution, ReserveSource, ResolvedDefaults, SettingsOp, SystemDep, VramBreakdown, VramModel, VramVerdict } from '
|
|
1
|
+
import type { CommandOutcome, Config, DefaultModel, ModelDirectoryEntry, EngineAdapter, EngineDepPlan, EnginePlatformConfig, Guideline, GuidelineResult, GpuUsageProbe, HardwareProfile, InstallResult, LaunchPlan, LaunchProgress, ModelRow, Preset, PresetSource, ProcessLine, ProviderLaunchRecord, ProviderRef, ProviderStatus, ReLinkOutcome, RegisterResult, ReserveResolution, ReserveSource, ResolvedDefaults, SettingsOp, SystemDep, VramBreakdown, VramModel, VramVerdict } from 'dsh-plugin-local-infer/core';
|
|
2
2
|
/**
|
|
3
3
|
* EstimateResult is the answer to an estimate call: the full VRAM breakdown,
|
|
4
4
|
* the one-word verdict the meter renders, the source the numbers came from,
|
|
@@ -37,6 +37,24 @@ export interface ProviderStatusOptions {
|
|
|
37
37
|
processes?: ProcessLine[];
|
|
38
38
|
gpuUsage?: GpuUsageProbe | null;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* EngineDeathEvent is the one-shot report that an engine process the plugin
|
|
42
|
+
* tracked has ended: an observed crash or pkill (the exit code and signal from
|
|
43
|
+
* a live child's close event, or a spawn error detail for a child that never
|
|
44
|
+
* ran), or a poll-detected death of a process adopted from a stored launch
|
|
45
|
+
* record after a DSH restart (a null code and signal with a detail). The route
|
|
46
|
+
* registers the observer that persists the provider entry's cleared state, so
|
|
47
|
+
* the card reads the provider stopped instead of carrying a stale record.
|
|
48
|
+
*/
|
|
49
|
+
export interface EngineDeathEvent {
|
|
50
|
+
engineId: string;
|
|
51
|
+
port?: number;
|
|
52
|
+
modelId?: string;
|
|
53
|
+
pid?: number;
|
|
54
|
+
code: number | null;
|
|
55
|
+
signal: string | null;
|
|
56
|
+
detail?: string;
|
|
57
|
+
}
|
|
40
58
|
/**
|
|
41
59
|
* LaunchResult is the answer to a launch call: the plan that was started,
|
|
42
60
|
* whether the health check passed, a detail for the UI, and the engine's
|
|
@@ -47,13 +65,18 @@ export interface ProviderStatusOptions {
|
|
|
47
65
|
* deploy pulls an image only), and `parentPid` is the DSH process pid that
|
|
48
66
|
* launched the engine (absent when the host process did not identify as
|
|
49
67
|
* DSH), the two fields the route persists as the provider entry's launch
|
|
50
|
-
* record for the restart re-link.
|
|
68
|
+
* record for the restart re-link. `buildLogs` is the deploy step's own
|
|
69
|
+
* captured output (the rebuild decision, the build command, and its output)
|
|
70
|
+
* kept separate from the engine's runtime `logs`, so the route can order the
|
|
71
|
+
* launch command after a build (or after a direct launch) and before the
|
|
72
|
+
* engine output.
|
|
51
73
|
*/
|
|
52
74
|
export interface LaunchResult {
|
|
53
75
|
plan: LaunchPlan;
|
|
54
76
|
healthy: boolean;
|
|
55
77
|
detail?: string;
|
|
56
78
|
logs?: string;
|
|
79
|
+
buildLogs?: string;
|
|
57
80
|
pid?: number;
|
|
58
81
|
parentPid?: number;
|
|
59
82
|
}
|
|
@@ -61,10 +84,22 @@ export interface LaunchResult {
|
|
|
61
84
|
* LaunchOptions tunes the launch health poll: the interval between attempts
|
|
62
85
|
* and the attempt cap. The service passes the config's healthPollMs and
|
|
63
86
|
* healthMaxAttempts through; an absent option keeps the adapter's default.
|
|
87
|
+
* `onProgress`, when present, is called as each launch phase (the deploy
|
|
88
|
+
* sub-steps and the health poll) begins, so the card's Launch control reads
|
|
89
|
+
* the phase that is actually running. `allowRunDeps` is the Play/Run trust gate
|
|
90
|
+
* (the card's "Allow one-click install (sudo)" checkbox) the deploy step reads
|
|
91
|
+
* to decide whether a rebuild's missing-binary symlink into /usr/local/bin runs
|
|
92
|
+
* (on) or is written to the card's Logs section (off). `autoRebuild` is the
|
|
93
|
+
* card's "Auto Rebuild" checkbox: when false, the deploy step skips the git
|
|
94
|
+
* sync (fetch, rebase, rebuild) and launches the checkout as-is. Absent reads
|
|
95
|
+
* as true (the rebuild runs).
|
|
64
96
|
*/
|
|
65
97
|
export interface LaunchOptions {
|
|
66
98
|
pollMs?: number;
|
|
67
99
|
maxAttempts?: number;
|
|
100
|
+
onProgress?: LaunchProgress;
|
|
101
|
+
allowRunDeps?: boolean;
|
|
102
|
+
autoRebuild?: boolean;
|
|
68
103
|
}
|
|
69
104
|
/**
|
|
70
105
|
* RegisterModelOptions tunes the model registration: agentDefault points the
|
|
@@ -216,6 +251,13 @@ export interface LocalInferService {
|
|
|
216
251
|
processAlive?: (pid: number) => boolean;
|
|
217
252
|
commandLine?: (pid: number) => string | undefined;
|
|
218
253
|
}): Promise<Record<string, ReLinkOutcome>>;
|
|
254
|
+
/**
|
|
255
|
+
* setEngineDeathObserver registers the callback the route uses to persist a
|
|
256
|
+
* provider entry's cleared state when its engine process dies (a crash, a
|
|
257
|
+
* pkill, or a poll-detected death of an adopted process). Passing undefined
|
|
258
|
+
* clears it, which the route's effect does on dispose.
|
|
259
|
+
*/
|
|
260
|
+
setEngineDeathObserver(observer: ((event: EngineDeathEvent) => void) | undefined): void;
|
|
219
261
|
registerModel(plan: LaunchPlan, model: ModelRow, options?: RegisterModelOptions): Promise<RegisterResult>;
|
|
220
262
|
removeModel(providerId: string, modelId: string): Promise<void>;
|
|
221
263
|
resolveDefaults(engineId: string, modelId: string, hw: HardwareProfile, explicit?: Config): Promise<ResolvedDefaults>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "dsh-plugin-local-infer-service",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"description": "Service Definition for the dsh local-inference capability seam (ctx.localInfer): the engine, preset, and guideline registries plus the orchestration entry points (estimate, guidelines, deps, installString, runInstall, launch, registerModel). Carries no store of its own.",
|
|
5
5
|
"keywords": [
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"
|
|
33
|
+
"dsh-plugin-local-infer-core": "workspace:^"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@deepseek-ai/cordis": ">=4.0.1"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* In the scaffold this plugin is a no-op placeholder that injects the seam so
|
|
9
9
|
* the bundle loads with zero tools. Phase 8 registers the four tools and the
|
|
10
10
|
* prompt section.
|
|
11
|
-
* @module
|
|
11
|
+
* @module dsh-plugin-local-infer/tool
|
|
12
12
|
*/
|
|
13
13
|
import type { Context } from '@deepseek-ai/cordis';
|
|
14
14
|
export declare const name = "local-infer-tool";
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* In the scaffold this plugin is a no-op placeholder that injects the seam so
|
|
9
9
|
* the bundle loads with zero tools. Phase 8 registers the four tools and the
|
|
10
10
|
* prompt section.
|
|
11
|
-
* @module
|
|
11
|
+
* @module dsh-plugin-local-infer/tool
|
|
12
12
|
*/
|
|
13
13
|
export const name = 'local-infer-tool';
|
|
14
14
|
export const inject = ['localInfer'];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "dsh-plugin-local-infer-tool",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"description": "Model-facing tools for the dsh local-inference plugin: local_infer_estimate, local_infer_launch, local_infer_status, and local_infer_register, each on its own timeout budget, plus the system-prompt section that tells the agent when to use them",
|
|
5
5
|
"keywords": [
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"
|
|
33
|
-
"
|
|
32
|
+
"dsh-plugin-local-infer-core": "workspace:^",
|
|
33
|
+
"dsh-plugin-local-infer-service": "workspace:^"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@deepseek-ai/cordis": ">=4.0.1"
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* "Get" gate the plan names (disabled until every dep reports satisfied).
|
|
6
6
|
* The model is pure data the card draws, so the mapping stays unit-testable
|
|
7
7
|
* here.
|
|
8
|
-
* @module
|
|
8
|
+
* @module dsh-plugin-local-infer/ui/deps
|
|
9
9
|
*/
|
|
10
|
-
import type { DepCheck, DepStatus } from '
|
|
10
|
+
import type { DepCheck, DepStatus } from 'dsh-plugin-local-infer/core';
|
|
11
11
|
/**
|
|
12
12
|
* DepItemModel is one row of the panel's dep list: the dep id, the purpose
|
|
13
13
|
* the user reads, the current status, and whether installing it needs sudo.
|
package/packages/ui/lib/deps.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* "Get" gate the plan names (disabled until every dep reports satisfied).
|
|
6
6
|
* The model is pure data the card draws, so the mapping stays unit-testable
|
|
7
7
|
* here.
|
|
8
|
-
* @module
|
|
8
|
+
* @module dsh-plugin-local-infer/ui/deps
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
11
|
* renderDeps maps the checkDeps results and the rendered install string to
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* `group`, and a field per `ParameterSpec`. The model is pure data, so the
|
|
4
4
|
* panel (Phase 8) renders it without touching the domain model, and the
|
|
5
5
|
* mapping from `form` kind to control kind stays unit-testable here.
|
|
6
|
-
* @module
|
|
6
|
+
* @module dsh-plugin-local-infer/ui/form
|
|
7
7
|
*/
|
|
8
|
-
import type { ComboOption, Config, EnumOption, HardwareProfile, ParameterForm, ParameterSpec } from '
|
|
8
|
+
import type { ComboOption, Config, EnumOption, HardwareProfile, ParameterForm, ParameterSpec } from 'dsh-plugin-local-infer/core';
|
|
9
9
|
/**
|
|
10
10
|
* The control kinds the panel switches on: checkbox, dropdown, slider plus
|
|
11
11
|
* number input, text, a directory/file picker, and a dropdown plus free-text
|
package/packages/ui/lib/form.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `group`, and a field per `ParameterSpec`. The model is pure data, so the
|
|
4
4
|
* panel (Phase 8) renders it without touching the domain model, and the
|
|
5
5
|
* mapping from `form` kind to control kind stays unit-testable here.
|
|
6
|
-
* @module
|
|
6
|
+
* @module dsh-plugin-local-infer/ui/form
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
9
|
* The group label a spec without a `group` renders under.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The render models (form, meter, warnings) are pure data the panel draws
|
|
9
9
|
* from; Phase 8 wires them into one panel with the lifecycle controls.
|
|
10
|
-
* @module
|
|
10
|
+
* @module dsh-plugin-local-infer/ui
|
|
11
11
|
*/
|
|
12
12
|
import type { Context } from '@deepseek-ai/cordis';
|
|
13
13
|
export declare const name = "local-infer-ui";
|
package/packages/ui/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The render models (form, meter, warnings) are pure data the panel draws
|
|
9
9
|
* from; Phase 8 wires them into one panel with the lifecycle controls.
|
|
10
|
-
* @module
|
|
10
|
+
* @module dsh-plugin-local-infer/ui
|
|
11
11
|
*/
|
|
12
12
|
import { registerLocalInferRoute } from "./route.js";
|
|
13
13
|
export const name = 'local-infer-ui';
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* as authoritative. The model is pure data, so the panel (Phase 8) renders
|
|
7
7
|
* it without touching the domain model, and the mapping stays unit-testable
|
|
8
8
|
* here.
|
|
9
|
-
* @module
|
|
9
|
+
* @module dsh-plugin-local-infer/ui/meter
|
|
10
10
|
*/
|
|
11
|
-
import type { ReserveSource, VramBreakdown, VramVerdict } from '
|
|
11
|
+
import type { ReserveSource, VramBreakdown, VramVerdict } from 'dsh-plugin-local-infer/core';
|
|
12
12
|
/**
|
|
13
13
|
* MeterSegment is one segment of the to-scale bar. `percent` is the share of
|
|
14
14
|
* the full card the segment occupies, so the bar reads against the card the
|