@otto-code/brain 0.7.6 → 0.8.1

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 (87) hide show
  1. package/dist/bench/context-corpus.js +3 -3
  2. package/dist/bench/corpus.js +2 -2
  3. package/dist/bench/curated-repos.js +3 -3
  4. package/dist/bench/health.d.ts +1 -1
  5. package/dist/bench/health.js +2 -2
  6. package/dist/bench/tasks.js +2 -2
  7. package/dist/cli.d.ts +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/commands/bench.d.ts +1 -1
  10. package/dist/commands/bench.js +27 -4
  11. package/dist/commands/calibrate.d.ts +1 -1
  12. package/dist/commands/calibrate.js +5 -2
  13. package/dist/commands/catalog.d.ts +1 -1
  14. package/dist/commands/config.d.ts +1 -1
  15. package/dist/commands/lifecycle.js +1 -1
  16. package/dist/commands/pull.d.ts +1 -1
  17. package/dist/commands/pull.js +9 -4
  18. package/dist/commands/report.d.ts +1 -1
  19. package/dist/commands/rescore.d.ts +1 -1
  20. package/dist/commands/rescore.js +1 -1
  21. package/dist/commands/runtime.d.ts +2 -1
  22. package/dist/commands/runtime.js +32 -3
  23. package/dist/commands/scan.d.ts +1 -1
  24. package/dist/commands/scan.js +6 -1
  25. package/dist/commands/search.d.ts +1 -1
  26. package/dist/commands/share.js +2 -2
  27. package/dist/commands/sweep.d.ts +2 -2
  28. package/dist/commands/sweep.js +22 -8
  29. package/dist/commands/ui.d.ts +1 -1
  30. package/dist/config/index.d.ts +2 -1
  31. package/dist/config/index.js +2 -1
  32. package/dist/config/otto-home.js +1 -1
  33. package/dist/config/paths.d.ts +1 -0
  34. package/dist/config/paths.js +4 -0
  35. package/dist/config/profile-edit.d.ts +94 -0
  36. package/dist/config/profile-edit.js +269 -0
  37. package/dist/config/profiles.d.ts +2 -2
  38. package/dist/config/profiles.js +1 -1
  39. package/dist/config/schema.d.ts +4 -4
  40. package/dist/config/schema.js +6 -6
  41. package/dist/config/store.d.ts +1 -1
  42. package/dist/config/store.js +1 -1
  43. package/dist/models/download.js +1 -1
  44. package/dist/models/enrich.d.ts +2 -2
  45. package/dist/models/index.d.ts +1 -1
  46. package/dist/models/index.js +1 -1
  47. package/dist/models/pick.d.ts +9 -0
  48. package/dist/models/pick.js +24 -1
  49. package/dist/ops/report.js +28 -28
  50. package/dist/ops/results.d.ts +128 -9
  51. package/dist/ops/results.js +77 -5
  52. package/dist/output/render.js +1 -1
  53. package/dist/output/types.d.ts +1 -1
  54. package/dist/runtime/args.d.ts +17 -4
  55. package/dist/runtime/args.js +20 -14
  56. package/dist/runtime/index.d.ts +9 -3
  57. package/dist/runtime/index.js +21 -3
  58. package/dist/runtime/lmstudio.d.ts +2 -2
  59. package/dist/runtime/lmstudio.js +115 -28
  60. package/dist/runtime/managed.d.ts +72 -8
  61. package/dist/runtime/managed.js +404 -38
  62. package/dist/service/activity.d.ts +83 -0
  63. package/dist/service/activity.js +216 -0
  64. package/dist/service/host-api.d.ts +132 -0
  65. package/dist/service/host-api.js +397 -0
  66. package/dist/service/http-util.d.ts +27 -0
  67. package/dist/service/http-util.js +72 -0
  68. package/dist/service/model-selector.d.ts +2 -2
  69. package/dist/service/model-selector.js +6 -6
  70. package/dist/service/router.d.ts +28 -4
  71. package/dist/service/router.js +128 -94
  72. package/dist/service/scheduler.d.ts +2 -2
  73. package/dist/service/scheduler.js +1 -1
  74. package/dist/service/serve.d.ts +2 -2
  75. package/dist/service/serve.js +51 -8
  76. package/dist/service/supervisor.d.ts +6 -0
  77. package/dist/service/supervisor.js +2 -0
  78. package/dist/service/tailscale.js +1 -1
  79. package/dist/service/tls.d.ts +4 -4
  80. package/dist/service/tls.js +3 -3
  81. package/dist/sysmon.d.ts +20 -4
  82. package/dist/sysmon.js +42 -18
  83. package/dist/tui/app.d.ts +12 -2
  84. package/dist/tui/app.js +46 -22
  85. package/dist/vram.d.ts +8 -1
  86. package/dist/vram.js +6 -3
  87. package/package.json +1 -1
@@ -0,0 +1,397 @@
1
+ import { calibrationInfo, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, } from "../config/profile-edit.js";
2
+ import { forModel, getCalibration, put } from "../config/profiles.js";
3
+ import { deleteModelFiles, diskUsage, planDelete, totalModelBytes } from "../models/manage.js";
4
+ import * as vram from "../vram.js";
5
+ import { errorMessage, readJsonBody, sendError, sendJson } from "./http-util.js";
6
+ const MAX_PATCH_BYTES = 256 * 1024;
7
+ const DEFAULT_LOG_LINES = 200;
8
+ function stateOf(supervisor, model) {
9
+ if (!supervisor.model || supervisor.model.id !== model.id)
10
+ return "not-loaded";
11
+ if (supervisor.state === "ready")
12
+ return "loaded";
13
+ if (supervisor.state === "starting")
14
+ return "loading";
15
+ return "not-loaded";
16
+ }
17
+ /**
18
+ * Join one model's scan row, GGUF metadata, saved profile, calibration, VRAM
19
+ * budget and benchmark score into the single shape the Models tab renders.
20
+ *
21
+ * Exported for testing: the join is the part worth pinning down, since the
22
+ * client would otherwise have to correlate three unrelated lists by display name.
23
+ */
24
+ export function buildInventoryRow(params) {
25
+ const { model, store, defaults, gpu, ranking, supervisor } = params;
26
+ const profile = forModel(store, model, defaults);
27
+ const calibration = getCalibration(store, model, profile);
28
+ const budgetOptions = gpu
29
+ ? { model, profile, calibration, totalVramBytes: gpu.totalBytes }
30
+ : null;
31
+ const ranked = ranking.find((r) => r.id === model.id || r.displayName === model.displayName) ?? null;
32
+ return {
33
+ id: model.id,
34
+ displayName: model.displayName,
35
+ publisher: model.publisher ?? null,
36
+ quant: model.quant,
37
+ sizeBytes: model.sizeBytes,
38
+ mmprojBytes: model.mmprojBytes,
39
+ origin: model.origin ?? null,
40
+ arch: model.metadata?.arch ?? null,
41
+ contextLength: model.metadata?.contextLength ?? null,
42
+ blockCount: model.metadata?.blockCount ?? null,
43
+ headCountKv: model.metadata?.headCountKv ?? null,
44
+ hasProjector: Boolean(model.mmprojPath),
45
+ reasoning: Boolean(model.metadata?.reasoning ?? model.thinking),
46
+ mtp: Boolean(model.features?.mtp),
47
+ distilled: Boolean(model.features?.distilled),
48
+ useCases: model.useCases ?? [],
49
+ tier: model.tier ?? null,
50
+ profile,
51
+ calibration: calibrationInfo(store, model, profile),
52
+ budget: budgetOptions ? vram.budget(budgetOptions) : null,
53
+ maxContextThatFits: budgetOptions ? vram.maxContextThatFits(budgetOptions) : null,
54
+ score: ranked,
55
+ state: stateOf(supervisor, model),
56
+ warnings: profileWarnings(profile, model, store),
57
+ };
58
+ }
59
+ /** Resolve a model by id or display name, the same way the completion path does. */
60
+ function resolveModel(catalog, needle) {
61
+ if (!needle)
62
+ return null;
63
+ return catalog.find((m) => m.id === needle || m.displayName === needle) ?? null;
64
+ }
65
+ /**
66
+ * Read a hypothetical profile from query parameters, so the client can show the
67
+ * VRAM budget updating as a field is edited without persisting a value the user
68
+ * may be in the middle of scrubbing past.
69
+ */
70
+ function profileFromQuery(base, params, model) {
71
+ const patch = {};
72
+ const numeric = ["contextSize", "gpuLayers", "parallelSlots", "reasoningBudget"];
73
+ for (const key of numeric) {
74
+ const raw = params.get(key);
75
+ if (raw !== null && raw !== "")
76
+ patch[key] = Number(raw);
77
+ }
78
+ for (const key of ["cacheTypeK", "cacheTypeV"]) {
79
+ const raw = params.get(key);
80
+ if (raw)
81
+ patch[key] = raw;
82
+ }
83
+ for (const key of ["flashAttention", "vision"]) {
84
+ const raw = params.get(key);
85
+ if (raw !== null && raw !== "")
86
+ patch[key] = raw === "true" || raw === "1";
87
+ }
88
+ if (Object.keys(patch).length === 0)
89
+ return base;
90
+ return sanitizeProfilePatch(base, patch, model).profile;
91
+ }
92
+ /**
93
+ * Build the `/__host/*` management handler.
94
+ */
95
+ export function createHostApi(deps) {
96
+ const capabilities = () => ({
97
+ profiles: true,
98
+ budget: true,
99
+ logs: true,
100
+ delete: true,
101
+ load: true,
102
+ resources: true,
103
+ inventory: true,
104
+ writable: deps.getAllowWrite(),
105
+ });
106
+ /** Refuse a write unless the owner opted into remote configuration. */
107
+ const guardWrite = (res) => {
108
+ if (deps.getAllowWrite())
109
+ return true;
110
+ sendError(res, 403, "remote configuration is disabled on this brain; enable it with `otto brain share --allow-config`");
111
+ return false;
112
+ };
113
+ const inventory = async () => {
114
+ const [gpu, store] = [await deps.queryGpuInfo(), deps.getProfilesStore()];
115
+ const defaults = deps.getProfileDefaults();
116
+ const ranking = deps.getRanking();
117
+ return deps
118
+ .getCatalog()
119
+ .map((model) => buildInventoryRow({ model, store, defaults, gpu, ranking, supervisor: deps.supervisor }));
120
+ };
121
+ const handleModelsList = (res) => {
122
+ void (async () => {
123
+ try {
124
+ const models = await inventory();
125
+ const dir = deps.getModelsDir();
126
+ const disk = dir ? await diskUsage(dir) : null;
127
+ sendJson(res, {
128
+ models,
129
+ disk: disk ? { ...disk, modelBytes: totalModelBytes(deps.getCatalog()) } : null,
130
+ });
131
+ }
132
+ catch (error) {
133
+ sendError(res, 500, `could not build the model inventory: ${errorMessage(error)}`);
134
+ }
135
+ })();
136
+ };
137
+ const handleProfileGet = (res, model) => {
138
+ const store = deps.getProfilesStore();
139
+ const profile = forModel(store, model, deps.getProfileDefaults());
140
+ sendJson(res, {
141
+ profile,
142
+ fields: profileFieldDescriptors(model),
143
+ warnings: profileWarnings(profile, model, store),
144
+ calibration: calibrationInfo(store, model, profile),
145
+ });
146
+ };
147
+ const handleProfileSet = (req, res, model) => {
148
+ readJsonBody(req, MAX_PATCH_BYTES, (result) => {
149
+ if (!result.ok) {
150
+ sendError(res, 400, result.error);
151
+ return;
152
+ }
153
+ void (async () => {
154
+ try {
155
+ const store = deps.getProfilesStore();
156
+ const current = forModel(store, model, deps.getProfileDefaults());
157
+ const { profile, adjustments } = sanitizeProfilePatch(current, result.body, model);
158
+ deps.saveProfiles(put(store, model, profile));
159
+ // Return the recomputed budget so an edit costs one round trip rather
160
+ // than a write followed by a read the UI has to sequence.
161
+ const gpu = await deps.queryGpuInfo();
162
+ const calibration = getCalibration(store, model, profile);
163
+ const options = gpu
164
+ ? { model, profile, calibration, totalVramBytes: gpu.totalBytes }
165
+ : null;
166
+ sendJson(res, {
167
+ profile,
168
+ adjustments,
169
+ warnings: profileWarnings(profile, model, store),
170
+ calibration: calibrationInfo(store, model, profile),
171
+ budget: options ? vram.budget(options) : null,
172
+ maxContextThatFits: options ? vram.maxContextThatFits(options) : null,
173
+ /** True when the running model is the one just edited: a restart applies it. */
174
+ requiresRestart: deps.supervisor.model?.id === model.id,
175
+ });
176
+ }
177
+ catch (error) {
178
+ sendError(res, 400, errorMessage(error));
179
+ }
180
+ })();
181
+ });
182
+ };
183
+ const handleBudget = (res, model, params) => {
184
+ void (async () => {
185
+ try {
186
+ const store = deps.getProfilesStore();
187
+ const saved = forModel(store, model, deps.getProfileDefaults());
188
+ const profile = profileFromQuery(saved, params, model);
189
+ const gpu = await deps.queryGpuInfo();
190
+ if (!gpu) {
191
+ sendJson(res, {
192
+ profile,
193
+ budget: null,
194
+ maxContextThatFits: null,
195
+ gpu: null,
196
+ reason: "no NVIDIA GPU detected",
197
+ });
198
+ return;
199
+ }
200
+ const options = {
201
+ model,
202
+ profile,
203
+ calibration: getCalibration(store, model, profile),
204
+ totalVramBytes: gpu.totalBytes,
205
+ };
206
+ sendJson(res, {
207
+ profile,
208
+ budget: vram.budget(options),
209
+ maxContextThatFits: vram.maxContextThatFits(options),
210
+ gpu: { name: gpu.name, totalBytes: gpu.totalBytes, usedBytes: gpu.usedBytes },
211
+ warnings: profileWarnings(profile, model, store),
212
+ });
213
+ }
214
+ catch (error) {
215
+ sendError(res, 400, errorMessage(error));
216
+ }
217
+ })();
218
+ };
219
+ const handleLoad = (res, model) => {
220
+ void (async () => {
221
+ const store = deps.getProfilesStore();
222
+ const profile = forModel(store, model, deps.getProfileDefaults());
223
+ // The TUI refuses this combination at the `s` key rather than letting
224
+ // llama-server fail to allocate the cache and time out at "starting".
225
+ const blocking = profileWarnings(profile, model, store).find((w) => w.blocksStart);
226
+ if (blocking) {
227
+ sendError(res, 409, blocking.message);
228
+ return;
229
+ }
230
+ try {
231
+ await deps.loadModel(model);
232
+ sendJson(res, {
233
+ status: deps.supervisor.status(),
234
+ // What actually got used: loadModel fits the profile to VRAM, so the
235
+ // context here may be lower than the one saved.
236
+ profile: deps.supervisor.profile,
237
+ });
238
+ }
239
+ catch (error) {
240
+ sendError(res, 409, `could not load ${model.displayName}: ${errorMessage(error)}`);
241
+ }
242
+ })();
243
+ };
244
+ const handleUnload = (res) => {
245
+ void (async () => {
246
+ try {
247
+ await deps.supervisor.stop();
248
+ sendJson(res, { status: deps.supervisor.status() });
249
+ }
250
+ catch (error) {
251
+ sendError(res, 500, `could not unload: ${errorMessage(error)}`);
252
+ }
253
+ })();
254
+ };
255
+ const handleDelete = (res, model) => {
256
+ if (deps.supervisor.model?.id === model.id && deps.supervisor.state !== "stopped") {
257
+ sendError(res, 409, "stop the model before deleting it");
258
+ return;
259
+ }
260
+ try {
261
+ const plan = deleteModelFiles(model);
262
+ const catalog = deps.rescan();
263
+ sendJson(res, {
264
+ deleted: plan.files,
265
+ freedBytes: plan.bytes,
266
+ includesProjector: plan.includesProjector,
267
+ remaining: catalog.length,
268
+ });
269
+ }
270
+ catch (error) {
271
+ sendError(res, 500, `could not delete ${model.displayName}: ${errorMessage(error)}`);
272
+ }
273
+ };
274
+ const handleLogs = (res, params) => {
275
+ const raw = Number(params.get("limit"));
276
+ const limit = Number.isFinite(raw) && raw > 0 ? Math.min(Math.round(raw), 1000) : DEFAULT_LOG_LINES;
277
+ const all = deps.supervisor.logLines;
278
+ sendJson(res, {
279
+ lines: all.slice(-limit),
280
+ total: all.length,
281
+ state: deps.supervisor.state,
282
+ command: deps.supervisor.command,
283
+ });
284
+ };
285
+ function handleHostApi(req, res) {
286
+ const raw = req.url || "";
287
+ if (!raw.startsWith("/__host/"))
288
+ return false;
289
+ const url = new URL(raw, "http://brain.local");
290
+ const route = url.pathname;
291
+ const params = url.searchParams;
292
+ const method = (req.method || "GET").toUpperCase();
293
+ if (route === "/__host/capabilities" && method === "GET") {
294
+ sendJson(res, capabilities());
295
+ return true;
296
+ }
297
+ if (route === "/__host/logs" && method === "GET") {
298
+ handleLogs(res, params);
299
+ return true;
300
+ }
301
+ if (route === "/__host/resources" && method === "GET") {
302
+ void (async () => {
303
+ try {
304
+ sendJson(res, await deps.sampleResources());
305
+ }
306
+ catch (error) {
307
+ sendError(res, 500, `could not sample resources: ${errorMessage(error)}`);
308
+ }
309
+ })();
310
+ return true;
311
+ }
312
+ if (route === "/__host/models" && method === "GET") {
313
+ handleModelsList(res);
314
+ return true;
315
+ }
316
+ if (route === "/__host/model/unload" && method === "POST") {
317
+ if (!guardWrite(res))
318
+ return true;
319
+ handleUnload(res);
320
+ return true;
321
+ }
322
+ // Everything below is model-scoped and needs ?id=.
323
+ const modelRoutes = new Set([
324
+ "/__host/model",
325
+ "/__host/model/profile",
326
+ "/__host/model/budget",
327
+ "/__host/model/load",
328
+ "/__host/model/fields",
329
+ ]);
330
+ if (!modelRoutes.has(route))
331
+ return false;
332
+ const needle = params.get("id");
333
+ const model = resolveModel(deps.getCatalog(), needle);
334
+ if (!model) {
335
+ sendError(res, 404, needle ? `model "${needle}" was not found` : "an ?id= is required");
336
+ return true;
337
+ }
338
+ if (route === "/__host/model/fields" && method === "GET") {
339
+ sendJson(res, { fields: profileFieldDescriptors(model) });
340
+ return true;
341
+ }
342
+ if (route === "/__host/model/profile" && method === "GET") {
343
+ handleProfileGet(res, model);
344
+ return true;
345
+ }
346
+ if (route === "/__host/model/budget" && method === "GET") {
347
+ handleBudget(res, model, params);
348
+ return true;
349
+ }
350
+ if (route === "/__host/model/profile" && method === "POST") {
351
+ if (!guardWrite(res))
352
+ return true;
353
+ handleProfileSet(req, res, model);
354
+ return true;
355
+ }
356
+ if (route === "/__host/model/load" && method === "POST") {
357
+ if (!guardWrite(res))
358
+ return true;
359
+ handleLoad(res, model);
360
+ return true;
361
+ }
362
+ if (route === "/__host/model" && method === "DELETE") {
363
+ if (!guardWrite(res))
364
+ return true;
365
+ handleDelete(res, model);
366
+ return true;
367
+ }
368
+ if (route === "/__host/model" && method === "GET") {
369
+ // A single inventory row, for a detail pane that does not want the whole list.
370
+ void (async () => {
371
+ try {
372
+ const gpu = await deps.queryGpuInfo();
373
+ sendJson(res, buildInventoryRow({
374
+ model,
375
+ store: deps.getProfilesStore(),
376
+ defaults: deps.getProfileDefaults(),
377
+ gpu,
378
+ ranking: deps.getRanking(),
379
+ supervisor: deps.supervisor,
380
+ }));
381
+ }
382
+ catch (error) {
383
+ sendError(res, 500, errorMessage(error));
384
+ }
385
+ })();
386
+ return true;
387
+ }
388
+ sendError(res, 405, `${method} is not allowed on ${route}`);
389
+ return true;
390
+ }
391
+ return { handle: handleHostApi, capabilities };
392
+ }
393
+ /** The delete plan without performing it, for a confirmation dialog. */
394
+ export function describeDelete(model) {
395
+ return planDelete(model);
396
+ }
397
+ //# sourceMappingURL=host-api.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The small HTTP primitives the service layer shares.
3
+ *
4
+ * These lived inside `router.ts` while it was the only thing serving requests.
5
+ * `host-api.ts` needs the same error envelope and the same bounded body reader,
6
+ * and importing them back out of `router.ts` would make a cycle, so they sit
7
+ * here. The error envelope is deliberately Anthropic-shaped: clients already
8
+ * parse that from the completion proxy, so a management-endpoint failure does
9
+ * not need a second error format.
10
+ */
11
+ import type http from "node:http";
12
+ /** Headers that must not be forwarded across a proxy hop. */
13
+ export declare const HOP_BY_HOP: Set<string>;
14
+ export declare function errorBody(status: number, message: string): string;
15
+ export declare function sendJson(res: http.ServerResponse, payload: unknown, status?: number): void;
16
+ export declare function sendError(res: http.ServerResponse, status: number, message: string): void;
17
+ export type JsonBodyResult = {
18
+ ok: true;
19
+ body: unknown;
20
+ } | {
21
+ ok: false;
22
+ error: string;
23
+ };
24
+ /** Buffer a bounded JSON request body. */
25
+ export declare function readJsonBody(req: http.IncomingMessage, limit: number, cb: (result: JsonBodyResult) => void): void;
26
+ export declare function errorMessage(error: unknown): string;
27
+ //# sourceMappingURL=http-util.d.ts.map
@@ -0,0 +1,72 @@
1
+ /** Headers that must not be forwarded across a proxy hop. */
2
+ export const HOP_BY_HOP = new Set([
3
+ "connection",
4
+ "keep-alive",
5
+ "proxy-authenticate",
6
+ "proxy-authorization",
7
+ "te",
8
+ "trailer",
9
+ "transfer-encoding",
10
+ "upgrade",
11
+ ]);
12
+ export function errorBody(status, message) {
13
+ return JSON.stringify({
14
+ type: "error",
15
+ error: { type: status === 503 ? "overloaded_error" : "api_error", message },
16
+ });
17
+ }
18
+ export function sendJson(res, payload, status = 200) {
19
+ const body = JSON.stringify(payload, null, 2);
20
+ res.writeHead(status, {
21
+ "content-type": "application/json",
22
+ "content-length": Buffer.byteLength(body),
23
+ });
24
+ res.end(body);
25
+ }
26
+ export function sendError(res, status, message) {
27
+ if (res.headersSent) {
28
+ if (!res.writableEnded)
29
+ res.destroy();
30
+ return;
31
+ }
32
+ const body = errorBody(status, message);
33
+ const headers = {
34
+ "content-type": "application/json",
35
+ "content-length": Buffer.byteLength(body),
36
+ };
37
+ if (status === 503)
38
+ headers["retry-after"] = "5";
39
+ res.writeHead(status, headers);
40
+ res.end(body);
41
+ }
42
+ /** Buffer a bounded JSON request body. */
43
+ export function readJsonBody(req, limit, cb) {
44
+ const chunks = [];
45
+ let size = 0;
46
+ let tooBig = false;
47
+ req.on("data", (chunk) => {
48
+ size += chunk.length;
49
+ if (size > limit)
50
+ tooBig = true;
51
+ else
52
+ chunks.push(chunk);
53
+ });
54
+ req.on("error", () => cb({ ok: false, error: "request stream error" }));
55
+ req.on("end", () => {
56
+ if (tooBig) {
57
+ cb({ ok: false, error: "request body too large" });
58
+ return;
59
+ }
60
+ try {
61
+ const text = Buffer.concat(chunks).toString("utf8") || "{}";
62
+ cb({ ok: true, body: JSON.parse(text) });
63
+ }
64
+ catch {
65
+ cb({ ok: false, error: "invalid JSON body" });
66
+ }
67
+ });
68
+ }
69
+ export function errorMessage(error) {
70
+ return error instanceof Error ? error.message : String(error);
71
+ }
72
+ //# sourceMappingURL=http-util.js.map
@@ -11,7 +11,7 @@ export interface SelectCodingModelOptions {
11
11
  /** Track A's per-model bench ranking (mean score + runs + std). */
12
12
  ranking: RankedModel[];
13
13
  /**
14
- * VRAM-fit predicate. Omit (or pass undefined) to skip the fit filter the
14
+ * VRAM-fit predicate. Omit (or pass undefined) to skip the fit filter - the
15
15
  * caller does this when GPU info is absent, mirroring serve.ts's "absent →
16
16
  * skip" behaviour.
17
17
  */
@@ -23,7 +23,7 @@ export interface SelectCodingModelOptions {
23
23
  }
24
24
  /**
25
25
  * Pick the best-ranked coding model that fits the VRAM budget. Pure and
26
- * deterministic no IO, no clock, no randomness.
26
+ * deterministic - no IO, no clock, no randomness.
27
27
  */
28
28
  export declare function selectCodingModel({ models, ranking, fits, preferLoadedId, fallback, }: SelectCodingModelOptions): Model | null;
29
29
  /**
@@ -2,13 +2,13 @@
2
2
  * Route-time model selection for the UNNAMED request path.
3
3
  *
4
4
  * When a client hits the brain without naming a model, the old default served
5
- * "whatever is loaded, else catalog[0]" blind to which local model is actually
5
+ * "whatever is loaded, else catalog[0]" - blind to which local model is actually
6
6
  * the best coder. This picks the best-ranked coding-capable model that fits the
7
7
  * VRAM budget instead, wiring together Track A (the bench ranking) and Track B1
8
8
  * (the catalog coding metadata carried onto the scanned Model).
9
9
  *
10
10
  * `selectCodingModel` is PURE: it takes the models, the ranking, an optional
11
- * VRAM-fit predicate, and a fallback, and returns the chosen model with no IO
11
+ * VRAM-fit predicate, and a fallback, and returns the chosen model with no IO -
12
12
  * so the decision logic is trivially testable. `makeVramFitPredicate` is the one
13
13
  * impure edge (it reads GPU total VRAM and runs a vram.budget), deliberately kept
14
14
  * out of the pure path.
@@ -19,7 +19,7 @@ import * as vram from "../vram.js";
19
19
  // it is backed by enough repeated runs and its spread across those runs is
20
20
  // tight. rankModels reports one entry per model with the MEAN overall score
21
21
  // (0..1), the run COUNT, and the sample STD (also 0..1). A single run has std 0
22
- // falsely confident so we require at least two runs, and we reject a mean
22
+ // - falsely confident - so we require at least two runs, and we reject a mean
23
23
  // whose runs disagree by more than MAX_TRUSTED_STD. Untrusted models stay
24
24
  // eligible but sort below every trusted one.
25
25
  export const MIN_TRUSTED_RUNS = 2;
@@ -77,17 +77,17 @@ function compareCandidates(a, b, preferLoadedId) {
77
77
  }
78
78
  /**
79
79
  * Pick the best-ranked coding model that fits the VRAM budget. Pure and
80
- * deterministic no IO, no clock, no randomness.
80
+ * deterministic - no IO, no clock, no randomness.
81
81
  */
82
82
  export function selectCodingModel({ models, ranking, fits, preferLoadedId = null, fallback, }) {
83
83
  if (models.length === 0)
84
84
  return fallback;
85
85
  // 1. Candidate set: coding-capable models. If nothing is tagged (a catalog
86
- // with no coding metadata, or hand-placed models), don't fail closed
86
+ // with no coding metadata, or hand-placed models), don't fail closed -
87
87
  // fall back to the whole set.
88
88
  const tagged = models.filter(isCodingCapable);
89
89
  let candidates = tagged.length > 0 ? tagged : models;
90
- // 2. VRAM fit filter (skipped when no predicate i.e. GPU info absent). If
90
+ // 2. VRAM fit filter (skipped when no predicate - i.e. GPU info absent). If
91
91
  // nothing coding-capable fits, keep the existing default rather than
92
92
  // forcing a model that overflows the budget.
93
93
  if (fits) {
@@ -3,6 +3,7 @@ import type { Supervisor } from "./supervisor.js";
3
3
  import { type RankedModel } from "../ops/results.js";
4
4
  import type { GpuInfo, Model } from "../types.js";
5
5
  import type { Profile } from "../config/schema.js";
6
+ import type { HostApi } from "./host-api.js";
6
7
  type Verdict = "ok" | "reasoning-only" | "truncated" | "failed";
7
8
  /** A logger sink; only `warn` is used by the router. */
8
9
  export interface Logger {
@@ -46,8 +47,17 @@ export declare class Telemetry {
46
47
  totals: TelemetryTotals;
47
48
  constructor(keep?: number);
48
49
  record(entry: TelemetryRecord): void;
49
- /** Advice derived from observed behaviour, not guesswork. */
50
+ /**
51
+ * Advice derived from the recent window (`records`), not lifetime `totals`.
52
+ * A ratio over the lifetime total barely moves once a service has served any
53
+ * real volume, so a handful of clean responses after a bad patch could never
54
+ * clear it. The sliding window lets a few good requests visibly clear the
55
+ * advice, and `reset()` gives a restarted model a clean slate instead of
56
+ * carrying blame from before the fix was applied.
57
+ */
50
58
  get warning(): string | null;
59
+ /** Clear the recent window so the warning starts fresh - called when the model (re)starts. */
60
+ reset(): void;
51
61
  }
52
62
  /** Classify a completion body (Anthropic or OpenAI shaped). */
53
63
  export declare function analyse(bodyText: string): Analysis | null;
@@ -118,9 +128,9 @@ export interface RouterOptions {
118
128
  queryGpuInfo?: () => Promise<GpuInfo | null>;
119
129
  /** The brain package version, reported on `/__host/status` for the host UI. */
120
130
  version?: string | null;
121
- /** Effective config with secrets redacted served on `/__host/config`. */
131
+ /** Effective config with secrets redacted - served on `/__host/config`. */
122
132
  getConfig?: (() => unknown) | null;
123
- /** Benchmark rankings/variance/latest served on `/__host/evals`. */
133
+ /** Benchmark rankings/variance/latest - served on `/__host/evals`. */
124
134
  getEvals?: (() => unknown) | null;
125
135
  /** Live: pin the host to one model (refuse completions naming a different one). */
126
136
  getLockModel?: () => boolean;
@@ -138,7 +148,21 @@ export interface RouterOptions {
138
148
  * until its owner opts in. Read/use are unaffected.
139
149
  */
140
150
  getAllowConfigWrite?: () => boolean;
151
+ /**
152
+ * The management API (`host-api.ts`): model inventory, per-model profiles, the
153
+ * VRAM budget, load/unload, delete, and logs. Absent means those routes are not
154
+ * offered, which is exactly what `/__host/capabilities` then reports, so an
155
+ * older brain degrades to "that tab is unavailable" rather than a 404 storm.
156
+ */
157
+ hostApi?: HostApi | null;
158
+ /**
159
+ * Live system telemetry (CPU, RAM, GPU, slots), folded into `/__host/status`
160
+ * ONLY when the caller asks with `?resources=1`. The daemon's liveness probe
161
+ * polls status frequently and must not pay an `nvidia-smi` spawn for it; the
162
+ * Brain page's Overview tab opts in.
163
+ */
164
+ getResources?: (() => Promise<unknown>) | null;
141
165
  }
142
- export declare function createRouter({ supervisor, telemetry, logger, getCatalog, loadModel, loadRanking, queryGpuInfo, version, getConfig, getEvals, getLockModel, getDefaultModel, applyConfigPatch, getAllowConfigWrite, }: RouterOptions): (req: http.IncomingMessage, res: http.ServerResponse) => void;
166
+ export declare function createRouter({ supervisor, telemetry, logger, getCatalog, loadModel, loadRanking, queryGpuInfo, version, getConfig, getEvals, getLockModel, getDefaultModel, applyConfigPatch, getAllowConfigWrite, hostApi, getResources, }: RouterOptions): (req: http.IncomingMessage, res: http.ServerResponse) => void;
143
167
  export {};
144
168
  //# sourceMappingURL=router.d.ts.map