@otto-code/brain 0.8.18 → 0.8.19

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.
@@ -3,6 +3,7 @@ import type { Runtime } from "../types.js";
3
3
  import { type InstallProgress, type RuntimeTarget } from "./managed.js";
4
4
  export { BACKENDS_DIR, LMSTUDIO_ROOT, listRuntimes as listLmStudioRuntimes } from "./lmstudio.js";
5
5
  export { buildArgs, buildEnv, formatCommand, type ServeTarget } from "./args.js";
6
+ export { llamaCppRuntimeDriver, type ModelServerDriverLaunchInput, type ModelServerLaunch, type ModelServerRuntimeDriver, } from "./model-server-driver.js";
6
7
  export { installManagedRuntime, removeManagedRuntime, listManagedRuntimes, listRuntimeDevices, verifyRuntimeExecutable, defaultRuntimeSpec, extractArchive, resolveRuntimeVariant, serverExeName, supportedVariants, DEFAULT_LLAMA_BUILD, listRuntimeReleases, latestRuntimeBuild, resolveLatestBuildOrPin, MissingAssetError, type ResolvedBuild, type RuntimeRelease, type RuntimeSpec, type RuntimeTarget, type RuntimeVariant, type InstallProgress, } from "./managed.js";
7
8
  /** Every runtime available on this machine, managed first then LM Studio. */
8
9
  export declare function listAllRuntimes(env?: NodeJS.ProcessEnv): Runtime[];
@@ -16,6 +16,7 @@ import { listRuntimes as listLmStudioRuntimes, resolveOverride } from "./lmstudi
16
16
  import { defaultRuntimeSpec, installManagedRuntime, listManagedRuntimes, } from "./managed.js";
17
17
  export { BACKENDS_DIR, LMSTUDIO_ROOT, listRuntimes as listLmStudioRuntimes } from "./lmstudio.js";
18
18
  export { buildArgs, buildEnv, formatCommand } from "./args.js";
19
+ export { llamaCppRuntimeDriver, } from "./model-server-driver.js";
19
20
  export { installManagedRuntime, removeManagedRuntime, listManagedRuntimes, listRuntimeDevices, verifyRuntimeExecutable, defaultRuntimeSpec, extractArchive, resolveRuntimeVariant, serverExeName, supportedVariants, DEFAULT_LLAMA_BUILD, listRuntimeReleases, latestRuntimeBuild, resolveLatestBuildOrPin, MissingAssetError, } from "./managed.js";
20
21
  /** Every runtime available on this machine, managed first then LM Studio. */
21
22
  export function listAllRuntimes(env = process.env) {
@@ -0,0 +1,42 @@
1
+ import type { Calibration, Profile } from "../config/schema.js";
2
+ import type { BrainPaths } from "../config/paths.js";
3
+ import type { Model, Runtime } from "../types.js";
4
+ export interface ModelServerLaunch {
5
+ executable: string;
6
+ args: string[];
7
+ cwd: string;
8
+ env: NodeJS.ProcessEnv;
9
+ command: string;
10
+ readinessPath: string;
11
+ propertiesPath: string | null;
12
+ formatLogLine(line: string): string;
13
+ }
14
+ export interface ModelServerDriverLaunchInput {
15
+ runtime: Runtime;
16
+ model: Model;
17
+ profile: Profile;
18
+ calibration: Calibration | null;
19
+ paths: BrainPaths;
20
+ host: string;
21
+ port: number;
22
+ logVerbosity: number;
23
+ }
24
+ /**
25
+ * Driver mechanics live here; the host owns process supervision, security,
26
+ * scheduler admission, status events, and the stable public endpoint.
27
+ */
28
+ export interface ModelServerRuntimeDriver {
29
+ readonly id: string;
30
+ readonly displayName: string;
31
+ /** Native process name for engine-originated diagnostics. */
32
+ readonly processName: string;
33
+ describeProcessExit(input: {
34
+ code: number | null;
35
+ signal: NodeJS.Signals | null;
36
+ }): string;
37
+ describeLaunchError(error: Error): string;
38
+ createLaunch(input: ModelServerDriverLaunchInput): ModelServerLaunch;
39
+ }
40
+ /** The first driver preserves the existing managed llama.cpp launch exactly. */
41
+ export declare const llamaCppRuntimeDriver: ModelServerRuntimeDriver;
42
+ //# sourceMappingURL=model-server-driver.d.ts.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The narrow, observed seam between the Brain host and a model-server engine.
3
+ *
4
+ * This intentionally starts with launch and introspection only. A method joins
5
+ * this contract when the common host needs the behavior from more than one
6
+ * driver; naming every current llama.cpp flag as a generic operation would
7
+ * create a false lowest common denominator before a second engine exists.
8
+ */
9
+ import path from "node:path";
10
+ import { mkdirSync } from "node:fs";
11
+ import { buildArgs, buildEnv, formatCommand } from "./args.js";
12
+ import { formatLlamaServerLog } from "../service/log-format.js";
13
+ /** The first driver preserves the existing managed llama.cpp launch exactly. */
14
+ export const llamaCppRuntimeDriver = {
15
+ id: "llama.cpp",
16
+ displayName: "llama.cpp",
17
+ processName: "llama-server",
18
+ describeProcessExit({ code, signal }) {
19
+ // 3221225781 == 0xC0000135 STATUS_DLL_NOT_FOUND: the vendor DLL trap.
20
+ const hint = code === 3221225781 ? " (missing runtime DLLs - the vendor directory was not on PATH)" : "";
21
+ return `llama-server exited with code ${code}${signal ? ` signal ${signal}` : ""}${hint}`;
22
+ },
23
+ describeLaunchError(error) {
24
+ return `could not launch llama-server: ${error.message}`;
25
+ },
26
+ createLaunch({ runtime, model, profile, calibration, paths, host, port, logVerbosity }) {
27
+ // llama.cpp enables scheduler-required slot erasure only when this existing
28
+ // directory is passed at launch. Failure to create it preserves the old
29
+ // behavior rather than making model startup fail for a cleanup feature.
30
+ const slotSavePath = path.join(paths.root, "slot-saves");
31
+ try {
32
+ mkdirSync(slotSavePath, { recursive: true });
33
+ }
34
+ catch {
35
+ /* launch without native slot actions */
36
+ }
37
+ const args = buildArgs({ ...profile, modelPath: model.modelPath, mmprojPath: model.mmprojPath }, { port, host, logVerbosity, slotSavePath }, model, calibration);
38
+ return {
39
+ executable: runtime.exe,
40
+ args,
41
+ cwd: runtime.dir,
42
+ env: buildEnv(runtime),
43
+ command: formatCommand(runtime, args),
44
+ readinessPath: "/health",
45
+ propertiesPath: "/props",
46
+ formatLogLine: formatLlamaServerLog,
47
+ };
48
+ },
49
+ };
50
+ //# sourceMappingURL=model-server-driver.js.map
@@ -1,5 +1,6 @@
1
1
  import { type ChildProcess } from "node:child_process";
2
2
  import { EventEmitter } from "node:events";
3
+ import { type ModelServerLaunch, type ModelServerRuntimeDriver } from "../runtime/index.js";
3
4
  import { type BrainPaths } from "../config/paths.js";
4
5
  import type { Model, Runtime } from "../types.js";
5
6
  import type { Profile, ProfilesStore } from "../config/schema.js";
@@ -18,6 +19,8 @@ export declare const DEFAULT_INTERNAL_PORT = 20800;
18
19
  export type SupervisorState = "stopped" | "starting" | "ready" | "failed" | "stopping";
19
20
  export interface SupervisorOptions {
20
21
  runtime: Runtime | null;
22
+ /** Native launch/introspection mechanics; lifecycle policy stays in this host. */
23
+ driver?: ModelServerRuntimeDriver;
21
24
  internalPort?: number;
22
25
  host?: string;
23
26
  logVerbosity?: number;
@@ -44,7 +47,7 @@ export interface SupervisorStatus {
44
47
  runtime: string;
45
48
  }
46
49
  /**
47
- * Owns the llama-server child process.
50
+ * Owns the model-server child process.
48
51
  *
49
52
  * The server always listens on a private port; `router.js` fronts it on a
50
53
  * stable one so switching models never asks a client to reconnect elsewhere.
@@ -58,6 +61,7 @@ export declare class Supervisor extends EventEmitter {
58
61
  readyTimeoutMs: number;
59
62
  paths: BrainPaths;
60
63
  getProfilesStore: () => ProfilesStore;
64
+ driver: ModelServerRuntimeDriver;
61
65
  state: SupervisorState;
62
66
  child: ChildProcess | null;
63
67
  model: Model | null;
@@ -75,10 +79,11 @@ export declare class Supervisor extends EventEmitter {
75
79
  * shell line is for reading, not for re-parsing.
76
80
  */
77
81
  args: string[] | null;
78
- constructor({ runtime, internalPort, host, logVerbosity, readyTimeoutMs, paths, getProfilesStore, }: SupervisorOptions);
82
+ launch: ModelServerLaunch | null;
83
+ constructor({ runtime, driver, internalPort, host, logVerbosity, readyTimeoutMs, paths, getProfilesStore, }: SupervisorOptions);
79
84
  get upstreamBase(): string;
80
85
  /**
81
- * Add a host-operation event to the same in-process tail as llama-server output.
86
+ * Add a host-operation event to the same in-process tail as model-server output.
82
87
  *
83
88
  * Calibrate, sweep, and benchmark deliberately reuse this supervisor rather
84
89
  * than creating invisible sidecar servers. Their lifecycle markers belong in
@@ -89,9 +94,9 @@ export declare class Supervisor extends EventEmitter {
89
94
  /**
90
95
  * Start (or restart) the server for a model + profile.
91
96
  *
92
- * This is the sole llama-server launch boundary, so it materializes the
93
- * selected hosting profile here. Keeping it beside `buildArgs()` makes the
94
- * Jinja template and router-visible system addendum mandatory for every
97
+ * This is the sole model-server launch boundary, so it materializes the
98
+ * selected hosting profile here. Keeping it at this one driver call makes
99
+ * the Jinja template and router-visible system addendum mandatory for every
95
100
  * caller, including future maintenance operations that start a sidecar.
96
101
  */
97
102
  start(model: Model, profile: Profile): Promise<this>;
@@ -5,17 +5,15 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
5
5
  };
6
6
  var _Supervisor_instances, _Supervisor_setState, _Supervisor_log, _Supervisor_health;
7
7
  import http from "node:http";
8
- import path from "node:path";
9
- import { mkdirSync } from "node:fs";
10
8
  import { spawn } from "node:child_process";
11
9
  import { EventEmitter } from "node:events";
12
- import { buildArgs, buildEnv, formatCommand } from "../runtime/index.js";
10
+ import { llamaCppRuntimeDriver, } from "../runtime/index.js";
13
11
  import { resolveHostingProfileForLaunch } from "../config/hosting-profiles.js";
14
12
  import { getCalibrationForBudget } from "../config/profiles.js";
15
13
  import { resolveBrainPaths } from "../config/paths.js";
16
14
  import { loadProfilesStore } from "../config/store.js";
17
15
  import { usedBytes } from "../gpu.js";
18
- import { formatBrainLog, formatLlamaServerLog } from "./log-format.js";
16
+ import { formatBrainLog } from "./log-format.js";
19
17
  const LOG_LINES_KEPT = 10000;
20
18
  /**
21
19
  * Default loopback port for the private llama-server child. Deliberately clear
@@ -29,16 +27,17 @@ const LOG_LINES_KEPT = 10000;
29
27
  */
30
28
  export const DEFAULT_INTERNAL_PORT = 20800;
31
29
  /**
32
- * Owns the llama-server child process.
30
+ * Owns the model-server child process.
33
31
  *
34
32
  * The server always listens on a private port; `router.js` fronts it on a
35
33
  * stable one so switching models never asks a client to reconnect elsewhere.
36
34
  */
37
35
  export class Supervisor extends EventEmitter {
38
- constructor({ runtime, internalPort = DEFAULT_INTERNAL_PORT, host = "127.0.0.1", logVerbosity = 3, readyTimeoutMs = 300000, paths = resolveBrainPaths(), getProfilesStore = loadProfilesStore, }) {
36
+ constructor({ runtime, driver = llamaCppRuntimeDriver, internalPort = DEFAULT_INTERNAL_PORT, host = "127.0.0.1", logVerbosity = 3, readyTimeoutMs = 300000, paths = resolveBrainPaths(), getProfilesStore = loadProfilesStore, }) {
39
37
  super();
40
38
  _Supervisor_instances.add(this);
41
39
  this.runtime = runtime;
40
+ this.driver = driver;
42
41
  this.internalPort = internalPort;
43
42
  this.host = host;
44
43
  this.logVerbosity = logVerbosity;
@@ -57,12 +56,13 @@ export class Supervisor extends EventEmitter {
57
56
  this.vramBaselineBytes = null;
58
57
  this.command = null;
59
58
  this.args = null;
59
+ this.launch = null;
60
60
  }
61
61
  get upstreamBase() {
62
62
  return `http://${this.host}:${this.internalPort}`;
63
63
  }
64
64
  /**
65
- * Add a host-operation event to the same in-process tail as llama-server output.
65
+ * Add a host-operation event to the same in-process tail as model-server output.
66
66
  *
67
67
  * Calibrate, sweep, and benchmark deliberately reuse this supervisor rather
68
68
  * than creating invisible sidecar servers. Their lifecycle markers belong in
@@ -75,61 +75,53 @@ export class Supervisor extends EventEmitter {
75
75
  /**
76
76
  * Start (or restart) the server for a model + profile.
77
77
  *
78
- * This is the sole llama-server launch boundary, so it materializes the
79
- * selected hosting profile here. Keeping it beside `buildArgs()` makes the
80
- * Jinja template and router-visible system addendum mandatory for every
78
+ * This is the sole model-server launch boundary, so it materializes the
79
+ * selected hosting profile here. Keeping it at this one driver call makes
80
+ * the Jinja template and router-visible system addendum mandatory for every
81
81
  * caller, including future maintenance operations that start a sidecar.
82
82
  */
83
83
  async start(model, profile) {
84
84
  await this.stop();
85
85
  if (!this.runtime) {
86
- this.lastError = "no llama.cpp runtime available";
86
+ this.lastError = `no ${this.driver.displayName} runtime available`;
87
87
  __classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_setState).call(this, "failed", this.lastError);
88
88
  throw new Error(this.lastError);
89
89
  }
90
90
  const runtime = this.runtime;
91
- // The engine's slot save/erase directory, under the brain's home so it
92
- // survives across model relaunches (the dir is persistent; the engine only
93
- // ever uses it for the `action=erase` the scheduler issues on a handoff,
94
- // which never writes a file). Created before the args are built because
95
- // llama.cpp validates it exists at launch and throws otherwise.
96
- const slotSavePath = path.join(this.paths.root, "slot-saves");
97
- try {
98
- mkdirSync(slotSavePath, { recursive: true });
99
- }
100
- catch {
101
- /* the engine then starts without slot actions - the pre-fix behavior */
102
- }
103
91
  const launchProfile = resolveHostingProfileForLaunch(this.paths, this.getProfilesStore(), profile, model.family);
104
92
  this.model = model;
105
93
  this.profile = launchProfile;
106
94
  this.lastError = null;
107
95
  this.vramBaselineBytes = await usedBytes();
108
96
  __classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_setState).call(this, "starting");
109
- const args = buildArgs({ ...launchProfile, modelPath: model.modelPath, mmprojPath: model.mmprojPath }, {
110
- port: this.internalPort,
97
+ const launch = this.driver.createLaunch({
98
+ runtime,
99
+ model,
100
+ profile: launchProfile,
101
+ // The prompt-cache budget is derived from measured KV bytes/token, so the
102
+ // launch boundary is where it has to be resolved - nothing downstream of
103
+ // here can reach the calibration store.
104
+ calibration: getCalibrationForBudget(this.getProfilesStore(), model, launchProfile),
105
+ paths: this.paths,
111
106
  host: this.host,
107
+ port: this.internalPort,
112
108
  logVerbosity: this.logVerbosity,
113
- slotSavePath,
114
- }, model,
115
- // The prompt-cache budget is derived from measured KV bytes/token, so the
116
- // launch boundary is where it has to be resolved - nothing downstream of
117
- // here can reach the calibration store.
118
- getCalibrationForBudget(this.getProfilesStore(), model, launchProfile));
119
- this.args = args;
120
- this.command = formatCommand(runtime, args);
109
+ });
110
+ this.launch = launch;
111
+ this.args = launch.args;
112
+ this.command = launch.command;
121
113
  __classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_log).call(this, formatBrainLog("model", `launching: ${this.command}`));
122
114
  const started = Date.now();
123
- this.child = spawn(runtime.exe, args, {
124
- cwd: runtime.dir,
125
- env: buildEnv(runtime),
115
+ this.child = spawn(launch.executable, launch.args, {
116
+ cwd: launch.cwd,
117
+ env: launch.env,
126
118
  windowsHide: true,
127
119
  stdio: ["ignore", "pipe", "pipe"],
128
120
  });
129
121
  const onChunk = (chunk) => {
130
122
  for (const line of String(chunk).split(/\r?\n/)) {
131
123
  if (line.trim())
132
- __classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_log).call(this, formatLlamaServerLog(line.trim()));
124
+ __classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_log).call(this, launch.formatLogLine(line.trim()));
133
125
  }
134
126
  };
135
127
  this.child.stdout?.on("data", onChunk);
@@ -143,15 +135,13 @@ export class Supervisor extends EventEmitter {
143
135
  return;
144
136
  }
145
137
  exitedEarly = { code, signal };
146
- // 3221225781 == 0xC0000135 STATUS_DLL_NOT_FOUND: the vendor DLL trap.
147
- const hint = code === 3221225781 ? " (missing runtime DLLs - the vendor directory was not on PATH)" : "";
148
- this.lastError = `llama-server exited with code ${code}${signal ? ` signal ${signal}` : ""}${hint}`;
138
+ this.lastError = this.driver.describeProcessExit({ code, signal });
149
139
  __classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_setState).call(this, "failed", this.lastError);
150
140
  if (wasReady)
151
141
  this.emit("crashed", this.lastError);
152
142
  });
153
143
  this.child.once("error", (error) => {
154
- this.lastError = `could not launch llama-server: ${error.message}`;
144
+ this.lastError = this.driver.describeLaunchError(error);
155
145
  __classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_setState).call(this, "failed", this.lastError);
156
146
  });
157
147
  // Poll /health until the model finishes loading.
@@ -164,7 +154,7 @@ export class Supervisor extends EventEmitter {
164
154
  const used = await usedBytes();
165
155
  if (used && used > peakVram)
166
156
  peakVram = used;
167
- const health = await __classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_health).call(this);
157
+ const health = await __classPrivateFieldGet(this, _Supervisor_instances, "m", _Supervisor_health).call(this, launch.readinessPath);
168
158
  if (health) {
169
159
  this.loadSeconds = (Date.now() - started) / 1000;
170
160
  this.startedAt = new Date();
@@ -185,8 +175,11 @@ export class Supervisor extends EventEmitter {
185
175
  }
186
176
  /** Fetch /props from the running server (modalities, template caps, defaults). */
187
177
  props() {
178
+ const pathname = this.launch?.propertiesPath;
179
+ if (!pathname)
180
+ return Promise.resolve(null);
188
181
  return new Promise((resolve) => {
189
- const req = http.get({ host: this.host, port: this.internalPort, path: "/props", timeout: 5000 }, (res) => {
182
+ const req = http.get({ host: this.host, port: this.internalPort, path: pathname, timeout: 5000 }, (res) => {
190
183
  let body = "";
191
184
  res.on("data", (c) => (body += c));
192
185
  res.on("end", () => {
@@ -263,9 +256,9 @@ _Supervisor_instances = new WeakSet(), _Supervisor_setState = function _Supervis
263
256
  if (this.logLines.length > LOG_LINES_KEPT)
264
257
  this.logLines.shift();
265
258
  this.emit("log", line);
266
- }, _Supervisor_health = function _Supervisor_health() {
259
+ }, _Supervisor_health = function _Supervisor_health(pathname) {
267
260
  return new Promise((resolve) => {
268
- const req = http.get({ host: this.host, port: this.internalPort, path: "/health", timeout: 2500 }, (res) => {
261
+ const req = http.get({ host: this.host, port: this.internalPort, path: pathname, timeout: 2500 }, (res) => {
269
262
  res.resume();
270
263
  resolve(res.statusCode === 200);
271
264
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-code/brain",
3
- "version": "0.8.18",
3
+ "version": "0.8.19",
4
4
  "description": "Otto Brain - self-contained host for local GGUF models, with measured VRAM budgeting and reasoning-budget control",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "bin": {