@otto-code/brain 0.7.6 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bench/context-corpus.js +3 -3
- package/dist/bench/corpus.js +2 -2
- package/dist/bench/curated-repos.js +3 -3
- package/dist/bench/health.d.ts +1 -1
- package/dist/bench/health.js +2 -2
- package/dist/bench/tasks.js +2 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/bench.d.ts +1 -1
- package/dist/commands/bench.js +27 -4
- package/dist/commands/calibrate.d.ts +1 -1
- package/dist/commands/calibrate.js +5 -2
- package/dist/commands/catalog.d.ts +1 -1
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/lifecycle.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +9 -4
- package/dist/commands/report.d.ts +1 -1
- package/dist/commands/rescore.d.ts +1 -1
- package/dist/commands/rescore.js +1 -1
- package/dist/commands/runtime.d.ts +1 -1
- package/dist/commands/scan.d.ts +1 -1
- package/dist/commands/scan.js +6 -1
- package/dist/commands/search.d.ts +1 -1
- package/dist/commands/share.js +2 -2
- package/dist/commands/sweep.d.ts +2 -2
- package/dist/commands/sweep.js +22 -8
- package/dist/commands/ui.d.ts +1 -1
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.js +2 -1
- package/dist/config/otto-home.js +1 -1
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +4 -0
- package/dist/config/profile-edit.d.ts +94 -0
- package/dist/config/profile-edit.js +269 -0
- package/dist/config/profiles.d.ts +2 -2
- package/dist/config/profiles.js +1 -1
- package/dist/config/schema.d.ts +4 -4
- package/dist/config/schema.js +6 -6
- package/dist/config/store.d.ts +1 -1
- package/dist/config/store.js +1 -1
- package/dist/models/download.js +1 -1
- package/dist/models/enrich.d.ts +2 -2
- package/dist/models/index.d.ts +1 -1
- package/dist/models/index.js +1 -1
- package/dist/models/pick.d.ts +9 -0
- package/dist/models/pick.js +24 -1
- package/dist/ops/report.js +28 -28
- package/dist/ops/results.d.ts +128 -9
- package/dist/ops/results.js +77 -5
- package/dist/output/render.js +1 -1
- package/dist/output/types.d.ts +1 -1
- package/dist/runtime/args.d.ts +1 -1
- package/dist/runtime/args.js +1 -1
- package/dist/runtime/managed.js +4 -4
- package/dist/service/activity.d.ts +83 -0
- package/dist/service/activity.js +216 -0
- package/dist/service/host-api.d.ts +132 -0
- package/dist/service/host-api.js +397 -0
- package/dist/service/http-util.d.ts +27 -0
- package/dist/service/http-util.js +72 -0
- package/dist/service/model-selector.d.ts +2 -2
- package/dist/service/model-selector.js +6 -6
- package/dist/service/router.d.ts +28 -4
- package/dist/service/router.js +128 -94
- package/dist/service/scheduler.d.ts +2 -2
- package/dist/service/scheduler.js +1 -1
- package/dist/service/serve.d.ts +2 -2
- package/dist/service/serve.js +51 -8
- package/dist/service/supervisor.d.ts +6 -0
- package/dist/service/supervisor.js +2 -0
- package/dist/service/tailscale.js +1 -1
- package/dist/service/tls.d.ts +4 -4
- package/dist/service/tls.js +3 -3
- package/dist/sysmon.d.ts +20 -4
- package/dist/sysmon.js +42 -18
- package/dist/tui/app.d.ts +12 -2
- package/dist/tui/app.js +46 -22
- package/dist/vram.d.ts +8 -1
- package/dist/vram.js +6 -3
- package/package.json +1 -1
|
@@ -133,7 +133,7 @@ function padTo(body, budgetChars, seed) {
|
|
|
133
133
|
}
|
|
134
134
|
/** Build one spec part covering `stages`, padded to `budgetChars`. */
|
|
135
135
|
function specPart(partIndex, stages, budgetChars) {
|
|
136
|
-
const heading = `# Pipeline specification
|
|
136
|
+
const heading = `# Pipeline specification - part ${String(partIndex).padStart(2, "0")}\n\n` +
|
|
137
137
|
`This part defines the rule for stages ${stages[0].index}–${stages[stages.length - 1].index}. ` +
|
|
138
138
|
`Each stage's module in the working copy is a passthrough placeholder; implement its \`apply(x)\` ` +
|
|
139
139
|
`exactly as written here. All arithmetic is bounded modulo 1000 and returns an integer.\n`;
|
|
@@ -176,8 +176,8 @@ export function generateContextCorpus({ targetTokens }) {
|
|
|
176
176
|
"`python -m unittest test_pipeline`.\n";
|
|
177
177
|
files["docs/OVERVIEW.md"] =
|
|
178
178
|
"# Specification overview\n\nThe pipeline applies each stage's `apply(x)` in order, starting " +
|
|
179
|
-
`from ${PIPELINE_START}. There are ${stageCount} stages. The rule for each stage
|
|
180
|
-
"operation and operand
|
|
179
|
+
`from ${PIPELINE_START}. There are ${stageCount} stages. The rule for each stage - its ` +
|
|
180
|
+
"operation and operand - is defined in the numbered parts under `docs/spec/`. The code carries " +
|
|
181
181
|
"no hint of the operands; you must read the spec.\n";
|
|
182
182
|
for (let p = 0; p < partCount; p += 1) {
|
|
183
183
|
const slice = stages.slice(p * partStride, (p + 1) * partStride);
|
package/dist/bench/corpus.js
CHANGED
|
@@ -193,7 +193,7 @@ Rules are applied in **descending priority** (highest \`priority\` first). Two
|
|
|
193
193
|
rules with equal priority may be applied in any order; the specification is
|
|
194
194
|
written so that the result does not depend on the order of equal-priority rules.
|
|
195
195
|
|
|
196
|
-
### Base amount
|
|
196
|
+
### Base amount - the load-bearing rule
|
|
197
197
|
|
|
198
198
|
Every rule's discount is computed against the **original subtotal**, NOT against
|
|
199
199
|
the running total after earlier rules. Discounts do **not** compound. A 30% rule
|
|
@@ -203,7 +203,7 @@ and a 40% rule on a $100.00 order remove \`$30.00 + $40.00 = $70.00\`, they do
|
|
|
203
203
|
This is the single most common mistake when re-implementing the engine: applying
|
|
204
204
|
each discount to the amount left by the previous one. Do not do that.
|
|
205
205
|
|
|
206
|
-
### Combined cap
|
|
206
|
+
### Combined cap - the other load-bearing rule
|
|
207
207
|
|
|
208
208
|
The **sum** of all rule discounts is capped at **50% of the original subtotal**.
|
|
209
209
|
Once the accumulated discount reaches half the subtotal, no further discount is
|
|
@@ -19,7 +19,7 @@ export const CURATED_REPOS = [
|
|
|
19
19
|
workspaceDir: "packages/protocol",
|
|
20
20
|
ref: "origin/main",
|
|
21
21
|
maxTasks: 3,
|
|
22
|
-
note: "schema/validation workspace
|
|
22
|
+
note: "schema/validation workspace - fast suite, no heavy deps",
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
name: "brain",
|
|
@@ -27,7 +27,7 @@ export const CURATED_REPOS = [
|
|
|
27
27
|
workspaceDir: "packages/brain",
|
|
28
28
|
ref: "origin/main",
|
|
29
29
|
maxTasks: 3,
|
|
30
|
-
note: "the brain package itself
|
|
30
|
+
note: "the brain package itself - pure-logic tests (vram, gguf, bench)",
|
|
31
31
|
},
|
|
32
32
|
];
|
|
33
33
|
/** Look up a curated preset by its handle. */
|
|
@@ -36,6 +36,6 @@ export function findCuratedRepo(name) {
|
|
|
36
36
|
}
|
|
37
37
|
/** A one-per-line listing of the available presets, for help/error text. */
|
|
38
38
|
export function describeCuratedRepos() {
|
|
39
|
-
return CURATED_REPOS.map((r) => ` ${r.name.padEnd(10)} ${r.workspace}
|
|
39
|
+
return CURATED_REPOS.map((r) => ` ${r.name.padEnd(10)} ${r.workspace} - ${r.note}`).join("\n");
|
|
40
40
|
}
|
|
41
41
|
//# sourceMappingURL=curated-repos.js.map
|
package/dist/bench/health.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare function summarize(samples: HealthSamples): HealthSummary;
|
|
|
42
42
|
/**
|
|
43
43
|
* Begin sampling; returns a handle with stop() that returns the summary.
|
|
44
44
|
* Sampling failures (no GPU, nvidia-smi missing) degrade to empty aggregates
|
|
45
|
-
* rather than throwing
|
|
45
|
+
* rather than throwing - health is diagnostic, never a reason to fail a bench.
|
|
46
46
|
*/
|
|
47
47
|
export declare function start({ intervalMs }?: {
|
|
48
48
|
intervalMs?: number;
|
package/dist/bench/health.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createCpuSampler } from "../sysmon.js";
|
|
|
5
5
|
* System-health sampling for a benchmark run.
|
|
6
6
|
*
|
|
7
7
|
* A score is only trustworthy if the machine was in a comparable state when it
|
|
8
|
-
* was measured
|
|
8
|
+
* was measured - a thermal-throttled or power-capped GPU produces a slower,
|
|
9
9
|
* lower run that says nothing about the model. So we sample GPU/CPU/RAM through
|
|
10
10
|
* the run and summarise it alongside the result, and flag when nvidia-smi itself
|
|
11
11
|
* reports a thermal or power slowdown (more reliable than inferring one from a
|
|
@@ -81,7 +81,7 @@ export function summarize(samples) {
|
|
|
81
81
|
/**
|
|
82
82
|
* Begin sampling; returns a handle with stop() that returns the summary.
|
|
83
83
|
* Sampling failures (no GPU, nvidia-smi missing) degrade to empty aggregates
|
|
84
|
-
* rather than throwing
|
|
84
|
+
* rather than throwing - health is diagnostic, never a reason to fail a bench.
|
|
85
85
|
*/
|
|
86
86
|
export function start({ intervalMs = 1000 } = {}) {
|
|
87
87
|
const samples = { gpu: [], cpu: [], ram: [] };
|
package/dist/bench/tasks.js
CHANGED
|
@@ -1037,7 +1037,7 @@ const contextStressTask = {
|
|
|
1037
1037
|
* so the depth task stops probing rather than zeroing the whole category.
|
|
1038
1038
|
*/
|
|
1039
1039
|
function isContextLimitError(message) {
|
|
1040
|
-
// Specific to a context-length rejection
|
|
1040
|
+
// Specific to a context-length rejection - a bare "context" would misclassify
|
|
1041
1041
|
// any unrelated 500 whose body happens to mention the word.
|
|
1042
1042
|
return /exceed|n_ctx|context (?:length|window|size)|too (?:many|long|large)|larger than|prompt is too long/i.test(message);
|
|
1043
1043
|
}
|
|
@@ -1166,7 +1166,7 @@ const contextDepthTask = {
|
|
|
1166
1166
|
};
|
|
1167
1167
|
// ---------------------------------------------------------------- concurrency
|
|
1168
1168
|
/**
|
|
1169
|
-
* Aggregate throughput with several requests in flight at once
|
|
1169
|
+
* Aggregate throughput with several requests in flight at once - the load a
|
|
1170
1170
|
* shared host actually sees when multiple chats or a model's own sub-agents hit
|
|
1171
1171
|
* it together. Reports tokens/sec in (prompt) and out (generation) summed across
|
|
1172
1172
|
* the concurrent requests; the score is how many returned real content, which
|
package/dist/cli.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* a group factory (`createBrainCommand`) that adds straight into the main `otto`
|
|
4
4
|
* program, and a `registerBrainCommands` that mounts the same verbs on a
|
|
5
5
|
* standalone root program (for `bin/otto-brain` on a server without the full CLI).
|
|
6
|
-
* Handlers stay pure
|
|
6
|
+
* Handlers stay pure - they return typed results wrapped by withOutput.
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
/** Mount every brain verb on the given command (a root program or the group). */
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* a group factory (`createBrainCommand`) that adds straight into the main `otto`
|
|
4
4
|
* program, and a `registerBrainCommands` that mounts the same verbs on a
|
|
5
5
|
* standalone root program (for `bin/otto-brain` on a server without the full CLI).
|
|
6
|
-
* Handlers stay pure
|
|
6
|
+
* Handlers stay pure - they return typed results wrapped by withOutput.
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { addBenchOptions, runBenchCommand } from "./commands/bench.js";
|
package/dist/commands/bench.d.ts
CHANGED
|
@@ -15,6 +15,6 @@ interface BenchOptions {
|
|
|
15
15
|
curated?: string;
|
|
16
16
|
}
|
|
17
17
|
export declare function addBenchOptions(cmd: Command): Command;
|
|
18
|
-
export declare function runBenchCommand(options: BenchOptions,
|
|
18
|
+
export declare function runBenchCommand(options: BenchOptions, command: Command): Promise<void>;
|
|
19
19
|
export {};
|
|
20
20
|
//# sourceMappingURL=bench.d.ts.map
|
package/dist/commands/bench.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain bench`
|
|
2
|
+
* `otto brain bench` - score a model's agentic coding ability on this machine.
|
|
3
3
|
* Either benchmarks an endpoint that is already serving (`--endpoint`) or loads
|
|
4
4
|
* each requested model itself. A long streaming run, so it prints a formatted
|
|
5
5
|
* report directly rather than going through the output layer.
|
|
@@ -11,6 +11,7 @@ import { query as queryGpu } from "../gpu.js";
|
|
|
11
11
|
import { pickModel, scanModels } from "../models/index.js";
|
|
12
12
|
import { CommandError } from "../output/types.js";
|
|
13
13
|
import { resolveRuntime } from "../runtime/index.js";
|
|
14
|
+
import { withActivity } from "../service/activity.js";
|
|
14
15
|
import { createRouter, Telemetry } from "../service/router.js";
|
|
15
16
|
import { Supervisor } from "../service/supervisor.js";
|
|
16
17
|
import * as bench from "../bench/index.js";
|
|
@@ -45,7 +46,14 @@ export function addBenchOptions(cmd) {
|
|
|
45
46
|
.option("--repo-max <n>", "max mined tasks to run", "5")
|
|
46
47
|
.option("--curated <name>", "run a curated mined-repo preset (needs --repo-dir); pass an unknown name to list presets");
|
|
47
48
|
}
|
|
48
|
-
export async function runBenchCommand(options,
|
|
49
|
+
export async function runBenchCommand(options, command) {
|
|
50
|
+
// Announced so the Brain rail can show the host as busy. A benchmark loads and
|
|
51
|
+
// unloads models and drives completions through them for minutes at a time,
|
|
52
|
+
// and it is the one op most likely to be running while somebody wonders why
|
|
53
|
+
// their prompt is slow.
|
|
54
|
+
return withActivity("benchmark", { target: options.model ?? options.endpoint ?? null }, () => runBenchSuite(options, command));
|
|
55
|
+
}
|
|
56
|
+
async function runBenchSuite(options, _command) {
|
|
49
57
|
const config = loadBrainConfig();
|
|
50
58
|
const execute = options.execute !== false;
|
|
51
59
|
const depths = options.depths
|
|
@@ -158,18 +166,27 @@ export async function runBenchCommand(options, _command) {
|
|
|
158
166
|
for (const needle of needles) {
|
|
159
167
|
const model = pickModel(catalog, needle);
|
|
160
168
|
let profile = forModel(store, model, config.defaults);
|
|
169
|
+
// Both are recorded with the run: the fit is what says whether the profile
|
|
170
|
+
// that ran is the profile that was configured, and the calibration is what
|
|
171
|
+
// says whether the fit's own VRAM figures were measured or guessed. A score
|
|
172
|
+
// read without them is a score nobody can diagnose.
|
|
173
|
+
const calibration = getCalibration(store, model, profile);
|
|
174
|
+
let fit = null;
|
|
161
175
|
const gpu = await queryGpu();
|
|
162
176
|
if (gpu) {
|
|
163
|
-
|
|
177
|
+
fit = vram.fitToBudget({
|
|
164
178
|
model,
|
|
165
179
|
profile,
|
|
166
|
-
calibration
|
|
180
|
+
calibration,
|
|
167
181
|
totalVramBytes: gpu.totalBytes,
|
|
168
182
|
});
|
|
169
183
|
if (!fit.adjusted && !fit.budget.fits) {
|
|
170
184
|
process.stderr.write(`\nskipping ${model.displayName}: ${fit.reason}\n`);
|
|
171
185
|
continue;
|
|
172
186
|
}
|
|
187
|
+
if (fit.adjusted) {
|
|
188
|
+
process.stderr.write(`\n${model.displayName}: ${fit.reason}\n`);
|
|
189
|
+
}
|
|
173
190
|
profile = fit.profile;
|
|
174
191
|
}
|
|
175
192
|
process.stdout.write(`\n${"=".repeat(74)}\n${model.displayName}\n${"=".repeat(74)}\n`);
|
|
@@ -205,6 +222,12 @@ export async function runBenchCommand(options, _command) {
|
|
|
205
222
|
gpu,
|
|
206
223
|
runtime: runtimeLabel,
|
|
207
224
|
archiveId,
|
|
225
|
+
// Straight off the supervisor rather than rebuilt here: this is the argv
|
|
226
|
+
// the child was spawned with, so it cannot drift from what actually ran.
|
|
227
|
+
args: supervisor.args,
|
|
228
|
+
fit,
|
|
229
|
+
calibration,
|
|
230
|
+
suite: { execute, concurrency, depths: depths ?? null, only, mined: Boolean(repoTasks) },
|
|
208
231
|
});
|
|
209
232
|
process.stderr.write(` saved to results/${path.basename(file)}\n`);
|
|
210
233
|
entries.push({ modelName: model.displayName, report });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain calibrate`
|
|
2
|
+
* `otto brain calibrate` - measure real KV bytes/token for a model and persist it,
|
|
3
3
|
* so the VRAM budget uses a measured figure instead of the (over-estimating)
|
|
4
4
|
* theoretical formula.
|
|
5
5
|
*/
|
|
@@ -4,6 +4,7 @@ import { pickModel, scanModels } from "../models/index.js";
|
|
|
4
4
|
import { CommandError } from "../output/types.js";
|
|
5
5
|
import { calibrate } from "../ops/calibrate.js";
|
|
6
6
|
import { resolveRuntime } from "../runtime/index.js";
|
|
7
|
+
import { withActivity } from "../service/activity.js";
|
|
7
8
|
import * as vram from "../vram.js";
|
|
8
9
|
const calibrateSchema = {
|
|
9
10
|
idField: "model",
|
|
@@ -34,7 +35,9 @@ export async function runCalibrateCommand(options, _command) {
|
|
|
34
35
|
const catalog = scanModels(config);
|
|
35
36
|
const model = pickModel(catalog, options.model ?? store.lastModelId ?? undefined);
|
|
36
37
|
const profile = forModel(store, model, config.defaults);
|
|
37
|
-
|
|
38
|
+
// Announced so the Brain rail can show the host as busy: a calibrate loads the
|
|
39
|
+
// model at several context sizes and will make anything else queue behind it.
|
|
40
|
+
const measurement = await withActivity("calibrate", { target: model.displayName }, () => calibrate({
|
|
38
41
|
runtime,
|
|
39
42
|
model,
|
|
40
43
|
profile,
|
|
@@ -46,7 +49,7 @@ export async function runCalibrateCommand(options, _command) {
|
|
|
46
49
|
if (p.phase === "skip")
|
|
47
50
|
process.stderr.write(` skipped ${p.contextSize}: ${p.reason}\n`);
|
|
48
51
|
},
|
|
49
|
-
});
|
|
52
|
+
}));
|
|
50
53
|
putCalibration(store, model, profile, measurement);
|
|
51
54
|
saveProfilesStore(store);
|
|
52
55
|
const gpu = await queryGpu();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain catalog`
|
|
2
|
+
* `otto brain catalog` - list the downloadable model catalog, each entry
|
|
3
3
|
* annotated with whether it is already installed. The installed flag reuses the
|
|
4
4
|
* authoritative catalog↔model join (enrichWithCatalog sets `catalogId` back-
|
|
5
5
|
* references on scanned models), so it agrees with `scan`/`pull` and works with
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain config show|set`
|
|
2
|
+
* `otto brain config show|set` - inspect and edit the persisted config at
|
|
3
3
|
* $OTTO_HOME/otto-brain/config.json. Opt-in flags (`enabled`, `autoStart`) live
|
|
4
4
|
* here; the Otto daemon reads them to decide whether to manage the brain.
|
|
5
5
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Service lifecycle commands. `serve` runs the brain in the foreground; `start`
|
|
3
3
|
* launches it detached (the shape the Otto daemon uses to supervise a managed
|
|
4
|
-
* child); `stop`/`status` operate on the pid file. All honor the opt-in config
|
|
4
|
+
* child); `stop`/`status` operate on the pid file. All honor the opt-in config -
|
|
5
5
|
* they are always explicit user actions, never auto-started.
|
|
6
6
|
*/
|
|
7
7
|
import { spawn } from "node:child_process";
|
package/dist/commands/pull.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain pull <model>`
|
|
2
|
+
* `otto brain pull <model>` - download a model from the catalog into the managed
|
|
3
3
|
* models directory, using only Node's fetch (no external downloader). The catalog
|
|
4
4
|
* is the same one seeded from docs/candidate-models.md. `--list-quants` shows what
|
|
5
5
|
* quantizations the repo offers and `--quant <label>` downloads a specific one.
|
package/dist/commands/pull.js
CHANGED
|
@@ -2,6 +2,7 @@ import { loadBrainConfig, loadCatalog } from "../config/index.js";
|
|
|
2
2
|
import { downloadRepoFiles, listRepoQuants, managedModelsDir, pullModel, resolveHfToken, } from "../models/index.js";
|
|
3
3
|
import { CommandError } from "../output/types.js";
|
|
4
4
|
import { formatBytes } from "../models/scan.js";
|
|
5
|
+
import { withActivity } from "../service/activity.js";
|
|
5
6
|
const pullSchema = {
|
|
6
7
|
idField: "model",
|
|
7
8
|
columns: [
|
|
@@ -78,19 +79,20 @@ export async function runPullCommand(modelArg, options, _command) {
|
|
|
78
79
|
const files = [...choice.files, ...(mmproj ? mmproj.files : [])];
|
|
79
80
|
const total = choice.sizeBytes + (mmproj?.sizeBytes ?? 0);
|
|
80
81
|
let lastPct = -1;
|
|
81
|
-
const written = await downloadRepoFiles({
|
|
82
|
+
const written = await withActivity("download", { target: `${model.name} (${choice.quant})` }, (activity) => downloadRepoFiles({
|
|
82
83
|
repo: model.hfRepo,
|
|
83
84
|
files,
|
|
84
85
|
destRoot: managedModelsDir(config),
|
|
85
86
|
token,
|
|
86
87
|
onProgress: (p) => {
|
|
88
|
+
activity.update(total ? p.receivedBytes / total : null);
|
|
87
89
|
const pct = total ? Math.floor((p.receivedBytes / total) * 100) : 0;
|
|
88
90
|
if (pct !== lastPct && pct % 5 === 0) {
|
|
89
91
|
lastPct = pct;
|
|
90
92
|
process.stderr.write(` ${model.name} ${choice.quant}: ${pct}%\r`);
|
|
91
93
|
}
|
|
92
94
|
},
|
|
93
|
-
});
|
|
95
|
+
}));
|
|
94
96
|
process.stderr.write("\n");
|
|
95
97
|
return {
|
|
96
98
|
type: "single",
|
|
@@ -105,7 +107,9 @@ export async function runPullCommand(modelArg, options, _command) {
|
|
|
105
107
|
}
|
|
106
108
|
const destRoot = managedModelsDir(config);
|
|
107
109
|
let lastPct = -1;
|
|
108
|
-
|
|
110
|
+
// Announced so the Brain rail can show the download: this is the longest-
|
|
111
|
+
// running thing the brain does, and the only one measured in tens of GiB.
|
|
112
|
+
const localPath = await withActivity("download", { target: model.name }, (activity) => pullModel({
|
|
109
113
|
model,
|
|
110
114
|
destRoot,
|
|
111
115
|
file: options.file,
|
|
@@ -113,13 +117,14 @@ export async function runPullCommand(modelArg, options, _command) {
|
|
|
113
117
|
onProgress: (p) => {
|
|
114
118
|
if (!p.totalBytes)
|
|
115
119
|
return;
|
|
120
|
+
activity.update(p.receivedBytes / p.totalBytes);
|
|
116
121
|
const pct = Math.floor((p.receivedBytes / p.totalBytes) * 100);
|
|
117
122
|
if (pct !== lastPct && pct % 5 === 0) {
|
|
118
123
|
lastPct = pct;
|
|
119
124
|
process.stderr.write(` ${model.name}: ${pct}%\r`);
|
|
120
125
|
}
|
|
121
126
|
},
|
|
122
|
-
});
|
|
127
|
+
}));
|
|
123
128
|
process.stderr.write("\n");
|
|
124
129
|
return {
|
|
125
130
|
type: "single",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain rescore`
|
|
2
|
+
* `otto brain rescore` - re-grade archived benchmark transcripts with the current
|
|
3
3
|
* scorer, no GPU used. Surfaces scorer changes against what was recorded live.
|
|
4
4
|
*/
|
|
5
5
|
import type { Command } from "commander";
|
package/dist/commands/rescore.js
CHANGED
|
@@ -34,7 +34,7 @@ export async function runRescoreCommand(options, _command) {
|
|
|
34
34
|
}
|
|
35
35
|
const before = record?.tasks.find((t) => t.id === task.taskId);
|
|
36
36
|
const now = `${(task.score * 100).toFixed(0)}%`;
|
|
37
|
-
const then = before ? `${(before.score * 100).toFixed(0)}%` : "
|
|
37
|
+
const then = before ? `${(before.score * 100).toFixed(0)}%` : "-";
|
|
38
38
|
const moved = before && Math.abs(before.score - task.score) > 0.005;
|
|
39
39
|
if (moved)
|
|
40
40
|
changed += 1;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain runtime install|list`
|
|
2
|
+
* `otto brain runtime install|list` - manage the self-contained llama.cpp runtime.
|
|
3
3
|
* `install` downloads a pinned build into $OTTO_HOME/otto-brain/runtimes so the
|
|
4
4
|
* tool needs no other software; `list` shows every runtime found (managed first,
|
|
5
5
|
* then LM Studio).
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain scan`
|
|
2
|
+
* `otto brain scan` - list detected models with their arch, quant, size, native
|
|
3
3
|
* context, vision, calibration state, and source. Returns a typed list the output
|
|
4
4
|
* layer renders as a table (default), json, or yaml.
|
|
5
5
|
*/
|
package/dist/commands/scan.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCalibration, forModel, loadBrainConfig, loadProfilesStore } from "../config/index.js";
|
|
2
2
|
import { formatBytes, scanModels } from "../models/index.js";
|
|
3
|
+
import { withActivity } from "../service/activity.js";
|
|
3
4
|
export const scanSchema = {
|
|
4
5
|
idField: "model",
|
|
5
6
|
columns: [
|
|
@@ -27,7 +28,11 @@ export function addScanOptions(cmd) {
|
|
|
27
28
|
export async function runScanCommand(options, _command) {
|
|
28
29
|
const config = loadBrainConfig();
|
|
29
30
|
const store = loadProfilesStore();
|
|
30
|
-
|
|
31
|
+
// Announced because a metadata scan opens and reads the GGUF header of every
|
|
32
|
+
// model on disk, which on a large library is seconds of I/O, not milliseconds.
|
|
33
|
+
// Only the explicit `scan` command announces: the other commands call
|
|
34
|
+
// `scanModels` too, and each announcing would fight over the one record.
|
|
35
|
+
const catalog = await withActivity("scan", {}, async () => scanModels(config, process.env, { withMetadata: options.metadata !== false }));
|
|
31
36
|
const rows = catalog.map((model) => {
|
|
32
37
|
const profile = forModel(store, model, config.defaults);
|
|
33
38
|
const md = model.metadata ?? {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain search <query>` and `otto brain repo-quants <repo>`
|
|
2
|
+
* `otto brain search <query>` and `otto brain repo-quants <repo>` - Hugging Face
|
|
3
3
|
* model discovery. These wrap the surface-agnostic primitives in models/hf.ts so
|
|
4
4
|
* the same code serves the TUI (in-process) and the Otto app (daemon shells out
|
|
5
5
|
* to `--json`, mirroring `catalog`/`scan`).
|
package/dist/commands/share.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain share`
|
|
2
|
+
* `otto brain share` - opt a brain into being reachable (and optionally
|
|
3
3
|
* configurable) by other Otto hosts. Off by default: a brain binds loopback and
|
|
4
4
|
* is invisible to the network until its owner runs this. Sets the bind, the
|
|
5
5
|
* access level (open on a trusted network, or a bearer key), HTTPS, and whether
|
|
6
|
-
* key holders may reconfigure it. Writes $OTTO_HOME/otto-brain/config.json
|
|
6
|
+
* key holders may reconfigure it. Writes $OTTO_HOME/otto-brain/config.json - the
|
|
7
7
|
* same fields the Otto app's Sharing UI sets, so the two stay in sync.
|
|
8
8
|
*/
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
package/dist/commands/sweep.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain sweep`
|
|
2
|
+
* `otto brain sweep` - find the reasoning budget that yields the best output, and
|
|
3
3
|
* save it to the model's profile. Guards against the thinking-model failure where
|
|
4
4
|
* an unrestricted budget returns pure reasoning and no content.
|
|
5
5
|
*/
|
|
6
6
|
import type { Command } from "commander";
|
|
7
7
|
import type { AnyCommandResult } from "../output/index.js";
|
|
8
8
|
export interface SweepRow {
|
|
9
|
-
budget: number
|
|
9
|
+
budget: number;
|
|
10
10
|
content: string;
|
|
11
11
|
files: string;
|
|
12
12
|
tokensPerSec: string;
|
package/dist/commands/sweep.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { forModel, loadBrainConfig, loadProfilesStore, put, saveProfilesStore, } from "../config/index.js";
|
|
1
|
+
import { forModel, formatReasoningBudget, loadBrainConfig, loadProfilesStore, put, saveProfilesStore, } from "../config/index.js";
|
|
2
2
|
import { pickModel, scanModels } from "../models/index.js";
|
|
3
3
|
import { CommandError } from "../output/types.js";
|
|
4
|
-
import { sweep } from "../ops/sweep.js";
|
|
4
|
+
import { DEFAULT_BUDGETS, sweep } from "../ops/sweep.js";
|
|
5
5
|
import { resolveRuntime } from "../runtime/index.js";
|
|
6
|
+
import { withActivity } from "../service/activity.js";
|
|
6
7
|
const sweepSchema = {
|
|
7
8
|
idField: "budget",
|
|
8
9
|
columns: [
|
|
9
|
-
|
|
10
|
+
// Labelled only for the table. `budget` stays the raw number on the row, so
|
|
11
|
+
// json/yaml and --quiet still emit -1 for machine consumers.
|
|
12
|
+
{
|
|
13
|
+
header: "BUDGET",
|
|
14
|
+
field: (row) => formatReasoningBudget(row.budget),
|
|
15
|
+
width: 8,
|
|
16
|
+
align: "right",
|
|
17
|
+
},
|
|
10
18
|
{ header: "CONTENT", field: "content", width: 9, align: "right" },
|
|
11
19
|
{ header: "FILES", field: "files", width: 6, align: "right" },
|
|
12
20
|
{ header: "TOK/S", field: "tokensPerSec", width: 7, align: "right" },
|
|
@@ -37,20 +45,26 @@ export async function runSweepCommand(options, _command) {
|
|
|
37
45
|
const budgets = options.budgets
|
|
38
46
|
? options.budgets.split(",").map((s) => Number(s.trim()))
|
|
39
47
|
: undefined;
|
|
40
|
-
|
|
48
|
+
// Progress lines stack, so pad to the widest label this run will print rather
|
|
49
|
+
// than a fixed width that "unrestricted" would overflow.
|
|
50
|
+
const labelWidth = Math.max(...(budgets ?? DEFAULT_BUDGETS).map((b) => formatReasoningBudget(b).length));
|
|
51
|
+
const label = (budget) => formatReasoningBudget(budget).padStart(labelWidth);
|
|
52
|
+
// Announced so the Brain rail can show the host as busy: a sweep reloads the
|
|
53
|
+
// model once per budget and owns the machine for the duration.
|
|
54
|
+
const report = await withActivity("sweep", { target: model.displayName }, () => sweep({
|
|
41
55
|
runtime,
|
|
42
56
|
model,
|
|
43
57
|
profile,
|
|
44
58
|
budgets,
|
|
45
59
|
onProgress: (p) => {
|
|
46
60
|
if (p.phase === "loading")
|
|
47
|
-
process.stderr.write(` budget ${
|
|
61
|
+
process.stderr.write(` budget ${label(p.budget)}: loading…\n`);
|
|
48
62
|
if (p.phase === "done")
|
|
49
|
-
process.stderr.write(` budget ${
|
|
63
|
+
process.stderr.write(` budget ${label(p.budget)}: done\n`);
|
|
50
64
|
if (p.phase === "failed")
|
|
51
|
-
process.stderr.write(` budget ${
|
|
65
|
+
process.stderr.write(` budget ${label(p.budget)}: failed ${p.error}\n`);
|
|
52
66
|
},
|
|
53
|
-
});
|
|
67
|
+
}));
|
|
54
68
|
if (report.recommended !== null && report.recommended !== undefined) {
|
|
55
69
|
profile.reasoningBudget = report.recommended;
|
|
56
70
|
put(store, model, profile);
|
package/dist/commands/ui.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `otto brain ui` (and the bare `otto brain`)
|
|
2
|
+
* `otto brain ui` (and the bare `otto brain`) - the full-screen interactive TUI,
|
|
3
3
|
* the tool's signature experience. Gated on a TTY; falls back with guidance when
|
|
4
4
|
* piped. Bypasses the output layer since it owns the whole screen.
|
|
5
5
|
*/
|
package/dist/config/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { ensurePrivateDirectory, writePrivateFileAtomicSync } from "./private-fi
|
|
|
4
4
|
export { resolveBrainPaths, packageRoot, type BrainPaths } from "./paths.js";
|
|
5
5
|
export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
|
|
6
6
|
export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
|
|
7
|
-
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, } from "./profiles.js";
|
|
7
|
+
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
|
|
8
|
+
export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, type CalibrationInfo, type CalibrationState, type ProfileFieldDescriptor, type ProfileWarning, } from "./profile-edit.js";
|
|
8
9
|
export * from "./schema.js";
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/config/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { ensurePrivateDirectory, writePrivateFileAtomicSync } from "./private-fi
|
|
|
4
4
|
export { resolveBrainPaths, packageRoot } from "./paths.js";
|
|
5
5
|
export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
|
|
6
6
|
export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
|
|
7
|
-
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, } from "./profiles.js";
|
|
7
|
+
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
|
|
8
|
+
export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, } from "./profile-edit.js";
|
|
8
9
|
export * from "./schema.js";
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
package/dist/config/otto-home.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Resolves $OTTO_HOME exactly as the Otto daemon does (see
|
|
3
3
|
* packages/server/src/server/otto-home.ts): env override, else `~/.otto`, tilde
|
|
4
4
|
* expanded, made private. Sharing this rule is what lets otto-brain's config sit
|
|
5
|
-
* next to Otto's
|
|
5
|
+
* next to Otto's - and follow it into the dev home whenever the dev scripts export
|
|
6
6
|
* OTTO_HOME. There is deliberately no dev-path logic here; that lives in shell.
|
|
7
7
|
*/
|
|
8
8
|
import { homedir } from "node:os";
|
package/dist/config/paths.d.ts
CHANGED
package/dist/config/paths.js
CHANGED
|
@@ -18,6 +18,10 @@ export function resolveBrainPaths(env = process.env) {
|
|
|
18
18
|
modelsDir: path.join(root, "models"),
|
|
19
19
|
runtimesDir: path.join(root, "runtimes"),
|
|
20
20
|
pidFile: path.join(root, "otto-brain.pid"),
|
|
21
|
+
// Which long-running op currently owns the host. A file rather than service
|
|
22
|
+
// state because calibrate/sweep/bench run as their own CLI processes and the
|
|
23
|
+
// service - which is what answers /__host/status - never sees them otherwise.
|
|
24
|
+
activityFile: path.join(root, "otto-brain.activity"),
|
|
21
25
|
logFile: path.join(root, "otto-brain.log"),
|
|
22
26
|
resultsDir: path.join(root, "results"),
|
|
23
27
|
};
|