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.
- package/README.md +10 -2
- package/docs/zh/README.md +10 -2
- package/dsh/client.js +669 -191
- package/package.json +6 -1
- package/packages/core/lib/adapter.d.ts +44 -2
- package/packages/core/lib/deps.d.ts +4 -3
- package/packages/core/lib/index.d.ts +1 -1
- package/packages/core/lib/registrar.d.ts +5 -5
- package/packages/core/lib/registrar.js +3 -3
- package/packages/core/package.json +1 -1
- package/packages/providers/lib/acquire.d.ts +5 -3
- package/packages/providers/lib/acquire.js +30 -9
- package/packages/providers/lib/adapters.d.ts +19 -209
- package/packages/providers/lib/adapters.js +13 -886
- package/packages/providers/lib/deps.d.ts +24 -2
- package/packages/providers/lib/deps.js +23 -4
- package/packages/providers/lib/engines/hyperqwenAdapter.d.ts +38 -0
- package/packages/providers/lib/engines/hyperqwenAdapter.js +105 -0
- package/packages/providers/lib/{infernix.js → engines/infernix.js} +1 -1
- package/packages/providers/lib/engines/infernixAdapter.d.ts +53 -0
- package/packages/providers/lib/engines/infernixAdapter.js +202 -0
- package/packages/providers/lib/engines/llamacppAdapter.d.ts +51 -0
- package/packages/providers/lib/engines/llamacppAdapter.js +232 -0
- package/packages/providers/lib/{ninfer.js → engines/ninfer.js} +1 -1
- package/packages/providers/lib/engines/ninferAdapter.d.ts +82 -0
- package/packages/providers/lib/engines/ninferAdapter.js +324 -0
- package/packages/providers/lib/engines/strata.d.ts +53 -0
- package/packages/providers/lib/engines/strata.js +197 -0
- package/packages/providers/lib/engines/strataAdapter.d.ts +98 -0
- package/packages/providers/lib/engines/strataAdapter.js +427 -0
- package/packages/providers/lib/engines.d.ts +5 -4
- package/packages/providers/lib/engines.js +10 -10
- package/packages/providers/lib/guidelines.js +1 -1
- package/packages/providers/lib/index.d.ts +14 -9
- package/packages/providers/lib/index.js +17 -7
- package/packages/providers/lib/model-defaults.d.ts +5 -4
- package/packages/providers/lib/model-defaults.js +22 -4
- package/packages/providers/lib/platform.js +7 -1
- package/packages/providers/package.json +1 -1
- package/packages/service/lib/index.d.ts +13 -4
- package/packages/service/lib/index.js +79 -5
- package/packages/service/lib/types.d.ts +5 -4
- package/packages/service/package.json +1 -1
- package/packages/tool/package.json +1 -1
- package/packages/ui/lib/config.d.ts +79 -0
- package/packages/ui/lib/config.js +235 -0
- package/packages/ui/lib/deps.d.ts +3 -3
- package/packages/ui/lib/ops/engine.d.ts +108 -0
- package/packages/ui/lib/ops/engine.js +493 -0
- package/packages/ui/lib/ops/hf.d.ts +27 -0
- package/packages/ui/lib/ops/hf.js +60 -0
- package/packages/ui/lib/ops/logs.d.ts +23 -0
- package/packages/ui/lib/ops/logs.js +58 -0
- package/packages/ui/lib/ops/model.d.ts +79 -0
- package/packages/ui/lib/ops/model.js +130 -0
- package/packages/ui/lib/request.d.ts +106 -0
- package/packages/ui/lib/request.js +80 -0
- package/packages/ui/lib/route.d.ts +88 -420
- package/packages/ui/lib/route.js +111 -1843
- package/packages/ui/lib/routes/chat-template.d.ts +14 -0
- package/packages/ui/lib/routes/chat-template.js +37 -0
- package/packages/ui/lib/routes/deps.d.ts +16 -0
- package/packages/ui/lib/routes/deps.js +27 -0
- package/packages/ui/lib/routes/download.d.ts +16 -0
- package/packages/ui/lib/routes/download.js +48 -0
- package/packages/ui/lib/routes/get.d.ts +18 -0
- package/packages/ui/lib/routes/get.js +57 -0
- package/packages/ui/lib/routes/install.d.ts +16 -0
- package/packages/ui/lib/routes/install.js +36 -0
- package/packages/ui/lib/routes/launch.d.ts +18 -0
- package/packages/ui/lib/routes/launch.js +50 -0
- package/packages/ui/lib/routes/provider-delete.d.ts +15 -0
- package/packages/ui/lib/routes/provider-delete.js +37 -0
- package/packages/ui/lib/routes/register.d.ts +16 -0
- package/packages/ui/lib/routes/register.js +28 -0
- package/packages/ui/lib/routes/save.d.ts +18 -0
- package/packages/ui/lib/routes/save.js +139 -0
- package/packages/ui/lib/routes/stop.d.ts +16 -0
- package/packages/ui/lib/routes/stop.js +29 -0
- package/packages/ui/lib/routes/summary.d.ts +15 -0
- package/packages/ui/lib/routes/summary.js +26 -0
- package/packages/ui/lib/routes/unregister.d.ts +14 -0
- package/packages/ui/lib/routes/unregister.js +26 -0
- package/packages/ui/lib/viewmodels/deps.d.ts +31 -0
- package/packages/ui/lib/viewmodels/deps.js +89 -0
- package/packages/ui/lib/viewmodels/depsHelpers.d.ts +28 -0
- package/packages/ui/lib/viewmodels/depsHelpers.js +33 -0
- package/packages/ui/lib/viewmodels/summary.d.ts +140 -0
- package/packages/ui/lib/viewmodels/summary.js +225 -0
- package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +30 -0
- package/packages/ui/lib/viewmodels/summaryHelpers.js +42 -0
- package/packages/ui/package.json +1 -1
- package/packages/providers/lib/strata.d.ts +0 -15
- package/packages/providers/lib/strata.js +0 -14
- /package/packages/providers/lib/{hyperqwen.d.ts → engines/hyperqwen.d.ts} +0 -0
- /package/packages/providers/lib/{hyperqwen.js → engines/hyperqwen.js} +0 -0
- /package/packages/providers/lib/{infernix.d.ts → engines/infernix.d.ts} +0 -0
- /package/packages/providers/lib/{llamacpp.d.ts → engines/llamacpp.d.ts} +0 -0
- /package/packages/providers/lib/{llamacpp.js → engines/llamacpp.js} +0 -0
- /package/packages/providers/lib/{ninfer.d.ts → engines/ninfer.d.ts} +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine lifecycle operations: launchEngine renders the plan the
|
|
3
|
+
* registered adapter produces and runs the service's launch entry point
|
|
4
|
+
* (deploy, then the health poll); stopEngine stops the exact container or
|
|
5
|
+
* process the launch started; getEngine runs the Get step (git sync, dep
|
|
6
|
+
* plan, build, model artifacts). The launch record (the engine pid, start
|
|
7
|
+
* time, launch arguments, and the DSH process pid that launched it) is what
|
|
8
|
+
* a DSH restart re-links a surviving engine to its config:
|
|
9
|
+
* relinkStoredProviders re-adopts surviving engines on mount and per summary
|
|
10
|
+
* read, handleEngineDeath settles the stored state the moment an engine dies
|
|
11
|
+
* outside the stop path, and persistLaunchLogs stores the captured output.
|
|
12
|
+
* The platform-config registry sync keeps the service registry in step with
|
|
13
|
+
* the stored config.
|
|
14
|
+
* @module dsh-plugin-local-infer/ui/ops/engine
|
|
15
|
+
*/
|
|
16
|
+
import type { Config, LaunchStep, LifecycleHost } from 'dsh-plugin-local-infer/core';
|
|
17
|
+
import type { EngineDeathEvent, LaunchResult, LocalInferService } from 'dsh-plugin-local-infer/service';
|
|
18
|
+
import type { StoredConfig, StoredProviderConfig } from '../config.ts';
|
|
19
|
+
import type { GetRequest, LaunchRequest } from '../request.ts';
|
|
20
|
+
/**
|
|
21
|
+
* resolveAdapter picks the registered engine adapter an engine id routes to. A
|
|
22
|
+
* card that names an engine no adapter serves gets a 404, not a blank panel.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveAdapter(localInfer: LocalInferService, engineId: string): import("dsh-plugin-local-infer/core").EngineAdapter;
|
|
25
|
+
/**
|
|
26
|
+
* launchEngine renders the plan the registered adapter produces for the config
|
|
27
|
+
* and the detected hardware, then runs the service's launch entry point
|
|
28
|
+
* (deploy, then the health poll). The detail the launch returns names the
|
|
29
|
+
* broken step when the engine does not come up. The launch line (the engine
|
|
30
|
+
* binary and its arguments) leads the run logs the card's Logs section and
|
|
31
|
+
* the log file carry, so a broken setup diagnoses against the exact child
|
|
32
|
+
* process the launch started. The launch step persists the
|
|
33
|
+
* launch record (the engine pid, start time, launch arguments, and the DSH
|
|
34
|
+
* process pid that launched it) on the request's provider entry, so a DSH
|
|
35
|
+
* restart can re-link a surviving engine to its config.
|
|
36
|
+
*/
|
|
37
|
+
export declare function launchEngine(localInfer: LocalInferService, request: LaunchRequest, onProgress?: (step: LaunchStep) => void): Promise<LaunchResult>;
|
|
38
|
+
/**
|
|
39
|
+
* handleEngineDeath persists a provider entry's state when its engine process
|
|
40
|
+
* dies outside the plugin's stop path: a crash, a pkill of a plugin-launched
|
|
41
|
+
* child seen through the service's exit watch, or the poll-detected exit of a
|
|
42
|
+
* process adopted after a DSH restart. The entry whose launch record names the
|
|
43
|
+
* dead pid (or the dead engine-and-port, when the report carries no pid match)
|
|
44
|
+
* has its record cleared, so a restart never re-links a dead pid, and the exit
|
|
45
|
+
* line lands in the card's last-run logs beside the engine's own output. An
|
|
46
|
+
* event that matches no entry carrying a record (a stop the route already
|
|
47
|
+
* cleared, an engine the plugin never tracked) is a no-op, which keeps an
|
|
48
|
+
* expected stop quiet.
|
|
49
|
+
*/
|
|
50
|
+
export declare function handleEngineDeath(event: EngineDeathEvent): void;
|
|
51
|
+
/**
|
|
52
|
+
* persistLaunchLogs stores a launch's captured output as the last run and
|
|
53
|
+
* writes it to the configured log file. The log file is only written when the
|
|
54
|
+
* run produced output and the log location is set; the last-run logs are always
|
|
55
|
+
* persisted (an empty string clears a stale run) so the card's Logs section
|
|
56
|
+
* shows the previous run after a reload.
|
|
57
|
+
*/
|
|
58
|
+
export declare function persistLaunchLogs(result: LaunchResult, engineId: string, config: Config): void;
|
|
59
|
+
/**
|
|
60
|
+
* stopEngine renders the same plan and runs the service's stop entry point, so
|
|
61
|
+
* the stop step targets the exact container or process the launch started. The
|
|
62
|
+
* stop then clears the target entry's launch record (the engine pid and the
|
|
63
|
+
* previous-pid list with it), so a recycled pid cannot re-link an engine the
|
|
64
|
+
* user just stopped.
|
|
65
|
+
*/
|
|
66
|
+
export declare function stopEngine(localInfer: LocalInferService, request: LaunchRequest): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* relinkStoredProviders prices every stored provider launch record against the
|
|
69
|
+
* live process table through the service's re-link rule and returns the
|
|
70
|
+
* provider list the card should read: an adopted engine is re-marked plugin-
|
|
71
|
+
* launched and tied to its model by the service, and a liveness watcher starts
|
|
72
|
+
* on its port so a later pkill settles the stored state through the death
|
|
73
|
+
* observer without another summary read; a dead engine's record is cleared on
|
|
74
|
+
* disk here (the previous-pid list with it), so a recycled pid cannot re-link
|
|
75
|
+
* to a config whose engine is down. The route calls it on mount and the
|
|
76
|
+
* summary calls it per read, making re-attach independent of the card opening.
|
|
77
|
+
*/
|
|
78
|
+
export declare function relinkStoredProviders(localInfer: LocalInferService, stored: StoredConfig): Promise<StoredProviderConfig[]>;
|
|
79
|
+
/**
|
|
80
|
+
* GetOutcome is the answer the Get step reports: ok with no detail, or a
|
|
81
|
+
* failure naming the broken step. The object literal union keeps the
|
|
82
|
+
* invariant the card reads: a detail only exists on a failure.
|
|
83
|
+
*/
|
|
84
|
+
export type GetOutcome = {
|
|
85
|
+
ok: true;
|
|
86
|
+
} | {
|
|
87
|
+
ok: false;
|
|
88
|
+
detail?: string;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* getEngine is the Get step: it syncs the engine checkout (clone the git
|
|
92
|
+
* repo, or the fork when the platform entry provides one; fetch and rebase
|
|
93
|
+
* a clean checkout; abort with a Logs alert when the checkout has local
|
|
94
|
+
* changes), runs the system dependency plan, and runs the engine's build
|
|
95
|
+
* steps. Every command and its output streams through log as it lands, so
|
|
96
|
+
* the card appends it to the Logs section live. The dependency install only
|
|
97
|
+
* runs while the Play/Run trust gate is on; with the gate off the deps are
|
|
98
|
+
* re-checked and a missing one aborts the step with the copy/paste note.
|
|
99
|
+
*/
|
|
100
|
+
export declare function getEngine(localInfer: LocalInferService, request: GetRequest, log: (chunk: string) => void, host?: LifecycleHost): Promise<GetOutcome>;
|
|
101
|
+
/**
|
|
102
|
+
* syncPlatformConfigs re-syncs the service registry with the stored platform
|
|
103
|
+
* map: it registers (or re-registers) every engine that carries a config, and
|
|
104
|
+
* unregisters every engine that no longer does, so a launch always reads the
|
|
105
|
+
* platform config the user last saved.
|
|
106
|
+
*/
|
|
107
|
+
export declare function syncPlatformConfigs(localInfer: LocalInferService, stored: StoredConfig): void;
|
|
108
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine lifecycle operations: launchEngine renders the plan the
|
|
3
|
+
* registered adapter produces and runs the service's launch entry point
|
|
4
|
+
* (deploy, then the health poll); stopEngine stops the exact container or
|
|
5
|
+
* process the launch started; getEngine runs the Get step (git sync, dep
|
|
6
|
+
* plan, build, model artifacts). The launch record (the engine pid, start
|
|
7
|
+
* time, launch arguments, and the DSH process pid that launched it) is what
|
|
8
|
+
* a DSH restart re-links a surviving engine to its config:
|
|
9
|
+
* relinkStoredProviders re-adopts surviving engines on mount and per summary
|
|
10
|
+
* read, handleEngineDeath settles the stored state the moment an engine dies
|
|
11
|
+
* outside the stop path, and persistLaunchLogs stores the captured output.
|
|
12
|
+
* The platform-config registry sync keeps the service registry in step with
|
|
13
|
+
* the stored config.
|
|
14
|
+
* @module dsh-plugin-local-infer/ui/ops/engine
|
|
15
|
+
*/
|
|
16
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { checkDeps, formatLaunchCommand, launchRecordFromPlan, resolveLogPath, } from '../../../core/lib/index.js';
|
|
20
|
+
import { buildEngineRepo, ENGINES, infernixGetDownloads, nodeLifecycleHost, syncEngineRepo, } from '../../../providers/lib/index.js';
|
|
21
|
+
import { readConfig, writeConfig } from "../config.js";
|
|
22
|
+
import { RouteError } from "../route.js";
|
|
23
|
+
import { resolveBuildMode } from "../viewmodels/deps.js";
|
|
24
|
+
import { resolveModelDirectory } from "./model.js";
|
|
25
|
+
/**
|
|
26
|
+
* resolveAdapter picks the registered engine adapter an engine id routes to. A
|
|
27
|
+
* card that names an engine no adapter serves gets a 404, not a blank panel.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveAdapter(localInfer, engineId) {
|
|
30
|
+
const adapter = localInfer.registeredEngines().find((engine) => engine.id === engineId);
|
|
31
|
+
if (adapter === undefined) {
|
|
32
|
+
throw new RouteError(404, `no engine adapter is registered for "${engineId}"`);
|
|
33
|
+
}
|
|
34
|
+
return adapter;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* launchCommandNote renders the plan's child-process launch line for the
|
|
38
|
+
* card's Logs section: the core formatter's command under a [local-infer]
|
|
39
|
+
* launch prefix, newline-terminated. A plan that names no command (a bare
|
|
40
|
+
* docker plan with no argv) has no note, and the run logs stay as they are.
|
|
41
|
+
*/
|
|
42
|
+
function launchCommandNote(plan) {
|
|
43
|
+
const command = formatLaunchCommand(plan);
|
|
44
|
+
if (command === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
return `[local-infer] launch: ${command}\n`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* applyComposeArgs seeds a compose plan's compose arguments from the stored
|
|
50
|
+
* platform config (the card's docker-tag field for a compose engine, e.g.
|
|
51
|
+
* `--profile single`), so the up line the deploy runs and the down line the
|
|
52
|
+
* stop runs both select the profile the user named. A compose plan with no
|
|
53
|
+
* stored docker-tag keeps the engine's default profile; a non-compose plan is
|
|
54
|
+
* untouched.
|
|
55
|
+
*/
|
|
56
|
+
function applyComposeArgs(plan, localInfer, engineId) {
|
|
57
|
+
if (plan.kind !== 'compose')
|
|
58
|
+
return;
|
|
59
|
+
const platform = localInfer.enginePlatformConfig(engineId);
|
|
60
|
+
if (platform?.dockerTag !== undefined) {
|
|
61
|
+
plan.composeArgs = platform.dockerTag;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* persistStoredLogs overwrites the stored last-run logs (the card's Logs
|
|
66
|
+
* section shows them after a reload) and leaves every other stored field as
|
|
67
|
+
* the launch reads it.
|
|
68
|
+
*/
|
|
69
|
+
function persistStoredLogs(logs) {
|
|
70
|
+
const stored = readConfig();
|
|
71
|
+
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, stored.providers, stored.logPath, logs);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* launchEngine renders the plan the registered adapter produces for the config
|
|
75
|
+
* and the detected hardware, then runs the service's launch entry point
|
|
76
|
+
* (deploy, then the health poll). The detail the launch returns names the
|
|
77
|
+
* broken step when the engine does not come up. The launch line (the engine
|
|
78
|
+
* binary and its arguments) leads the run logs the card's Logs section and
|
|
79
|
+
* the log file carry, so a broken setup diagnoses against the exact child
|
|
80
|
+
* process the launch started. The launch step persists the
|
|
81
|
+
* launch record (the engine pid, start time, launch arguments, and the DSH
|
|
82
|
+
* process pid that launched it) on the request's provider entry, so a DSH
|
|
83
|
+
* restart can re-link a surviving engine to its config.
|
|
84
|
+
*/
|
|
85
|
+
export async function launchEngine(localInfer, request, onProgress) {
|
|
86
|
+
const stored = readConfig();
|
|
87
|
+
const engineId = request.engine ?? stored.engine;
|
|
88
|
+
const config = request.config ?? stored.config;
|
|
89
|
+
const adapter = resolveAdapter(localInfer, engineId);
|
|
90
|
+
const hardware = await localInfer.detectHardware();
|
|
91
|
+
// Register the request's platform override (the card's unsaved Engine
|
|
92
|
+
// Directory / Engine Fork edits) so the service's launch resolves it against
|
|
93
|
+
// the adapter's deploy step. An absent platform keeps the stored entry,
|
|
94
|
+
// which the save route and route registration already synced.
|
|
95
|
+
if (request.platform !== undefined) {
|
|
96
|
+
setPlatformConfig(localInfer, engineId, request.platform);
|
|
97
|
+
}
|
|
98
|
+
const plan = adapter.renderLaunch(config, hardware);
|
|
99
|
+
applyComposeArgs(plan, localInfer, engineId);
|
|
100
|
+
const commandNote = launchCommandNote(plan);
|
|
101
|
+
if (commandNote !== undefined) {
|
|
102
|
+
// Persist before the deploy starts: a slow or hung launch keeps the
|
|
103
|
+
// command readable in the Logs section across a card reload, where the
|
|
104
|
+
// engine output has not landed yet.
|
|
105
|
+
persistStoredLogs(commandNote);
|
|
106
|
+
}
|
|
107
|
+
// The prereqs phase: the system-dependency install the local build needs. It
|
|
108
|
+
// runs only while the Play/Run trust gate is on, the same rule the Get step
|
|
109
|
+
// applies; a failed install aborts the launch before the deploy starts. The
|
|
110
|
+
// card's button reads "Prereqs..." for this phase, so the phase is reported
|
|
111
|
+
// only while an install actually runs (the gate on), not when the deps are
|
|
112
|
+
// already satisfied and the phase is skipped.
|
|
113
|
+
if (stored.allowRunDeps === true) {
|
|
114
|
+
onProgress?.('prereqs');
|
|
115
|
+
// Price the dep plan for the build mode the config names (docker, local,
|
|
116
|
+
// or the Strata binary install), not just the plan kind, so a binary
|
|
117
|
+
// launch installs its own (thinner) dep list.
|
|
118
|
+
const engine = ENGINES.find((entry) => entry.id === engineId) ?? ENGINES[0];
|
|
119
|
+
const buildMode = resolveBuildMode(engine, config);
|
|
120
|
+
const install = await localInfer.runInstall(engineId, buildMode, hardware);
|
|
121
|
+
if (!install.ok) {
|
|
122
|
+
const result = {
|
|
123
|
+
plan,
|
|
124
|
+
healthy: false,
|
|
125
|
+
detail: 'a system dependency install failed',
|
|
126
|
+
};
|
|
127
|
+
persistLaunchLogs(result, engineId, config);
|
|
128
|
+
persistLaunchRecord(result, engineId, config, request.providerId);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// The trust gate and the auto-rebuild flag ride into the deploy step, which
|
|
133
|
+
// reads them to decide whether a rebuild's missing-binary symlink into
|
|
134
|
+
// /usr/local/bin runs (on) or is written to the card's Logs section (off),
|
|
135
|
+
// and whether the git sync (fetch, rebase, rebuild) runs at all.
|
|
136
|
+
const result = await localInfer.launch(plan, {
|
|
137
|
+
...(onProgress !== undefined ? { onProgress } : {}),
|
|
138
|
+
allowRunDeps: stored.allowRunDeps,
|
|
139
|
+
...(request.autoRebuild !== undefined ? { autoRebuild: request.autoRebuild } : {}),
|
|
140
|
+
});
|
|
141
|
+
// Order the run logs: the build output (the rebuild decision, the build
|
|
142
|
+
// command, and its output) leads, the launch command follows it (after a
|
|
143
|
+
// successful build, or a direct launch with no build), and the engine's
|
|
144
|
+
// runtime output comes last. A deploy that failed before the engine printed
|
|
145
|
+
// anything still reports the command it tried to run.
|
|
146
|
+
const buildLogs = result.buildLogs ?? '';
|
|
147
|
+
const engineLogs = result.logs ?? '';
|
|
148
|
+
result.logs = buildLogs + (commandNote ?? '') + engineLogs;
|
|
149
|
+
if (result.buildLogs !== undefined) {
|
|
150
|
+
delete result.buildLogs;
|
|
151
|
+
}
|
|
152
|
+
persistLaunchLogs(result, engineId, config);
|
|
153
|
+
persistLaunchRecord(result, engineId, config, request.providerId);
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* findLaunchTarget resolves the stored provider entry a launch or stop
|
|
158
|
+
* targets: the entry the request names by id, or (an id the card does not
|
|
159
|
+
* send, a launch of the active config) the stored entry with the same engine
|
|
160
|
+
* and model. A launch with no resolvable entry persists no record, which
|
|
161
|
+
* reads as "no provider config to re-link" after a restart.
|
|
162
|
+
*/
|
|
163
|
+
function findLaunchTarget(stored, engineId, providerId, config) {
|
|
164
|
+
if (providerId !== undefined) {
|
|
165
|
+
const byId = stored.providers.find((entry) => entry.id === providerId);
|
|
166
|
+
if (byId !== undefined)
|
|
167
|
+
return byId;
|
|
168
|
+
}
|
|
169
|
+
const model = typeof config.model === 'string' ? config.model : undefined;
|
|
170
|
+
if (model === undefined)
|
|
171
|
+
return undefined;
|
|
172
|
+
return stored.providers.find((entry) => entry.engine === engineId &&
|
|
173
|
+
(typeof entry.config.model === 'string' ? entry.config.model : undefined) === model);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* persistLaunchRecord stores a launch's record on the target provider entry:
|
|
177
|
+
* the engine pid the deploy step started, the start time, the plan's argv,
|
|
178
|
+
* the DSH process pid the launch ran under (only when it identified as DSH),
|
|
179
|
+
* and the previous-pid list the re-link rule prices (the entry's own list
|
|
180
|
+
* plus the current DSH pid, capped by the core builder). A launch whose
|
|
181
|
+
* deploy started no tracked process (a compose up) persists nothing, and a
|
|
182
|
+
* launch with no resolvable entry persists nothing.
|
|
183
|
+
*/
|
|
184
|
+
function persistLaunchRecord(result, engineId, config, providerId) {
|
|
185
|
+
if (result.pid === undefined)
|
|
186
|
+
return;
|
|
187
|
+
const stored = readConfig();
|
|
188
|
+
const target = findLaunchTarget(stored, engineId, providerId, config);
|
|
189
|
+
if (target === undefined)
|
|
190
|
+
return;
|
|
191
|
+
const record = launchRecordFromPlan(result.plan, result.pid, result.parentPid, target.launch?.previousPids ?? []);
|
|
192
|
+
const providers = stored.providers.map((entry) => entry.id === target.id ? { ...entry, launch: record } : entry);
|
|
193
|
+
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath, stored.lastLogs);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* clearLaunchRecord removes the target provider entry's launch record (the
|
|
197
|
+
* engine pid and the previous-pid list with it), so a stopped engine's pid
|
|
198
|
+
* cannot be re-linked to its config by a recycled pid. A stop with no
|
|
199
|
+
* resolvable entry, or an entry that carries no record, is a no-op.
|
|
200
|
+
*/
|
|
201
|
+
function clearLaunchRecord(engineId, config, providerId) {
|
|
202
|
+
const stored = readConfig();
|
|
203
|
+
const target = findLaunchTarget(stored, engineId, providerId, config);
|
|
204
|
+
if (target === undefined || target.launch === undefined)
|
|
205
|
+
return;
|
|
206
|
+
const providers = stored.providers.map((entry) => {
|
|
207
|
+
if (entry.id !== target.id)
|
|
208
|
+
return entry;
|
|
209
|
+
// Rebuild the entry without the launch key, so the record (including the
|
|
210
|
+
// previous-pid list) is gone from the file, not just ignored.
|
|
211
|
+
return { id: entry.id, engine: entry.engine, config: entry.config, platform: entry.platform };
|
|
212
|
+
});
|
|
213
|
+
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath, stored.lastLogs);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* handleEngineDeath persists a provider entry's state when its engine process
|
|
217
|
+
* dies outside the plugin's stop path: a crash, a pkill of a plugin-launched
|
|
218
|
+
* child seen through the service's exit watch, or the poll-detected exit of a
|
|
219
|
+
* process adopted after a DSH restart. The entry whose launch record names the
|
|
220
|
+
* dead pid (or the dead engine-and-port, when the report carries no pid match)
|
|
221
|
+
* has its record cleared, so a restart never re-links a dead pid, and the exit
|
|
222
|
+
* line lands in the card's last-run logs beside the engine's own output. An
|
|
223
|
+
* event that matches no entry carrying a record (a stop the route already
|
|
224
|
+
* cleared, an engine the plugin never tracked) is a no-op, which keeps an
|
|
225
|
+
* expected stop quiet.
|
|
226
|
+
*/
|
|
227
|
+
export function handleEngineDeath(event) {
|
|
228
|
+
const stored = readConfig();
|
|
229
|
+
let matched = false;
|
|
230
|
+
const providers = stored.providers.map((entry) => {
|
|
231
|
+
const record = entry.launch;
|
|
232
|
+
if (record === undefined)
|
|
233
|
+
return entry;
|
|
234
|
+
const byPid = event.pid !== undefined && record.pid === event.pid;
|
|
235
|
+
const byEnginePort = event.port !== undefined && record.engineId === entry.engine && record.port === event.port;
|
|
236
|
+
if (!byPid && !byEnginePort)
|
|
237
|
+
return entry;
|
|
238
|
+
matched = true;
|
|
239
|
+
// Rebuild the entry without the launch key, so the record (including the
|
|
240
|
+
// previous-pid list) is gone from the file, not just ignored.
|
|
241
|
+
return { id: entry.id, engine: entry.engine, config: entry.config, platform: entry.platform };
|
|
242
|
+
});
|
|
243
|
+
if (!matched)
|
|
244
|
+
return;
|
|
245
|
+
const signalNote = event.signal !== null && event.signal !== undefined
|
|
246
|
+
? ` on signal ${event.signal}`
|
|
247
|
+
: ` with exit code ${event.code}`;
|
|
248
|
+
const note = `[local-infer] engine ${event.engineId}` +
|
|
249
|
+
`${event.port !== undefined ? ` (port ${event.port})` : ''}` +
|
|
250
|
+
` pid ${event.pid ?? 'unknown'} exited${signalNote}` +
|
|
251
|
+
`${event.detail !== undefined ? `: ${event.detail}` : ''}\n`;
|
|
252
|
+
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath,
|
|
253
|
+
// Keep the last-run tail bounded, the same rule the launch logs cap uses.
|
|
254
|
+
(stored.lastLogs + note).slice(-65536));
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* persistLaunchLogs stores a launch's captured output as the last run and
|
|
258
|
+
* writes it to the configured log file. The log file is only written when the
|
|
259
|
+
* run produced output and the log location is set; the last-run logs are always
|
|
260
|
+
* persisted (an empty string clears a stale run) so the card's Logs section
|
|
261
|
+
* shows the previous run after a reload.
|
|
262
|
+
*/
|
|
263
|
+
export function persistLaunchLogs(result, engineId, config) {
|
|
264
|
+
const stored = readConfig();
|
|
265
|
+
// The "Launching..." separator leads the persisted run, so the Logs
|
|
266
|
+
// section and the log file read the launch as a fresh process section, the
|
|
267
|
+
// same line the card writes when a Launch button is clicked.
|
|
268
|
+
const logs = result.logs === undefined || result.logs === '' ? '' : `--- Launching...\n${result.logs}`;
|
|
269
|
+
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, stored.providers, stored.logPath, logs);
|
|
270
|
+
if (logs === '') {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const modelId = typeof config.model === 'string' ? config.model : undefined;
|
|
274
|
+
const logPath = resolveLogPath(stored.logPath, engineId, modelId, new Date(), homedir());
|
|
275
|
+
try {
|
|
276
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
277
|
+
writeFileSync(logPath, logs);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// A log file that cannot be written (a read-only dir, a bad path) must not
|
|
281
|
+
// fail the launch; the in-memory last-run logs still reach the card.
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* stopEngine renders the same plan and runs the service's stop entry point, so
|
|
286
|
+
* the stop step targets the exact container or process the launch started. The
|
|
287
|
+
* stop then clears the target entry's launch record (the engine pid and the
|
|
288
|
+
* previous-pid list with it), so a recycled pid cannot re-link an engine the
|
|
289
|
+
* user just stopped.
|
|
290
|
+
*/
|
|
291
|
+
export async function stopEngine(localInfer, request) {
|
|
292
|
+
const stored = readConfig();
|
|
293
|
+
const engineId = request.engine ?? stored.engine;
|
|
294
|
+
const config = request.config ?? stored.config;
|
|
295
|
+
const adapter = resolveAdapter(localInfer, engineId);
|
|
296
|
+
const hardware = await localInfer.detectHardware();
|
|
297
|
+
const plan = adapter.renderLaunch(config, hardware);
|
|
298
|
+
applyComposeArgs(plan, localInfer, engineId);
|
|
299
|
+
await localInfer.stop(plan);
|
|
300
|
+
clearLaunchRecord(engineId, config, request.providerId);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* relinkStoredProviders prices every stored provider launch record against the
|
|
304
|
+
* live process table through the service's re-link rule and returns the
|
|
305
|
+
* provider list the card should read: an adopted engine is re-marked plugin-
|
|
306
|
+
* launched and tied to its model by the service, and a liveness watcher starts
|
|
307
|
+
* on its port so a later pkill settles the stored state through the death
|
|
308
|
+
* observer without another summary read; a dead engine's record is cleared on
|
|
309
|
+
* disk here (the previous-pid list with it), so a recycled pid cannot re-link
|
|
310
|
+
* to a config whose engine is down. The route calls it on mount and the
|
|
311
|
+
* summary calls it per read, making re-attach independent of the card opening.
|
|
312
|
+
*/
|
|
313
|
+
export async function relinkStoredProviders(localInfer, stored) {
|
|
314
|
+
const recorded = stored.providers.filter((provider) => provider.launch !== undefined);
|
|
315
|
+
if (recorded.length === 0)
|
|
316
|
+
return stored.providers;
|
|
317
|
+
const outcomes = await localInfer.reLinkProviders(recorded.map((provider) => ({ id: provider.id, record: provider.launch })));
|
|
318
|
+
if (!recorded.some((provider) => outcomes[provider.id] === 'dead')) {
|
|
319
|
+
return stored.providers;
|
|
320
|
+
}
|
|
321
|
+
const providers = stored.providers.map((provider) => {
|
|
322
|
+
if (provider.launch === undefined || outcomes[provider.id] !== 'dead') {
|
|
323
|
+
return provider;
|
|
324
|
+
}
|
|
325
|
+
// Rebuild the dead entry without the launch key, so the record (including
|
|
326
|
+
// the previous-pid list) is gone from the file, not just ignored.
|
|
327
|
+
return {
|
|
328
|
+
id: provider.id,
|
|
329
|
+
engine: provider.engine,
|
|
330
|
+
config: provider.config,
|
|
331
|
+
platform: provider.platform,
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
writeConfig(stored.engine, stored.config, stored.allowRunDeps, stored.platform, stored.showHelp, providers, stored.logPath, stored.lastLogs);
|
|
335
|
+
return providers;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* resolveEngineDirectory picks the checkout the Get step syncs: the Engine
|
|
339
|
+
* Directory override when it names a path, else a per-engine folder inside
|
|
340
|
+
* the model directory (the default model directory, or the config's
|
|
341
|
+
* override), so a Get with no override still has a real place to live.
|
|
342
|
+
*/
|
|
343
|
+
function resolveEngineDirectory(localInfer, config, engineId, platform) {
|
|
344
|
+
const override = platform?.engineDirectory;
|
|
345
|
+
if (override !== undefined && override.trim() !== '') {
|
|
346
|
+
return override;
|
|
347
|
+
}
|
|
348
|
+
const rawModelDir = config.modelDirectory;
|
|
349
|
+
const modelDir = typeof rawModelDir === 'string' && rawModelDir.trim() !== ''
|
|
350
|
+
? rawModelDir
|
|
351
|
+
: localInfer.defaultModelDirectory();
|
|
352
|
+
return join(modelDir, engineId);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* getEngine is the Get step: it syncs the engine checkout (clone the git
|
|
356
|
+
* repo, or the fork when the platform entry provides one; fetch and rebase
|
|
357
|
+
* a clean checkout; abort with a Logs alert when the checkout has local
|
|
358
|
+
* changes), runs the system dependency plan, and runs the engine's build
|
|
359
|
+
* steps. Every command and its output streams through log as it lands, so
|
|
360
|
+
* the card appends it to the Logs section live. The dependency install only
|
|
361
|
+
* runs while the Play/Run trust gate is on; with the gate off the deps are
|
|
362
|
+
* re-checked and a missing one aborts the step with the copy/paste note.
|
|
363
|
+
*/
|
|
364
|
+
export async function getEngine(localInfer, request, log, host = nodeLifecycleHost) {
|
|
365
|
+
const stored = readConfig();
|
|
366
|
+
const engineId = request.engine ?? stored.engine;
|
|
367
|
+
const config = request.config ?? stored.config;
|
|
368
|
+
// ENGINES is a known non-empty constant, so the fallback is always defined.
|
|
369
|
+
const engine = ENGINES.find((entry) => entry.id === engineId) ?? ENGINES[0];
|
|
370
|
+
// Register the request's platform override the same way launchEngine does,
|
|
371
|
+
// so the next launch reads the Engine Directory / Engine Fork the Get used.
|
|
372
|
+
if (request.platform !== undefined) {
|
|
373
|
+
setPlatformConfig(localInfer, engineId, request.platform);
|
|
374
|
+
}
|
|
375
|
+
const platform = request.platform ?? stored.platform[engineId];
|
|
376
|
+
const directory = resolveEngineDirectory(localInfer, config, engineId, platform);
|
|
377
|
+
const fork = platform?.engineFork;
|
|
378
|
+
log(`get ${engineId} in ${directory}\n`);
|
|
379
|
+
const sync = await syncEngineRepo(host, { engineId, directory, ...(fork !== undefined && fork.trim() !== '' ? { fork } : {}) }, log);
|
|
380
|
+
if (!sync.ok) {
|
|
381
|
+
// A dirty checkout already alerted in the sync step; any other failure
|
|
382
|
+
// names its step here.
|
|
383
|
+
if (sync.aborted !== true && sync.detail !== undefined) {
|
|
384
|
+
log(`!! ${sync.detail}\n`);
|
|
385
|
+
}
|
|
386
|
+
return { ok: false, ...(sync.detail !== undefined ? { detail: sync.detail } : {}) };
|
|
387
|
+
}
|
|
388
|
+
const hardware = await localInfer.detectHardware();
|
|
389
|
+
const buildMode = resolveBuildMode(engine, config);
|
|
390
|
+
log(`system dependencies (${buildMode})\n`);
|
|
391
|
+
if (stored.allowRunDeps === true) {
|
|
392
|
+
const result = await localInfer.runInstall(engineId, buildMode, hardware);
|
|
393
|
+
for (const entry of result.results) {
|
|
394
|
+
log(` ${entry.id}: ${entry.status}${entry.detail !== undefined ? ` (${entry.detail})` : ''}\n`);
|
|
395
|
+
}
|
|
396
|
+
if (!result.ok) {
|
|
397
|
+
log('!! a system dependency install failed; copy the install string from the panel\n');
|
|
398
|
+
return { ok: false, detail: 'a system dependency install failed' };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
const deps = await localInfer.deps(engineId, buildMode, hardware);
|
|
403
|
+
const checks = await checkDeps(deps, hardware);
|
|
404
|
+
let missing = false;
|
|
405
|
+
for (const check of checks) {
|
|
406
|
+
log(` ${check.dep.id}: ${check.status}\n`);
|
|
407
|
+
if (check.status !== 'satisfied')
|
|
408
|
+
missing = true;
|
|
409
|
+
}
|
|
410
|
+
if (missing) {
|
|
411
|
+
log('!! a system dependency is missing and one-click install is off; copy the install string from the panel\n');
|
|
412
|
+
return { ok: false, detail: 'a system dependency is missing' };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// The user's local build command (the card's Build commands field) overrides
|
|
416
|
+
// the engine's default build steps; absent runs the engine's acquisition
|
|
417
|
+
// steps.
|
|
418
|
+
const build = await buildEngineRepo(host, engineId, directory, log, platform?.buildCommands);
|
|
419
|
+
if (!build.ok) {
|
|
420
|
+
if (build.detail !== undefined) {
|
|
421
|
+
log(`!! ${build.detail}\n`);
|
|
422
|
+
}
|
|
423
|
+
return { ok: false, ...(build.detail !== undefined ? { detail: build.detail } : {}) };
|
|
424
|
+
}
|
|
425
|
+
// The model artifacts a Get leaves ready to launch: the Infernix DFlash2
|
|
426
|
+
// drafter (when a preset is selected) and the PLE n-gram volume (when the
|
|
427
|
+
// model key names one), pulled through the hf CLI into the model directory.
|
|
428
|
+
// A download failure aborts the Get, so a launch never reads a missing
|
|
429
|
+
// drafter or table.
|
|
430
|
+
if (engineId === 'infernix') {
|
|
431
|
+
const modelDir = resolveModelDirectory(localInfer, config);
|
|
432
|
+
for (const command of infernixGetDownloads(config, modelDir)) {
|
|
433
|
+
log(`$ ${command}\n`);
|
|
434
|
+
const outcome = host.runStream !== undefined
|
|
435
|
+
? await host.runStream(command, (chunk) => log(chunk))
|
|
436
|
+
: await host.run(command);
|
|
437
|
+
if (!outcome.ok) {
|
|
438
|
+
log(`!! ${command} failed: ${outcome.detail ?? 'unknown'}\n`);
|
|
439
|
+
return { ok: false, detail: 'a model artifact download failed' };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
log('get complete\n');
|
|
444
|
+
return { ok: true };
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* The route's view of which engine platform configs are registered with the
|
|
448
|
+
* service, keyed by engine id. The service registry is the single source the
|
|
449
|
+
* launch step reads, so the route keeps it in sync with the stored config:
|
|
450
|
+
* on route registration, after every save, and when a launch request carries
|
|
451
|
+
* a platform override. A disposer is tracked per engine, so an engine whose
|
|
452
|
+
* stored config is cleared is unregistered rather than left stale.
|
|
453
|
+
*/
|
|
454
|
+
const platformDisposers = new Map();
|
|
455
|
+
/**
|
|
456
|
+
* setPlatformConfig registers one engine's platform config with the service,
|
|
457
|
+
* replacing any stored entry. A config with no fields is read as "no platform
|
|
458
|
+
* config", which unregisters the engine so the deploy step falls back to the
|
|
459
|
+
* adapter's default source and cache directory.
|
|
460
|
+
*/
|
|
461
|
+
function setPlatformConfig(localInfer, engineId, config) {
|
|
462
|
+
platformDisposers.get(engineId)?.();
|
|
463
|
+
platformDisposers.delete(engineId);
|
|
464
|
+
if (config === undefined) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (config.engineDirectory === undefined &&
|
|
468
|
+
config.engineFork === undefined &&
|
|
469
|
+
config.buildCommands === undefined &&
|
|
470
|
+
config.dockerTag === undefined) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const platform = { engineId, ...config };
|
|
474
|
+
platformDisposers.set(engineId, localInfer.registerEnginePlatformConfig(platform));
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* syncPlatformConfigs re-syncs the service registry with the stored platform
|
|
478
|
+
* map: it registers (or re-registers) every engine that carries a config, and
|
|
479
|
+
* unregisters every engine that no longer does, so a launch always reads the
|
|
480
|
+
* platform config the user last saved.
|
|
481
|
+
*/
|
|
482
|
+
export function syncPlatformConfigs(localInfer, stored) {
|
|
483
|
+
const wanted = stored.platform;
|
|
484
|
+
for (const engineId of [...platformDisposers.keys()]) {
|
|
485
|
+
if (wanted[engineId] === undefined) {
|
|
486
|
+
setPlatformConfig(localInfer, engineId, undefined);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
for (const [engineId, config] of Object.entries(wanted)) {
|
|
490
|
+
setPlatformConfig(localInfer, engineId, config);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hf CLI download operations: downloadFor runs the download for a default
|
|
3
|
+
* model, and chatTemplateDownloadFor pulls a selected chat template into the
|
|
4
|
+
* model directory. Both resolve the directory against the request, the
|
|
5
|
+
* stored config, and the plugin's default model directory in that order.
|
|
6
|
+
* @module dsh-plugin-local-infer/ui/ops/hf
|
|
7
|
+
*/
|
|
8
|
+
import type { DownloadResult, LocalInferService } from 'dsh-plugin-local-infer/service';
|
|
9
|
+
import type { ChatTemplateDownloadRequest, DownloadRequest } from '../request.ts';
|
|
10
|
+
/**
|
|
11
|
+
* downloadFor runs the hf CLI download for the request's default model: it
|
|
12
|
+
* resolves the entry through the providers' default list (a 404 when no
|
|
13
|
+
* entry names the pair), resolves the directory against the request, the
|
|
14
|
+
* stored config, and the plugin's default model directory in that order, and
|
|
15
|
+
* runs the download through the service.
|
|
16
|
+
*/
|
|
17
|
+
export declare function downloadFor(localInfer: LocalInferService, request: DownloadRequest): Promise<DownloadResult>;
|
|
18
|
+
/**
|
|
19
|
+
* chatTemplateDownloadFor runs the hf CLI download for the request's chat
|
|
20
|
+
* template: it resolves the template repo reference from the request or the
|
|
21
|
+
* stored config's chatTemplate (a 400 when neither names an HF repo
|
|
22
|
+
* reference), resolves the directory against the request, the stored config,
|
|
23
|
+
* and the plugin's default model directory in that order, and runs the
|
|
24
|
+
* download through the service.
|
|
25
|
+
*/
|
|
26
|
+
export declare function chatTemplateDownloadFor(localInfer: LocalInferService, request: ChatTemplateDownloadRequest): Promise<DownloadResult>;
|
|
27
|
+
//# sourceMappingURL=hf.d.ts.map
|