@otto-code/brain 0.7.5

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 (150) hide show
  1. package/bin/otto-brain +2 -0
  2. package/dist/bench/context-corpus.d.ts +58 -0
  3. package/dist/bench/context-corpus.js +210 -0
  4. package/dist/bench/corpus.d.ts +31 -0
  5. package/dist/bench/corpus.js +379 -0
  6. package/dist/bench/curated-repos.d.ts +35 -0
  7. package/dist/bench/curated-repos.js +41 -0
  8. package/dist/bench/health.d.ts +50 -0
  9. package/dist/bench/health.js +112 -0
  10. package/dist/bench/index.d.ts +103 -0
  11. package/dist/bench/index.js +209 -0
  12. package/dist/bench/mine.d.ts +32 -0
  13. package/dist/bench/mine.js +101 -0
  14. package/dist/bench/repo-task.d.ts +93 -0
  15. package/dist/bench/repo-task.js +380 -0
  16. package/dist/bench/repo.d.ts +125 -0
  17. package/dist/bench/repo.js +195 -0
  18. package/dist/bench/rescore.d.ts +41 -0
  19. package/dist/bench/rescore.js +127 -0
  20. package/dist/bench/tasks.d.ts +124 -0
  21. package/dist/bench/tasks.js +1249 -0
  22. package/dist/bench/verify.d.ts +142 -0
  23. package/dist/bench/verify.js +350 -0
  24. package/dist/cli.d.ts +13 -0
  25. package/dist/cli.js +72 -0
  26. package/dist/commands/bench.d.ts +20 -0
  27. package/dist/commands/bench.js +222 -0
  28. package/dist/commands/calibrate.d.ts +19 -0
  29. package/dist/commands/calibrate.js +75 -0
  30. package/dist/commands/catalog.d.ts +32 -0
  31. package/dist/commands/catalog.js +48 -0
  32. package/dist/commands/config.d.ts +13 -0
  33. package/dist/commands/config.js +90 -0
  34. package/dist/commands/lifecycle.d.ts +29 -0
  35. package/dist/commands/lifecycle.js +194 -0
  36. package/dist/commands/pull.d.ts +22 -0
  37. package/dist/commands/pull.js +135 -0
  38. package/dist/commands/report.d.ts +10 -0
  39. package/dist/commands/report.js +34 -0
  40. package/dist/commands/rescore.d.ts +11 -0
  41. package/dist/commands/rescore.js +48 -0
  42. package/dist/commands/runtime.d.ts +21 -0
  43. package/dist/commands/runtime.js +62 -0
  44. package/dist/commands/scan.d.ts +24 -0
  45. package/dist/commands/scan.js +55 -0
  46. package/dist/commands/search.d.ts +31 -0
  47. package/dist/commands/search.js +135 -0
  48. package/dist/commands/share.d.ts +26 -0
  49. package/dist/commands/share.js +148 -0
  50. package/dist/commands/sweep.d.ts +21 -0
  51. package/dist/commands/sweep.js +69 -0
  52. package/dist/commands/ui.d.ts +9 -0
  53. package/dist/commands/ui.js +28 -0
  54. package/dist/config/env.d.ts +10 -0
  55. package/dist/config/env.js +48 -0
  56. package/dist/config/index.d.ts +9 -0
  57. package/dist/config/index.js +9 -0
  58. package/dist/config/otto-home.d.ts +2 -0
  59. package/dist/config/otto-home.js +24 -0
  60. package/dist/config/paths.d.ts +20 -0
  61. package/dist/config/paths.js +33 -0
  62. package/dist/config/private-files.d.ts +3 -0
  63. package/dist/config/private-files.js +32 -0
  64. package/dist/config/profiles.d.ts +34 -0
  65. package/dist/config/profiles.js +116 -0
  66. package/dist/config/schema.d.ts +1035 -0
  67. package/dist/config/schema.js +191 -0
  68. package/dist/config/store.d.ts +11 -0
  69. package/dist/config/store.js +73 -0
  70. package/dist/gguf.d.ts +61 -0
  71. package/dist/gguf.js +231 -0
  72. package/dist/gpu.d.ts +12 -0
  73. package/dist/gpu.js +49 -0
  74. package/dist/index.d.ts +14 -0
  75. package/dist/index.js +13 -0
  76. package/dist/main.d.ts +2 -0
  77. package/dist/main.js +5 -0
  78. package/dist/models/dirs.d.ts +9 -0
  79. package/dist/models/dirs.js +21 -0
  80. package/dist/models/download.d.ts +31 -0
  81. package/dist/models/download.js +105 -0
  82. package/dist/models/enrich.d.ts +36 -0
  83. package/dist/models/enrich.js +68 -0
  84. package/dist/models/hf.d.ts +62 -0
  85. package/dist/models/hf.js +114 -0
  86. package/dist/models/index.d.ts +15 -0
  87. package/dist/models/index.js +49 -0
  88. package/dist/models/manage.d.ts +27 -0
  89. package/dist/models/manage.js +112 -0
  90. package/dist/models/pick.d.ts +3 -0
  91. package/dist/models/pick.js +36 -0
  92. package/dist/models/scan.d.ts +13 -0
  93. package/dist/models/scan.js +185 -0
  94. package/dist/ops/archive.d.ts +34 -0
  95. package/dist/ops/archive.js +105 -0
  96. package/dist/ops/calibrate.d.ts +49 -0
  97. package/dist/ops/calibrate.js +74 -0
  98. package/dist/ops/report.d.ts +26 -0
  99. package/dist/ops/report.js +532 -0
  100. package/dist/ops/results.d.ts +207 -0
  101. package/dist/ops/results.js +250 -0
  102. package/dist/ops/sweep.d.ts +77 -0
  103. package/dist/ops/sweep.js +126 -0
  104. package/dist/output/index.d.ts +5 -0
  105. package/dist/output/index.js +5 -0
  106. package/dist/output/render.d.ts +4 -0
  107. package/dist/output/render.js +96 -0
  108. package/dist/output/types.d.ts +52 -0
  109. package/dist/output/types.js +9 -0
  110. package/dist/output/with-output.d.ts +3 -0
  111. package/dist/output/with-output.js +42 -0
  112. package/dist/run.d.ts +5 -0
  113. package/dist/run.js +19 -0
  114. package/dist/runtime/args.d.ts +21 -0
  115. package/dist/runtime/args.js +71 -0
  116. package/dist/runtime/index.d.ts +13 -0
  117. package/dist/runtime/index.js +41 -0
  118. package/dist/runtime/lmstudio.d.ts +10 -0
  119. package/dist/runtime/lmstudio.js +88 -0
  120. package/dist/runtime/managed.d.ts +26 -0
  121. package/dist/runtime/managed.js +148 -0
  122. package/dist/service/model-selector.d.ts +36 -0
  123. package/dist/service/model-selector.js +123 -0
  124. package/dist/service/pid-lock.d.ts +18 -0
  125. package/dist/service/pid-lock.js +50 -0
  126. package/dist/service/router.d.ts +144 -0
  127. package/dist/service/router.js +746 -0
  128. package/dist/service/scheduler.d.ts +74 -0
  129. package/dist/service/scheduler.js +110 -0
  130. package/dist/service/serve.d.ts +41 -0
  131. package/dist/service/serve.js +278 -0
  132. package/dist/service/supervisor.d.ts +67 -0
  133. package/dist/service/supervisor.js +222 -0
  134. package/dist/service/tailscale.d.ts +19 -0
  135. package/dist/service/tailscale.js +62 -0
  136. package/dist/service/tls.d.ts +77 -0
  137. package/dist/service/tls.js +192 -0
  138. package/dist/sysmon.d.ts +50 -0
  139. package/dist/sysmon.js +103 -0
  140. package/dist/tui/app.d.ts +208 -0
  141. package/dist/tui/app.js +1801 -0
  142. package/dist/tui/screen.d.ts +72 -0
  143. package/dist/tui/screen.js +176 -0
  144. package/dist/types.d.ts +76 -0
  145. package/dist/types.js +7 -0
  146. package/dist/version.d.ts +2 -0
  147. package/dist/version.js +8 -0
  148. package/dist/vram.d.ts +67 -0
  149. package/dist/vram.js +132 -0
  150. package/package.json +58 -0
@@ -0,0 +1,72 @@
1
+ /** Minimal ANSI screen and input primitives - no dependencies. */
2
+ export declare const ansi: {
3
+ altScreenOn: string;
4
+ altScreenOff: string;
5
+ hideCursor: string;
6
+ showCursor: string;
7
+ home: string;
8
+ clearScreen: string;
9
+ clearToEol: string;
10
+ clearBelow: string;
11
+ reset: string;
12
+ };
13
+ export declare const style: {
14
+ reset: string;
15
+ bold: string;
16
+ dim: string;
17
+ inverse: string;
18
+ red: string;
19
+ green: string;
20
+ yellow: string;
21
+ blue: string;
22
+ magenta: string;
23
+ cyan: string;
24
+ white: string;
25
+ grey: string;
26
+ brightGreen: string;
27
+ brightYellow: string;
28
+ brightCyan: string;
29
+ };
30
+ export declare const BOX: {
31
+ h: string;
32
+ v: string;
33
+ tl: string;
34
+ tr: string;
35
+ bl: string;
36
+ br: string;
37
+ full: string;
38
+ light: string;
39
+ };
40
+ /** Options for a titled box. */
41
+ export interface BoxOptions {
42
+ title?: string;
43
+ lines: string[];
44
+ innerWidth: number;
45
+ footer?: string | null;
46
+ accent?: string;
47
+ }
48
+ /** Printable width, ignoring ANSI escapes. */
49
+ export declare function width(text: string): number;
50
+ export declare function truncate(text: string, max: number): string;
51
+ export declare function pad(text: string, max: number): string;
52
+ /** A titled box as an array of lines. */
53
+ export declare function box({ title, lines, innerWidth, footer, accent, }: BoxOptions): string[];
54
+ /** Horizontal meter, coloured by how close to the limit it sits. */
55
+ export declare function meter(fraction: number, cells: number): string;
56
+ export declare class Screen {
57
+ out: NodeJS.WriteStream;
58
+ previous: string[];
59
+ entered: boolean;
60
+ constructor(stream?: NodeJS.WriteStream);
61
+ get columns(): number;
62
+ get rows(): number;
63
+ enter(): void;
64
+ leave(): void;
65
+ /** Repaint only the lines that changed. */
66
+ render(lines: string[]): void;
67
+ invalidate(): void;
68
+ }
69
+ /** Decode a raw stdin chunk into a key name. */
70
+ export declare function decodeKey(chunk: unknown): string | null;
71
+ export declare function onKeys(handler: (key: string, chunk: unknown) => void, stream?: NodeJS.ReadStream): () => void;
72
+ //# sourceMappingURL=screen.d.ts.map
@@ -0,0 +1,176 @@
1
+ /** Minimal ANSI screen and input primitives - no dependencies. */
2
+ const ESC = "\x1b";
3
+ const CSI = `${ESC}[`;
4
+ export const ansi = {
5
+ altScreenOn: `${CSI}?1049h`,
6
+ altScreenOff: `${CSI}?1049l`,
7
+ hideCursor: `${CSI}?25l`,
8
+ showCursor: `${CSI}?25h`,
9
+ home: `${CSI}H`,
10
+ clearScreen: `${CSI}2J`,
11
+ clearToEol: `${CSI}K`,
12
+ clearBelow: `${CSI}J`,
13
+ reset: `${CSI}0m`,
14
+ };
15
+ export const style = {
16
+ reset: `${CSI}0m`,
17
+ bold: `${CSI}1m`,
18
+ dim: `${CSI}2m`,
19
+ inverse: `${CSI}7m`,
20
+ red: `${CSI}31m`,
21
+ green: `${CSI}32m`,
22
+ yellow: `${CSI}33m`,
23
+ blue: `${CSI}34m`,
24
+ magenta: `${CSI}35m`,
25
+ cyan: `${CSI}36m`,
26
+ white: `${CSI}37m`,
27
+ grey: `${CSI}90m`,
28
+ brightGreen: `${CSI}92m`,
29
+ brightYellow: `${CSI}93m`,
30
+ brightCyan: `${CSI}96m`,
31
+ };
32
+ export const BOX = {
33
+ h: "─",
34
+ v: "│",
35
+ tl: "┌",
36
+ tr: "┐",
37
+ bl: "└",
38
+ br: "┘",
39
+ full: "█",
40
+ light: "░",
41
+ };
42
+ /** Printable width, ignoring ANSI escapes. */
43
+ export function width(text) {
44
+ return String(text).replace(/\x1b\[[0-9;]*m/g, "").length;
45
+ }
46
+ export function truncate(text, max) {
47
+ const plain = String(text);
48
+ if (width(plain) <= max)
49
+ return plain;
50
+ // Only called on unstyled strings, so a plain slice is safe.
51
+ return `${plain.slice(0, Math.max(0, max - 1))}…`;
52
+ }
53
+ export function pad(text, max) {
54
+ const gap = max - width(text);
55
+ return gap > 0 ? text + " ".repeat(gap) : text;
56
+ }
57
+ /** A titled box as an array of lines. */
58
+ export function box({ title, lines, innerWidth, footer = null, accent = style.grey, }) {
59
+ const out = [];
60
+ const label = title ? ` ${title} ` : "";
61
+ const dashes = Math.max(0, innerWidth - width(label));
62
+ out.push(`${accent}${BOX.tl}${label}${BOX.h.repeat(dashes)}${BOX.tr}${style.reset}`);
63
+ for (const line of lines) {
64
+ out.push(`${accent}${BOX.v}${style.reset}${pad(truncate(line, innerWidth), innerWidth)}${accent}${BOX.v}${style.reset}`);
65
+ }
66
+ if (footer !== null) {
67
+ out.push(`${accent}${BOX.v}${style.reset}${pad(truncate(footer, innerWidth), innerWidth)}${accent}${BOX.v}${style.reset}`);
68
+ }
69
+ out.push(`${accent}${BOX.bl}${BOX.h.repeat(innerWidth)}${BOX.br}${style.reset}`);
70
+ return out;
71
+ }
72
+ /** Horizontal meter, coloured by how close to the limit it sits. */
73
+ export function meter(fraction, cells) {
74
+ const clamped = Math.max(0, Math.min(1.2, fraction));
75
+ const filled = Math.min(cells, Math.round(clamped * cells));
76
+ let colour = style.brightGreen;
77
+ if (clamped > 1)
78
+ colour = style.red;
79
+ else if (clamped > 0.9)
80
+ colour = style.brightYellow;
81
+ return `${colour}${BOX.full.repeat(filled)}${style.grey}${BOX.light.repeat(Math.max(0, cells - filled))}${style.reset}`;
82
+ }
83
+ export class Screen {
84
+ constructor(stream = process.stdout) {
85
+ this.out = stream;
86
+ this.previous = [];
87
+ this.entered = false;
88
+ }
89
+ get columns() {
90
+ return this.out.columns || 100;
91
+ }
92
+ get rows() {
93
+ return this.out.rows || 30;
94
+ }
95
+ enter() {
96
+ if (this.entered)
97
+ return;
98
+ this.entered = true;
99
+ this.out.write(ansi.altScreenOn + ansi.hideCursor + ansi.clearScreen + ansi.home);
100
+ }
101
+ leave() {
102
+ if (!this.entered)
103
+ return;
104
+ this.entered = false;
105
+ this.out.write(ansi.showCursor + ansi.altScreenOff);
106
+ }
107
+ /** Repaint only the lines that changed. */
108
+ render(lines) {
109
+ let frame = ansi.home;
110
+ const height = Math.min(lines.length, this.rows - 1);
111
+ for (let i = 0; i < height; i += 1) {
112
+ if (this.previous[i] === lines[i]) {
113
+ frame += `${CSI}${i + 2};1H`;
114
+ continue;
115
+ }
116
+ frame += `${CSI}${i + 1};1H${lines[i]}${ansi.clearToEol}`;
117
+ }
118
+ if (this.previous.length > height) {
119
+ frame += `${CSI}${height + 1};1H${ansi.clearBelow}`;
120
+ }
121
+ this.out.write(frame);
122
+ this.previous = lines.slice(0, height);
123
+ }
124
+ invalidate() {
125
+ this.previous = [];
126
+ this.out.write(ansi.clearScreen);
127
+ }
128
+ }
129
+ /** Decode a raw stdin chunk into a key name. */
130
+ export function decodeKey(chunk) {
131
+ const s = String(chunk);
132
+ const named = {
133
+ "\x1b[A": "up",
134
+ "\x1b[B": "down",
135
+ "\x1b[C": "right",
136
+ "\x1b[D": "left",
137
+ "\x1b[H": "home",
138
+ "\x1b[F": "end",
139
+ "\x1b[5~": "pageup",
140
+ "\x1b[6~": "pagedown",
141
+ "\r": "enter",
142
+ "\n": "enter",
143
+ "\t": "tab",
144
+ "\x1b[Z": "shifttab",
145
+ "\x7f": "backspace",
146
+ "\b": "backspace",
147
+ "\x1b": "escape",
148
+ "\x03": "ctrl-c",
149
+ "\x04": "ctrl-d",
150
+ " ": "space",
151
+ };
152
+ if (named[s])
153
+ return named[s];
154
+ if (s.length === 1)
155
+ return s;
156
+ return null;
157
+ }
158
+ export function onKeys(handler, stream = process.stdin) {
159
+ if (stream.isTTY)
160
+ stream.setRawMode(true);
161
+ stream.resume();
162
+ stream.setEncoding("utf8");
163
+ const listener = (chunk) => {
164
+ const key = decodeKey(chunk);
165
+ if (key)
166
+ handler(key, chunk);
167
+ };
168
+ stream.on("data", listener);
169
+ return () => {
170
+ stream.off("data", listener);
171
+ if (stream.isTTY)
172
+ stream.setRawMode(false);
173
+ stream.pause();
174
+ };
175
+ }
176
+ //# sourceMappingURL=screen.js.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Shared domain interfaces used across discovery, decision, and runtime layers.
3
+ * Kept dependency-free so any module can import the shapes without pulling in
4
+ * behavior. Behavioral defaults live with the code that produces each value.
5
+ */
6
+ export interface ModelMetadata {
7
+ arch?: string | null;
8
+ contextLength?: number | null;
9
+ blockCount?: number | null;
10
+ headCount?: number | null;
11
+ headCountKv?: number | null;
12
+ keyLength?: number | null;
13
+ valueLength?: number | null;
14
+ embeddingLength?: number | null;
15
+ /**
16
+ * Whether the model's chat template exposes a thinking/reasoning channel
17
+ * (a `<think>` block, `reasoning_content`, or an `enable_thinking` toggle).
18
+ * Derived from the GGUF template in `gguf.summarize`, mirroring how LM Studio
19
+ * flags a reasoning model - works for any local model, catalog or not.
20
+ */
21
+ reasoning?: boolean;
22
+ [key: string]: unknown;
23
+ }
24
+ export interface ModelFeatures {
25
+ mtp: boolean;
26
+ imatrix: boolean;
27
+ distilled: boolean;
28
+ }
29
+ /** A GGUF model discovered on disk (or resolvable from the download catalog). */
30
+ export interface Model {
31
+ id: string;
32
+ displayName: string;
33
+ modelPath: string;
34
+ mmprojPath: string | null;
35
+ mmprojBytes: number;
36
+ quant: string | null;
37
+ sizeBytes: number;
38
+ publisher?: string | null;
39
+ dir?: string;
40
+ sharded?: boolean;
41
+ features: ModelFeatures;
42
+ metadata: ModelMetadata | null;
43
+ metadataError?: string | null;
44
+ /** Which source the model was found in, for scan display. */
45
+ origin?: "managed" | "lmstudio";
46
+ /** Curated use-case tags from the catalog (e.g. "coding", "reasoning"). */
47
+ useCases?: string[];
48
+ /** Curation tier label from the catalog. */
49
+ tier?: string;
50
+ /** Whether the catalog marks this as a thinking/reasoning model. */
51
+ thinking?: boolean;
52
+ /** The catalog's advertised max context, if known. */
53
+ contextMax?: number;
54
+ /** Back-reference: the id of the reconciled catalog entry, if matched. */
55
+ catalogId?: string;
56
+ /** Back-reference: the hfRepo of the reconciled catalog entry, if matched. */
57
+ catalogHfRepo?: string;
58
+ }
59
+ /** A resolved llama.cpp runtime: an executable paired with its vendor DLL dir. */
60
+ export interface Runtime {
61
+ label: string;
62
+ version: string;
63
+ dir: string;
64
+ exe: string;
65
+ vendorDir: string | null;
66
+ source: "lmstudio" | "managed";
67
+ }
68
+ export interface GpuInfo {
69
+ name: string;
70
+ totalBytes: number;
71
+ usedBytes: number;
72
+ freeBytes: number;
73
+ driver: string;
74
+ computeCapability: string;
75
+ }
76
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared domain interfaces used across discovery, decision, and runtime layers.
3
+ * Kept dependency-free so any module can import the shapes without pulling in
4
+ * behavior. Behavioral defaults live with the code that produces each value.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,2 @@
1
+ export declare function resolveVersion(): string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,8 @@
1
+ /** Resolves the package version at runtime from package.json. */
2
+ import { createRequire } from "node:module";
3
+ const require = createRequire(import.meta.url);
4
+ export function resolveVersion() {
5
+ const pkg = require("../package.json");
6
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
7
+ }
8
+ //# sourceMappingURL=version.js.map
package/dist/vram.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * VRAM budgeting.
3
+ *
4
+ * A purely theoretical KV-cache formula is not trustworthy here. For a dense
5
+ * model, KV bytes/token = layers * kv_heads * (k_dim + v_dim) * bytes_per_elem.
6
+ * Measured against Qwen3.6-27B (`qwen35`) that formula overestimates by ~4x,
7
+ * because the architecture only carries a full KV cache on a subset of its
8
+ * layers. Guessing the ratio per architecture would be fragile, so the
9
+ * theoretical value is treated as a worst-case bound and a measured
10
+ * bytes-per-token from `calibrate` is preferred whenever one exists.
11
+ */
12
+ import type { Calibration, Profile } from "./config/schema.js";
13
+ import type { Model, ModelMetadata } from "./types.js";
14
+ export declare const GIB: number;
15
+ export declare const CACHE_TYPE_BYTES: Record<string, number>;
16
+ export declare function cacheTypeBytes(type: string): number;
17
+ export declare function listCacheTypes(): string[];
18
+ /** Worst-case KV bytes per token, assuming every layer holds a full cache. */
19
+ export declare function theoreticalKvBytesPerToken(metadata: ModelMetadata | null, cacheTypeK: string, cacheTypeV: string): number | null;
20
+ export type BudgetSource = "measured" | "theoretical" | "unknown";
21
+ export interface Budget {
22
+ weightsBytes: number;
23
+ mmprojBytes: number;
24
+ kvBytes: number;
25
+ overheadBytes: number;
26
+ totalBytes: number;
27
+ usableBytes: number;
28
+ totalVramBytes: number;
29
+ reserveBytes: number;
30
+ kvBytesPerToken: number;
31
+ source: BudgetSource;
32
+ theoreticalKvBytesPerToken: number | null;
33
+ fits: boolean;
34
+ headroomBytes: number;
35
+ utilization: number;
36
+ }
37
+ export interface BudgetOptions {
38
+ model: Model;
39
+ profile: Profile;
40
+ calibration?: Calibration | null;
41
+ totalVramBytes: number;
42
+ reserveBytes?: number;
43
+ }
44
+ /** Compute a VRAM budget for a profile. */
45
+ export declare function budget({ model, profile, calibration, totalVramBytes, reserveBytes, }: BudgetOptions): Budget;
46
+ export interface MaxContextOptions extends BudgetOptions {
47
+ step?: number;
48
+ }
49
+ /** Largest context that still fits, rounded down to a friendly step. */
50
+ export declare function maxContextThatFits({ model, profile, calibration, totalVramBytes, reserveBytes, step, }: MaxContextOptions): number | null;
51
+ export interface FitResult {
52
+ profile: Profile;
53
+ adjusted: boolean;
54
+ reason: string | null;
55
+ budget: Budget;
56
+ }
57
+ /**
58
+ * Adapt a profile to the hardware it is about to run on.
59
+ *
60
+ * Refusing to load because a saved profile asks for more context than this
61
+ * machine has is unhelpful when a slightly smaller context would work — and it
62
+ * is the difference between a model being usable on a 32GB desktop and a 24GB
63
+ * laptop. Clamp the context instead, and report what changed.
64
+ */
65
+ export declare function fitToBudget({ model, profile, calibration, totalVramBytes, reserveBytes, }: BudgetOptions): FitResult;
66
+ export declare function formatGiB(bytes: number, digits?: number): string;
67
+ //# sourceMappingURL=vram.d.ts.map
package/dist/vram.js ADDED
@@ -0,0 +1,132 @@
1
+ export const GIB = 1024 ** 3;
2
+ // Bytes per element for each KV cache type (block size / elements per block).
3
+ export const CACHE_TYPE_BYTES = {
4
+ f32: 4,
5
+ f16: 2,
6
+ bf16: 2,
7
+ q8_0: 34 / 32,
8
+ q5_1: 24 / 32,
9
+ q5_0: 22 / 32,
10
+ q4_1: 20 / 32,
11
+ q4_0: 18 / 32,
12
+ };
13
+ export function cacheTypeBytes(type) {
14
+ const bytes = CACHE_TYPE_BYTES[String(type).toLowerCase()];
15
+ if (bytes === undefined)
16
+ throw new Error(`unknown KV cache type "${type}"`);
17
+ return bytes;
18
+ }
19
+ export function listCacheTypes() {
20
+ return Object.keys(CACHE_TYPE_BYTES);
21
+ }
22
+ /** Worst-case KV bytes per token, assuming every layer holds a full cache. */
23
+ export function theoreticalKvBytesPerToken(metadata, cacheTypeK, cacheTypeV) {
24
+ if (!metadata)
25
+ return null;
26
+ const { blockCount, headCountKv, keyLength, valueLength } = metadata;
27
+ if (![blockCount, headCountKv, keyLength, valueLength].every((v) => typeof v === "number" && v > 0)) {
28
+ return null;
29
+ }
30
+ return (blockCount *
31
+ headCountKv *
32
+ (keyLength * cacheTypeBytes(cacheTypeK) +
33
+ valueLength * cacheTypeBytes(cacheTypeV)));
34
+ }
35
+ /** Compute a VRAM budget for a profile. */
36
+ export function budget({ model, profile, calibration = null, totalVramBytes, reserveBytes = 1.5 * GIB, }) {
37
+ const weights = model.sizeBytes;
38
+ const mmproj = profile.vision && model.mmprojPath ? model.mmprojBytes : 0;
39
+ const theoretical = theoreticalKvBytesPerToken(model.metadata, profile.cacheTypeK, profile.cacheTypeV);
40
+ let kvBytesPerToken;
41
+ let source;
42
+ if (calibration && calibration.kvBytesPerToken > 0) {
43
+ kvBytesPerToken = calibration.kvBytesPerToken;
44
+ source = "measured";
45
+ }
46
+ else if (theoretical !== null) {
47
+ kvBytesPerToken = theoretical;
48
+ source = "theoretical";
49
+ }
50
+ else {
51
+ kvBytesPerToken = 0;
52
+ source = "unknown";
53
+ }
54
+ const kv = kvBytesPerToken * profile.contextSize;
55
+ // Compute buffers and the CUDA context; measured at ~0.6 GB for a 27B and
56
+ // superseded by the calibrated value when available.
57
+ const overhead = calibration && calibration.baseOverheadBytes > 0 ? calibration.baseOverheadBytes : 0.6 * GIB;
58
+ const total = weights + mmproj + kv + overhead;
59
+ const usable = totalVramBytes - reserveBytes;
60
+ return {
61
+ weightsBytes: weights,
62
+ mmprojBytes: mmproj,
63
+ kvBytes: kv,
64
+ overheadBytes: overhead,
65
+ totalBytes: total,
66
+ usableBytes: usable,
67
+ totalVramBytes,
68
+ reserveBytes,
69
+ kvBytesPerToken,
70
+ source,
71
+ theoreticalKvBytesPerToken: theoretical,
72
+ fits: total <= usable,
73
+ headroomBytes: usable - total,
74
+ utilization: total / usable,
75
+ };
76
+ }
77
+ /** Largest context that still fits, rounded down to a friendly step. */
78
+ export function maxContextThatFits({ model, profile, calibration, totalVramBytes, reserveBytes = 1.5 * GIB, step = 4096, }) {
79
+ const probe = budget({
80
+ model,
81
+ profile: { ...profile, contextSize: step },
82
+ calibration,
83
+ totalVramBytes,
84
+ reserveBytes,
85
+ });
86
+ if (probe.kvBytesPerToken <= 0)
87
+ return null;
88
+ const fixed = probe.weightsBytes + probe.mmprojBytes + probe.overheadBytes;
89
+ const room = probe.usableBytes - fixed;
90
+ if (room <= 0)
91
+ return null;
92
+ const tokens = Math.floor(room / probe.kvBytesPerToken);
93
+ const capped = model.metadata?.contextLength
94
+ ? Math.min(tokens, model.metadata.contextLength)
95
+ : tokens;
96
+ return Math.max(0, Math.floor(capped / step) * step);
97
+ }
98
+ /**
99
+ * Adapt a profile to the hardware it is about to run on.
100
+ *
101
+ * Refusing to load because a saved profile asks for more context than this
102
+ * machine has is unhelpful when a slightly smaller context would work — and it
103
+ * is the difference between a model being usable on a 32GB desktop and a 24GB
104
+ * laptop. Clamp the context instead, and report what changed.
105
+ */
106
+ export function fitToBudget({ model, profile, calibration, totalVramBytes, reserveBytes = 1.5 * GIB, }) {
107
+ const initial = budget({ model, profile, calibration, totalVramBytes, reserveBytes });
108
+ if (initial.fits) {
109
+ return { profile, adjusted: false, reason: null, budget: initial };
110
+ }
111
+ const max = maxContextThatFits({ model, profile, calibration, totalVramBytes, reserveBytes });
112
+ if (!max || max < 4096) {
113
+ return {
114
+ profile,
115
+ adjusted: false,
116
+ reason: `does not fit at any usable context (needs ${formatGiB(initial.totalBytes)}, ` +
117
+ `${formatGiB(initial.usableBytes)} usable)`,
118
+ budget: initial,
119
+ };
120
+ }
121
+ const fitted = { ...profile, contextSize: max };
122
+ return {
123
+ profile: fitted,
124
+ adjusted: true,
125
+ reason: `context reduced ${profile.contextSize.toLocaleString()} -> ${max.toLocaleString()} to fit ${formatGiB(totalVramBytes)} of VRAM`,
126
+ budget: budget({ model, profile: fitted, calibration, totalVramBytes, reserveBytes }),
127
+ };
128
+ }
129
+ export function formatGiB(bytes, digits = 1) {
130
+ return `${(bytes / GIB).toFixed(digits)}G`;
131
+ }
132
+ //# sourceMappingURL=vram.js.map
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@otto-code/brain",
3
+ "version": "0.7.5",
4
+ "description": "Otto Brain - self-contained host for local GGUF models, with measured VRAM budgeting and reasoning-budget control",
5
+ "license": "AGPL-3.0-or-later",
6
+ "bin": {
7
+ "otto-brain": "bin/otto-brain"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "dist",
12
+ "!dist/**/*.map"
13
+ ],
14
+ "type": "module",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "node": "./dist/index.js",
20
+ "import": "./src/index.ts",
21
+ "default": "./src/index.ts"
22
+ },
23
+ "./cli": {
24
+ "types": "./dist/cli.d.ts",
25
+ "node": "./dist/cli.js",
26
+ "import": "./src/cli.ts",
27
+ "default": "./src/cli.ts"
28
+ }
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "clean": "node ../../scripts/clean-package-dist.mjs",
35
+ "build": "tsc -p tsconfig.json --incremental false",
36
+ "build:clean": "npm run clean && npm run build",
37
+ "prepack": "npm run build:clean",
38
+ "typecheck": "tsgo --noEmit",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest"
41
+ },
42
+ "dependencies": {
43
+ "@clack/prompts": "^1.0.0",
44
+ "chalk": "^5.3.0",
45
+ "commander": "^12.0.0",
46
+ "selfsigned": "^2.4.1",
47
+ "yaml": "^2.8.4",
48
+ "zod": "^3.23.8"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^20.9.0",
52
+ "typescript": "^5.9.3",
53
+ "vitest": "^4.1.6"
54
+ },
55
+ "engines": {
56
+ "node": ">=20.6.0"
57
+ }
58
+ }