@otto-code/brain 0.8.7 → 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.
Files changed (61) hide show
  1. package/dist/cli.js +2 -1
  2. package/dist/commands/bench.js +19 -5
  3. package/dist/commands/catalog.d.ts +3 -0
  4. package/dist/commands/catalog.js +2 -0
  5. package/dist/commands/pull.d.ts +1 -0
  6. package/dist/commands/pull.js +47 -14
  7. package/dist/commands/repo-download.d.ts +14 -0
  8. package/dist/commands/repo-download.js +29 -0
  9. package/dist/commands/runtime.d.ts +3 -0
  10. package/dist/commands/runtime.js +65 -18
  11. package/dist/commands/search.d.ts +3 -0
  12. package/dist/commands/search.js +38 -17
  13. package/dist/config/builtin-hosting-profiles.d.ts +8 -0
  14. package/dist/config/builtin-hosting-profiles.js +32 -0
  15. package/dist/config/hosting-profiles.d.ts +33 -0
  16. package/dist/config/hosting-profiles.js +71 -0
  17. package/dist/config/index.d.ts +1 -0
  18. package/dist/config/index.js +1 -0
  19. package/dist/config/paths.d.ts +2 -0
  20. package/dist/config/paths.js +1 -0
  21. package/dist/config/profile-edit.d.ts +5 -3
  22. package/dist/config/profile-edit.js +134 -14
  23. package/dist/config/profiles.js +66 -3
  24. package/dist/config/schema.d.ts +998 -0
  25. package/dist/config/schema.js +79 -0
  26. package/dist/config/store.js +28 -16
  27. package/dist/gguf.d.ts +1 -0
  28. package/dist/gguf.js +1 -0
  29. package/dist/models/download.d.ts +7 -0
  30. package/dist/models/download.js +165 -17
  31. package/dist/models/enrich.d.ts +6 -19
  32. package/dist/models/enrich.js +138 -4
  33. package/dist/models/hf.d.ts +14 -1
  34. package/dist/models/hf.js +239 -6
  35. package/dist/models/index.d.ts +2 -2
  36. package/dist/models/index.js +8 -5
  37. package/dist/models/manage.d.ts +4 -0
  38. package/dist/models/manage.js +34 -4
  39. package/dist/models/scan.js +8 -42
  40. package/dist/ops/calibrate.d.ts +4 -1
  41. package/dist/ops/calibrate.js +10 -7
  42. package/dist/ops/sweep.d.ts +3 -1
  43. package/dist/ops/sweep.js +3 -3
  44. package/dist/runtime/args.d.ts +2 -2
  45. package/dist/runtime/args.js +18 -1
  46. package/dist/runtime/index.d.ts +8 -1
  47. package/dist/runtime/index.js +11 -1
  48. package/dist/runtime/managed.d.ts +40 -0
  49. package/dist/runtime/managed.js +146 -7
  50. package/dist/service/host-api.d.ts +20 -3
  51. package/dist/service/host-api.js +313 -20
  52. package/dist/service/router.d.ts +18 -0
  53. package/dist/service/router.js +89 -4
  54. package/dist/service/serve.js +221 -22
  55. package/dist/service/supervisor.d.ts +32 -4
  56. package/dist/service/supervisor.js +30 -6
  57. package/dist/tui/app.js +1 -1
  58. package/dist/types.d.ts +24 -0
  59. package/dist/vram.d.ts +3 -0
  60. package/dist/vram.js +24 -6
  61. package/package.json +1 -1
@@ -16,7 +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 { installManagedRuntime, listManagedRuntimes, listRuntimeDevices, verifyRuntimeExecutable, defaultRuntimeSpec, extractArchive, resolveRuntimeVariant, serverExeName, supportedVariants, DEFAULT_LLAMA_BUILD, } from "./managed.js";
19
+ export { installManagedRuntime, removeManagedRuntime, listManagedRuntimes, listRuntimeDevices, verifyRuntimeExecutable, defaultRuntimeSpec, extractArchive, resolveRuntimeVariant, serverExeName, supportedVariants, DEFAULT_LLAMA_BUILD, listRuntimeReleases, latestRuntimeBuild, resolveLatestBuildOrPin, MissingAssetError, } from "./managed.js";
20
20
  /** Every runtime available on this machine, managed first then LM Studio. */
21
21
  export function listAllRuntimes(env = process.env) {
22
22
  const paths = resolveBrainPaths(env);
@@ -44,6 +44,16 @@ export function resolveRuntime(config, env = process.env) {
44
44
  return lmstudio[0] ?? null;
45
45
  return managed[0] ?? lmstudio[0] ?? null; // auto
46
46
  }
47
+ /**
48
+ * The numeric llama.cpp build carried by a resolved runtime, when its source
49
+ * identifies one. LM Studio and explicit overrides need not expose their
50
+ * upstream build, so callers must treat null as incompatible with a component
51
+ * that declares a minimum build rather than guessing compatibility.
52
+ */
53
+ export function runtimeBuild(runtime) {
54
+ const match = /^b(\d+)$/iu.exec(runtime?.version ?? "");
55
+ return match ? Number(match[1]) : null;
56
+ }
47
57
  /** Ensure a runtime exists, downloading the default managed build if none does. */
48
58
  export async function ensureRuntime(config, env = process.env, onProgress, target = {}) {
49
59
  const existing = resolveRuntime(config, env);
@@ -30,6 +30,33 @@ export interface InstallProgress {
30
30
  * names (see the module header) - the naming scheme is not stable across tags.
31
31
  */
32
32
  export declare const DEFAULT_LLAMA_BUILD = "b10265";
33
+ export interface RuntimeRelease {
34
+ build: string;
35
+ publishedAt: string | null;
36
+ }
37
+ /** Official llama.cpp builds, newest first. Only numbered build releases are actionable. */
38
+ export declare function listRuntimeReleases(limit?: number): Promise<RuntimeRelease[]>;
39
+ /** Resolve the latest official build at the last responsible moment. */
40
+ export declare function latestRuntimeBuild(): Promise<string>;
41
+ export interface ResolvedBuild {
42
+ build: string;
43
+ /** A single line explaining a fallback, or null when "latest" resolved. */
44
+ warning: string | null;
45
+ }
46
+ /**
47
+ * The build to install for a "latest" request, with the pin as the safety net.
48
+ *
49
+ * Asking upstream is a best effort, never a precondition. `listRuntimeReleases`
50
+ * hits api.github.com unauthenticated, which is rate limited to 60 requests per
51
+ * hour per IP: behind NAT, on a corporate egress or on a CI runner that is a 403
52
+ * on an address that has spent its budget on something else entirely. An install
53
+ * the pinned build can serve must not fail because a version lookup did.
54
+ *
55
+ * The warning is deliberately one line. The daemon's BrainOpsManager keeps the
56
+ * *last* stderr line as the job's message, so a wrapped warning would surface in
57
+ * the GUI as a dangling fragment.
58
+ */
59
+ export declare function resolveLatestBuildOrPin(): Promise<ResolvedBuild>;
33
60
  /** The binary name llama.cpp ships for a platform. */
34
61
  export declare function serverExeName(platform?: NodeJS.Platform): string;
35
62
  /**
@@ -49,6 +76,17 @@ export declare function supportedVariants(platform?: NodeJS.Platform, arch?: str
49
76
  * best accelerator first and newest build first within an accelerator.
50
77
  */
51
78
  export declare function listManagedRuntimes(runtimesDir: string, platform?: NodeJS.Platform): Runtime[];
79
+ /**
80
+ * An asset upstream does not serve at all, which is what a renamed asset or a
81
+ * tag without the expected build looks like from here. Distinguished from every
82
+ * other download failure because it is the one a caller can answer by retrying
83
+ * a build whose asset names are pinned and tested, rather than by retrying the
84
+ * same URL later.
85
+ */
86
+ export declare class MissingAssetError extends Error {
87
+ readonly url: string;
88
+ constructor(url: string);
89
+ }
52
90
  /**
53
91
  * The actionable message for a dynamic-loader failure, or null when the output
54
92
  * is not one. Pure, so the classification is testable without spawning.
@@ -87,4 +125,6 @@ export declare function listRuntimeDevices(runtime: Runtime, platform?: NodeJS.P
87
125
  export declare function extractArchive(archivePath: string, destDir: string, platform?: NodeJS.Platform): Promise<void>;
88
126
  /** Download + extract a runtime spec into runtimesDir and return the Runtime. */
89
127
  export declare function installManagedRuntime(spec: RuntimeSpec, runtimesDir: string, onProgress?: (progress: InstallProgress) => void): Promise<Runtime>;
128
+ /** Remove one Otto-managed runtime. LM Studio files are outside this root. */
129
+ export declare function removeManagedRuntime(runtimesDir: string, name: string): void;
90
130
  //# sourceMappingURL=managed.d.ts.map
@@ -63,8 +63,75 @@ import { buildEnv } from "./args.js";
63
63
  */
64
64
  export const DEFAULT_LLAMA_BUILD = "b10265";
65
65
  const LLAMA_RELEASE_BASE = "https://github.com/ggml-org/llama.cpp/releases/download";
66
+ const LLAMA_RELEASE_API = "https://api.github.com/repos/ggml-org/llama.cpp/releases";
67
+ /**
68
+ * How many releases to read when resolving "latest".
69
+ *
70
+ * Not 1: the newest release is not necessarily a numbered build (upstream also
71
+ * publishes differently tagged and pre-release entries), and a single
72
+ * non-matching entry would otherwise read as "llama.cpp published no release
73
+ * builds" while the very next one qualified.
74
+ */
75
+ const LATEST_BUILD_SCAN_PAGE = 30;
76
+ /** Official llama.cpp builds, newest first. Only numbered build releases are actionable. */
77
+ export async function listRuntimeReleases(limit = 100) {
78
+ const response = await fetch(`${LLAMA_RELEASE_API}?per_page=${Math.min(Math.max(limit, 1), 100)}`, {
79
+ headers: { Accept: "application/vnd.github+json" },
80
+ });
81
+ if (!response.ok)
82
+ throw new Error(`could not fetch llama.cpp releases (${response.status})`);
83
+ const releases = await response.json();
84
+ if (!Array.isArray(releases))
85
+ throw new Error("llama.cpp releases response was invalid");
86
+ return releases.flatMap((release) => {
87
+ if (!release || typeof release !== "object")
88
+ return [];
89
+ const value = release;
90
+ const build = typeof value.tag_name === "string" ? value.tag_name : "";
91
+ return /^b\d+$/.test(build)
92
+ ? [{ build, publishedAt: typeof value.published_at === "string" ? value.published_at : null }]
93
+ : [];
94
+ });
95
+ }
96
+ /** Resolve the latest official build at the last responsible moment. */
97
+ export async function latestRuntimeBuild() {
98
+ const [latest] = await listRuntimeReleases(LATEST_BUILD_SCAN_PAGE);
99
+ if (!latest)
100
+ throw new Error("llama.cpp published no release builds");
101
+ return latest.build;
102
+ }
103
+ /**
104
+ * The build to install for a "latest" request, with the pin as the safety net.
105
+ *
106
+ * Asking upstream is a best effort, never a precondition. `listRuntimeReleases`
107
+ * hits api.github.com unauthenticated, which is rate limited to 60 requests per
108
+ * hour per IP: behind NAT, on a corporate egress or on a CI runner that is a 403
109
+ * on an address that has spent its budget on something else entirely. An install
110
+ * the pinned build can serve must not fail because a version lookup did.
111
+ *
112
+ * The warning is deliberately one line. The daemon's BrainOpsManager keeps the
113
+ * *last* stderr line as the job's message, so a wrapped warning would surface in
114
+ * the GUI as a dangling fragment.
115
+ */
116
+ export async function resolveLatestBuildOrPin() {
117
+ try {
118
+ return { build: await latestRuntimeBuild(), warning: null };
119
+ }
120
+ catch (error) {
121
+ return {
122
+ build: DEFAULT_LLAMA_BUILD,
123
+ warning: `could not look up the latest llama.cpp build (${oneLine(error)}), so Otto installed` +
124
+ ` the pinned build ${DEFAULT_LLAMA_BUILD} instead.`,
125
+ };
126
+ }
127
+ }
128
+ /** Flatten a message to one line, so a warning survives the last-line rule. */
129
+ function oneLine(error) {
130
+ return (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim();
131
+ }
66
132
  /** The CUDA toolkit version whose Windows assets we pin. */
67
133
  const WINDOWS_CUDA = "12.4";
134
+ const MANAGED_RUNTIME_METADATA_FILE = ".otto-runtime.json";
68
135
  /** The binary name llama.cpp ships for a platform. */
69
136
  export function serverExeName(platform = process.platform) {
70
137
  return platform === "win32" ? "llama-server.exe" : "llama-server";
@@ -181,6 +248,34 @@ function slug(spec) {
181
248
  .replace(/[^a-z0-9]+/g, "-")
182
249
  .replace(/(^-|-$)/g, "");
183
250
  }
251
+ function displayNameForManagedRuntime(label, version) {
252
+ return `${label.replace(/\s*\(managed\)$/iu, "")} · ${version} (Otto managed)`;
253
+ }
254
+ function legacyManagedDisplayName(dirName, version) {
255
+ const cuda = /^cuda-(\d+)-(\d+)-managed(?:-|$)/iu.exec(dirName);
256
+ if (cuda)
257
+ return `CUDA ${cuda[1]}.${cuda[2]} · ${version} (Otto managed)`;
258
+ if (/^vulkan-managed(?:-|$)/iu.test(dirName))
259
+ return `Vulkan · ${version} (Otto managed)`;
260
+ if (/^metal-managed(?:-|$)/iu.test(dirName))
261
+ return `Metal · ${version} (Otto managed)`;
262
+ if (/^cpu-managed(?:-|$)/iu.test(dirName))
263
+ return `CPU · ${version} (Otto managed)`;
264
+ return `${version} (Otto managed)`;
265
+ }
266
+ function readManagedRuntimeDisplayName(root, dirName, version) {
267
+ try {
268
+ const parsed = JSON.parse(fs.readFileSync(path.join(root, MANAGED_RUNTIME_METADATA_FILE), "utf8"));
269
+ if (typeof parsed.displayName === "string" && parsed.displayName.trim()) {
270
+ return parsed.displayName;
271
+ }
272
+ }
273
+ catch {
274
+ // Existing installations predate metadata; their directory name remains a
275
+ // backend-only migration source, never a UI presentation contract.
276
+ }
277
+ return legacyManagedDisplayName(dirName, version);
278
+ }
184
279
  /** Recursively find the first file named `name` under `dir`. */
185
280
  function findFile(dir, name) {
186
281
  const entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -230,9 +325,11 @@ export function listManagedRuntimes(runtimesDir, platform = process.platform) {
230
325
  const exe = findFile(root, exeName);
231
326
  if (!exe)
232
327
  continue;
328
+ const version = entry.name.replace(/^.*-/, "");
233
329
  found.push({
234
330
  label: entry.name,
235
- version: entry.name.replace(/^.*-/, ""),
331
+ displayName: readManagedRuntimeDisplayName(root, entry.name, version),
332
+ version,
236
333
  dir: path.dirname(exe),
237
334
  exe,
238
335
  vendorDir: null,
@@ -242,8 +339,24 @@ export function listManagedRuntimes(runtimesDir, platform = process.platform) {
242
339
  return found.sort((a, b) => managedVariantRank(a.label) - managedVariantRank(b.label) ||
243
340
  buildNumber(b.version) - buildNumber(a.version));
244
341
  }
342
+ /**
343
+ * An asset upstream does not serve at all, which is what a renamed asset or a
344
+ * tag without the expected build looks like from here. Distinguished from every
345
+ * other download failure because it is the one a caller can answer by retrying
346
+ * a build whose asset names are pinned and tested, rather than by retrying the
347
+ * same URL later.
348
+ */
349
+ export class MissingAssetError extends Error {
350
+ constructor(url) {
351
+ super(`download failed (404) for ${url}`);
352
+ this.url = url;
353
+ this.name = "MissingAssetError";
354
+ }
355
+ }
245
356
  async function downloadFile(url, dest, onProgress) {
246
357
  const response = await fetch(url);
358
+ if (response.status === 404)
359
+ throw new MissingAssetError(url);
247
360
  if (!response.ok || !response.body) {
248
361
  throw new Error(`download failed (${response.status}) for ${url}`);
249
362
  }
@@ -480,13 +593,26 @@ export async function extractArchive(archivePath, destDir, platform = process.pl
480
593
  export async function installManagedRuntime(spec, runtimesDir, onProgress) {
481
594
  const platform = spec.platform ?? process.platform;
482
595
  const targetDir = path.join(runtimesDir, slug(spec));
596
+ const preexisting = fs.existsSync(targetDir);
483
597
  fs.mkdirSync(targetDir, { recursive: true });
484
- for (const url of spec.assets) {
485
- const archivePath = path.join(targetDir, path.basename(new URL(url).pathname));
486
- await downloadFile(url, archivePath, onProgress);
487
- onProgress?.({ phase: "extracting", asset: url });
488
- await extractArchive(archivePath, targetDir, platform);
489
- fs.rmSync(archivePath, { force: true });
598
+ try {
599
+ for (const url of spec.assets) {
600
+ const archivePath = path.join(targetDir, path.basename(new URL(url).pathname));
601
+ await downloadFile(url, archivePath, onProgress);
602
+ onProgress?.({ phase: "extracting", asset: url });
603
+ await extractArchive(archivePath, targetDir, platform);
604
+ fs.rmSync(archivePath, { force: true });
605
+ }
606
+ }
607
+ catch (error) {
608
+ // A half-installed directory is worse than no directory: Windows CUDA needs
609
+ // two archives, so a 404 on the companion leaves a llama-server with no CUDA
610
+ // runtime beside it, and `listManagedRuntimes` ranks by build number - that
611
+ // newer, broken build would then outrank the working runtime it replaced.
612
+ // Only a directory this call created is removed; an existing install stands.
613
+ if (!preexisting)
614
+ fs.rmSync(targetDir, { recursive: true, force: true });
615
+ throw error;
490
616
  }
491
617
  const exeName = serverExeName(platform);
492
618
  const exe = findFile(targetDir, exeName);
@@ -499,6 +625,7 @@ export async function installManagedRuntime(spec, runtimesDir, onProgress) {
499
625
  }
500
626
  const runtime = {
501
627
  label: spec.label,
628
+ displayName: displayNameForManagedRuntime(spec.label, spec.version),
502
629
  version: spec.version,
503
630
  dir: path.dirname(exe),
504
631
  exe,
@@ -508,7 +635,19 @@ export async function installManagedRuntime(spec, runtimesDir, onProgress) {
508
635
  // Before reporting success. An install that cannot exec is not an install, and
509
636
  // the loader error names the cause far better than the later spawn failure does.
510
637
  await verifyRuntimeExecutable(runtime, platform);
638
+ fs.writeFileSync(path.join(targetDir, MANAGED_RUNTIME_METADATA_FILE), `${JSON.stringify({ displayName: runtime.displayName })}\n`);
511
639
  onProgress?.({ phase: "done" });
512
640
  return runtime;
513
641
  }
642
+ /** Remove one Otto-managed runtime. LM Studio files are outside this root. */
643
+ export function removeManagedRuntime(runtimesDir, name) {
644
+ if (!/^[a-z0-9][a-z0-9-]*$/i.test(name))
645
+ throw new Error("invalid runtime name");
646
+ const root = path.resolve(runtimesDir);
647
+ const target = path.resolve(root, name);
648
+ if (path.dirname(target) !== root || !fs.existsSync(target)) {
649
+ throw new Error(`managed runtime not found: ${name}`);
650
+ }
651
+ fs.rmSync(target, { recursive: true, force: false });
652
+ }
514
653
  //# sourceMappingURL=managed.js.map
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import type http from "node:http";
22
22
  import { calibrationInfo, profileWarnings } from "../config/profile-edit.js";
23
- import type { Profile, ProfileDefaults, ProfilesStore } from "../config/schema.js";
23
+ import { type Profile, type ProfileDefaults, type ProfilesStore } from "../config/schema.js";
24
24
  import { planDelete } from "../models/manage.js";
25
25
  import type { RankedModel } from "../ops/results.js";
26
26
  import type { GpuInfo, Model } from "../types.js";
@@ -82,11 +82,11 @@ export interface HostCapabilities {
82
82
  /** A long-running operation owned by this brain host, not its caller. */
83
83
  export interface HostJob {
84
84
  id: string;
85
- kind: "pull" | "runtime-install" | "calibrate" | "sweep" | "bench";
85
+ kind: "pull" | "runtime-install" | "runtime-remove" | "calibrate" | "sweep" | "bench";
86
86
  label: string;
87
87
  target: string | null;
88
88
  status: "running" | "succeeded" | "failed" | "canceled";
89
- percent: null;
89
+ percent: number | null;
90
90
  message: string | null;
91
91
  error: string | null;
92
92
  startedAt: string;
@@ -130,6 +130,8 @@ export interface HostApiDeps {
130
130
  export interface InventoryRow {
131
131
  id: string;
132
132
  displayName: string;
133
+ /** Curated model-family identity for the Otto Brain client glyph. */
134
+ family: string | null;
133
135
  publisher: string | null;
134
136
  quant: string | null;
135
137
  sizeBytes: number;
@@ -153,7 +155,21 @@ export interface InventoryRow {
153
155
  score: RankedModel | null;
154
156
  state: "loaded" | "loading" | "not-loaded";
155
157
  warnings: ReturnType<typeof profileWarnings>;
158
+ components: NonNullable<Model["components"]> | null;
156
159
  }
160
+ /**
161
+ * Apply the hosting-profile half of a profile patch, mutating both `profile`
162
+ * (this model's selection) and `store` (the shared library and family default).
163
+ *
164
+ * Separate from `sanitizeProfilePatch` because these keys are not profile
165
+ * fields: three of the five write to the store rather than the profile.
166
+ * Exported for testing - the ordering and the cross-profile cleanup are the
167
+ * parts worth pinning down, and they are unreachable through the HTTP surface
168
+ * without standing up a service.
169
+ *
170
+ * Throws on any invalid input; the caller turns that into a 400.
171
+ */
172
+ export declare function applyHostingProfilePatch(store: ProfilesStore, model: Model, profile: Profile, patch: Record<string, unknown>, onDelete?: ((id: string) => void) | undefined): void;
157
173
  /**
158
174
  * Join one model's scan row, GGUF metadata, saved profile, calibration, VRAM
159
175
  * budget and benchmark score into the single shape the Models tab renders.
@@ -168,6 +184,7 @@ export declare function buildInventoryRow(params: {
168
184
  gpu: GpuInfo | null;
169
185
  ranking: RankedModel[];
170
186
  supervisor: Supervisor;
187
+ runtimeBuild?: number | null;
171
188
  }): InventoryRow;
172
189
  export interface HostApi {
173
190
  /** Returns true when it answered the request, false to fall through. */