sublera-cli 0.0.0-stage → 1.3.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/LICENSE +21 -0
  3. package/README.md +585 -2
  4. package/SECURITY.md +78 -0
  5. package/THIRD_PARTY_NOTICES.md +37 -0
  6. package/dist/agent.d.ts +270 -0
  7. package/dist/agent.js +1867 -0
  8. package/dist/commands.d.ts +76 -0
  9. package/dist/commands.js +841 -0
  10. package/dist/config.d.ts +27 -0
  11. package/dist/config.js +95 -0
  12. package/dist/engine/binaryManager.d.ts +67 -0
  13. package/dist/engine/binaryManager.js +590 -0
  14. package/dist/engine/gguf.d.ts +88 -0
  15. package/dist/engine/gguf.js +458 -0
  16. package/dist/engine/llamaRunner.d.ts +78 -0
  17. package/dist/engine/llamaRunner.js +282 -0
  18. package/dist/engine/modelManager.d.ts +63 -0
  19. package/dist/engine/modelManager.js +510 -0
  20. package/dist/engine/selfCheck.d.ts +34 -0
  21. package/dist/engine/selfCheck.js +108 -0
  22. package/dist/exitCodes.d.ts +20 -0
  23. package/dist/exitCodes.js +20 -0
  24. package/dist/hardware/gpuMonitor.d.ts +32 -0
  25. package/dist/hardware/gpuMonitor.js +378 -0
  26. package/dist/hardware/sleepGuard.d.ts +42 -0
  27. package/dist/hardware/sleepGuard.js +149 -0
  28. package/dist/hardware/types.d.ts +40 -0
  29. package/dist/hardware/types.js +2 -0
  30. package/dist/health.d.ts +17 -0
  31. package/dist/health.js +38 -0
  32. package/dist/index.d.ts +11 -0
  33. package/dist/index.js +268 -0
  34. package/dist/logger.d.ts +53 -0
  35. package/dist/logger.js +201 -0
  36. package/dist/network/proxy.d.ts +58 -0
  37. package/dist/network/proxy.js +328 -0
  38. package/dist/network/streamProxy.d.ts +51 -0
  39. package/dist/network/streamProxy.js +286 -0
  40. package/dist/network/tunnelClient.d.ts +54 -0
  41. package/dist/network/tunnelClient.js +257 -0
  42. package/dist/settings.d.ts +53 -0
  43. package/dist/settings.js +171 -0
  44. package/dist/state.d.ts +52 -0
  45. package/dist/state.js +71 -0
  46. package/dist/storageCommand.d.ts +22 -0
  47. package/dist/storageCommand.js +81 -0
  48. package/dist/types.d.ts +222 -0
  49. package/dist/types.js +43 -0
  50. package/dist/ui/progress.d.ts +29 -0
  51. package/dist/ui/progress.js +82 -0
  52. package/dist/ui/terminalDashboard.d.ts +88 -0
  53. package/dist/ui/terminalDashboard.js +295 -0
  54. package/dist/util/env.d.ts +3 -0
  55. package/dist/util/env.js +6 -0
  56. package/dist/util/exec.d.ts +23 -0
  57. package/dist/util/exec.js +116 -0
  58. package/dist/util/format.d.ts +47 -0
  59. package/dist/util/format.js +125 -0
  60. package/dist/util/fs.d.ts +18 -0
  61. package/dist/util/fs.js +140 -0
  62. package/dist/util/http.d.ts +28 -0
  63. package/dist/util/http.js +128 -0
  64. package/dist/util/instanceLock.d.ts +3 -0
  65. package/dist/util/instanceLock.js +108 -0
  66. package/dist/util/open.d.ts +16 -0
  67. package/dist/util/open.js +65 -0
  68. package/dist/util/storage.d.ts +10 -0
  69. package/dist/util/storage.js +69 -0
  70. package/dist/version.d.ts +15 -0
  71. package/dist/version.js +29 -0
  72. package/package.json +68 -4
@@ -0,0 +1,458 @@
1
+ import { readFirstBytes } from '../util/fs.js';
2
+ import { USER_AGENT } from '../version.js';
3
+ const FILE_TYPE_QUANT = {
4
+ 0: 'F32', 1: 'F16', 2: 'Q4_0', 3: 'Q4_1', 7: 'Q8_0', 8: 'Q5_0', 9: 'Q5_1',
5
+ 10: 'Q2_K', 11: 'Q3_K_S', 12: 'Q3_K_M', 13: 'Q3_K_L', 14: 'Q4_K_S', 15: 'Q4_K_M',
6
+ 16: 'Q5_K_S', 17: 'Q5_K_M', 18: 'Q6_K', 19: 'IQ2_XXS', 20: 'IQ2_XS', 21: 'Q2_K_S',
7
+ 22: 'IQ3_XS', 23: 'IQ3_XXS', 24: 'IQ1_S', 25: 'IQ4_NL', 26: 'IQ3_S', 27: 'IQ3_M',
8
+ 28: 'IQ2_S', 29: 'IQ2_M', 30: 'IQ4_XS', 31: 'IQ1_M', 32: 'BF16',
9
+ };
10
+ export function quantBytesPerParam(quant) {
11
+ const q = quant.toUpperCase();
12
+ if (q.includes('Q2'))
13
+ return 0.35;
14
+ if (q.includes('Q3'))
15
+ return 0.42;
16
+ if (q.includes('Q4') || q.includes('IQ4'))
17
+ return 0.55;
18
+ if (q.includes('Q5'))
19
+ return 0.66;
20
+ if (q.includes('Q6'))
21
+ return 0.78;
22
+ if (q.includes('Q8'))
23
+ return 1.05;
24
+ if (q.includes('F16') || q.includes('FP16') || q.includes('BF16'))
25
+ return 2;
26
+ if (q.includes('F32'))
27
+ return 4;
28
+ return 0.6;
29
+ }
30
+ export function guessLayers(paramB) {
31
+ if (paramB < 4)
32
+ return 24;
33
+ if (paramB < 9)
34
+ return 32;
35
+ if (paramB < 16)
36
+ return 40;
37
+ if (paramB < 35)
38
+ return 48;
39
+ if (paramB < 75)
40
+ return 80;
41
+ return 96;
42
+ }
43
+ export function estimateVramMb(opts) {
44
+ const modelMb = opts.paramB * quantBytesPerParam(opts.quant) * 1024;
45
+ const layers = opts.layers || guessLayers(opts.paramB);
46
+ const slots = Math.max(1, Math.round(opts.slots ?? 1));
47
+ const kvMb = (opts.context * slots * layers * 4096) / (1024 * 1024);
48
+ return Math.round(modelMb + kvMb + 700);
49
+ }
50
+ export function paramBFromName(name) {
51
+ const matches = [...name.matchAll(/(\d+(?:\.\d+)?)\s*[bB](?![a-zA-Z])/g)];
52
+ if (!matches.length)
53
+ return null;
54
+ const n = Number(matches[matches.length - 1]?.[1]);
55
+ if (Number.isFinite(n) && n >= 0.1 && n <= 2000)
56
+ return n;
57
+ return null;
58
+ }
59
+ export function quantFromName(name) {
60
+ const match = name.match(/\b(IQ\d_[A-Z0-9]+|Q\d(?:_[A-Z0-9]+)?|F16|F32|BF16)\b/i);
61
+ return match?.[1] ? match[1].toUpperCase() : null;
62
+ }
63
+ export function layerMismatch(claimedParamB, layers) {
64
+ if (claimedParamB == null || layers == null)
65
+ return null;
66
+ if (claimedParamB >= 50 && layers < 48)
67
+ return `GGUF header has ${layers} layers, which cannot be a ${claimedParamB}B model`;
68
+ if (claimedParamB <= 8 && layers >= 64)
69
+ return `GGUF header has ${layers} layers, which is too deep for a ${claimedParamB}B model`;
70
+ return null;
71
+ }
72
+ export function rejectModelFile(file) {
73
+ const lower = file.toLowerCase();
74
+ if (/-\d{5}-of-\d{5}\.gguf$/.test(lower)) {
75
+ return 'Split GGUF files (-00001-of-0000N.gguf) are not supported yet. Pick a single-file GGUF.';
76
+ }
77
+ if (lower.endsWith('.gguf'))
78
+ return null;
79
+ if (/\.(bin|pkl|pickle|pt|pth|safetensors|onnx|ggml|ckpt)$/.test(lower)) {
80
+ return 'Sublera only runs GGUF weights. Pickle and PyTorch files can execute code on a host PC, so they are refused.';
81
+ }
82
+ return 'Model file must be a .gguf from HuggingFace.';
83
+ }
84
+ class Reader {
85
+ buf;
86
+ offset = 0;
87
+ constructor(buf) {
88
+ this.buf = buf;
89
+ }
90
+ need(n) {
91
+ if (n < 0 || this.offset + n > this.buf.length)
92
+ throw new Error('truncated header');
93
+ }
94
+ u8() { this.need(1); const v = this.buf.readUInt8(this.offset); this.offset += 1; return v; }
95
+ u16() { this.need(2); const v = this.buf.readUInt16LE(this.offset); this.offset += 2; return v; }
96
+ u32() { this.need(4); const v = this.buf.readUInt32LE(this.offset); this.offset += 4; return v; }
97
+ i32() { this.need(4); const v = this.buf.readInt32LE(this.offset); this.offset += 4; return v; }
98
+ f32() { this.need(4); const v = this.buf.readFloatLE(this.offset); this.offset += 4; return v; }
99
+ u64() {
100
+ this.need(8);
101
+ const v = this.buf.readBigUInt64LE(this.offset);
102
+ this.offset += 8;
103
+ if (v > BigInt(Number.MAX_SAFE_INTEGER))
104
+ throw new Error('integer too large');
105
+ return Number(v);
106
+ }
107
+ i64() { this.need(8); const v = this.buf.readBigInt64LE(this.offset); this.offset += 8; return Number(v); }
108
+ f64() { this.need(8); const v = this.buf.readDoubleLE(this.offset); this.offset += 8; return v; }
109
+ str(max = 4_000_000) {
110
+ const len = this.u64();
111
+ if (len > max)
112
+ throw new Error('string too large');
113
+ this.need(len);
114
+ const s = this.buf.subarray(this.offset, this.offset + len).toString('utf8');
115
+ this.offset += len;
116
+ return s;
117
+ }
118
+ skipString() {
119
+ const len = this.u64();
120
+ if (len > this.buf.length)
121
+ throw new Error('string too large');
122
+ this.need(len);
123
+ this.offset += len;
124
+ }
125
+ }
126
+ function skipValue(r, type) {
127
+ switch (type) {
128
+ case 0:
129
+ case 1:
130
+ case 7:
131
+ r.u8();
132
+ return;
133
+ case 2:
134
+ case 3:
135
+ r.u16();
136
+ return;
137
+ case 4:
138
+ case 5:
139
+ case 6:
140
+ r.u32();
141
+ return;
142
+ case 10:
143
+ case 11:
144
+ case 12:
145
+ r.u64();
146
+ return;
147
+ case 8:
148
+ r.skipString();
149
+ return;
150
+ case 9: {
151
+ const elem = r.u32();
152
+ const count = r.u64();
153
+ if (count > 5_000_000)
154
+ throw new Error('array too large');
155
+ for (let i = 0; i < count; i++)
156
+ skipValue(r, elem);
157
+ return;
158
+ }
159
+ default: throw new Error(`unknown GGUF type ${type}`);
160
+ }
161
+ }
162
+ function readScalar(r, type) {
163
+ switch (type) {
164
+ case 0:
165
+ case 1:
166
+ case 7: return r.u8();
167
+ case 2:
168
+ case 3: return r.u16();
169
+ case 4: return r.u32();
170
+ case 5: return r.i32();
171
+ case 6: return r.f32();
172
+ case 8: return r.str();
173
+ case 10: return r.u64();
174
+ case 11: return r.i64();
175
+ case 12: return r.f64();
176
+ default:
177
+ skipValue(r, type);
178
+ return null;
179
+ }
180
+ }
181
+ export function parseGgufHeader(buf) {
182
+ const info = {
183
+ version: 0, architecture: null, name: null, layers: null, embeddingLength: null,
184
+ fileType: null, quantLabel: null, headCount: null, headCountKv: null, feedForward: null,
185
+ contextTrain: null, vocabSize: null, paramB: null, keysRead: 0, complete: false,
186
+ };
187
+ if (buf.length < 24 || buf.subarray(0, 4).toString('latin1') !== 'GGUF')
188
+ throw new Error('Not a GGUF file');
189
+ const r = new Reader(buf);
190
+ r.offset = 4;
191
+ info.version = r.u32();
192
+ if (info.version < 2)
193
+ throw new Error('GGUF version 1 is not supported');
194
+ r.u64();
195
+ const kvCount = r.u64();
196
+ if (kvCount > 1_000_000)
197
+ throw new Error('GGUF metadata count is invalid');
198
+ try {
199
+ for (let i = 0; i < kvCount; i++) {
200
+ const key = r.str(4096);
201
+ const type = r.u32();
202
+ const known = /^(general\.(architecture|name|file_type)|.*\.(block_count|embedding_length|feed_forward_length|attention\.head_count|attention\.head_count_kv|context_length|vocab_size)|tokenizer\.ggml\.vocab_size)$/.test(key);
203
+ const value = known ? readScalar(r, type) : (skipValue(r, type), null);
204
+ info.keysRead++;
205
+ if (key === 'general.architecture' && typeof value === 'string')
206
+ info.architecture = value;
207
+ if (key === 'general.name' && typeof value === 'string')
208
+ info.name = value;
209
+ if (key === 'general.file_type' && typeof value === 'number') {
210
+ info.fileType = value;
211
+ info.quantLabel = FILE_TYPE_QUANT[value] || null;
212
+ }
213
+ if (key.endsWith('.block_count') && typeof value === 'number')
214
+ info.layers = value;
215
+ if (key.endsWith('.embedding_length') && typeof value === 'number')
216
+ info.embeddingLength = value;
217
+ if (key.endsWith('.feed_forward_length') && typeof value === 'number')
218
+ info.feedForward = value;
219
+ if (key.endsWith('.attention.head_count') && typeof value === 'number')
220
+ info.headCount = value;
221
+ if (key.endsWith('.attention.head_count_kv') && typeof value === 'number')
222
+ info.headCountKv = value;
223
+ if (key.endsWith('.context_length') && typeof value === 'number')
224
+ info.contextTrain = value;
225
+ if ((key.endsWith('.vocab_size') || key === 'tokenizer.ggml.vocab_size') && typeof value === 'number')
226
+ info.vocabSize = value;
227
+ const essential = info.architecture && info.layers && info.embeddingLength && info.fileType != null;
228
+ if (essential && i > 12)
229
+ break;
230
+ }
231
+ info.complete = true;
232
+ }
233
+ catch {
234
+ info.complete = false;
235
+ }
236
+ if (info.layers && info.embeddingLength) {
237
+ const kvRatio = info.headCount && info.headCountKv ? info.headCountKv / info.headCount : 0.25;
238
+ const ffn = info.feedForward ?? info.embeddingLength * 3.5;
239
+ const L = info.layers;
240
+ const E = info.embeddingLength;
241
+ const attn = L * (E * E + 2 * E * E * kvRatio + E * E);
242
+ const mlp = L * 3 * E * ffn;
243
+ const embed = (info.vocabSize ?? 128256) * E;
244
+ info.paramB = Math.round(((attn + mlp + embed) / 1e9) * 10) / 10;
245
+ }
246
+ return info;
247
+ }
248
+ export async function readGgufHeader(path, maxBytes = 4 * 1024 * 1024) {
249
+ const buf = await readFirstBytes(path, maxBytes);
250
+ if (buf.length < 4 || buf.subarray(0, 4).toString('latin1') !== 'GGUF')
251
+ throw new Error('Not a GGUF file (bad magic)');
252
+ return parseGgufHeader(buf);
253
+ }
254
+ const HF_HOSTS = new Set(['huggingface.co', 'www.huggingface.co', 'hf.co', 'www.hf.co']);
255
+ export function parseHfUrl(raw) {
256
+ let url;
257
+ try {
258
+ url = new URL(raw);
259
+ }
260
+ catch {
261
+ return null;
262
+ }
263
+ if (url.protocol !== 'https:' || !HF_HOSTS.has(url.hostname.toLowerCase()))
264
+ return null;
265
+ const parts = url.pathname.split('/').filter(Boolean).map((p) => decodeURIComponent(p));
266
+ if (parts.length < 5)
267
+ return null;
268
+ const repo = `${parts[0]}/${parts[1]}`;
269
+ const mode = parts[2];
270
+ if (mode !== 'resolve' && mode !== 'blob' && mode !== 'raw')
271
+ return null;
272
+ const rev = parts[3] || 'main';
273
+ const file = parts.slice(4).join('/');
274
+ const encoded = parts.slice(4).map((p) => encodeURIComponent(p)).join('/');
275
+ return {
276
+ repo,
277
+ file,
278
+ rev,
279
+ downloadUrl: `https://huggingface.co/${repo}/resolve/${rev}/${encoded}`,
280
+ };
281
+ }
282
+ // HuggingFace stores at most 50 GB per file, so a bigger single GGUF is not a real Hub model.
283
+ export const MAX_MODEL_BYTES = 64 * 1024 ** 3;
284
+ const MAX_REDIRECTS = 5;
285
+ function isLoopback(hostname) {
286
+ return /^(localhost|127\.\d+\.\d+\.\d+|\[::1\])$/i.test(hostname);
287
+ }
288
+ function isHfHost(hostname) {
289
+ const host = hostname.toLowerCase();
290
+ return ['huggingface.co', 'hf.co'].some((domain) => host === domain || host.endsWith(`.${domain}`));
291
+ }
292
+ function checkModelHop(url, options) {
293
+ if (url.username || url.password)
294
+ throw new Error('Model URLs must not contain credentials');
295
+ if (options.allowLocalhost && isLoopback(url.hostname) && /^https?:$/.test(url.protocol))
296
+ return;
297
+ if (url.protocol !== 'https:')
298
+ throw new Error(`Model downloads must use https:// (refused ${url.protocol}//${url.host})`);
299
+ if (url.port && url.port !== '443')
300
+ throw new Error(`Model downloads must use the default https port (refused ${url.host})`);
301
+ if (!isHfHost(url.hostname))
302
+ throw new Error(`Model downloads are limited to HuggingFace (refused ${url.hostname})`);
303
+ }
304
+ /** Validates a model link and returns the canonical HuggingFace resolve URL to download. */
305
+ export function checkModelUrl(raw, options = {}) {
306
+ let url;
307
+ try {
308
+ url = new URL(raw);
309
+ }
310
+ catch {
311
+ throw new Error('Model URL is not a valid URL');
312
+ }
313
+ checkModelHop(url, options);
314
+ let path;
315
+ try {
316
+ path = decodeURIComponent(url.pathname);
317
+ }
318
+ catch {
319
+ throw new Error('Model URL has an invalid path');
320
+ }
321
+ const refused = rejectModelFile(path);
322
+ if (refused)
323
+ throw new Error(refused);
324
+ if (options.allowLocalhost && isLoopback(url.hostname))
325
+ return url.href;
326
+ const parsed = parseHfUrl(url.href);
327
+ if (!parsed)
328
+ throw new Error('Use a full HuggingFace link to a .gguf file, like https://huggingface.co/<org>/<repo>/resolve/main/<file>.gguf');
329
+ return parsed.downloadUrl;
330
+ }
331
+ /** fetch() that follows redirects itself, so every hop must also be https on HuggingFace. */
332
+ export async function fetchModelUrl(url, init, options = {}) {
333
+ const fetchImpl = options.fetchImpl ?? fetch;
334
+ let current = new URL(url);
335
+ for (let hop = 0;; hop++) {
336
+ checkModelHop(current, options);
337
+ const response = await fetchImpl(current.href, { ...init, redirect: 'manual' });
338
+ const location = response.headers.get('location');
339
+ if (response.status < 300 || response.status >= 400 || !location)
340
+ return response;
341
+ await response.body?.cancel().catch(() => undefined);
342
+ if (hop >= MAX_REDIRECTS)
343
+ throw new Error('Too many redirects while fetching the model');
344
+ current = new URL(location, current);
345
+ }
346
+ }
347
+ export async function fetchHfFileInfo(repo, file, timeoutMs = 15_000, rev = 'main', fetchImpl = fetch) {
348
+ const response = await fetchImpl(`https://huggingface.co/api/models/${repo}/revision/${encodeURIComponent(rev)}?blobs=true`, {
349
+ headers: { 'user-agent': USER_AGENT },
350
+ signal: AbortSignal.timeout(timeoutMs),
351
+ });
352
+ if (!response.ok)
353
+ throw new Error(`HuggingFace API returned ${response.status}`);
354
+ const data = await response.json();
355
+ const siblings = data.siblings || [];
356
+ const hit = siblings.find((s) => s.rfilename === file);
357
+ return {
358
+ sha256: hit?.lfs?.sha256 || hit?.lfs?.oid || null,
359
+ size: hit?.lfs?.size ?? null,
360
+ siblings: siblings.map((s) => s.rfilename),
361
+ };
362
+ }
363
+ export async function inspectHuggingFace(rawUrl, timeoutMs = 15_000) {
364
+ const parsed = parseHfUrl(rawUrl);
365
+ if (!parsed)
366
+ throw new Error('Paste a full HuggingFace resolve link to a .gguf file');
367
+ const refused = rejectModelFile(parsed.file);
368
+ if (refused)
369
+ throw new Error(refused);
370
+ const nameParam = paramBFromName(`${parsed.repo} ${parsed.file}`);
371
+ const nameQuant = quantFromName(parsed.file);
372
+ const result = {
373
+ repo: parsed.repo,
374
+ file: parsed.file,
375
+ url: parsed.downloadUrl,
376
+ sha256: null,
377
+ size: null,
378
+ info: null,
379
+ paramB: nameParam,
380
+ quant: nameQuant,
381
+ layers: null,
382
+ contextTrain: null,
383
+ needsMb: nameParam ? estimateVramMb({ paramB: nameParam, quant: nameQuant || 'Q4_K_M', context: 8192 }) : null,
384
+ headerChecked: false,
385
+ note: 'Filename only - header was not read',
386
+ };
387
+ const signal = AbortSignal.timeout(timeoutMs);
388
+ try {
389
+ const meta = await fetchHfFileInfo(parsed.repo, parsed.file, timeoutMs, parsed.rev);
390
+ result.sha256 = meta.sha256;
391
+ result.size = meta.size;
392
+ }
393
+ catch {
394
+ /* still try header */
395
+ }
396
+ try {
397
+ const response = await fetchModelUrl(parsed.downloadUrl, {
398
+ headers: { range: 'bytes=0-4194303', 'user-agent': USER_AGENT },
399
+ signal,
400
+ });
401
+ if (response.ok || response.status === 206) {
402
+ const buf = Buffer.from(await response.arrayBuffer());
403
+ if (buf.subarray(0, 4).toString('latin1') === 'GGUF') {
404
+ const info = parseGgufHeader(buf);
405
+ result.info = info;
406
+ result.layers = info.layers;
407
+ result.contextTrain = info.contextTrain;
408
+ result.headerChecked = info.layers != null;
409
+ if (info.quantLabel)
410
+ result.quant = info.quantLabel;
411
+ const mismatch = layerMismatch(nameParam, info.layers);
412
+ if (mismatch)
413
+ throw new Error(mismatch);
414
+ if (info.paramB && (!result.paramB || Math.abs(info.paramB - result.paramB) / result.paramB < 0.45))
415
+ result.paramB = info.paramB;
416
+ if (result.paramB) {
417
+ result.needsMb = estimateVramMb({
418
+ paramB: result.paramB,
419
+ quant: result.quant || 'Q4_K_M',
420
+ context: 8192,
421
+ layers: info.layers ?? undefined,
422
+ });
423
+ }
424
+ result.note = result.headerChecked ? 'GGUF header read' : 'Partial header';
425
+ }
426
+ }
427
+ }
428
+ catch (error) {
429
+ if (error instanceof Error && /layers/.test(error.message))
430
+ throw error;
431
+ }
432
+ return result;
433
+ }
434
+ export class BufferWriter {
435
+ parts = [];
436
+ u8(n) { const b = Buffer.alloc(1); b.writeUInt8(n); this.parts.push(b); }
437
+ u16(n) { const b = Buffer.alloc(2); b.writeUInt16LE(n); this.parts.push(b); }
438
+ u32(n) { const b = Buffer.alloc(4); b.writeUInt32LE(n); this.parts.push(b); }
439
+ i32(n) { const b = Buffer.alloc(4); b.writeInt32LE(n); this.parts.push(b); }
440
+ u64(n) { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(n)); this.parts.push(b); }
441
+ str(s) { const body = Buffer.from(s); this.u64(body.length); this.parts.push(body); }
442
+ raw(b) { this.parts.push(b); }
443
+ buf() { return Buffer.concat(this.parts); }
444
+ }
445
+ export function buildGgufSample(pairs) {
446
+ const w = new BufferWriter();
447
+ w.raw(Buffer.from('GGUF'));
448
+ w.u32(3);
449
+ w.u64(0);
450
+ w.u64(pairs.length);
451
+ for (const pair of pairs) {
452
+ w.str(pair.key);
453
+ w.u32(pair.type);
454
+ pair.write(w);
455
+ }
456
+ return w.buf();
457
+ }
458
+ //# sourceMappingURL=gguf.js.map
@@ -0,0 +1,78 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { Logger } from '../logger.js';
3
+ export type RunnerStatus = 'stopped' | 'starting' | 'ready' | 'restarting' | 'failed';
4
+ export type LlamaRunnerOptions = {
5
+ binaryPath: string;
6
+ modelPath: string;
7
+ context: number;
8
+ slots?: number;
9
+ host?: string;
10
+ port?: number;
11
+ gpuLayers?: number;
12
+ extraArgs?: string[];
13
+ /** Used by tests to put a script before the llama flags. */
14
+ baseArgs?: string[];
15
+ env?: NodeJS.ProcessEnv;
16
+ logger?: Logger;
17
+ logFile?: string;
18
+ healthTimeoutMs?: number;
19
+ restart?: false | {
20
+ maxRestarts?: number;
21
+ windowMs?: number;
22
+ delayMs?: number;
23
+ };
24
+ onLog?: (line: string) => void;
25
+ };
26
+ export type LlamaProps = {
27
+ n_ctx?: number;
28
+ default_generation_settings?: {
29
+ n_ctx?: number;
30
+ };
31
+ total_slots?: number;
32
+ model_path?: string;
33
+ [key: string]: unknown;
34
+ };
35
+ export declare function buildLlamaArgs(options: Pick<LlamaRunnerOptions, 'modelPath' | 'context' | 'slots' | 'host' | 'port' | 'gpuLayers' | 'extraArgs'>): string[];
36
+ /**
37
+ * llama-server reads its own settings from the environment: LLAMA_ARG_* (any flag, such as a
38
+ * chat template, a KV-cache type, or sampling defaults), LLAMA_LOG_* and LLAMA_API_KEY. A host
39
+ * who also uses llama.cpp privately may have these set. They must not reach the engine: buyers
40
+ * were promised the locked flags, the server's model checks expect them, an API key would lock
41
+ * the agent out of its own engine, and a log setting could write prompts to disk.
42
+ */
43
+ export declare function engineEnv(base: NodeJS.ProcessEnv, extra?: NodeJS.ProcessEnv): {
44
+ env: NodeJS.ProcessEnv;
45
+ dropped: string[];
46
+ };
47
+ export declare function findFreePort(host?: string): Promise<number>;
48
+ export declare class LlamaRunner extends EventEmitter {
49
+ private readonly options;
50
+ private readonly logger?;
51
+ private child;
52
+ private logStream;
53
+ private _status;
54
+ private _port;
55
+ private _url;
56
+ private _props;
57
+ private _pid;
58
+ private stopping;
59
+ private restartTimes;
60
+ private startPromise;
61
+ private restartTimer;
62
+ private _restarts;
63
+ private failuresSinceReady;
64
+ constructor(options: LlamaRunnerOptions);
65
+ get status(): RunnerStatus;
66
+ get port(): number;
67
+ get url(): string;
68
+ get pid(): number | null;
69
+ get props(): LlamaProps | null;
70
+ get restarts(): number;
71
+ setStatus(status: RunnerStatus): void;
72
+ start(): Promise<LlamaProps>;
73
+ private startInternal;
74
+ private waitForHealth;
75
+ private scheduleRestart;
76
+ stop(): Promise<void>;
77
+ isHealthy(): Promise<boolean>;
78
+ }