@bendyline/gezel 0.1.0
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 +39 -0
- package/dist/checks/index.d.ts +693 -0
- package/dist/checks/index.js +1848 -0
- package/dist/device-safety-DezzpNyR.d.ts +10 -0
- package/dist/index-D_dch9Qh.d.ts +59398 -0
- package/dist/index.d.ts +3217 -0
- package/dist/index.js +25602 -0
- package/dist/markdown/index.d.ts +174 -0
- package/dist/markdown/index.js +4022 -0
- package/dist/native/index.d.ts +650 -0
- package/dist/native/index.js +1121 -0
- package/dist/paths.d.ts +578 -0
- package/dist/paths.js +573 -0
- package/dist/report-action-DzdQHzGG.d.ts +4270 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.js +15382 -0
- package/package.json +85 -0
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
// src/native/llama-backend.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import {
|
|
4
|
+
constants,
|
|
5
|
+
accessSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from "fs";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import { arch as nodeArch, platform as nodePlatform } from "process";
|
|
13
|
+
function resolveAvailableLlamaBinary(preferredBackend, resolveBinary, allowFallbacks, isUsable) {
|
|
14
|
+
const fallbackOrder = {
|
|
15
|
+
cuda: ["cuda", "vulkan", "cpu"],
|
|
16
|
+
vulkan: ["vulkan", "cpu"],
|
|
17
|
+
metal: ["metal", "cpu"],
|
|
18
|
+
cpu: ["cpu"]
|
|
19
|
+
};
|
|
20
|
+
const candidates = allowFallbacks ? fallbackOrder[preferredBackend] : [preferredBackend];
|
|
21
|
+
const skippedUnusable = [];
|
|
22
|
+
for (const backend of candidates) {
|
|
23
|
+
const path = resolveBinary(backend);
|
|
24
|
+
if (!path) continue;
|
|
25
|
+
if (allowFallbacks && isUsable && !isUsable(backend, path)) {
|
|
26
|
+
skippedUnusable.push(backend);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
backend,
|
|
31
|
+
path,
|
|
32
|
+
...backend === preferredBackend ? {} : { fallbackFrom: preferredBackend },
|
|
33
|
+
...skippedUnusable.length > 0 ? { skippedUnusable: [...skippedUnusable] } : {}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
var PROBE_SCHEMA_VERSION = 1;
|
|
39
|
+
function fileExistsDefault(path) {
|
|
40
|
+
try {
|
|
41
|
+
accessSync(path, constants.F_OK);
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function anyExists(paths, probe) {
|
|
48
|
+
for (const p of paths) {
|
|
49
|
+
if (probe(p)) return p;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function commandOkDefault(cmd) {
|
|
54
|
+
try {
|
|
55
|
+
execSync(cmd, { stdio: "ignore" });
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function readFileTextDefault(path) {
|
|
62
|
+
try {
|
|
63
|
+
return readFileSync(path, "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function readDirDefault(path) {
|
|
69
|
+
try {
|
|
70
|
+
return readdirSync(path);
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function detectVendor(os, probeFile, readFileText, readDir) {
|
|
76
|
+
if (os === "linux") {
|
|
77
|
+
const found = /* @__PURE__ */ new Set();
|
|
78
|
+
for (const entry of readDir("/sys/class/drm")) {
|
|
79
|
+
if (!/^card\d+$/.test(entry)) continue;
|
|
80
|
+
const vid = readFileText(`/sys/class/drm/${entry}/device/vendor`)?.trim().toLowerCase();
|
|
81
|
+
if (vid === "0x1002") found.add("amd");
|
|
82
|
+
else if (vid === "0x10de") found.add("nvidia");
|
|
83
|
+
else if (vid === "0x8086") found.add("intel");
|
|
84
|
+
}
|
|
85
|
+
if (found.has("nvidia")) return "nvidia";
|
|
86
|
+
if (found.has("amd")) return "amd";
|
|
87
|
+
if (found.has("intel")) return "intel";
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
const sys32 = process.env.SYSTEMROOT ? join(process.env.SYSTEMROOT, "System32") : "C:\\Windows\\System32";
|
|
91
|
+
if (probeFile(join(sys32, "nvcuda.dll")) || probeFile(join(sys32, "nvapi64.dll"))) {
|
|
92
|
+
return "nvidia";
|
|
93
|
+
}
|
|
94
|
+
if (probeFile(join(sys32, "aticfx64.dll")) || probeFile(join(sys32, "amdxc64.dll")) || probeFile(join(sys32, "amdhip64.dll"))) {
|
|
95
|
+
return "amd";
|
|
96
|
+
}
|
|
97
|
+
if (probeFile(join(sys32, "igdumdim64.dll")) || probeFile(join(sys32, "igc64.dll"))) {
|
|
98
|
+
return "intel";
|
|
99
|
+
}
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
function detectLinuxOrWin(os, probeFile, probeCmd, readFileText, readDir) {
|
|
103
|
+
const vendorHint = detectVendor(os, probeFile, readFileText, readDir);
|
|
104
|
+
if (os === "linux") {
|
|
105
|
+
const libcuda = anyExists(
|
|
106
|
+
[
|
|
107
|
+
"/usr/lib/x86_64-linux-gnu/libcuda.so.1",
|
|
108
|
+
"/usr/lib/aarch64-linux-gnu/libcuda.so.1",
|
|
109
|
+
"/usr/lib64/libcuda.so.1",
|
|
110
|
+
"/usr/lib/libcuda.so.1",
|
|
111
|
+
"/lib/x86_64-linux-gnu/libcuda.so.1",
|
|
112
|
+
"/lib/aarch64-linux-gnu/libcuda.so.1"
|
|
113
|
+
],
|
|
114
|
+
probeFile
|
|
115
|
+
);
|
|
116
|
+
if (libcuda) {
|
|
117
|
+
const smiOk = probeCmd("nvidia-smi -L");
|
|
118
|
+
return {
|
|
119
|
+
backend: "cuda",
|
|
120
|
+
reason: `found ${libcuda}${smiOk ? ", nvidia-smi ok" : ", nvidia-smi absent or failing (proceeding anyway)"}`,
|
|
121
|
+
vendorHint
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
const sys32 = process.env.SYSTEMROOT ? join(process.env.SYSTEMROOT, "System32") : "C:\\Windows\\System32";
|
|
126
|
+
const nvcuda = anyExists([join(sys32, "nvcuda.dll"), join(sys32, "nvml.dll")], probeFile);
|
|
127
|
+
if (nvcuda) {
|
|
128
|
+
return { backend: "cuda", reason: `found ${nvcuda}`, vendorHint };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (os === "linux") {
|
|
132
|
+
const libvulkan = anyExists(
|
|
133
|
+
[
|
|
134
|
+
"/usr/lib/x86_64-linux-gnu/libvulkan.so.1",
|
|
135
|
+
"/usr/lib/aarch64-linux-gnu/libvulkan.so.1",
|
|
136
|
+
"/usr/lib64/libvulkan.so.1",
|
|
137
|
+
"/usr/lib/libvulkan.so.1"
|
|
138
|
+
],
|
|
139
|
+
probeFile
|
|
140
|
+
);
|
|
141
|
+
if (libvulkan) {
|
|
142
|
+
return { backend: "vulkan", reason: `found ${libvulkan}`, vendorHint };
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
const sys32 = process.env.SYSTEMROOT ? join(process.env.SYSTEMROOT, "System32") : "C:\\Windows\\System32";
|
|
146
|
+
const vk = anyExists([join(sys32, "vulkan-1.dll")], probeFile);
|
|
147
|
+
if (vk) {
|
|
148
|
+
return { backend: "vulkan", reason: `found ${vk}`, vendorHint };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { backend: "cpu", reason: "no CUDA driver and no Vulkan loader found", vendorHint };
|
|
152
|
+
}
|
|
153
|
+
function detectLlamaBackend(input) {
|
|
154
|
+
const probed = probeOrCached(input);
|
|
155
|
+
if (input.override && input.override !== "auto") {
|
|
156
|
+
return {
|
|
157
|
+
backend: input.override,
|
|
158
|
+
detectedBackend: probed.backend,
|
|
159
|
+
cached: probed.cached,
|
|
160
|
+
reason: `pinned by config.llamaCppBackendOverride=${input.override} (hardware probe: ${probed.reason})`,
|
|
161
|
+
probedAt: probed.probedAt,
|
|
162
|
+
...probed.vendorHint ? { vendorHint: probed.vendorHint } : {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
backend: probed.backend,
|
|
167
|
+
detectedBackend: probed.backend,
|
|
168
|
+
cached: probed.cached,
|
|
169
|
+
reason: probed.reason,
|
|
170
|
+
probedAt: probed.probedAt,
|
|
171
|
+
...probed.vendorHint ? { vendorHint: probed.vendorHint } : {}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function probeOrCached(input) {
|
|
175
|
+
const probeFile = input.probe?.fileExists ?? fileExistsDefault;
|
|
176
|
+
const probeCmd = input.probe?.commandOk ?? commandOkDefault;
|
|
177
|
+
const probeReadFile = input.probe?.readFileText ?? readFileTextDefault;
|
|
178
|
+
const probeReadDir = input.probe?.readDir ?? readDirDefault;
|
|
179
|
+
const cachePath = join(input.home, "engines", "llama-cpp", "backend.json");
|
|
180
|
+
if (fileExistsDefault(cachePath)) {
|
|
181
|
+
try {
|
|
182
|
+
const cached = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
183
|
+
const schemaOk = (cached.probeSchemaVersion ?? 0) >= PROBE_SCHEMA_VERSION;
|
|
184
|
+
if (cached.engineVersion === input.engineVersion && schemaOk) {
|
|
185
|
+
return {
|
|
186
|
+
backend: cached.backend,
|
|
187
|
+
cached: true,
|
|
188
|
+
reason: `cached: ${cached.reason}`,
|
|
189
|
+
probedAt: cached.probedAt,
|
|
190
|
+
...cached.vendorHint ? { vendorHint: cached.vendorHint } : {}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const plat = input.probe?.platform ?? nodePlatform;
|
|
197
|
+
const ar = input.probe?.arch ?? nodeArch;
|
|
198
|
+
let result;
|
|
199
|
+
if (plat === "darwin") {
|
|
200
|
+
if (ar === "arm64") {
|
|
201
|
+
result = { backend: "metal", reason: "Apple Silicon \u2014 Metal always available" };
|
|
202
|
+
} else {
|
|
203
|
+
result = { backend: "cpu", reason: "Intel Mac \u2014 no Metal path, using CPU" };
|
|
204
|
+
}
|
|
205
|
+
} else if (plat === "linux" && (ar === "x64" || ar === "arm64")) {
|
|
206
|
+
result = detectLinuxOrWin("linux", probeFile, probeCmd, probeReadFile, probeReadDir);
|
|
207
|
+
} else if (plat === "win32" && ar === "x64") {
|
|
208
|
+
result = detectLinuxOrWin("win32", probeFile, probeCmd, probeReadFile, probeReadDir);
|
|
209
|
+
} else {
|
|
210
|
+
result = { backend: "cpu", reason: `unsupported platform ${plat}/${ar} \u2014 CPU only` };
|
|
211
|
+
}
|
|
212
|
+
const probedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
213
|
+
try {
|
|
214
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
215
|
+
const payload = {
|
|
216
|
+
probeSchemaVersion: PROBE_SCHEMA_VERSION,
|
|
217
|
+
engineVersion: input.engineVersion,
|
|
218
|
+
backend: result.backend,
|
|
219
|
+
reason: result.reason,
|
|
220
|
+
probedAt,
|
|
221
|
+
...result.vendorHint ? { vendorHint: result.vendorHint } : {}
|
|
222
|
+
};
|
|
223
|
+
writeFileSync(cachePath, JSON.stringify(payload, null, 2));
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
backend: result.backend,
|
|
228
|
+
cached: false,
|
|
229
|
+
reason: result.reason,
|
|
230
|
+
probedAt,
|
|
231
|
+
...result.vendorHint ? { vendorHint: result.vendorHint } : {}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/native/llama-quarantine.ts
|
|
236
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
237
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
238
|
+
var SCHEMA_VERSION = 1;
|
|
239
|
+
function llamaQuarantinePath(home) {
|
|
240
|
+
return join2(home, "engines", "llama-cpp", "unusable.json");
|
|
241
|
+
}
|
|
242
|
+
function binaryFingerprint(path, io = {}) {
|
|
243
|
+
const statFile = io.statFile ?? ((p) => statSync(p));
|
|
244
|
+
try {
|
|
245
|
+
const info = statFile(path);
|
|
246
|
+
return `${info.size}:${Math.trunc(info.mtimeMs)}`;
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function readLlamaQuarantine(home, io = {}) {
|
|
252
|
+
const readFile = io.readFile ?? ((p) => readFileSync2(p, "utf8"));
|
|
253
|
+
try {
|
|
254
|
+
const parsed = JSON.parse(readFile(llamaQuarantinePath(home)));
|
|
255
|
+
if (parsed.schemaVersion !== SCHEMA_VERSION || !Array.isArray(parsed.entries)) return [];
|
|
256
|
+
return parsed.entries.filter(isEntry);
|
|
257
|
+
} catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function isBinaryQuarantined(entries, backend, binaryPath, io = {}) {
|
|
262
|
+
const entry = entries.find((e) => e.backend === backend);
|
|
263
|
+
if (!entry) return false;
|
|
264
|
+
const current = binaryFingerprint(binaryPath, io);
|
|
265
|
+
return current !== null && current === entry.fingerprint;
|
|
266
|
+
}
|
|
267
|
+
function recordLlamaQuarantine(home, input, io = {}) {
|
|
268
|
+
const fingerprint = binaryFingerprint(input.binaryPath, io);
|
|
269
|
+
if (!fingerprint) return null;
|
|
270
|
+
const now = io.now?.() ?? /* @__PURE__ */ new Date();
|
|
271
|
+
const entry = {
|
|
272
|
+
backend: input.backend,
|
|
273
|
+
fingerprint,
|
|
274
|
+
signal: input.signal,
|
|
275
|
+
reason: input.reason,
|
|
276
|
+
at: now.toISOString()
|
|
277
|
+
};
|
|
278
|
+
const kept = readLlamaQuarantine(home, io).filter((e) => e.backend !== input.backend);
|
|
279
|
+
const path = llamaQuarantinePath(home);
|
|
280
|
+
const mkdir = io.mkdir ?? ((p) => void mkdirSync2(p, { recursive: true }));
|
|
281
|
+
const writeFile = io.writeFile ?? ((p, data) => writeFileSync2(p, data, "utf8"));
|
|
282
|
+
const file = { schemaVersion: SCHEMA_VERSION, entries: [...kept, entry] };
|
|
283
|
+
mkdir(dirname2(path));
|
|
284
|
+
writeFile(path, `${JSON.stringify(file, null, 2)}
|
|
285
|
+
`);
|
|
286
|
+
return entry;
|
|
287
|
+
}
|
|
288
|
+
function isEntry(value) {
|
|
289
|
+
if (value === null || typeof value !== "object") return false;
|
|
290
|
+
const e = value;
|
|
291
|
+
return typeof e.backend === "string" && typeof e.fingerprint === "string" && typeof e.signal === "string" && typeof e.reason === "string" && typeof e.at === "string";
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/native/discover.ts
|
|
295
|
+
import { existsSync } from "fs";
|
|
296
|
+
import { join as join3 } from "path";
|
|
297
|
+
|
|
298
|
+
// src/native/llama-engine-version.ts
|
|
299
|
+
var LLAMA_ENGINE_VERSION = "b10353";
|
|
300
|
+
|
|
301
|
+
// src/native/platform-key.ts
|
|
302
|
+
function resolvePlatformKey(platform = process.platform, arch = process.arch) {
|
|
303
|
+
if (platform === "darwin") {
|
|
304
|
+
return arch === "arm64" ? "darwin-arm64" : "darwin-x64";
|
|
305
|
+
}
|
|
306
|
+
if (platform === "linux") {
|
|
307
|
+
if (arch === "x64") return "linux-x64";
|
|
308
|
+
if (arch === "arm64") return "linux-arm64";
|
|
309
|
+
}
|
|
310
|
+
if (platform === "win32" && arch === "x64") {
|
|
311
|
+
return "win32-x64";
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/native/discover.ts
|
|
317
|
+
function resolveNativeBinaryUnder(root, name, subdir, platform = process.platform, fileExists = existsSync) {
|
|
318
|
+
const ext = platform === "win32" ? ".exe" : "";
|
|
319
|
+
const candidates = name === "uv" ? [`uv${ext}`] : [`gezel-${name}${ext}`, `${name}${ext}`];
|
|
320
|
+
for (const file of candidates) {
|
|
321
|
+
const p = join3(root, subdir, file);
|
|
322
|
+
if (fileExists(p)) return p;
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
function discoverNativeBinaries(input) {
|
|
327
|
+
const log = input.logger;
|
|
328
|
+
const fileExists = input.fileExists ?? existsSync;
|
|
329
|
+
const platform = input.platform ?? process.platform;
|
|
330
|
+
const arch = input.arch ?? process.arch;
|
|
331
|
+
const dir = input.nativeBinDirOverride ?? process.env.GEZEL_NATIVE_BIN_DIR;
|
|
332
|
+
const binaries = [];
|
|
333
|
+
const result = { binaries };
|
|
334
|
+
const platformKey = resolvePlatformKey(platform, arch);
|
|
335
|
+
if (!platformKey) {
|
|
336
|
+
for (const name of [
|
|
337
|
+
"llama-server",
|
|
338
|
+
"ds4-server",
|
|
339
|
+
"sd-server",
|
|
340
|
+
"whisper-server",
|
|
341
|
+
"device-health",
|
|
342
|
+
"uv"
|
|
343
|
+
]) {
|
|
344
|
+
binaries.push({ name, source: "no-platform-key" });
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
if (process.env.GEZEL_LLAMA_SERVER_BIN) {
|
|
349
|
+
binaries.push({
|
|
350
|
+
name: "llama-server",
|
|
351
|
+
source: "pre-set",
|
|
352
|
+
path: process.env.GEZEL_LLAMA_SERVER_BIN
|
|
353
|
+
});
|
|
354
|
+
} else {
|
|
355
|
+
const probe = detectLlamaBackend({
|
|
356
|
+
engineVersion: LLAMA_ENGINE_VERSION,
|
|
357
|
+
home: input.home,
|
|
358
|
+
...input.llamaCppBackendOverride ? { override: input.llamaCppBackendOverride } : {},
|
|
359
|
+
...input.llamaProbeOverride ? { probe: { ...input.llamaProbeOverride, platform, arch } } : { probe: { platform, arch } }
|
|
360
|
+
});
|
|
361
|
+
process.env.GEZEL_LLAMA_DETECTED_BACKEND = probe.detectedBackend;
|
|
362
|
+
if (probe.vendorHint) {
|
|
363
|
+
process.env.GEZEL_LLAMA_DETECTED_VENDOR = probe.vendorHint;
|
|
364
|
+
}
|
|
365
|
+
result.llamaBackend = {
|
|
366
|
+
backend: probe.backend,
|
|
367
|
+
detectedBackend: probe.detectedBackend,
|
|
368
|
+
reason: probe.reason,
|
|
369
|
+
cached: probe.cached,
|
|
370
|
+
...probe.vendorHint ? { vendorHint: probe.vendorHint } : {}
|
|
371
|
+
};
|
|
372
|
+
if (!dir) {
|
|
373
|
+
binaries.push({ name: "llama-server", source: "no-native-bin-dir", variant: probe.backend });
|
|
374
|
+
} else {
|
|
375
|
+
const allowFallbacks = input.llamaCppBackendOverride === void 0 || input.llamaCppBackendOverride === "auto";
|
|
376
|
+
const quarantine = input.quarantine ?? readLlamaQuarantine(input.home);
|
|
377
|
+
const resolved = resolveAvailableLlamaBinary(
|
|
378
|
+
probe.backend,
|
|
379
|
+
(backend) => resolveNativeBinaryUnder(
|
|
380
|
+
dir,
|
|
381
|
+
"llama-server",
|
|
382
|
+
`${platformKey}-${backend}`,
|
|
383
|
+
platform,
|
|
384
|
+
fileExists
|
|
385
|
+
) ?? // Variant-less fallback covers Mac's single Metal build and
|
|
386
|
+
// native trees staged before multi-variant packaging.
|
|
387
|
+
resolveNativeBinaryUnder(dir, "llama-server", platformKey, platform, fileExists),
|
|
388
|
+
allowFallbacks,
|
|
389
|
+
quarantine.length > 0 ? (backend, path) => !isBinaryQuarantined(quarantine, backend, path) : void 0
|
|
390
|
+
);
|
|
391
|
+
if (resolved) {
|
|
392
|
+
process.env.GEZEL_LLAMA_SERVER_BIN = resolved.path;
|
|
393
|
+
process.env.GEZEL_LLAMA_SERVER_BACKEND = resolved.backend;
|
|
394
|
+
for (const skipped of resolved.skippedUnusable ?? []) {
|
|
395
|
+
const entry = quarantine.find((e) => e.backend === skipped);
|
|
396
|
+
log?.warn?.(
|
|
397
|
+
`[native] llama-server ${skipped} build is quarantined on this machine (${entry?.signal ?? "crashed"} at ${entry?.at ?? "unknown time"}): ${entry?.reason ?? "crashed before becoming ready"}`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
if (resolved.skippedUnusable?.length) {
|
|
401
|
+
result.llamaBackend.backend = resolved.backend;
|
|
402
|
+
result.llamaBackend.reason = `${probe.reason}; ${resolved.skippedUnusable.join(", ")} quarantined after crashing on this machine, using ${resolved.backend}`;
|
|
403
|
+
result.llamaBackend.quarantined = [...resolved.skippedUnusable];
|
|
404
|
+
process.env.GEZEL_LLAMA_QUARANTINED = resolved.skippedUnusable.join(",");
|
|
405
|
+
} else if (resolved.fallbackFrom) {
|
|
406
|
+
result.llamaBackend.backend = resolved.backend;
|
|
407
|
+
result.llamaBackend.reason = `${probe.reason}; no bundled ${resolved.fallbackFrom} binary, using ${resolved.backend}`;
|
|
408
|
+
log?.info?.(
|
|
409
|
+
`[native] no bundled llama-server for ${resolved.fallbackFrom}; using ${resolved.backend} fallback: ${resolved.path}`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
binaries.push({
|
|
413
|
+
name: "llama-server",
|
|
414
|
+
source: "discovered",
|
|
415
|
+
path: resolved.path,
|
|
416
|
+
variant: resolved.backend
|
|
417
|
+
});
|
|
418
|
+
log?.info?.(
|
|
419
|
+
`[native] discovered llama-server (${resolved.backend}${probe.cached ? ", cached" : ""}): ${resolved.path}`
|
|
420
|
+
);
|
|
421
|
+
} else {
|
|
422
|
+
binaries.push({ name: "llama-server", source: "not-found", variant: probe.backend });
|
|
423
|
+
log?.warn?.(
|
|
424
|
+
`[native] no llama-server binary bundled for ${probe.backend} under ${dir} (probe: ${probe.reason})`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const subdirFor = (name) => name === "ds4-server" && platform === "linux" ? `${platformKey}-cuda` : platformKey;
|
|
430
|
+
for (const { name, envVar } of [
|
|
431
|
+
{ name: "ds4-server", envVar: "GEZEL_DS4_SERVER_BIN" },
|
|
432
|
+
{ name: "sd-server", envVar: "GEZEL_SD_SERVER_BIN" },
|
|
433
|
+
{ name: "whisper-server", envVar: "GEZEL_WHISPER_SERVER_BIN" },
|
|
434
|
+
{ name: "device-health", envVar: "GEZEL_DEVICE_HEALTH_BIN" },
|
|
435
|
+
{ name: "uv", envVar: "GEZEL_UV_BIN" }
|
|
436
|
+
]) {
|
|
437
|
+
if (process.env[envVar]) {
|
|
438
|
+
binaries.push({ name, source: "pre-set", path: process.env[envVar] });
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (!dir) {
|
|
442
|
+
binaries.push({ name, source: "no-native-bin-dir" });
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const subdir = subdirFor(name);
|
|
446
|
+
const bin = resolveNativeBinaryUnder(dir, name, subdir, platform, fileExists) ?? (subdir === platformKey ? null : resolveNativeBinaryUnder(dir, name, platformKey, platform, fileExists));
|
|
447
|
+
if (bin) {
|
|
448
|
+
process.env[envVar] = bin;
|
|
449
|
+
binaries.push({ name, source: "discovered", path: bin });
|
|
450
|
+
log?.info?.(`[native] discovered ${name}: ${bin}`);
|
|
451
|
+
} else {
|
|
452
|
+
binaries.push({ name, source: "not-found" });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/native/console-detach.ts
|
|
459
|
+
function windowsDetachedSpawnOptions(platform = process.platform) {
|
|
460
|
+
return platform === "win32" ? { detached: true } : {};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/native/device-health.ts
|
|
464
|
+
import { execFile as nodeExecFile } from "child_process";
|
|
465
|
+
|
|
466
|
+
// src/device-safety.ts
|
|
467
|
+
var DEVICE_HARD_TEMPERATURE_C = 105;
|
|
468
|
+
|
|
469
|
+
// src/native/device-health.ts
|
|
470
|
+
var DEFAULT_DEVICE_SAFETY_POLICY = {
|
|
471
|
+
mode: "observe",
|
|
472
|
+
maxStartTemperatureC: 80,
|
|
473
|
+
resumeTemperatureC: 75,
|
|
474
|
+
minThermalMarginC: 8,
|
|
475
|
+
pollIntervalMs: 5e3,
|
|
476
|
+
maxWaitMs: 10 * 6e4,
|
|
477
|
+
consecutiveHealthySamples: 3,
|
|
478
|
+
onTelemetryFailure: "allow"
|
|
479
|
+
};
|
|
480
|
+
function envNumber(env, key) {
|
|
481
|
+
const raw = env[key]?.trim();
|
|
482
|
+
if (!raw) return void 0;
|
|
483
|
+
const value = Number(raw);
|
|
484
|
+
return Number.isFinite(value) ? value : void 0;
|
|
485
|
+
}
|
|
486
|
+
function resolveDeviceSafetyPolicy(input, env = process.env) {
|
|
487
|
+
const envMode = env.GEZEL_DEVICE_SAFETY_MODE?.trim().toLowerCase();
|
|
488
|
+
const mode = envMode === "off" || envMode === "observe" || envMode === "guard" ? envMode : input?.mode;
|
|
489
|
+
const envTelemetryFailure = env.GEZEL_DEVICE_SAFETY_TELEMETRY_FAILURE?.trim().toLowerCase();
|
|
490
|
+
const onTelemetryFailure = envTelemetryFailure === "allow" || envTelemetryFailure === "block" ? envTelemetryFailure : input?.onTelemetryFailure;
|
|
491
|
+
const resolved = {
|
|
492
|
+
mode: mode ?? DEFAULT_DEVICE_SAFETY_POLICY.mode,
|
|
493
|
+
maxStartTemperatureC: envNumber(env, "GEZEL_DEVICE_SAFETY_MAX_START_TEMP_C") ?? input?.maxStartTemperatureC ?? DEFAULT_DEVICE_SAFETY_POLICY.maxStartTemperatureC,
|
|
494
|
+
resumeTemperatureC: envNumber(env, "GEZEL_DEVICE_SAFETY_RESUME_TEMP_C") ?? input?.resumeTemperatureC ?? DEFAULT_DEVICE_SAFETY_POLICY.resumeTemperatureC,
|
|
495
|
+
minThermalMarginC: envNumber(env, "GEZEL_DEVICE_SAFETY_MIN_THERMAL_MARGIN_C") ?? input?.minThermalMarginC ?? DEFAULT_DEVICE_SAFETY_POLICY.minThermalMarginC,
|
|
496
|
+
pollIntervalMs: envNumber(env, "GEZEL_DEVICE_SAFETY_POLL_MS") ?? input?.pollIntervalMs ?? DEFAULT_DEVICE_SAFETY_POLICY.pollIntervalMs,
|
|
497
|
+
maxWaitMs: envNumber(env, "GEZEL_DEVICE_SAFETY_MAX_WAIT_MS") ?? input?.maxWaitMs ?? DEFAULT_DEVICE_SAFETY_POLICY.maxWaitMs,
|
|
498
|
+
consecutiveHealthySamples: envNumber(env, "GEZEL_DEVICE_SAFETY_HEALTHY_SAMPLES") ?? input?.consecutiveHealthySamples ?? DEFAULT_DEVICE_SAFETY_POLICY.consecutiveHealthySamples,
|
|
499
|
+
onTelemetryFailure: onTelemetryFailure ?? DEFAULT_DEVICE_SAFETY_POLICY.onTelemetryFailure
|
|
500
|
+
};
|
|
501
|
+
resolved.pollIntervalMs = Math.max(500, Math.min(6e4, resolved.pollIntervalMs));
|
|
502
|
+
resolved.maxWaitMs = Math.max(0, Math.min(36e5, resolved.maxWaitMs));
|
|
503
|
+
resolved.consecutiveHealthySamples = Math.max(
|
|
504
|
+
1,
|
|
505
|
+
Math.min(20, Math.trunc(resolved.consecutiveHealthySamples))
|
|
506
|
+
);
|
|
507
|
+
resolved.resumeTemperatureC = Math.min(
|
|
508
|
+
resolved.resumeTemperatureC,
|
|
509
|
+
resolved.maxStartTemperatureC
|
|
510
|
+
);
|
|
511
|
+
return resolved;
|
|
512
|
+
}
|
|
513
|
+
function parseNumber(raw) {
|
|
514
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
515
|
+
if (typeof raw !== "string") return void 0;
|
|
516
|
+
const match = raw.replaceAll(",", "").match(/-?\d+(?:\.\d+)?/);
|
|
517
|
+
if (!match) return void 0;
|
|
518
|
+
const value = Number(match[0]);
|
|
519
|
+
return Number.isFinite(value) ? value : void 0;
|
|
520
|
+
}
|
|
521
|
+
function isActive(raw) {
|
|
522
|
+
if (typeof raw === "boolean") return raw;
|
|
523
|
+
if (typeof raw === "number") return raw !== 0;
|
|
524
|
+
if (typeof raw !== "string") return void 0;
|
|
525
|
+
const value = raw.trim().toLowerCase();
|
|
526
|
+
if (["active", "yes", "true", "1", "enabled"].includes(value)) return true;
|
|
527
|
+
if (["not active", "no", "false", "0", "disabled", "n/a"].includes(value)) return false;
|
|
528
|
+
return void 0;
|
|
529
|
+
}
|
|
530
|
+
function parseNvidiaSmiCsv(stdout) {
|
|
531
|
+
return stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
532
|
+
const fields = line.split(",").map((field) => field.trim());
|
|
533
|
+
if (fields.length < 10) return [];
|
|
534
|
+
const [
|
|
535
|
+
index,
|
|
536
|
+
name,
|
|
537
|
+
temperature,
|
|
538
|
+
thermalMargin,
|
|
539
|
+
utilization,
|
|
540
|
+
memoryUsed,
|
|
541
|
+
memoryTotal,
|
|
542
|
+
hwThermal,
|
|
543
|
+
swThermal,
|
|
544
|
+
powerBrake
|
|
545
|
+
] = fields;
|
|
546
|
+
return [
|
|
547
|
+
{
|
|
548
|
+
vendor: "nvidia",
|
|
549
|
+
deviceId: index || "0",
|
|
550
|
+
...name ? { name } : {},
|
|
551
|
+
...parseNumber(temperature) !== void 0 ? { temperatureC: parseNumber(temperature) } : {},
|
|
552
|
+
...parseNumber(thermalMargin) !== void 0 ? { thermalMarginC: parseNumber(thermalMargin) } : {},
|
|
553
|
+
...parseNumber(utilization) !== void 0 ? { utilizationPercent: parseNumber(utilization) } : {},
|
|
554
|
+
...parseNumber(memoryUsed) !== void 0 ? { memoryUsedMb: parseNumber(memoryUsed) } : {},
|
|
555
|
+
...parseNumber(memoryTotal) !== void 0 ? { memoryTotalMb: parseNumber(memoryTotal) } : {},
|
|
556
|
+
thermalSlowdown: isActive(hwThermal) === true || isActive(swThermal) === true,
|
|
557
|
+
powerBrake: isActive(powerBrake) === true
|
|
558
|
+
}
|
|
559
|
+
];
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function flattenMetrics(value, path, out) {
|
|
563
|
+
if (Array.isArray(value)) {
|
|
564
|
+
value.forEach((entry, index) => flattenMetrics(entry, `${path}.${index}`, out));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (value && typeof value === "object") {
|
|
568
|
+
for (const [key, child] of Object.entries(value)) {
|
|
569
|
+
flattenMetrics(child, path ? `${path}.${key}` : key, out);
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
out.push({ path: path.toLowerCase(), value });
|
|
574
|
+
}
|
|
575
|
+
function maxMetric(metrics, predicate) {
|
|
576
|
+
const values = metrics.filter((metric) => predicate(metric.path)).map((metric) => parseNumber(metric.value)).filter((value) => value !== void 0);
|
|
577
|
+
return values.length > 0 ? Math.max(...values) : void 0;
|
|
578
|
+
}
|
|
579
|
+
function parseAmdSmiJson(stdout, source = "amd-smi") {
|
|
580
|
+
let root;
|
|
581
|
+
try {
|
|
582
|
+
root = JSON.parse(stdout);
|
|
583
|
+
} catch {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
const candidates = [];
|
|
587
|
+
if (Array.isArray(root)) {
|
|
588
|
+
root.forEach((value, index) => candidates.push({ id: String(index), value }));
|
|
589
|
+
} else if (root && typeof root === "object") {
|
|
590
|
+
const entries = Object.entries(root);
|
|
591
|
+
const deviceEntries = entries.filter(
|
|
592
|
+
([key, value]) => value !== null && typeof value === "object" && /(?:^|[-_\s])(gpu|card|device)\s*\d*/i.test(key)
|
|
593
|
+
);
|
|
594
|
+
if (deviceEntries.length > 0) {
|
|
595
|
+
for (const [id, value] of deviceEntries) candidates.push({ id, value });
|
|
596
|
+
} else {
|
|
597
|
+
candidates.push({ id: "0", value: root });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return candidates.flatMap(({ id, value }) => {
|
|
601
|
+
const metrics = [];
|
|
602
|
+
flattenMetrics(value, "", metrics);
|
|
603
|
+
const temperatureC = maxMetric(
|
|
604
|
+
metrics,
|
|
605
|
+
(path) => /temp|temperature|junction|hotspot|edge/.test(path) && !/limit|threshold|critical|maximum|max_temp/.test(path)
|
|
606
|
+
);
|
|
607
|
+
const utilizationPercent = maxMetric(
|
|
608
|
+
metrics,
|
|
609
|
+
(path) => /gpu.*(?:use|util)|gfx.*(?:use|util|activity)|busy_percent|gpu%/.test(path)
|
|
610
|
+
);
|
|
611
|
+
const memoryUsedMb = maxMetric(
|
|
612
|
+
metrics,
|
|
613
|
+
(path) => /(?:vram|memory).*(?:used|usage_mb)/.test(path) && !/percent|%/.test(path)
|
|
614
|
+
);
|
|
615
|
+
const memoryTotalMb = maxMetric(
|
|
616
|
+
metrics,
|
|
617
|
+
(path) => /(?:vram|memory).*(?:total|size_mb)/.test(path)
|
|
618
|
+
);
|
|
619
|
+
const throttleMetrics = metrics.filter((metric) => /thrott|thermal.*slow/.test(metric.path));
|
|
620
|
+
const thermalSlowdown = throttleMetrics.some((metric) => isActive(metric.value) === true);
|
|
621
|
+
const hasTelemetry = temperatureC !== void 0 || utilizationPercent !== void 0 || memoryUsedMb !== void 0 || throttleMetrics.length > 0;
|
|
622
|
+
if (!hasTelemetry) return [];
|
|
623
|
+
return [
|
|
624
|
+
{
|
|
625
|
+
vendor: "amd",
|
|
626
|
+
deviceId: id,
|
|
627
|
+
name: source,
|
|
628
|
+
...temperatureC !== void 0 ? { temperatureC } : {},
|
|
629
|
+
...utilizationPercent !== void 0 ? { utilizationPercent } : {},
|
|
630
|
+
...memoryUsedMb !== void 0 ? { memoryUsedMb } : {},
|
|
631
|
+
...memoryTotalMb !== void 0 ? { memoryTotalMb } : {},
|
|
632
|
+
thermalSlowdown
|
|
633
|
+
}
|
|
634
|
+
];
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
function optionalFiniteNumber(value) {
|
|
638
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
639
|
+
}
|
|
640
|
+
function parseDeviceHealthHelperJson(stdout) {
|
|
641
|
+
let raw;
|
|
642
|
+
try {
|
|
643
|
+
raw = JSON.parse(stdout);
|
|
644
|
+
} catch {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
if (!raw || typeof raw !== "object") return null;
|
|
648
|
+
const root = raw;
|
|
649
|
+
if (root.schemaVersion !== 1 || !Array.isArray(root.readings)) return null;
|
|
650
|
+
const readings = [];
|
|
651
|
+
for (const entry of root.readings) {
|
|
652
|
+
if (!entry || typeof entry !== "object") return null;
|
|
653
|
+
const reading = entry;
|
|
654
|
+
if (!["nvidia", "amd", "apple", "generic"].includes(String(reading.vendor)) || typeof reading.deviceId !== "string") {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
const normalized = {
|
|
658
|
+
vendor: reading.vendor,
|
|
659
|
+
deviceId: reading.deviceId
|
|
660
|
+
};
|
|
661
|
+
if (typeof reading.name === "string") normalized.name = reading.name;
|
|
662
|
+
const temperatureC = optionalFiniteNumber(reading.temperatureC);
|
|
663
|
+
const thermalMarginC = optionalFiniteNumber(reading.thermalMarginC);
|
|
664
|
+
const utilizationPercent = optionalFiniteNumber(reading.utilizationPercent);
|
|
665
|
+
const memoryUsedMb = optionalFiniteNumber(reading.memoryUsedMb);
|
|
666
|
+
const memoryTotalMb = optionalFiniteNumber(reading.memoryTotalMb);
|
|
667
|
+
if (temperatureC !== void 0) normalized.temperatureC = temperatureC;
|
|
668
|
+
if (thermalMarginC !== void 0) normalized.thermalMarginC = thermalMarginC;
|
|
669
|
+
if (utilizationPercent !== void 0) normalized.utilizationPercent = utilizationPercent;
|
|
670
|
+
if (memoryUsedMb !== void 0) normalized.memoryUsedMb = memoryUsedMb;
|
|
671
|
+
if (memoryTotalMb !== void 0) normalized.memoryTotalMb = memoryTotalMb;
|
|
672
|
+
if (typeof reading.thermalSlowdown === "boolean") {
|
|
673
|
+
normalized.thermalSlowdown = reading.thermalSlowdown;
|
|
674
|
+
}
|
|
675
|
+
if (typeof reading.powerBrake === "boolean") normalized.powerBrake = reading.powerBrake;
|
|
676
|
+
readings.push(normalized);
|
|
677
|
+
}
|
|
678
|
+
const processes = [];
|
|
679
|
+
if (root.processes !== void 0) {
|
|
680
|
+
if (!Array.isArray(root.processes)) return null;
|
|
681
|
+
for (const entry of root.processes) {
|
|
682
|
+
if (!entry || typeof entry !== "object") return null;
|
|
683
|
+
const process2 = entry;
|
|
684
|
+
const pid = optionalFiniteNumber(process2.pid);
|
|
685
|
+
const dedicatedBytes = optionalFiniteNumber(process2.dedicatedBytes);
|
|
686
|
+
const owner = String(process2.owner);
|
|
687
|
+
if (pid === void 0 || !Number.isInteger(pid) || pid <= 0 || dedicatedBytes === void 0 || dedicatedBytes < 0 || ![
|
|
688
|
+
"machine-engine",
|
|
689
|
+
"app-engine",
|
|
690
|
+
"development-engine",
|
|
691
|
+
"gezel-engine",
|
|
692
|
+
"external"
|
|
693
|
+
].includes(owner)) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
processes.push({
|
|
697
|
+
pid,
|
|
698
|
+
...typeof process2.name === "string" ? { name: process2.name } : {},
|
|
699
|
+
dedicatedBytes,
|
|
700
|
+
owner
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const strings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
705
|
+
return {
|
|
706
|
+
sample: {
|
|
707
|
+
sampledAt: typeof root.sampledAt === "string" && root.sampledAt.length > 0 ? root.sampledAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
708
|
+
sources: strings(root.sources),
|
|
709
|
+
readings,
|
|
710
|
+
...processes.length > 0 ? { processes } : {},
|
|
711
|
+
errors: strings(root.errors)
|
|
712
|
+
},
|
|
713
|
+
diagnostics: strings(root.diagnostics)
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
var NVIDIA_QUERY = [
|
|
717
|
+
"index",
|
|
718
|
+
"name",
|
|
719
|
+
"temperature.gpu",
|
|
720
|
+
"temperature.gpu.tlimit",
|
|
721
|
+
"utilization.gpu",
|
|
722
|
+
"memory.used",
|
|
723
|
+
"memory.total",
|
|
724
|
+
"clocks_event_reasons.hw_thermal_slowdown",
|
|
725
|
+
"clocks_event_reasons.sw_thermal_slowdown",
|
|
726
|
+
"clocks_event_reasons.hw_power_brake_slowdown"
|
|
727
|
+
].join(",");
|
|
728
|
+
function defaultCommandRunner(command, args, timeoutMs) {
|
|
729
|
+
return new Promise((resolve, reject) => {
|
|
730
|
+
nodeExecFile(
|
|
731
|
+
command,
|
|
732
|
+
args,
|
|
733
|
+
// nvidia-smi / amd-smi are console-subsystem, and the Session 0 service
|
|
734
|
+
// has no console for the loader to allocate from, so they start with
|
|
735
|
+
// DETACHED_PROCESS; `windowsHide` (CREATE_NO_WINDOW) still allocates
|
|
736
|
+
// one. When these probes fail the daemon sees no GPU at all and plans
|
|
737
|
+
// capacity against system RAM — the symptom that made a token-level
|
|
738
|
+
// spawn denial look like a GPU-detection bug.
|
|
739
|
+
{ timeout: timeoutMs, ...windowsDetachedSpawnOptions() },
|
|
740
|
+
(error, stdout, stderr) => {
|
|
741
|
+
if (error) {
|
|
742
|
+
reject(error);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
resolve({ stdout: String(stdout), stderr: String(stderr) });
|
|
746
|
+
}
|
|
747
|
+
);
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
function createSystemDeviceHealthProbe(opts = {}) {
|
|
751
|
+
const run = opts.commandRunner ?? defaultCommandRunner;
|
|
752
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
753
|
+
const helperPath = opts.helperPath === void 0 ? process.env.GEZEL_DEVICE_HEALTH_BIN : opts.helperPath;
|
|
754
|
+
return {
|
|
755
|
+
async sample() {
|
|
756
|
+
const readings = [];
|
|
757
|
+
const sources = [];
|
|
758
|
+
const errors = [];
|
|
759
|
+
let processes;
|
|
760
|
+
if (helperPath) {
|
|
761
|
+
try {
|
|
762
|
+
const result = await run(helperPath, ["sample", "--json"], timeoutMs);
|
|
763
|
+
const parsed = parseDeviceHealthHelperJson(result.stdout);
|
|
764
|
+
if (!parsed) {
|
|
765
|
+
errors.push("device-health helper: invalid JSON contract");
|
|
766
|
+
} else {
|
|
767
|
+
readings.push(...parsed.sample.readings);
|
|
768
|
+
sources.push(...parsed.sample.sources);
|
|
769
|
+
processes = parsed.sample.processes;
|
|
770
|
+
errors.push(...parsed.sample.errors.map((error) => `device-health helper: ${error}`));
|
|
771
|
+
if (parsed.sample.readings.length > 0) {
|
|
772
|
+
return {
|
|
773
|
+
sampledAt: parsed.sample.sampledAt,
|
|
774
|
+
sources,
|
|
775
|
+
readings,
|
|
776
|
+
...processes ? { processes } : {},
|
|
777
|
+
errors
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
errors.push(
|
|
781
|
+
...parsed.diagnostics.map((diagnostic) => `device-health helper: ${diagnostic}`)
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
} catch (error) {
|
|
785
|
+
errors.push(
|
|
786
|
+
`device-health helper: ${error instanceof Error ? error.message : String(error)}`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (!opts.preferredVendor || opts.preferredVendor === "nvidia") {
|
|
791
|
+
try {
|
|
792
|
+
const result = await run(
|
|
793
|
+
"nvidia-smi",
|
|
794
|
+
[`--query-gpu=${NVIDIA_QUERY}`, "--format=csv,noheader,nounits"],
|
|
795
|
+
timeoutMs
|
|
796
|
+
);
|
|
797
|
+
const parsed = parseNvidiaSmiCsv(result.stdout);
|
|
798
|
+
if (parsed.length > 0) {
|
|
799
|
+
readings.push(...parsed);
|
|
800
|
+
sources.push("nvidia-smi");
|
|
801
|
+
}
|
|
802
|
+
} catch (error) {
|
|
803
|
+
errors.push(`nvidia-smi: ${error instanceof Error ? error.message : String(error)}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (!opts.preferredVendor || opts.preferredVendor === "amd") {
|
|
807
|
+
let amdReadings = [];
|
|
808
|
+
try {
|
|
809
|
+
const result = await run(
|
|
810
|
+
"amd-smi",
|
|
811
|
+
["metric", "--temperature", "--usage", "--mem-usage", "--violation", "--json"],
|
|
812
|
+
timeoutMs
|
|
813
|
+
);
|
|
814
|
+
amdReadings = parseAmdSmiJson(result.stdout, "amd-smi");
|
|
815
|
+
if (amdReadings.length > 0) sources.push("amd-smi");
|
|
816
|
+
} catch (error) {
|
|
817
|
+
errors.push(`amd-smi: ${error instanceof Error ? error.message : String(error)}`);
|
|
818
|
+
}
|
|
819
|
+
if (amdReadings.length === 0) {
|
|
820
|
+
try {
|
|
821
|
+
const result = await run(
|
|
822
|
+
"rocm-smi",
|
|
823
|
+
["--showtemp", "--showuse", "--showmemuse", "--json"],
|
|
824
|
+
timeoutMs
|
|
825
|
+
);
|
|
826
|
+
amdReadings = parseAmdSmiJson(result.stdout, "rocm-smi");
|
|
827
|
+
if (amdReadings.length > 0) sources.push("rocm-smi");
|
|
828
|
+
} catch (error) {
|
|
829
|
+
errors.push(`rocm-smi: ${error instanceof Error ? error.message : String(error)}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
readings.push(...amdReadings);
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
sampledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
836
|
+
sources,
|
|
837
|
+
readings,
|
|
838
|
+
...processes ? { processes } : {},
|
|
839
|
+
errors
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function evaluateDeviceHealth(sample, policy, cooling = false) {
|
|
845
|
+
if (policy.mode === "off") {
|
|
846
|
+
return {
|
|
847
|
+
admissible: true,
|
|
848
|
+
hardBlocked: false,
|
|
849
|
+
telemetryAvailable: sample.readings.length > 0,
|
|
850
|
+
reasons: [],
|
|
851
|
+
summary: "device safety is off"
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
if (sample.readings.length === 0) {
|
|
855
|
+
const admissible = policy.onTelemetryFailure === "allow";
|
|
856
|
+
const reasons2 = admissible ? [] : ["device telemetry unavailable"];
|
|
857
|
+
return {
|
|
858
|
+
admissible,
|
|
859
|
+
hardBlocked: false,
|
|
860
|
+
telemetryAvailable: false,
|
|
861
|
+
reasons: reasons2,
|
|
862
|
+
summary: admissible ? "device telemetry unavailable; policy allows work" : "device telemetry unavailable; policy blocks work"
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
const reasons = [];
|
|
866
|
+
let hardBlocked = false;
|
|
867
|
+
const temperatureLimit = cooling ? policy.resumeTemperatureC : policy.maxStartTemperatureC;
|
|
868
|
+
for (const reading of sample.readings) {
|
|
869
|
+
const label = `${reading.vendor}:${reading.name ?? reading.deviceId}`;
|
|
870
|
+
if (reading.temperatureC !== void 0 && reading.temperatureC > temperatureLimit) {
|
|
871
|
+
reasons.push(
|
|
872
|
+
`${label} temperature ${reading.temperatureC}C exceeds ${temperatureLimit}C ${cooling ? "resume" : "start"} limit`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (reading.temperatureC !== void 0 && reading.temperatureC > DEVICE_HARD_TEMPERATURE_C) {
|
|
876
|
+
hardBlocked = true;
|
|
877
|
+
reasons.push(
|
|
878
|
+
`${label} temperature ${reading.temperatureC}C exceeds hard ${DEVICE_HARD_TEMPERATURE_C}C safety limit`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (reading.thermalMarginC !== void 0 && reading.thermalMarginC < policy.minThermalMarginC) {
|
|
882
|
+
reasons.push(
|
|
883
|
+
`${label} thermal margin ${reading.thermalMarginC}C is below ${policy.minThermalMarginC}C`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (reading.thermalSlowdown) reasons.push(`${label} reports thermal slowdown`);
|
|
887
|
+
if (reading.powerBrake) reasons.push(`${label} reports hardware power braking`);
|
|
888
|
+
}
|
|
889
|
+
return {
|
|
890
|
+
admissible: reasons.length === 0,
|
|
891
|
+
hardBlocked,
|
|
892
|
+
telemetryAvailable: true,
|
|
893
|
+
reasons,
|
|
894
|
+
summary: reasons.length === 0 ? `device telemetry healthy (${sample.sources.join(", ") || "unknown source"})` : reasons.join("; ")
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
var DeviceHealthGate = class {
|
|
898
|
+
policy;
|
|
899
|
+
probe;
|
|
900
|
+
log;
|
|
901
|
+
sleep;
|
|
902
|
+
now;
|
|
903
|
+
cooling = false;
|
|
904
|
+
pendingAdmission;
|
|
905
|
+
pendingSample;
|
|
906
|
+
lastSample;
|
|
907
|
+
lastSampleAt = 0;
|
|
908
|
+
admissionBlocked = false;
|
|
909
|
+
lastUnavailableDiagnostic = "";
|
|
910
|
+
constructor(opts) {
|
|
911
|
+
this.probe = opts.probe;
|
|
912
|
+
this.policy = resolveDeviceSafetyPolicy(opts.policy);
|
|
913
|
+
this.log = opts.log ?? (() => {
|
|
914
|
+
});
|
|
915
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
916
|
+
this.now = opts.now ?? Date.now;
|
|
917
|
+
}
|
|
918
|
+
setPolicy(policy) {
|
|
919
|
+
this.policy = resolveDeviceSafetyPolicy(policy);
|
|
920
|
+
if (this.policy.mode === "off") this.cooling = false;
|
|
921
|
+
}
|
|
922
|
+
getPolicy() {
|
|
923
|
+
return { ...this.policy };
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Return a cached-or-fresh normalized snapshot for status surfaces. The
|
|
927
|
+
* cache keeps a UI polling every few seconds from spawning overlapping SMI
|
|
928
|
+
* processes, while admission can still force a fresh sample after it ages.
|
|
929
|
+
*/
|
|
930
|
+
async status(maxAgeMs = 5e3) {
|
|
931
|
+
const sample = this.lastSample && this.now() - this.lastSampleAt <= maxAgeMs ? this.lastSample : await this.sampleDevice();
|
|
932
|
+
if (this.policy.mode === "off") {
|
|
933
|
+
return {
|
|
934
|
+
state: "off",
|
|
935
|
+
mode: "off",
|
|
936
|
+
sampledAt: sample.sampledAt,
|
|
937
|
+
sources: [...sample.sources],
|
|
938
|
+
readings: sample.readings.map((reading) => ({ ...reading })),
|
|
939
|
+
...sample.processes ? { processes: sample.processes.map((process2) => ({ ...process2 })) } : {},
|
|
940
|
+
reasons: [],
|
|
941
|
+
summary: "Device safety is off"
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
const decision = evaluateDeviceHealth(sample, this.policy, this.cooling);
|
|
945
|
+
const state = !decision.telemetryAvailable ? "unavailable" : decision.admissible ? "healthy" : decision.hardBlocked || this.admissionBlocked ? "blocked" : this.cooling ? "cooling" : "warm";
|
|
946
|
+
return {
|
|
947
|
+
state,
|
|
948
|
+
mode: this.policy.mode,
|
|
949
|
+
sampledAt: sample.sampledAt,
|
|
950
|
+
sources: [...sample.sources],
|
|
951
|
+
readings: sample.readings.map((reading) => ({ ...reading })),
|
|
952
|
+
...sample.processes ? { processes: sample.processes.map((process2) => ({ ...process2 })) } : {},
|
|
953
|
+
reasons: [...decision.reasons],
|
|
954
|
+
summary: decision.summary
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
async admit(context) {
|
|
958
|
+
if (this.policy.mode === "off") {
|
|
959
|
+
return {
|
|
960
|
+
admissible: true,
|
|
961
|
+
hardBlocked: false,
|
|
962
|
+
telemetryAvailable: false,
|
|
963
|
+
reasons: [],
|
|
964
|
+
summary: "device safety is off"
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
if (!this.pendingAdmission) {
|
|
968
|
+
this.pendingAdmission = this.runAdmission(context).finally(() => {
|
|
969
|
+
this.pendingAdmission = void 0;
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
return this.pendingAdmission;
|
|
973
|
+
}
|
|
974
|
+
async runAdmission(context) {
|
|
975
|
+
const startedAt = this.now();
|
|
976
|
+
let healthySamples = 0;
|
|
977
|
+
let loggedWaiting = false;
|
|
978
|
+
while (true) {
|
|
979
|
+
const sample = await this.sampleDevice();
|
|
980
|
+
const decision = evaluateDeviceHealth(sample, this.policy, this.cooling);
|
|
981
|
+
if (this.policy.mode === "observe") {
|
|
982
|
+
if (!decision.hardBlocked) {
|
|
983
|
+
if (!decision.admissible || !decision.telemetryAvailable) {
|
|
984
|
+
this.log(`[device-health] observe ${context}: ${decision.summary}`);
|
|
985
|
+
}
|
|
986
|
+
if (this.cooling) {
|
|
987
|
+
this.log(
|
|
988
|
+
`[device-health] ${context}: temperature returned to ${DEVICE_HARD_TEMPERATURE_C}C or below; admitting work`
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
this.cooling = false;
|
|
992
|
+
this.admissionBlocked = false;
|
|
993
|
+
return { ...decision, admissible: true };
|
|
994
|
+
}
|
|
995
|
+
healthySamples = 0;
|
|
996
|
+
this.cooling = true;
|
|
997
|
+
if (!loggedWaiting) {
|
|
998
|
+
this.log(`[device-health] ${context}: hard temperature gate \u2014 ${decision.summary}`);
|
|
999
|
+
loggedWaiting = true;
|
|
1000
|
+
}
|
|
1001
|
+
} else if (decision.admissible) {
|
|
1002
|
+
healthySamples += 1;
|
|
1003
|
+
const required = this.cooling ? this.policy.consecutiveHealthySamples : 1;
|
|
1004
|
+
if (healthySamples >= required) {
|
|
1005
|
+
if (this.cooling || loggedWaiting) {
|
|
1006
|
+
this.log(
|
|
1007
|
+
`[device-health] ${context}: healthy for ${healthySamples} sample(s); admitting work`
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
this.cooling = false;
|
|
1011
|
+
this.admissionBlocked = false;
|
|
1012
|
+
return decision;
|
|
1013
|
+
}
|
|
1014
|
+
} else {
|
|
1015
|
+
healthySamples = 0;
|
|
1016
|
+
this.cooling = true;
|
|
1017
|
+
if (!loggedWaiting) {
|
|
1018
|
+
this.log(`[device-health] ${context}: cooling before admission \u2014 ${decision.summary}`);
|
|
1019
|
+
loggedWaiting = true;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const elapsed = this.now() - startedAt;
|
|
1023
|
+
if (elapsed >= this.policy.maxWaitMs) {
|
|
1024
|
+
this.admissionBlocked = true;
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`[device-health] ${context} blocked after ${elapsed}ms: ${decision.summary}`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
await this.sleep(Math.min(this.policy.pollIntervalMs, this.policy.maxWaitMs - elapsed));
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async sampleDevice() {
|
|
1033
|
+
if (!this.pendingSample) {
|
|
1034
|
+
this.pendingSample = this.probe.sample().catch((error) => ({
|
|
1035
|
+
sampledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1036
|
+
sources: [],
|
|
1037
|
+
readings: [],
|
|
1038
|
+
errors: [error instanceof Error ? error.message : String(error)]
|
|
1039
|
+
})).then((sample) => {
|
|
1040
|
+
const diagnostic = sample.readings.length === 0 ? sample.errors.join("; ") : "";
|
|
1041
|
+
if (diagnostic && diagnostic !== this.lastUnavailableDiagnostic) {
|
|
1042
|
+
this.log(`[device-health] telemetry unavailable: ${diagnostic}`);
|
|
1043
|
+
}
|
|
1044
|
+
this.lastUnavailableDiagnostic = diagnostic;
|
|
1045
|
+
this.lastSample = sample;
|
|
1046
|
+
this.lastSampleAt = this.now();
|
|
1047
|
+
return sample;
|
|
1048
|
+
}).finally(() => {
|
|
1049
|
+
this.pendingSample = void 0;
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return this.pendingSample;
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
// src/native/gpu-panic.ts
|
|
1057
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1058
|
+
import { homedir } from "os";
|
|
1059
|
+
import { join as join4 } from "path";
|
|
1060
|
+
var PANIC_DIRS = [
|
|
1061
|
+
"/Library/Logs/DiagnosticReports",
|
|
1062
|
+
join4(homedir(), "Library", "Logs", "DiagnosticReports")
|
|
1063
|
+
];
|
|
1064
|
+
var GPU_PANIC_RE = /IOGPUMemory|IOAccelerator|AGXAccelerator|AGXG\d|completeMemory\(\)\s+prepare\s+count\s+underflow|IOGPUFamily/i;
|
|
1065
|
+
function findRecentGpuPanics(opts = {}) {
|
|
1066
|
+
const dirs = opts.dirs ?? (process.platform === "darwin" ? PANIC_DIRS : []);
|
|
1067
|
+
const withinMs = opts.withinMs ?? 24 * 60 * 60 * 1e3;
|
|
1068
|
+
const now = opts.now ?? Date.now();
|
|
1069
|
+
const out = [];
|
|
1070
|
+
for (const dir of dirs) {
|
|
1071
|
+
let names;
|
|
1072
|
+
try {
|
|
1073
|
+
names = readdirSync2(dir).filter((f) => f.endsWith(".panic") || f.endsWith(".ips"));
|
|
1074
|
+
} catch {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
for (const name of names) {
|
|
1078
|
+
const path = join4(dir, name);
|
|
1079
|
+
let mtimeMs;
|
|
1080
|
+
try {
|
|
1081
|
+
mtimeMs = statSync2(path).mtimeMs;
|
|
1082
|
+
} catch {
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
if (now - mtimeMs > withinMs) continue;
|
|
1086
|
+
let text;
|
|
1087
|
+
try {
|
|
1088
|
+
text = readFileSync3(path, "utf8").slice(0, 16384);
|
|
1089
|
+
} catch {
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const m = GPU_PANIC_RE.exec(text);
|
|
1093
|
+
if (m) out.push({ file: path, when: new Date(mtimeMs), signature: m[0] });
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return out.sort((a, b) => b.when.getTime() - a.when.getTime());
|
|
1097
|
+
}
|
|
1098
|
+
export {
|
|
1099
|
+
DEFAULT_DEVICE_SAFETY_POLICY,
|
|
1100
|
+
DEVICE_HARD_TEMPERATURE_C,
|
|
1101
|
+
DeviceHealthGate,
|
|
1102
|
+
GPU_PANIC_RE,
|
|
1103
|
+
LLAMA_ENGINE_VERSION,
|
|
1104
|
+
binaryFingerprint,
|
|
1105
|
+
createSystemDeviceHealthProbe,
|
|
1106
|
+
detectLlamaBackend,
|
|
1107
|
+
discoverNativeBinaries,
|
|
1108
|
+
evaluateDeviceHealth,
|
|
1109
|
+
findRecentGpuPanics,
|
|
1110
|
+
isBinaryQuarantined,
|
|
1111
|
+
llamaQuarantinePath,
|
|
1112
|
+
parseAmdSmiJson,
|
|
1113
|
+
parseNvidiaSmiCsv,
|
|
1114
|
+
readLlamaQuarantine,
|
|
1115
|
+
recordLlamaQuarantine,
|
|
1116
|
+
resolveAvailableLlamaBinary,
|
|
1117
|
+
resolveDeviceSafetyPolicy,
|
|
1118
|
+
resolveNativeBinaryUnder,
|
|
1119
|
+
resolvePlatformKey,
|
|
1120
|
+
windowsDetachedSpawnOptions
|
|
1121
|
+
};
|