@otto-code/brain 0.8.8 → 0.8.9
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/dist/commands/bench.js +19 -5
- package/dist/commands/catalog.d.ts +1 -0
- package/dist/commands/catalog.js +1 -0
- package/dist/commands/pull.js +14 -33
- package/dist/commands/repo-download.d.ts +14 -0
- package/dist/commands/repo-download.js +29 -0
- package/dist/commands/search.js +6 -14
- package/dist/config/builtin-hosting-profiles.d.ts +8 -0
- package/dist/config/builtin-hosting-profiles.js +32 -0
- package/dist/config/hosting-profiles.d.ts +33 -0
- package/dist/config/hosting-profiles.js +71 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/config/paths.d.ts +2 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/profile-edit.d.ts +4 -2
- package/dist/config/profile-edit.js +94 -4
- package/dist/config/profiles.js +25 -2
- package/dist/config/schema.d.ts +494 -24
- package/dist/config/schema.js +55 -0
- package/dist/config/store.js +12 -2
- package/dist/gguf.d.ts +1 -0
- package/dist/gguf.js +1 -0
- package/dist/models/download.js +5 -0
- package/dist/models/enrich.d.ts +6 -0
- package/dist/models/enrich.js +50 -2
- package/dist/ops/calibrate.d.ts +4 -1
- package/dist/ops/calibrate.js +10 -7
- package/dist/ops/sweep.d.ts +3 -1
- package/dist/ops/sweep.js +3 -3
- package/dist/runtime/args.d.ts +2 -2
- package/dist/runtime/args.js +13 -1
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +10 -0
- package/dist/service/host-api.d.ts +17 -1
- package/dist/service/host-api.js +196 -13
- package/dist/service/router.d.ts +18 -0
- package/dist/service/router.js +89 -4
- package/dist/service/serve.js +184 -12
- package/dist/service/supervisor.d.ts +32 -4
- package/dist/service/supervisor.js +30 -6
- package/dist/tui/app.js +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/vram.js +3 -3
- package/package.json +1 -1
package/dist/service/serve.js
CHANGED
|
@@ -13,7 +13,7 @@ import { execFile, spawn } from "node:child_process";
|
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import http from "node:http";
|
|
15
15
|
import https from "node:https";
|
|
16
|
-
import { getCalibration, forModel, loadPersistedConfig, loadProfilesStore, saveBrainConfig, saveProfilesStore, } from "../config/index.js";
|
|
16
|
+
import { getCalibration, forModel, loadPersistedConfig, loadProfilesStore, put, putCalibration, saveBrainConfig, saveProfilesStore, } from "../config/index.js";
|
|
17
17
|
import { resolveBrainPaths } from "../config/paths.js";
|
|
18
18
|
import { query as queryGpu } from "../gpu.js";
|
|
19
19
|
import { managedModelsDir, pickAutoModel, pickModel, scanModels } from "../models/index.js";
|
|
@@ -22,6 +22,10 @@ import { resolveRuntime } from "../runtime/index.js";
|
|
|
22
22
|
import * as vram from "../vram.js";
|
|
23
23
|
import { resolveVersion } from "../version.js";
|
|
24
24
|
import * as results from "../ops/results.js";
|
|
25
|
+
import * as archive from "../ops/archive.js";
|
|
26
|
+
import { calibrate } from "../ops/calibrate.js";
|
|
27
|
+
import { sweep } from "../ops/sweep.js";
|
|
28
|
+
import * as bench from "../bench/index.js";
|
|
25
29
|
import { createCpuSampler, sample as sampleSystem } from "../sysmon.js";
|
|
26
30
|
import { createHostApi } from "./host-api.js";
|
|
27
31
|
import { errorMessage } from "./http-util.js";
|
|
@@ -58,14 +62,10 @@ function collectEvals() {
|
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
const REMOTE_JOB_RETENTION_MS = 5 * 60000;
|
|
61
|
-
/**
|
|
62
|
-
* Runs a benchmark as a child of the brain service. This is intentionally here
|
|
63
|
-
* rather than in the connecting daemon: its process, model store, results
|
|
64
|
-
* directory and GPU all belong to the host that is being benchmarked.
|
|
65
|
-
*/
|
|
66
65
|
class ServiceJobRunner {
|
|
67
|
-
constructor(onPullCompleted) {
|
|
66
|
+
constructor(onPullCompleted, runResidentJob) {
|
|
68
67
|
this.onPullCompleted = onPullCompleted;
|
|
68
|
+
this.runResidentJob = runResidentJob;
|
|
69
69
|
this.jobs = new Map();
|
|
70
70
|
}
|
|
71
71
|
start(kind, target, args) {
|
|
@@ -88,7 +88,26 @@ class ServiceJobRunner {
|
|
|
88
88
|
startedAt: new Date().toISOString(),
|
|
89
89
|
finishedAt: null,
|
|
90
90
|
child: null,
|
|
91
|
+
controller: null,
|
|
91
92
|
};
|
|
93
|
+
this.jobs.set(job.id, job);
|
|
94
|
+
if (kind === "calibrate" || kind === "sweep" || kind === "bench") {
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
job.controller = controller;
|
|
97
|
+
void this.runResidentJob(kind, target, {
|
|
98
|
+
message: (value) => {
|
|
99
|
+
if (job.status === "running")
|
|
100
|
+
job.message = value.slice(-1000);
|
|
101
|
+
},
|
|
102
|
+
percent: (value) => {
|
|
103
|
+
if (job.status === "running")
|
|
104
|
+
job.percent = value;
|
|
105
|
+
},
|
|
106
|
+
}, controller.signal)
|
|
107
|
+
.then(() => this.finish(job, "succeeded", null))
|
|
108
|
+
.catch((error) => this.finish(job, controller.signal.aborted ? "canceled" : "failed", errorMessage(error)));
|
|
109
|
+
return this.publicJob(job);
|
|
110
|
+
}
|
|
92
111
|
// The service is launched by the same CLI entry point as `otto-brain bench`.
|
|
93
112
|
// Reusing that entry point keeps its config/path resolution on this host.
|
|
94
113
|
const entry = process.argv[1];
|
|
@@ -101,7 +120,6 @@ class ServiceJobRunner {
|
|
|
101
120
|
windowsHide: true,
|
|
102
121
|
});
|
|
103
122
|
job.child = child;
|
|
104
|
-
this.jobs.set(job.id, job);
|
|
105
123
|
child.stderr?.setEncoding("utf8");
|
|
106
124
|
child.stderr?.on("data", (chunk) => this.ingestOutput(job, chunk));
|
|
107
125
|
child.once("error", (error) => this.finish(job, "failed", error.message));
|
|
@@ -166,8 +184,13 @@ class ServiceJobRunner {
|
|
|
166
184
|
}
|
|
167
185
|
async cancel(jobId) {
|
|
168
186
|
const job = this.jobs.get(jobId);
|
|
169
|
-
if (!job || job.status !== "running"
|
|
187
|
+
if (!job || job.status !== "running")
|
|
188
|
+
return this.list();
|
|
189
|
+
if (!job.child) {
|
|
190
|
+
job.controller?.abort();
|
|
191
|
+
this.finish(job, "canceled", "Canceled.");
|
|
170
192
|
return this.list();
|
|
193
|
+
}
|
|
171
194
|
const child = job.child;
|
|
172
195
|
if (process.platform === "win32" && child.pid) {
|
|
173
196
|
await new Promise((resolve) => execFile("taskkill", ["/pid", String(child.pid), "/t", "/f"], () => resolve()));
|
|
@@ -196,13 +219,14 @@ class ServiceJobRunner {
|
|
|
196
219
|
if (job.status !== "running")
|
|
197
220
|
return;
|
|
198
221
|
job.child = null;
|
|
222
|
+
job.controller = null;
|
|
199
223
|
job.status = status;
|
|
200
224
|
job.error = error;
|
|
201
225
|
job.finishedAt = new Date().toISOString();
|
|
202
226
|
if (status === "succeeded")
|
|
203
227
|
job.percent = 100;
|
|
204
228
|
}
|
|
205
|
-
publicJob({ child: _child, ...job }) {
|
|
229
|
+
publicJob({ child: _child, controller: _controller, ...job }) {
|
|
206
230
|
return job;
|
|
207
231
|
}
|
|
208
232
|
}
|
|
@@ -334,7 +358,7 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
334
358
|
}
|
|
335
359
|
}
|
|
336
360
|
const telemetry = new Telemetry();
|
|
337
|
-
const supervisor = new Supervisor({ runtime });
|
|
361
|
+
const supervisor = new Supervisor({ runtime, paths, getProfilesStore: () => store });
|
|
338
362
|
supervisor.on("log", (line) => {
|
|
339
363
|
runLog.write(line);
|
|
340
364
|
if (/error|failed|warn/i.test(line))
|
|
@@ -367,6 +391,7 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
367
391
|
fitProfile = fit.profile;
|
|
368
392
|
}
|
|
369
393
|
await supervisor.start(target, fitProfile);
|
|
394
|
+
delete store.pendingReloadModelIds[target.id];
|
|
370
395
|
store.lastModelId = target.id;
|
|
371
396
|
saveProfilesStore(store, paths);
|
|
372
397
|
};
|
|
@@ -419,7 +444,153 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
419
444
|
// /__host/events. One instance, so `capabilities.events` and the stream can
|
|
420
445
|
// never disagree about whether this brain publishes.
|
|
421
446
|
const statusEvents = new BrainStatusPublisher();
|
|
422
|
-
const
|
|
447
|
+
const runResidentJob = async (kind, target, update, signal) => {
|
|
448
|
+
const ensureActive = () => {
|
|
449
|
+
if (signal.aborted)
|
|
450
|
+
throw new Error("Operation canceled.");
|
|
451
|
+
};
|
|
452
|
+
const modelId = target ?? supervisor.model?.id ?? store.lastModelId ?? null;
|
|
453
|
+
const targetModel = modelId
|
|
454
|
+
? catalog.find((candidate) => candidate.id === modelId || candidate.displayName === modelId)
|
|
455
|
+
: null;
|
|
456
|
+
if (!targetModel)
|
|
457
|
+
throw new Error("No installed model is available for this operation.");
|
|
458
|
+
const restoreModel = supervisor.model;
|
|
459
|
+
const restoreProfile = supervisor.profile;
|
|
460
|
+
const restore = async () => {
|
|
461
|
+
if (restoreModel && restoreProfile && !signal.aborted) {
|
|
462
|
+
await supervisor.start(restoreModel, restoreProfile, { preserveLogs: true });
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
if (kind === "calibrate") {
|
|
466
|
+
const runtime = supervisor.runtime ?? resolveRuntime(config, env);
|
|
467
|
+
if (!runtime)
|
|
468
|
+
throw new Error("no llama.cpp runtime available; install one from the Library tab");
|
|
469
|
+
const profile = forModel(store, targetModel, config.defaults);
|
|
470
|
+
update.message(`Calibrating ${targetModel.displayName}`);
|
|
471
|
+
supervisor.recordLog(`operation calibrate: ${targetModel.displayName}`);
|
|
472
|
+
try {
|
|
473
|
+
const measurement = await calibrate({
|
|
474
|
+
runtime,
|
|
475
|
+
model: targetModel,
|
|
476
|
+
profile,
|
|
477
|
+
supervisor,
|
|
478
|
+
onProgress: (event) => {
|
|
479
|
+
ensureActive();
|
|
480
|
+
const message = event.phase === "loading"
|
|
481
|
+
? `Calibrating ${event.contextSize.toLocaleString()} context`
|
|
482
|
+
: event.phase === "measured"
|
|
483
|
+
? `Measured ${event.contextSize.toLocaleString()} context`
|
|
484
|
+
: (event.reason ??
|
|
485
|
+
event.error ??
|
|
486
|
+
`Skipped ${event.contextSize.toLocaleString()} context`);
|
|
487
|
+
update.message(message);
|
|
488
|
+
supervisor.recordLog(`operation calibrate: ${message}`);
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
ensureActive();
|
|
492
|
+
putCalibration(store, targetModel, profile, measurement);
|
|
493
|
+
saveProfilesStore(store, paths);
|
|
494
|
+
update.percent(100);
|
|
495
|
+
supervisor.recordLog(`operation calibrate: saved measurement for ${targetModel.displayName}`);
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
await restore();
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (kind === "sweep") {
|
|
503
|
+
const runtime = supervisor.runtime ?? resolveRuntime(config, env);
|
|
504
|
+
if (!runtime)
|
|
505
|
+
throw new Error("no llama.cpp runtime available; install one from the Library tab");
|
|
506
|
+
const profile = forModel(store, targetModel, config.defaults);
|
|
507
|
+
update.message(`Sweeping ${targetModel.displayName}`);
|
|
508
|
+
supervisor.recordLog(`operation sweep: ${targetModel.displayName}`);
|
|
509
|
+
try {
|
|
510
|
+
const report = await sweep({
|
|
511
|
+
runtime,
|
|
512
|
+
model: targetModel,
|
|
513
|
+
profile,
|
|
514
|
+
supervisor,
|
|
515
|
+
onProgress: (event) => {
|
|
516
|
+
ensureActive();
|
|
517
|
+
const message = event.phase === "loading"
|
|
518
|
+
? `Budget ${event.budget}: loading`
|
|
519
|
+
: event.phase === "generating"
|
|
520
|
+
? `Budget ${event.budget}: generating`
|
|
521
|
+
: event.phase === "done"
|
|
522
|
+
? `Budget ${event.budget}: complete`
|
|
523
|
+
: `Budget ${event.budget}: ${event.error ?? "failed"}`;
|
|
524
|
+
update.message(message);
|
|
525
|
+
supervisor.recordLog(`operation sweep: ${message}`);
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
ensureActive();
|
|
529
|
+
if (report.recommended !== null) {
|
|
530
|
+
profile.reasoningBudget = report.recommended;
|
|
531
|
+
put(store, targetModel, profile);
|
|
532
|
+
saveProfilesStore(store, paths);
|
|
533
|
+
supervisor.recordLog(`operation sweep: saved budget ${report.recommended}`);
|
|
534
|
+
}
|
|
535
|
+
update.percent(100);
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
await restore();
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
update.message(`Benchmarking ${targetModel.displayName}`);
|
|
543
|
+
supervisor.recordLog(`operation benchmark: ${targetModel.displayName}`);
|
|
544
|
+
await loadModel(targetModel);
|
|
545
|
+
ensureActive();
|
|
546
|
+
supervisor.recordLog(`operation benchmark: resident model ready`);
|
|
547
|
+
const profile = supervisor.profile ?? forModel(store, targetModel, config.defaults);
|
|
548
|
+
const gpuInfo = await queryGpu();
|
|
549
|
+
const calibration = getCalibration(store, targetModel, profile);
|
|
550
|
+
const fit = gpuInfo
|
|
551
|
+
? vram.fitToBudget({
|
|
552
|
+
model: targetModel,
|
|
553
|
+
profile,
|
|
554
|
+
calibration,
|
|
555
|
+
totalVramBytes: gpuInfo.totalBytes,
|
|
556
|
+
})
|
|
557
|
+
: null;
|
|
558
|
+
const archiveId = archive.runId(targetModel);
|
|
559
|
+
const report = await bench.runSuite({
|
|
560
|
+
host: supervisor.host,
|
|
561
|
+
port: supervisor.internalPort,
|
|
562
|
+
concurrency: 3,
|
|
563
|
+
reasoningBudget: profile.reasoningBudget ?? null,
|
|
564
|
+
contextWindow: profile.contextSize ?? null,
|
|
565
|
+
archiveId,
|
|
566
|
+
onProgress: (event) => {
|
|
567
|
+
ensureActive();
|
|
568
|
+
const message = event.title
|
|
569
|
+
? `${event.title}: ${event.phase}`
|
|
570
|
+
: (event.summary ?? event.phase);
|
|
571
|
+
update.message(message);
|
|
572
|
+
supervisor.recordLog(`operation benchmark: ${message}`);
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
ensureActive();
|
|
576
|
+
results.save({
|
|
577
|
+
model: targetModel,
|
|
578
|
+
profile,
|
|
579
|
+
report,
|
|
580
|
+
gpu: gpuInfo,
|
|
581
|
+
runtime: supervisor.runtime
|
|
582
|
+
? `${supervisor.runtime.label} v${supervisor.runtime.version}`
|
|
583
|
+
: "unknown runtime",
|
|
584
|
+
archiveId,
|
|
585
|
+
args: supervisor.args,
|
|
586
|
+
fit,
|
|
587
|
+
calibration,
|
|
588
|
+
suite: { execute: true, concurrency: 3, depths: null, only: null, mined: false },
|
|
589
|
+
});
|
|
590
|
+
update.percent(100);
|
|
591
|
+
supervisor.recordLog(`operation benchmark: saved result for ${targetModel.displayName}`);
|
|
592
|
+
};
|
|
593
|
+
const jobs = new ServiceJobRunner(rescanCatalog, runResidentJob);
|
|
423
594
|
// Assigned once `stop` exists below. This indirection lets the management API
|
|
424
595
|
// answer a remote restart request before closing its own socket.
|
|
425
596
|
let requestRestart = () => { };
|
|
@@ -496,6 +667,7 @@ export async function startService({ config, modelNeedle, env = process.env, onL
|
|
|
496
667
|
certManager?.start();
|
|
497
668
|
if (model && profile && runtime) {
|
|
498
669
|
await supervisor.start(model, profile);
|
|
670
|
+
delete store.pendingReloadModelIds[model.id];
|
|
499
671
|
store.lastModelId = model.id;
|
|
500
672
|
saveProfilesStore(store, paths);
|
|
501
673
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type ChildProcess } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
+
import { type BrainPaths } from "../config/paths.js";
|
|
3
4
|
import type { Model, Runtime } from "../types.js";
|
|
4
|
-
import type { Profile } from "../config/schema.js";
|
|
5
|
+
import type { Profile, ProfilesStore } from "../config/schema.js";
|
|
5
6
|
/**
|
|
6
7
|
* Default loopback port for the private llama-server child. Deliberately clear
|
|
7
8
|
* of Otto's space: 8081 (the old default) is the Expo/Metro dev port, so a brain
|
|
@@ -19,6 +20,14 @@ export interface SupervisorOptions {
|
|
|
19
20
|
internalPort?: number;
|
|
20
21
|
host?: string;
|
|
21
22
|
readyTimeoutMs?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Long-lived hosts provide their live store so profile edits applied just
|
|
25
|
+
* before a model switch are visible without a second disk read. Standalone
|
|
26
|
+
* operations use the current persisted store, which still preserves the
|
|
27
|
+
* launch-resolution invariant.
|
|
28
|
+
*/
|
|
29
|
+
paths?: BrainPaths;
|
|
30
|
+
getProfilesStore?: () => ProfilesStore;
|
|
22
31
|
}
|
|
23
32
|
export interface SupervisorStatus {
|
|
24
33
|
state: SupervisorState;
|
|
@@ -44,6 +53,8 @@ export declare class Supervisor extends EventEmitter {
|
|
|
44
53
|
internalPort: number;
|
|
45
54
|
host: string;
|
|
46
55
|
readyTimeoutMs: number;
|
|
56
|
+
paths: BrainPaths;
|
|
57
|
+
getProfilesStore: () => ProfilesStore;
|
|
47
58
|
state: SupervisorState;
|
|
48
59
|
child: ChildProcess | null;
|
|
49
60
|
model: Model | null;
|
|
@@ -61,10 +72,27 @@ export declare class Supervisor extends EventEmitter {
|
|
|
61
72
|
* shell line is for reading, not for re-parsing.
|
|
62
73
|
*/
|
|
63
74
|
args: string[] | null;
|
|
64
|
-
constructor({ runtime, internalPort, host, readyTimeoutMs, }: SupervisorOptions);
|
|
75
|
+
constructor({ runtime, internalPort, host, readyTimeoutMs, paths, getProfilesStore, }: SupervisorOptions);
|
|
65
76
|
get upstreamBase(): string;
|
|
66
|
-
/**
|
|
67
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Add a host-operation event to the same bounded tail as llama-server output.
|
|
79
|
+
*
|
|
80
|
+
* Calibrate, sweep, and benchmark deliberately reuse this supervisor rather
|
|
81
|
+
* than creating invisible sidecar servers. Their lifecycle markers belong in
|
|
82
|
+
* the same log stream as the child they exercise.
|
|
83
|
+
*/
|
|
84
|
+
recordLog(line: string): void;
|
|
85
|
+
/**
|
|
86
|
+
* Start (or restart) the server for a model + profile.
|
|
87
|
+
*
|
|
88
|
+
* This is the sole llama-server launch boundary, so it materializes the
|
|
89
|
+
* selected hosting profile here. Keeping it beside `buildArgs()` makes the
|
|
90
|
+
* Jinja template and router-visible system addendum mandatory for every
|
|
91
|
+
* caller, including future maintenance operations that start a sidecar.
|
|
92
|
+
*/
|
|
93
|
+
start(model: Model, profile: Profile, options?: {
|
|
94
|
+
preserveLogs?: boolean;
|
|
95
|
+
}): Promise<this>;
|
|
68
96
|
/** Fetch /props from the running server (modalities, template caps, defaults). */
|
|
69
97
|
props(): Promise<unknown>;
|
|
70
98
|
stop(): Promise<void>;
|
|
@@ -8,6 +8,9 @@ import http from "node:http";
|
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
10
|
import { buildArgs, buildEnv, formatCommand } from "../runtime/index.js";
|
|
11
|
+
import { resolveHostingProfileForLaunch } from "../config/hosting-profiles.js";
|
|
12
|
+
import { resolveBrainPaths } from "../config/paths.js";
|
|
13
|
+
import { loadProfilesStore } from "../config/store.js";
|
|
11
14
|
import { usedBytes } from "../gpu.js";
|
|
12
15
|
const LOG_LINES_KEPT = 300;
|
|
13
16
|
/**
|
|
@@ -28,13 +31,15 @@ export const DEFAULT_INTERNAL_PORT = 20800;
|
|
|
28
31
|
* stable one so switching models never asks a client to reconnect elsewhere.
|
|
29
32
|
*/
|
|
30
33
|
export class Supervisor extends EventEmitter {
|
|
31
|
-
constructor({ runtime, internalPort = DEFAULT_INTERNAL_PORT, host = "127.0.0.1", readyTimeoutMs = 300000, }) {
|
|
34
|
+
constructor({ runtime, internalPort = DEFAULT_INTERNAL_PORT, host = "127.0.0.1", readyTimeoutMs = 300000, paths = resolveBrainPaths(), getProfilesStore = loadProfilesStore, }) {
|
|
32
35
|
super();
|
|
33
36
|
_Supervisor_instances.add(this);
|
|
34
37
|
this.runtime = runtime;
|
|
35
38
|
this.internalPort = internalPort;
|
|
36
39
|
this.host = host;
|
|
37
40
|
this.readyTimeoutMs = readyTimeoutMs;
|
|
41
|
+
this.paths = paths;
|
|
42
|
+
this.getProfilesStore = getProfilesStore;
|
|
38
43
|
this.state = "stopped"; // stopped | starting | ready | failed
|
|
39
44
|
this.child = null;
|
|
40
45
|
this.model = null;
|
|
@@ -51,8 +56,25 @@ export class Supervisor extends EventEmitter {
|
|
|
51
56
|
get upstreamBase() {
|
|
52
57
|
return `http://${this.host}:${this.internalPort}`;
|
|
53
58
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Add a host-operation event to the same bounded tail as llama-server output.
|
|
61
|
+
*
|
|
62
|
+
* Calibrate, sweep, and benchmark deliberately reuse this supervisor rather
|
|
63
|
+
* than creating invisible sidecar servers. Their lifecycle markers belong in
|
|
64
|
+
* the same log stream as the child they exercise.
|
|
65
|
+
*/
|
|
66
|
+
recordLog(line) {
|
|
67
|
+
__classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_log).call(this, line);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Start (or restart) the server for a model + profile.
|
|
71
|
+
*
|
|
72
|
+
* This is the sole llama-server launch boundary, so it materializes the
|
|
73
|
+
* selected hosting profile here. Keeping it beside `buildArgs()` makes the
|
|
74
|
+
* Jinja template and router-visible system addendum mandatory for every
|
|
75
|
+
* caller, including future maintenance operations that start a sidecar.
|
|
76
|
+
*/
|
|
77
|
+
async start(model, profile, options = {}) {
|
|
56
78
|
await this.stop();
|
|
57
79
|
if (!this.runtime) {
|
|
58
80
|
this.lastError = "no llama.cpp runtime available";
|
|
@@ -60,13 +82,15 @@ export class Supervisor extends EventEmitter {
|
|
|
60
82
|
throw new Error(this.lastError);
|
|
61
83
|
}
|
|
62
84
|
const runtime = this.runtime;
|
|
85
|
+
const launchProfile = resolveHostingProfileForLaunch(this.paths, this.getProfilesStore(), profile, model.family);
|
|
63
86
|
this.model = model;
|
|
64
|
-
this.profile =
|
|
87
|
+
this.profile = launchProfile;
|
|
65
88
|
this.lastError = null;
|
|
66
|
-
|
|
89
|
+
if (!options.preserveLogs)
|
|
90
|
+
this.logLines = [];
|
|
67
91
|
this.vramBaselineBytes = await usedBytes();
|
|
68
92
|
__classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_setState).call(this, "starting");
|
|
69
|
-
const args = buildArgs({ ...
|
|
93
|
+
const args = buildArgs({ ...launchProfile, modelPath: model.modelPath, mmprojPath: model.mmprojPath }, { port: this.internalPort, host: this.host }, model);
|
|
70
94
|
this.args = args;
|
|
71
95
|
this.command = formatCommand(runtime, args);
|
|
72
96
|
__classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_log).call(this, `launching: ${this.command}`);
|
package/dist/tui/app.js
CHANGED
|
@@ -162,7 +162,7 @@ export class App {
|
|
|
162
162
|
this.rankings = new Map(); // model id/name -> averaged benchmark rank + score
|
|
163
163
|
this.rankedModels = []; // ranked list (mean of runs), best first | help
|
|
164
164
|
this.telemetry = new Telemetry();
|
|
165
|
-
this.supervisor = new Supervisor({ runtime });
|
|
165
|
+
this.supervisor = new Supervisor({ runtime, getProfilesStore: () => this.store });
|
|
166
166
|
this.routerServer = null;
|
|
167
167
|
this.supervisor.on("state", () => this.draw());
|
|
168
168
|
this.supervisor.on("log", () => {
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export interface ModelMetadata {
|
|
7
7
|
arch?: string | null;
|
|
8
|
+
name?: string | null;
|
|
9
|
+
basename?: string | null;
|
|
8
10
|
contextLength?: number | null;
|
|
9
11
|
blockCount?: number | null;
|
|
10
12
|
headCount?: number | null;
|
|
@@ -73,6 +75,8 @@ export interface Model {
|
|
|
73
75
|
catalogId?: string;
|
|
74
76
|
/** Back-reference: the hfRepo of the reconciled catalog entry, if matched. */
|
|
75
77
|
catalogHfRepo?: string;
|
|
78
|
+
/** Hosting-profile family from the catalog or normalized GGUF metadata. */
|
|
79
|
+
family?: string;
|
|
76
80
|
/** Present only when this catalog entry declares a component manifest. */
|
|
77
81
|
components?: ModelComponent[];
|
|
78
82
|
}
|
package/dist/vram.js
CHANGED
|
@@ -108,9 +108,9 @@ export function maxContextThatFits({ model, profile, calibration, totalVramBytes
|
|
|
108
108
|
if (room <= 0)
|
|
109
109
|
return null;
|
|
110
110
|
const tokens = Math.floor(room / probe.kvBytesPerToken);
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
const native = model.metadata?.contextLength;
|
|
112
|
+
const contextLimit = typeof native === "number" && native > 0 ? native * profile.contextMultiplier : tokens;
|
|
113
|
+
const capped = Math.min(tokens, contextLimit);
|
|
114
114
|
return Math.max(0, Math.floor(capped / step) * step);
|
|
115
115
|
}
|
|
116
116
|
/**
|
package/package.json
CHANGED