@openvulcan/dsh-vulcancode 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +126 -2
- package/assets/openvulcan-icon.svg +33 -0
- package/cordis.patch.yml +19 -0
- package/node_modules/@openvulcan/dsh-decision-guard/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-decision-guard/README.md +14 -0
- package/node_modules/@openvulcan/dsh-decision-guard/assets/decision-guard-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/calls.d.ts +40 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/calls.js +150 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/client.js +2250 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/decide.d.ts +90 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/decide.js +108 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/index.d.ts +129 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/index.js +572 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/intent.d.ts +22 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/intent.js +77 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/panel.d.ts +158 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/panel.js +340 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/records.d.ts +133 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/records.js +186 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/review.d.ts +55 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/review.js +202 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/session-state.d.ts +62 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/session-state.js +155 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/version.d.ts +2 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/version.js +4 -0
- package/node_modules/@openvulcan/dsh-decision-guard/package.json +68 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/assets/account-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/lib/client.js +573 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/lib/index.d.ts +92 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/lib/index.js +194 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/package.json +57 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/assets/answers-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/assets/prompts/answer-mode-interactive.md +79 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/assets/prompts/answer-mode-standard.md +1 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/answer-model.d.ts +258 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/answer-model.js +698 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/client.js +671 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/fences.d.ts +29 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/fences.js +70 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/index.d.ts +19 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/index.js +52 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/limits.d.ts +9 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/limits.js +9 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/prompt.d.ts +22 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/prompt.js +39 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/reply-format.d.ts +20 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/reply-format.js +17 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/session-state.d.ts +66 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/session-state.js +118 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/package.json +59 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/assets/luaskills-icon.svg +1 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/client.js +949 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/constants.d.ts +46 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/constants.js +51 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/engine.d.ts +55 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/engine.js +149 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/hub.d.ts +49 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/hub.js +116 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/index.d.ts +26 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/index.js +276 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/protocol.d.ts +65 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/protocol.js +2 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/runtime.d.ts +111 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/runtime.js +281 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/sdk-install.d.ts +26 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/sdk-install.js +87 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/settings.d.ts +79 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/settings.js +165 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/skills.d.ts +34 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/skills.js +61 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/tools.d.ts +59 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/tools.js +107 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/worker.d.ts +10 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/worker.js +140 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/package.json +57 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/assets/markdown-memory-guide.md +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/assets/memory-curator/SKILL.md +31 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/assets/memory-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/client.js +1090 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/index.d.ts +30 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/index.js +240 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/inject.d.ts +30 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/inject.js +46 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/constants.d.ts +23 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/constants.js +25 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/document.d.ts +108 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/document.js +373 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/errors.d.ts +14 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/errors.js +31 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/index.d.ts +196 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/index.js +204 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/operations.d.ts +92 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/operations.js +472 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/package.d.ts +126 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/package.js +553 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/registry.d.ts +56 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/registry.js +182 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/scope.d.ts +13 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/scope.js +24 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/search.d.ts +65 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/search.js +227 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/snapshot.d.ts +34 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/snapshot.js +171 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/store.d.ts +72 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/store.js +250 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/timestamp.d.ts +6 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/timestamp.js +12 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/tokens.d.ts +8 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/tokens.js +38 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/page.d.ts +132 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/page.js +235 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/project.d.ts +14 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/project.js +27 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/settings.d.ts +29 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/settings.js +62 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/package.json +63 -0
- package/node_modules/@openvulcan/dsh-vulcan-reasoning/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-reasoning/lib/client.js +509 -0
- package/node_modules/@openvulcan/dsh-vulcan-reasoning/lib/index.d.ts +6 -0
- package/node_modules/@openvulcan/dsh-vulcan-reasoning/lib/index.js +8 -0
- package/node_modules/@openvulcan/dsh-vulcan-reasoning/package.json +42 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/assets/style-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/lib/client.js +1121 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/lib/index.d.ts +34 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/lib/index.js +42 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/package.json +51 -0
- package/node_modules/@openvulcan/openvulcan-account/LICENSE +21 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/account.d.ts +140 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/account.js +347 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/index.d.ts +1 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/index.js +1 -0
- package/node_modules/@openvulcan/openvulcan-account/package.json +29 -0
- package/node_modules/@openvulcan/vulcan-decision/LICENSE +21 -0
- package/node_modules/@openvulcan/vulcan-decision/README.md +23 -0
- package/node_modules/@openvulcan/vulcan-decision/bin/vulcan-decision.js +4 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cli.d.ts +5 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cli.js +267 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cloud/edge.d.ts +211 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cloud/edge.js +594 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/download.d.ts +36 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/download.js +99 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/engine.d.ts +37 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/engine.js +39 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/hardware.d.ts +105 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/hardware.js +207 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/home.d.ts +26 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/home.js +54 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/index.d.ts +17 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/index.js +17 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/engine.d.ts +54 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/engine.js +157 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/models.d.ts +126 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/models.js +261 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/packs.d.ts +102 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/packs.js +195 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/worker.d.ts +61 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/worker.js +123 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/protocol.d.ts +74 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/protocol.js +31 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/render.d.ts +15 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/render.js +33 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/version.d.ts +2 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/version.js +4 -0
- package/node_modules/@openvulcan/vulcan-decision/package.json +40 -0
- package/node_modules/@openvulcan/vulcan-decision/python/vulcan_decision_worker.py +363 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/onnx-cpu.json +627 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/onnx-cuda.json +423 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/torch-cpu.json +1047 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/torch-cu128.json +647 -0
- package/package.json +55 -4
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local backend: keeps one worker process with the selected model loaded, restarts it when
|
|
3
|
+
* the selection changes and stops it after a quiet period.
|
|
4
|
+
*/
|
|
5
|
+
import { DecisionError, checkResult } from '../protocol.js';
|
|
6
|
+
import { backendOf, modelRoot, readModel } from './models.js';
|
|
7
|
+
import { installedPack, packPython } from './packs.js';
|
|
8
|
+
import { WorkerProcess } from './worker.js';
|
|
9
|
+
function selectionKey(selection) {
|
|
10
|
+
return `${selection.pack}\n${selection.model}`;
|
|
11
|
+
}
|
|
12
|
+
/** Split `inputs` into requests of at most `limit` inputs, in order; no limit means one request. */
|
|
13
|
+
export function mergeRequests(inputs, limit) {
|
|
14
|
+
const size = limit === undefined || limit < 1 ? Math.max(inputs.length, 1) : limit;
|
|
15
|
+
const requests = [];
|
|
16
|
+
for (let start = 0; start < inputs.length; start += size)
|
|
17
|
+
requests.push(inputs.slice(start, start + size));
|
|
18
|
+
return requests;
|
|
19
|
+
}
|
|
20
|
+
/** Hand each caller back its own slice of the merged results. */
|
|
21
|
+
export function splitResults(groups, results) {
|
|
22
|
+
let offset = 0;
|
|
23
|
+
return groups.map(group => {
|
|
24
|
+
const slice = results.slice(offset, offset + group.length);
|
|
25
|
+
offset += group.length;
|
|
26
|
+
return slice;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Stop the process after this long without a judgement, to give its memory back. */
|
|
30
|
+
export const LOCAL_IDLE_MS = 30 * 60_000;
|
|
31
|
+
export class LocalEngine {
|
|
32
|
+
home;
|
|
33
|
+
idleMs;
|
|
34
|
+
worker;
|
|
35
|
+
current;
|
|
36
|
+
idleTimer;
|
|
37
|
+
/** Loads and merged judgements run one at a time, in arrival order. */
|
|
38
|
+
queue = Promise.resolve();
|
|
39
|
+
/** Callers waiting for the next merged judgement. */
|
|
40
|
+
waiting = [];
|
|
41
|
+
/** Whether a drain of `waiting` is already queued behind the running work. */
|
|
42
|
+
drainQueued = false;
|
|
43
|
+
constructor(home, idleMs = LOCAL_IDLE_MS) {
|
|
44
|
+
this.home = home;
|
|
45
|
+
this.idleMs = idleMs;
|
|
46
|
+
}
|
|
47
|
+
status() {
|
|
48
|
+
return {
|
|
49
|
+
running: this.worker?.running === true,
|
|
50
|
+
...this.current === undefined ? {} : { selection: this.current.selection },
|
|
51
|
+
...this.current?.loaded === undefined ? {} : { loaded: this.current.loaded },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Judge `inputs` with `selection`, loading it first when needed. Calls that arrive while the
|
|
56
|
+
* worker is busy wait together and go out as one merged request, split into requests of at most
|
|
57
|
+
* the package's `max_request_items` calls.
|
|
58
|
+
*/
|
|
59
|
+
judge(selection, inputs) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
this.waiting.push({ selection, inputs, resolve, reject });
|
|
62
|
+
if (this.drainQueued)
|
|
63
|
+
return;
|
|
64
|
+
this.drainQueued = true;
|
|
65
|
+
const run = this.queue.then(() => {
|
|
66
|
+
this.drainQueued = false;
|
|
67
|
+
return this.drain();
|
|
68
|
+
});
|
|
69
|
+
this.queue = run.catch(() => { });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Load `selection` ahead of the first judgement. */
|
|
73
|
+
async warm(selection) {
|
|
74
|
+
const run = this.queue.then(() => this.ensure(selection));
|
|
75
|
+
this.queue = run.catch(() => { });
|
|
76
|
+
return await run;
|
|
77
|
+
}
|
|
78
|
+
stop() {
|
|
79
|
+
clearTimeout(this.idleTimer);
|
|
80
|
+
void this.worker?.stop();
|
|
81
|
+
this.worker = undefined;
|
|
82
|
+
this.current = undefined;
|
|
83
|
+
}
|
|
84
|
+
/** Answer every waiting caller, one selection at a time in arrival order. */
|
|
85
|
+
async drain() {
|
|
86
|
+
for (let first = this.waiting[0]; first !== undefined; first = this.waiting[0]) {
|
|
87
|
+
const key = selectionKey(first.selection);
|
|
88
|
+
const group = this.waiting.filter(entry => selectionKey(entry.selection) === key);
|
|
89
|
+
this.waiting = this.waiting.filter(entry => selectionKey(entry.selection) !== key);
|
|
90
|
+
try {
|
|
91
|
+
const results = await this.run(first.selection, group.map(entry => entry.inputs));
|
|
92
|
+
group.forEach((entry, index) => entry.resolve(results[index] ?? []));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
for (const entry of group)
|
|
96
|
+
entry.reject(error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Judge several callers' inputs with `selection` in merged requests; answers per caller. */
|
|
101
|
+
async run(selection, groups) {
|
|
102
|
+
const loaded = await this.ensure(selection);
|
|
103
|
+
const worker = this.worker;
|
|
104
|
+
if (worker === undefined)
|
|
105
|
+
throw new DecisionError('backend_unavailable', 'the local model process is not running');
|
|
106
|
+
try {
|
|
107
|
+
const flat = groups.flat();
|
|
108
|
+
const results = [];
|
|
109
|
+
for (const request of mergeRequests(flat, loaded.limits?.max_request_items)) {
|
|
110
|
+
const answered = await worker.judge(request);
|
|
111
|
+
if (!Array.isArray(answered) || answered.length !== request.length)
|
|
112
|
+
throw new DecisionError('model_failed', 'the local model did not answer every call');
|
|
113
|
+
results.push(...answered.map(checkResult));
|
|
114
|
+
}
|
|
115
|
+
return splitResults(groups, results);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
this.armIdle();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async ensure(selection) {
|
|
122
|
+
const key = `${selection.pack}\n${selection.model}`;
|
|
123
|
+
if (this.current?.key === key && this.worker?.running === true)
|
|
124
|
+
return await this.current.loading;
|
|
125
|
+
this.stop();
|
|
126
|
+
const pack = installedPack(this.home, selection.pack);
|
|
127
|
+
if (pack === undefined)
|
|
128
|
+
throw new DecisionError('not_installed', `the runtime pack ${selection.pack} is not installed`);
|
|
129
|
+
const model = readModel(this.home, selection.model);
|
|
130
|
+
if (model === undefined)
|
|
131
|
+
throw new DecisionError('not_installed', `the model ${selection.model} is not installed`);
|
|
132
|
+
if (backendOf(model.package_format) !== pack.backend) {
|
|
133
|
+
throw new DecisionError('invalid_request', `${model.id} needs a ${backendOf(model.package_format)} runtime pack, not ${pack.pack}`);
|
|
134
|
+
}
|
|
135
|
+
const worker = new WorkerProcess(packPython(this.home, pack));
|
|
136
|
+
this.worker = worker;
|
|
137
|
+
const current = {
|
|
138
|
+
key, selection,
|
|
139
|
+
loading: worker.load(model.package_format, modelRoot(this.home, model.id), pack.device),
|
|
140
|
+
};
|
|
141
|
+
this.current = current;
|
|
142
|
+
try {
|
|
143
|
+
current.loaded = await current.loading;
|
|
144
|
+
return current.loaded;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (this.current === current)
|
|
148
|
+
this.stop();
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
armIdle() {
|
|
153
|
+
clearTimeout(this.idleTimer);
|
|
154
|
+
this.idleTimer = setTimeout(() => this.stop(), this.idleMs);
|
|
155
|
+
this.idleTimer.unref();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local model packages: the published catalog and the installed packages.
|
|
3
|
+
*
|
|
4
|
+
* Two package formats exist:
|
|
5
|
+
* - `torch-release`: a published model repository (`manifest.json`, `decision.py`, `runtime/`,
|
|
6
|
+
* `tokenizer/`, `weights/`). Only the files needed to run it with its recommended fp32 weights
|
|
7
|
+
* are installed; training records, evaluation data and experimental weight formats are skipped.
|
|
8
|
+
* - `onnx-delivery`: a package of the vulcan-decision-shell-v1-runtime repository (`MANIFEST.json`
|
|
9
|
+
* format 2 naming its family and format, `runtime-config.json`, `assets/`, `model.onnx`),
|
|
10
|
+
* installed whole.
|
|
11
|
+
*
|
|
12
|
+
* Every installed file is checked against the package's own manifest while it is copied or
|
|
13
|
+
* downloaded, into a staging directory that replaces the installed package only when complete.
|
|
14
|
+
*/
|
|
15
|
+
import { type DecisionHome } from '../home.js';
|
|
16
|
+
import type { Backend, PackId } from './packs.js';
|
|
17
|
+
import type { ModelFormat } from './worker.js';
|
|
18
|
+
/** The model families published for local use. */
|
|
19
|
+
export declare const MODEL_FAMILIES: readonly ["mini", "flash", "standard"];
|
|
20
|
+
export type ModelFamily = typeof MODEL_FAMILIES[number];
|
|
21
|
+
/** Where a published model can be downloaded from. */
|
|
22
|
+
export declare const MODEL_SOURCES: readonly ["huggingface", "modelscope-intl", "modelscope-cn"];
|
|
23
|
+
export type ModelSource = typeof MODEL_SOURCES[number];
|
|
24
|
+
/** One published repository revision. */
|
|
25
|
+
export interface PublishedRepository {
|
|
26
|
+
readonly repo: string;
|
|
27
|
+
/** A commit, never a branch, so the files cannot change under the pins. */
|
|
28
|
+
readonly revision: string;
|
|
29
|
+
}
|
|
30
|
+
/** Precisions the runtime packages are published in. */
|
|
31
|
+
export declare const PACKAGE_PRECISIONS: readonly ["fp16", "fp32"];
|
|
32
|
+
export type PackagePrecision = typeof PACKAGE_PRECISIONS[number];
|
|
33
|
+
/** One package directory of the runtime repository, identical on every hub. */
|
|
34
|
+
export interface RuntimePackage {
|
|
35
|
+
/** Directory inside the repository, such as `mini/fp16`. */
|
|
36
|
+
readonly path: string;
|
|
37
|
+
/** SHA-256 of the package's `MANIFEST.json`. */
|
|
38
|
+
readonly manifestSha256: string;
|
|
39
|
+
}
|
|
40
|
+
export interface CatalogModel {
|
|
41
|
+
readonly family: ModelFamily;
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly parameters: string;
|
|
44
|
+
readonly format: ModelFormat;
|
|
45
|
+
/** The ONNX packages published for this family, by precision. */
|
|
46
|
+
readonly packages: Readonly<Partial<Record<PackagePrecision, RuntimePackage>>>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The vulcan-decision-shell-v1-runtime repository on each hub, holding every catalog package;
|
|
50
|
+
* null until it is published there.
|
|
51
|
+
*/
|
|
52
|
+
export declare const RUNTIME_SOURCES: Readonly<Record<ModelSource, PublishedRepository | null>>;
|
|
53
|
+
/** Published local models: ONNX packages of the runtime repository. */
|
|
54
|
+
export declare const MODEL_CATALOG: readonly CatalogModel[];
|
|
55
|
+
/** The hubs `model` can be downloaded from. */
|
|
56
|
+
export declare function sourcesOf(model: CatalogModel): ModelSource[];
|
|
57
|
+
/**
|
|
58
|
+
* The precision to download: fp16 where the CUDA ONNX pack runs, fp32 on the CPU, or the only
|
|
59
|
+
* precision published.
|
|
60
|
+
*/
|
|
61
|
+
export declare function preferredPrecision(model: CatalogModel, onnxPack: PackId): PackagePrecision | undefined;
|
|
62
|
+
/** The backend a package format runs on. */
|
|
63
|
+
export declare function backendOf(format: ModelFormat): Backend;
|
|
64
|
+
/** URL of one file of a published repository. */
|
|
65
|
+
export declare function repositoryFileUrl(source: ModelSource, published: PublishedRepository, file: string): string;
|
|
66
|
+
export interface FileDigest {
|
|
67
|
+
readonly bytes: number;
|
|
68
|
+
readonly sha256: string;
|
|
69
|
+
}
|
|
70
|
+
/** `model.json` of an installed package. */
|
|
71
|
+
export interface InstalledModel {
|
|
72
|
+
readonly format: 1;
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly family?: ModelFamily;
|
|
76
|
+
readonly package_format: ModelFormat;
|
|
77
|
+
readonly bytes: number;
|
|
78
|
+
readonly installed_at: string;
|
|
79
|
+
readonly source: {
|
|
80
|
+
readonly kind: 'directory';
|
|
81
|
+
readonly path: string;
|
|
82
|
+
} | {
|
|
83
|
+
readonly kind: 'download';
|
|
84
|
+
readonly source: ModelSource;
|
|
85
|
+
readonly repo: string;
|
|
86
|
+
readonly revision: string;
|
|
87
|
+
readonly path: string;
|
|
88
|
+
};
|
|
89
|
+
readonly files: Readonly<Record<string, FileDigest>>;
|
|
90
|
+
}
|
|
91
|
+
/** What a package directory or repository declares. */
|
|
92
|
+
interface PackageDeclaration {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
readonly name: string;
|
|
95
|
+
readonly format: ModelFormat;
|
|
96
|
+
readonly family?: ModelFamily;
|
|
97
|
+
/** Files to install, with their declared digests. */
|
|
98
|
+
readonly files: Readonly<Record<string, FileDigest>>;
|
|
99
|
+
}
|
|
100
|
+
/** Parse a package's manifest into what to install. */
|
|
101
|
+
export declare function declarationOf(manifestName: string, text: string): PackageDeclaration;
|
|
102
|
+
/** The files a manifest lists, refusing any path that is not a plain relative path with `/`. */
|
|
103
|
+
export declare function manifestFiles(manifest: Record<string, unknown>): Record<string, FileDigest>;
|
|
104
|
+
/** Every installed model with a readable `model.json`. */
|
|
105
|
+
export declare function listModels(home: DecisionHome): InstalledModel[];
|
|
106
|
+
export declare function readModel(home: DecisionHome, id: string): InstalledModel | undefined;
|
|
107
|
+
export declare function modelRoot(home: DecisionHome, id: string): string;
|
|
108
|
+
export interface ModelInstallOptions {
|
|
109
|
+
readonly signal?: AbortSignal;
|
|
110
|
+
readonly onProgress?: (done: number, total: number, file: string) => void;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Install the package in `dir`, a torch release repository or an ONNX delivery, verifying every
|
|
114
|
+
* file against the package's manifest.
|
|
115
|
+
*/
|
|
116
|
+
export declare function installModelFromDirectory(home: DecisionHome, dir: string, options?: ModelInstallOptions): Promise<InstalledModel>;
|
|
117
|
+
/**
|
|
118
|
+
* Download and install the `precision` package of a published catalog model from `source`.
|
|
119
|
+
* @throws Error when that package or the runtime repository is not published there yet.
|
|
120
|
+
*/
|
|
121
|
+
export declare function downloadModel(home: DecisionHome, family: ModelFamily, precision: PackagePrecision, source: ModelSource, options?: ModelInstallOptions): Promise<InstalledModel>;
|
|
122
|
+
/** Remove an installed model. */
|
|
123
|
+
export declare function removeModel(home: DecisionHome, id: string): boolean;
|
|
124
|
+
/** Verify every file of an installed model again. @returns the files that no longer match. */
|
|
125
|
+
export declare function verifyModel(home: DecisionHome, model: InstalledModel): Promise<string[]>;
|
|
126
|
+
export {};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local model packages: the published catalog and the installed packages.
|
|
3
|
+
*
|
|
4
|
+
* Two package formats exist:
|
|
5
|
+
* - `torch-release`: a published model repository (`manifest.json`, `decision.py`, `runtime/`,
|
|
6
|
+
* `tokenizer/`, `weights/`). Only the files needed to run it with its recommended fp32 weights
|
|
7
|
+
* are installed; training records, evaluation data and experimental weight formats are skipped.
|
|
8
|
+
* - `onnx-delivery`: a package of the vulcan-decision-shell-v1-runtime repository (`MANIFEST.json`
|
|
9
|
+
* format 2 naming its family and format, `runtime-config.json`, `assets/`, `model.onnx`),
|
|
10
|
+
* installed whole.
|
|
11
|
+
*
|
|
12
|
+
* Every installed file is checked against the package's own manifest while it is copied or
|
|
13
|
+
* downloaded, into a staging directory that replaces the installed package only when complete.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fetchPinned, matches, sha256Of } from '../download.js';
|
|
19
|
+
import { writeFileAtomic } from '../home.js';
|
|
20
|
+
import { replaceDirectory } from './packs.js';
|
|
21
|
+
/** The model families published for local use. */
|
|
22
|
+
export const MODEL_FAMILIES = ['mini', 'flash', 'standard'];
|
|
23
|
+
/** Where a published model can be downloaded from. */
|
|
24
|
+
export const MODEL_SOURCES = ['huggingface', 'modelscope-intl', 'modelscope-cn'];
|
|
25
|
+
/** Precisions the runtime packages are published in. */
|
|
26
|
+
export const PACKAGE_PRECISIONS = ['fp16', 'fp32'];
|
|
27
|
+
/**
|
|
28
|
+
* The vulcan-decision-shell-v1-runtime repository on each hub, holding every catalog package;
|
|
29
|
+
* null until it is published there.
|
|
30
|
+
*/
|
|
31
|
+
export const RUNTIME_SOURCES = {
|
|
32
|
+
'huggingface': { repo: 'openvulcan/vulcan-decision-shell-v1-runtime', revision: 'fd5e1d09f54df4133956bb9e192ac7771401d7d7' },
|
|
33
|
+
'modelscope-intl': null,
|
|
34
|
+
'modelscope-cn': { repo: 'OpenVulcan/vulcan-decision-shell-v1-runtime', revision: 'e96d96744c20f691ea6045cf21c232cbc8fa8e09' },
|
|
35
|
+
};
|
|
36
|
+
/** Published local models: ONNX packages of the runtime repository. */
|
|
37
|
+
export const MODEL_CATALOG = [
|
|
38
|
+
{
|
|
39
|
+
family: 'mini', name: 'vulcan-decision-shell-v1-mini', parameters: '0.2B', format: 'onnx-delivery',
|
|
40
|
+
packages: {
|
|
41
|
+
fp16: { path: 'mini/fp16', manifestSha256: 'dc57ba263b4b32c1443b78b42ce81371aed85f93fb3f3afd5e5f77afd059dbf9' },
|
|
42
|
+
fp32: { path: 'mini/fp32', manifestSha256: 'eb3d48b93d030b9f67bbcf7fcea1179b854287b44e5c16b72a8e0be9694c194e' },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
family: 'flash', name: 'vulcan-decision-shell-v1-flash', parameters: '0.35B', format: 'onnx-delivery',
|
|
47
|
+
packages: {
|
|
48
|
+
fp16: { path: 'flash/fp16', manifestSha256: '075e61a05063272b13d966c1f9b6e55ce99acbf5055de16b675ae1b442e6ffed' },
|
|
49
|
+
fp32: { path: 'flash/fp32', manifestSha256: '6b652427b61922ea573fd3509ed41b47cbc6037ffa47db7d6edf325cd12c4374' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
family: 'standard', name: 'vulcan-decision-shell-v1', parameters: '0.8B', format: 'onnx-delivery',
|
|
54
|
+
packages: {
|
|
55
|
+
fp16: { path: 'standard/fp16', manifestSha256: 'de1d7e101998b4ac978038c2ae3251a0fdb86567ec22b263fe7cec5b76b26ade' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
/** The hubs `model` can be downloaded from. */
|
|
60
|
+
export function sourcesOf(model) {
|
|
61
|
+
return Object.keys(model.packages).length === 0 ? [] : MODEL_SOURCES.filter(source => RUNTIME_SOURCES[source] !== null);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The precision to download: fp16 where the CUDA ONNX pack runs, fp32 on the CPU, or the only
|
|
65
|
+
* precision published.
|
|
66
|
+
*/
|
|
67
|
+
export function preferredPrecision(model, onnxPack) {
|
|
68
|
+
const preferred = onnxPack === 'onnx-cuda' ? 'fp16' : 'fp32';
|
|
69
|
+
return model.packages[preferred] === undefined ? PACKAGE_PRECISIONS.find(precision => model.packages[precision] !== undefined) : preferred;
|
|
70
|
+
}
|
|
71
|
+
/** The backend a package format runs on. */
|
|
72
|
+
export function backendOf(format) {
|
|
73
|
+
return format === 'onnx-delivery' ? 'onnx' : 'torch';
|
|
74
|
+
}
|
|
75
|
+
/** URL of one file of a published repository. */
|
|
76
|
+
export function repositoryFileUrl(source, published, file) {
|
|
77
|
+
const encoded = file.split('/').map(encodeURIComponent).join('/');
|
|
78
|
+
switch (source) {
|
|
79
|
+
case 'huggingface':
|
|
80
|
+
return `https://huggingface.co/${published.repo}/resolve/${published.revision}/${encoded}`;
|
|
81
|
+
case 'modelscope-intl':
|
|
82
|
+
return `https://www.modelscope.ai/models/${published.repo}/resolve/${published.revision}/${encoded}`;
|
|
83
|
+
case 'modelscope-cn':
|
|
84
|
+
return `https://www.modelscope.cn/models/${published.repo}/resolve/${published.revision}/${encoded}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The only `MANIFEST.json` format of ONNX packages this version reads. */
|
|
88
|
+
const ONNX_MANIFEST_FORMAT = 2;
|
|
89
|
+
/** Files of a torch release that running it needs, besides the recommended weights. */
|
|
90
|
+
const TORCH_RUNTIME_PATHS = [/^decision\.py$/, /^runtime\//, /^tokenizer\//, /^weights\/formats\.json$/, /^weights\/fp32\//,
|
|
91
|
+
/^(LICENSE|LICENSE-DATA\.md|NOTICE\.md|README(\.zh)?\.md|config\.json|configuration\.json)$/, /^licenses\//];
|
|
92
|
+
/** Parse a package's manifest into what to install. */
|
|
93
|
+
export function declarationOf(manifestName, text) {
|
|
94
|
+
const manifest = JSON.parse(text);
|
|
95
|
+
const files = manifestFiles(manifest);
|
|
96
|
+
if (manifestName === 'manifest.json') {
|
|
97
|
+
const name = manifest.model;
|
|
98
|
+
if (typeof name !== 'string' || !/^[a-z0-9][a-z0-9.-]{0,63}$/.test(name))
|
|
99
|
+
throw new Error('manifest.json names no model');
|
|
100
|
+
const selected = Object.fromEntries(Object.entries(files).filter(([file]) => TORCH_RUNTIME_PATHS.some(pattern => pattern.test(file))));
|
|
101
|
+
for (const required of ['decision.py', 'weights/formats.json', 'weights/fp32/model.safetensors']) {
|
|
102
|
+
if (selected[required] === undefined)
|
|
103
|
+
throw new Error(`manifest.json does not list ${required}`);
|
|
104
|
+
}
|
|
105
|
+
return { id: name, name, format: 'torch-release', files: selected };
|
|
106
|
+
}
|
|
107
|
+
if (manifest.format_version !== ONNX_MANIFEST_FORMAT) {
|
|
108
|
+
throw new Error(`MANIFEST.json format ${String(manifest.format_version)} is not supported; use a vulcan-decision-shell-v1-runtime package`);
|
|
109
|
+
}
|
|
110
|
+
const family = MODEL_FAMILIES.find(known => known === manifest.family);
|
|
111
|
+
const format = manifest.format;
|
|
112
|
+
if (family === undefined || typeof format !== 'string' || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(format)) {
|
|
113
|
+
throw new Error('MANIFEST.json names no known family and format');
|
|
114
|
+
}
|
|
115
|
+
for (const required of ['runtime-config.json', 'assets/policy-v2.json', 'assets/tokenizer.json', 'model.onnx']) {
|
|
116
|
+
if (files[required] === undefined)
|
|
117
|
+
throw new Error(`MANIFEST.json does not list ${required}`);
|
|
118
|
+
}
|
|
119
|
+
const name = `vulcan-decision-shell-v1-${family}-${format}`;
|
|
120
|
+
return { id: name, name, format: 'onnx-delivery', family, files };
|
|
121
|
+
}
|
|
122
|
+
/** The files a manifest lists, refusing any path that is not a plain relative path with `/`. */
|
|
123
|
+
export function manifestFiles(manifest) {
|
|
124
|
+
const listed = manifest.files;
|
|
125
|
+
if (typeof listed !== 'object' || listed === null)
|
|
126
|
+
throw new Error('the manifest lists no files');
|
|
127
|
+
const files = {};
|
|
128
|
+
for (const [relative, entry] of Object.entries(listed)) {
|
|
129
|
+
const segments = relative.split('/');
|
|
130
|
+
if (relative === '' || relative.includes('\\') || segments.some(segment => segment === '' || segment === '.' || segment === '..' || /^[A-Za-z]:/.test(segment))) {
|
|
131
|
+
throw new Error(`the manifest lists an unsafe path ${relative}`);
|
|
132
|
+
}
|
|
133
|
+
if (typeof entry.bytes !== 'number' || typeof entry.sha256 !== 'string' || !/^[0-9a-f]{64}$/i.test(entry.sha256)) {
|
|
134
|
+
throw new Error(`the manifest entry ${relative} is malformed`);
|
|
135
|
+
}
|
|
136
|
+
files[relative] = { bytes: entry.bytes, sha256: entry.sha256.toLowerCase() };
|
|
137
|
+
}
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
/** Every installed model with a readable `model.json`. */
|
|
141
|
+
export function listModels(home) {
|
|
142
|
+
const models = [];
|
|
143
|
+
for (const entry of fs.readdirSync(home.models, { withFileTypes: true })) {
|
|
144
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
145
|
+
continue;
|
|
146
|
+
const model = readModel(home, entry.name);
|
|
147
|
+
if (model !== undefined)
|
|
148
|
+
models.push(model);
|
|
149
|
+
}
|
|
150
|
+
return models.sort((a, b) => a.id.localeCompare(b.id));
|
|
151
|
+
}
|
|
152
|
+
export function readModel(home, id) {
|
|
153
|
+
try {
|
|
154
|
+
const model = JSON.parse(fs.readFileSync(path.join(home.models, id, 'model.json'), 'utf8'));
|
|
155
|
+
return model.format === 1 && model.id === id ? model : undefined;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export function modelRoot(home, id) {
|
|
162
|
+
return path.join(home.models, id);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Install the package in `dir`, a torch release repository or an ONNX delivery, verifying every
|
|
166
|
+
* file against the package's manifest.
|
|
167
|
+
*/
|
|
168
|
+
export async function installModelFromDirectory(home, dir, options = {}) {
|
|
169
|
+
const root = path.resolve(dir);
|
|
170
|
+
const manifestName = ['manifest.json', 'MANIFEST.json'].find(name => fs.readdirSync(root).includes(name));
|
|
171
|
+
if (manifestName === undefined)
|
|
172
|
+
throw new Error(`${root} has no manifest.json or MANIFEST.json`);
|
|
173
|
+
const manifestText = fs.readFileSync(path.join(root, manifestName), 'utf8');
|
|
174
|
+
const declaration = declarationOf(manifestName, manifestText);
|
|
175
|
+
return await installPackage(home, declaration, manifestName, manifestText, { kind: 'directory', path: root }, async (file, dest) => {
|
|
176
|
+
fs.copyFileSync(path.join(root, ...file.split('/')), dest);
|
|
177
|
+
}, options);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Download and install the `precision` package of a published catalog model from `source`.
|
|
181
|
+
* @throws Error when that package or the runtime repository is not published there yet.
|
|
182
|
+
*/
|
|
183
|
+
export async function downloadModel(home, family, precision, source, options = {}) {
|
|
184
|
+
const pinned = MODEL_CATALOG.find(model => model.family === family)?.packages[precision];
|
|
185
|
+
if (pinned === undefined)
|
|
186
|
+
throw new Error(`${family} has no published ${precision} package`);
|
|
187
|
+
const published = RUNTIME_SOURCES[source];
|
|
188
|
+
if (published === null)
|
|
189
|
+
throw new Error(`the runtime packages are not published on ${source} yet; install one from a directory instead`);
|
|
190
|
+
const url = (file) => repositoryFileUrl(source, published, `${pinned.path}/${file}`);
|
|
191
|
+
const response = await fetch(url('MANIFEST.json'), options.signal === undefined ? {} : { signal: options.signal });
|
|
192
|
+
if (!response.ok)
|
|
193
|
+
throw new Error(`cannot download MANIFEST.json: HTTP ${response.status}`);
|
|
194
|
+
const manifestText = await response.text();
|
|
195
|
+
const digest = createHash('sha256').update(manifestText, 'utf8').digest('hex');
|
|
196
|
+
if (digest !== pinned.manifestSha256)
|
|
197
|
+
throw new Error('the published MANIFEST.json does not match its pin');
|
|
198
|
+
const declaration = declarationOf('MANIFEST.json', manifestText);
|
|
199
|
+
if (declaration.family !== family)
|
|
200
|
+
throw new Error(`${pinned.path}/MANIFEST.json declares the ${String(declaration.family)} family`);
|
|
201
|
+
const work = path.join(home.downloads, `model-${pinned.manifestSha256.slice(0, 16)}`);
|
|
202
|
+
return await installPackage(home, declaration, 'MANIFEST.json', manifestText, { kind: 'download', source, repo: published.repo, revision: published.revision, path: pinned.path }, async (file, dest, digest) => {
|
|
203
|
+
const cached = path.join(work, ...file.split('/'));
|
|
204
|
+
await fetchPinned({ name: file, url: url(file), ...digest }, cached, options.signal === undefined ? {} : { signal: options.signal });
|
|
205
|
+
fs.renameSync(cached, dest);
|
|
206
|
+
}, options).finally(() => fs.rmSync(work, { recursive: true, force: true }));
|
|
207
|
+
}
|
|
208
|
+
async function installPackage(home, declaration, manifestName, manifestText, source, place, options) {
|
|
209
|
+
const staging = path.join(home.models, `.staging-${declaration.id}`);
|
|
210
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
211
|
+
fs.mkdirSync(staging, { recursive: true });
|
|
212
|
+
try {
|
|
213
|
+
const total = Object.values(declaration.files).reduce((sum, file) => sum + file.bytes, 0);
|
|
214
|
+
let done = 0;
|
|
215
|
+
for (const [file, digest] of Object.entries(declaration.files)) {
|
|
216
|
+
options.signal?.throwIfAborted();
|
|
217
|
+
options.onProgress?.(done, total, file);
|
|
218
|
+
const dest = path.join(staging, ...file.split('/'));
|
|
219
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
220
|
+
await place(file, dest, digest);
|
|
221
|
+
if (!await matches(dest, digest))
|
|
222
|
+
throw new Error(`${file} does not match the package manifest`);
|
|
223
|
+
done += digest.bytes;
|
|
224
|
+
}
|
|
225
|
+
options.onProgress?.(total, total, '');
|
|
226
|
+
fs.writeFileSync(path.join(staging, manifestName), manifestText);
|
|
227
|
+
const family = declaration.family ?? MODEL_CATALOG.find(model => model.name === declaration.name)?.family;
|
|
228
|
+
const model = {
|
|
229
|
+
format: 1, id: declaration.id, name: declaration.name, ...family === undefined ? {} : { family },
|
|
230
|
+
package_format: declaration.format, bytes: total, installed_at: new Date().toISOString(), source, files: declaration.files,
|
|
231
|
+
};
|
|
232
|
+
writeFileAtomic(path.join(staging, 'model.json'), `${JSON.stringify(model, null, 2)}\n`);
|
|
233
|
+
replaceDirectory(staging, modelRoot(home, declaration.id));
|
|
234
|
+
return model;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Remove an installed model. */
|
|
242
|
+
export function removeModel(home, id) {
|
|
243
|
+
if (!/^[a-z0-9][a-z0-9.-]*$/.test(id))
|
|
244
|
+
return false;
|
|
245
|
+
const dir = modelRoot(home, id);
|
|
246
|
+
const existed = fs.existsSync(dir);
|
|
247
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
248
|
+
return existed;
|
|
249
|
+
}
|
|
250
|
+
/** Verify every file of an installed model again. @returns the files that no longer match. */
|
|
251
|
+
export async function verifyModel(home, model) {
|
|
252
|
+
const root = modelRoot(home, model.id);
|
|
253
|
+
const broken = [];
|
|
254
|
+
for (const [file, digest] of Object.entries(model.files)) {
|
|
255
|
+
const local = path.join(root, ...file.split('/'));
|
|
256
|
+
const stat = fs.statSync(local, { throwIfNoEntry: false });
|
|
257
|
+
if (stat?.size !== digest.bytes || await sha256Of(local) !== digest.sha256)
|
|
258
|
+
broken.push(file);
|
|
259
|
+
}
|
|
260
|
+
return broken;
|
|
261
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime packs: the optional local runtimes, installed on demand next to the cloud-only default.
|
|
3
|
+
*
|
|
4
|
+
* A pack is a prebuilt python-build-standalone interpreter plus the official wheels its lock file
|
|
5
|
+
* (`runtime-packs/<pack>.json`, written by `scripts/lock_runtime.py`) pins for this platform.
|
|
6
|
+
* Installing downloads exactly those files, verifies each against its size and SHA-256, unpacks
|
|
7
|
+
* the interpreter, installs the wheels offline with the interpreter's own pip and checks that the
|
|
8
|
+
* runtime imports. Nothing is compiled and no driver is installed: GPU packs bring their CUDA
|
|
9
|
+
* libraries as NVIDIA's wheels and only need a recent enough driver.
|
|
10
|
+
*/
|
|
11
|
+
import { type DownloadSource, type PinnedFile } from '../download.js';
|
|
12
|
+
import { type DecisionHome } from '../home.js';
|
|
13
|
+
import { type ProbeResult } from './worker.js';
|
|
14
|
+
/** Packs with a lock file, in the order they are offered. */
|
|
15
|
+
export declare const PACK_IDS: readonly ["onnx-cpu", "onnx-cuda", "torch-cpu", "torch-cu128"];
|
|
16
|
+
export type PackId = typeof PACK_IDS[number];
|
|
17
|
+
export type Backend = 'onnx' | 'torch';
|
|
18
|
+
export type Device = 'cpu' | 'cuda';
|
|
19
|
+
/**
|
|
20
|
+
* Packs waiting for a release: an NPU needs an ONNX model exported for it, which is not
|
|
21
|
+
* published yet. They are listed so a host can show what this machine will be able to use.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PLANNED_PACKS: readonly [{
|
|
24
|
+
readonly id: "onnx-openvino";
|
|
25
|
+
readonly backend: "onnx";
|
|
26
|
+
readonly accelerator: "intel-npu";
|
|
27
|
+
}, {
|
|
28
|
+
readonly id: "onnx-qnn";
|
|
29
|
+
readonly backend: "onnx";
|
|
30
|
+
readonly accelerator: "qualcomm-npu";
|
|
31
|
+
}];
|
|
32
|
+
/** Platforms a pack can target, named as in the lock files. */
|
|
33
|
+
export type Target = 'win-x64' | 'linux-x64' | 'linux-arm64' | 'mac-arm64';
|
|
34
|
+
export interface LockedFile extends PinnedFile {
|
|
35
|
+
readonly kind: 'python' | 'wheel';
|
|
36
|
+
}
|
|
37
|
+
export interface PackLock {
|
|
38
|
+
readonly format: 1;
|
|
39
|
+
/** Identity of the exact file set; an installed pack with another id is outdated. */
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly pack: PackId;
|
|
42
|
+
readonly backend: Backend;
|
|
43
|
+
readonly device: Device;
|
|
44
|
+
readonly python: string;
|
|
45
|
+
/** Lowest NVIDIA driver version and CUDA compute capability, for GPU packs. */
|
|
46
|
+
readonly requires: {
|
|
47
|
+
readonly nvidia_driver?: string;
|
|
48
|
+
readonly compute_capability?: string;
|
|
49
|
+
};
|
|
50
|
+
readonly targets: Readonly<Partial<Record<Target, {
|
|
51
|
+
readonly bytes: number;
|
|
52
|
+
readonly macos?: string;
|
|
53
|
+
readonly files: readonly LockedFile[];
|
|
54
|
+
}>>>;
|
|
55
|
+
}
|
|
56
|
+
/** `pack.json` of an installed pack. */
|
|
57
|
+
export interface InstalledPack {
|
|
58
|
+
readonly format: 1;
|
|
59
|
+
readonly pack: PackId;
|
|
60
|
+
readonly lock: string;
|
|
61
|
+
readonly target: Target;
|
|
62
|
+
readonly backend: Backend;
|
|
63
|
+
readonly device: Device;
|
|
64
|
+
/** Interpreter path relative to the pack directory. */
|
|
65
|
+
readonly python: string;
|
|
66
|
+
readonly installed_at: string;
|
|
67
|
+
readonly probe: ProbeResult;
|
|
68
|
+
}
|
|
69
|
+
/** The target this process runs on, or undefined when no pack supports it. */
|
|
70
|
+
export declare function currentTarget(platform?: NodeJS.Platform, arch?: string): Target | undefined;
|
|
71
|
+
export declare function isPackId(value: unknown): value is PackId;
|
|
72
|
+
/** The lock file shipped with this package for `pack`. */
|
|
73
|
+
export declare function readLock(pack: PackId): PackLock;
|
|
74
|
+
/** The installed pack in `home`, or undefined when it is missing or unreadable. */
|
|
75
|
+
export declare function installedPack(home: DecisionHome, pack: PackId): InstalledPack | undefined;
|
|
76
|
+
/** The interpreter of an installed pack. */
|
|
77
|
+
export declare function packPython(home: DecisionHome, record: InstalledPack): string;
|
|
78
|
+
/** Installation progress, reported as it happens. */
|
|
79
|
+
export type InstallProgress = {
|
|
80
|
+
readonly phase: 'download';
|
|
81
|
+
readonly done: number;
|
|
82
|
+
readonly total: number;
|
|
83
|
+
readonly file: string;
|
|
84
|
+
} | {
|
|
85
|
+
readonly phase: 'unpack' | 'install' | 'probe';
|
|
86
|
+
};
|
|
87
|
+
export interface InstallOptions {
|
|
88
|
+
readonly source?: DownloadSource;
|
|
89
|
+
readonly signal?: AbortSignal;
|
|
90
|
+
readonly onProgress?: (progress: InstallProgress) => void;
|
|
91
|
+
readonly target?: Target;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Install `pack` for this platform, replacing an installed one only once the new one works.
|
|
95
|
+
* @throws Error when the pack has no build for this platform, a download fails verification, or
|
|
96
|
+
* the installed runtime does not start.
|
|
97
|
+
*/
|
|
98
|
+
export declare function installPack(home: DecisionHome, pack: PackId, options?: InstallOptions): Promise<InstalledPack>;
|
|
99
|
+
/** Remove an installed pack and any partial download of it. */
|
|
100
|
+
export declare function removePack(home: DecisionHome, pack: PackId): boolean;
|
|
101
|
+
/** Move `staging` to `dest`, keeping the old `dest` until the move succeeded. */
|
|
102
|
+
export declare function replaceDirectory(staging: string, dest: string): void;
|