@otto-code/brain 0.8.10 → 0.8.13

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 (65) hide show
  1. package/dist/commands/bench.js +2 -2
  2. package/dist/commands/calibrate.js +11 -2
  3. package/dist/commands/catalog.d.ts +1 -0
  4. package/dist/commands/catalog.js +1 -0
  5. package/dist/commands/pull.d.ts +1 -0
  6. package/dist/commands/pull.js +12 -3
  7. package/dist/commands/search.d.ts +1 -0
  8. package/dist/commands/search.js +12 -2
  9. package/dist/config/index.d.ts +2 -2
  10. package/dist/config/index.js +2 -2
  11. package/dist/config/profile-edit.d.ts +88 -1
  12. package/dist/config/profile-edit.js +294 -43
  13. package/dist/config/profiles.d.ts +19 -3
  14. package/dist/config/profiles.js +52 -4
  15. package/dist/config/schema.d.ts +616 -0
  16. package/dist/config/schema.js +65 -3
  17. package/dist/config/store.js +7 -4
  18. package/dist/gguf.d.ts +7 -0
  19. package/dist/gguf.js +15 -2
  20. package/dist/models/download.d.ts +1 -1
  21. package/dist/models/download.js +2 -2
  22. package/dist/models/enrich.d.ts +6 -0
  23. package/dist/models/enrich.js +27 -1
  24. package/dist/models/index.d.ts +1 -1
  25. package/dist/models/index.js +4 -3
  26. package/dist/ops/archive.d.ts +14 -1
  27. package/dist/ops/archive.js +9 -5
  28. package/dist/ops/calibrate.d.ts +38 -3
  29. package/dist/ops/calibrate.js +68 -19
  30. package/dist/ops/report.js +51 -1
  31. package/dist/ops/results.d.ts +77 -11
  32. package/dist/ops/results.js +84 -14
  33. package/dist/ops/sweep.d.ts +38 -1
  34. package/dist/ops/sweep.js +61 -10
  35. package/dist/runtime/args.d.ts +15 -2
  36. package/dist/runtime/args.js +60 -5
  37. package/dist/runtime/managed.js +2 -2
  38. package/dist/service/activity.d.ts +19 -0
  39. package/dist/service/activity.js +47 -4
  40. package/dist/service/host-api.d.ts +28 -4
  41. package/dist/service/host-api.js +109 -28
  42. package/dist/service/log-format.d.ts +18 -0
  43. package/dist/service/log-format.js +32 -0
  44. package/dist/service/process-pool.d.ts +45 -0
  45. package/dist/service/process-pool.js +271 -0
  46. package/dist/service/router.d.ts +74 -3
  47. package/dist/service/router.js +277 -51
  48. package/dist/service/run-log.d.ts +6 -1
  49. package/dist/service/run-log.js +46 -4
  50. package/dist/service/scheduler.d.ts +250 -31
  51. package/dist/service/scheduler.js +408 -63
  52. package/dist/service/serve.d.ts +4 -0
  53. package/dist/service/serve.js +376 -142
  54. package/dist/service/status-events.d.ts +14 -1
  55. package/dist/service/status-events.js +112 -12
  56. package/dist/service/supervisor.d.ts +9 -7
  57. package/dist/service/supervisor.js +37 -12
  58. package/dist/sysmon.d.ts +15 -0
  59. package/dist/sysmon.js +56 -9
  60. package/dist/tui/app.d.ts +8 -2
  61. package/dist/tui/app.js +83 -26
  62. package/dist/types.d.ts +18 -0
  63. package/dist/vram.d.ts +37 -0
  64. package/dist/vram.js +57 -18
  65. package/package.json +1 -1
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import http from "node:http";
10
10
  import path from "node:path";
11
- import { forModel, getCalibration, loadBrainConfig, loadProfilesStore, resolveBrainPaths, } from "../config/index.js";
11
+ import { forModel, getCalibrationForBudget, loadBrainConfig, loadProfilesStore, resolveBrainPaths, } from "../config/index.js";
12
12
  import { query as queryGpu } from "../gpu.js";
13
13
  import { pickModel, scanModels } from "../models/index.js";
14
14
  import { CommandError } from "../output/types.js";
@@ -172,7 +172,7 @@ async function runBenchSuite(options, _command) {
172
172
  // that ran is the profile that was configured, and the calibration is what
173
173
  // says whether the fit's own VRAM figures were measured or guessed. A score
174
174
  // read without them is a score nobody can diagnose.
175
- const calibration = getCalibration(store, model, profile);
175
+ const calibration = getCalibrationForBudget(store, model, profile);
176
176
  let fit = null;
177
177
  const gpu = await queryGpu();
178
178
  if (gpu) {
@@ -1,4 +1,4 @@
1
- import { getCalibration, forModel, loadBrainConfig, loadProfilesStore, putCalibration, saveProfilesStore, } from "../config/index.js";
1
+ import { getCalibrationForBudget, forModel, loadBrainConfig, loadProfilesStore, putCalibration, saveProfilesStore, } from "../config/index.js";
2
2
  import { query as queryGpu } from "../gpu.js";
3
3
  import { pickModel, scanModels } from "../models/index.js";
4
4
  import { CommandError } from "../output/types.js";
@@ -35,12 +35,19 @@ export async function runCalibrateCommand(options, _command) {
35
35
  const catalog = scanModels(config);
36
36
  const model = pickModel(catalog, options.model ?? store.lastModelId ?? undefined);
37
37
  const profile = forModel(store, model, config.defaults);
38
+ // The last direct measurement is the best prior on bytes/token available for
39
+ // capping the next sample. It keeps the calibration path stable while a
40
+ // changed profile is awaiting a fresh measurement. Inherited family
41
+ // calibrations are excluded: they were measured on another file and are not
42
+ // a trustworthy budget input here.
43
+ const prior = getCalibrationForBudget(store, model, profile);
38
44
  // Announced so the Brain rail can show the host as busy: a calibrate loads the
39
45
  // model at several context sizes and will make anything else queue behind it.
40
46
  const measurement = await withActivity("calibrate", { target: model.displayName }, () => calibrate({
41
47
  runtime,
42
48
  model,
43
49
  profile,
50
+ priorCalibration: prior && !prior.inherited ? prior : null,
44
51
  onProgress: (p) => {
45
52
  if (p.phase === "loading")
46
53
  process.stderr.write(` loading at ${p.contextSize?.toLocaleString()} ctx…\n`);
@@ -48,6 +55,8 @@ export async function runCalibrateCommand(options, _command) {
48
55
  process.stderr.write(` used ${vram.formatGiB(p.deltaBytes ?? 0)}\n`);
49
56
  if (p.phase === "skip")
50
57
  process.stderr.write(` skipped ${p.contextSize}: ${p.reason}\n`);
58
+ if (p.phase === "failed")
59
+ process.stderr.write(` failed at ${p.contextSize?.toLocaleString()}: ${p.error ?? p.reason}\n`);
51
60
  },
52
61
  }));
53
62
  putCalibration(store, model, profile, measurement);
@@ -57,7 +66,7 @@ export async function runCalibrateCommand(options, _command) {
57
66
  ? vram.maxContextThatFits({
58
67
  model,
59
68
  profile,
60
- calibration: getCalibration(store, model, profile),
69
+ calibration: getCalibrationForBudget(store, model, profile),
61
70
  totalVramBytes: gpu.totalBytes,
62
71
  })
63
72
  : null;
@@ -14,6 +14,7 @@ export interface CatalogRow {
14
14
  id: string;
15
15
  name: string;
16
16
  family: string | null;
17
+ favorite: boolean;
17
18
  installed: boolean;
18
19
  publisher: string;
19
20
  repo: string;
@@ -30,6 +30,7 @@ export async function runCatalogCommand(_options, _command) {
30
30
  id: model.id,
31
31
  name: model.name,
32
32
  family: model.family ?? null,
33
+ favorite: model.favorite,
33
34
  installed: installedCatalogIds.has(model.id),
34
35
  publisher: model.publisher ?? "",
35
36
  repo: model.hfRepo,
@@ -18,6 +18,7 @@ export interface PullOptionsInput {
18
18
  quant?: string;
19
19
  listQuants?: boolean;
20
20
  component?: string[];
21
+ componentsOnly?: boolean;
21
22
  }
22
23
  export declare function runPullCommand(modelArg: string, options: PullOptionsInput, _command: Command): Promise<AnyCommandResult<PullRow>>;
23
24
  //# sourceMappingURL=pull.d.ts.map
@@ -40,13 +40,16 @@ function findCatalogModel(models, needle) {
40
40
  return matches[0];
41
41
  }
42
42
  export function addPullOptions(cmd) {
43
- return cmd
43
+ return (cmd
44
44
  .description("Download a model from the catalog")
45
45
  .argument("<model>", "catalog id or name fragment")
46
46
  .option("--file <name.gguf>", "explicit GGUF file name in the repo")
47
47
  .option("--quant <label>", "download a specific quantization (e.g. Q5_K_M)")
48
48
  .option("--component <id...>", "download optional bundle component ids")
49
- .option("--list-quants", "list the quantizations the repo offers and exit");
49
+ // The daemon uses this for a bundle job that gained companions after its
50
+ // primary transfer began. It is deliberately not a normal user action.
51
+ .option("--components-only", "download selected bundle components without the primary quant")
52
+ .option("--list-quants", "list the quantizations the repo offers and exit"));
50
53
  }
51
54
  export async function runPullCommand(modelArg, options, _command) {
52
55
  const config = loadBrainConfig();
@@ -67,7 +70,13 @@ export async function runPullCommand(modelArg, options, _command) {
67
70
  message: `${model.hfRepo} has no ${options.quant}`,
68
71
  });
69
72
  }
70
- const plan = bundleDownloadPlan(model, options.component ?? [], choice?.files, choice?.sizeBytes);
73
+ if (options.componentsOnly && (options.component?.length ?? 0) === 0) {
74
+ throw new CommandError({
75
+ code: "NO_COMPONENT",
76
+ message: "--components-only requires at least one bundle component",
77
+ });
78
+ }
79
+ const plan = bundleDownloadPlan(model, options.component ?? [], options.componentsOnly ? [] : choice?.files, options.componentsOnly ? 0 : choice?.sizeBytes, !options.componentsOnly);
71
80
  const progressLabel = `${model.name}${choice ? ` ${choice.quant}` : ""}`;
72
81
  const written = await downloadRepoFilesWithProgress({
73
82
  activityTarget: model.name,
@@ -30,5 +30,6 @@ export declare function runAddCommand(repo: string, options: {
30
30
  listQuants?: boolean;
31
31
  component?: string[];
32
32
  primaryOnly?: boolean;
33
+ componentsOnly?: boolean;
33
34
  }, _command: Command): Promise<AnyCommandResult<AddRow>>;
34
35
  //# sourceMappingURL=search.d.ts.map
@@ -80,6 +80,7 @@ export function addAddOptions(cmd) {
80
80
  .option("--quant <label>", "quantization to download (e.g. Q5_K_M)")
81
81
  .option("--component <id...>", "download optional discovered component ids")
82
82
  .option("--primary-only", "download only the selected primary quant")
83
+ .option("--components-only", "download selected bundle components without the primary quant")
83
84
  .option("--list-quants", "list the quantizations the repo offers and exit");
84
85
  }
85
86
  export async function runAddCommand(repo, options, _command) {
@@ -131,8 +132,17 @@ export async function runAddCommand(repo, options, _command) {
131
132
  const includeProjector = Boolean(mmproj) &&
132
133
  (requested.has("vision-projector") ||
133
134
  (!options.primaryOnly && options.component === undefined));
134
- const files = [...choice.files, ...(includeProjector ? mmproj.files : [])];
135
- const total = choice.sizeBytes + (includeProjector ? mmproj.sizeBytes : 0);
135
+ if (options.componentsOnly && !includeProjector) {
136
+ throw new CommandError({
137
+ code: "NO_COMPONENT",
138
+ message: "--components-only requires a selected bundle component",
139
+ });
140
+ }
141
+ const files = [
142
+ ...(options.componentsOnly ? [] : choice.files),
143
+ ...(includeProjector ? mmproj.files : []),
144
+ ];
145
+ const total = (options.componentsOnly ? 0 : choice.sizeBytes) + (includeProjector ? mmproj.sizeBytes : 0);
136
146
  const written = await downloadRepoFilesWithProgress({
137
147
  activityTarget: `${repo} (${choice.quant})`,
138
148
  progressLabel: `${repo} ${choice.quant}`,
@@ -4,8 +4,8 @@ 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, hasStaleCalibration, } from "./profiles.js";
7
+ export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, getCalibrationForBudget, getLastCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
8
8
  export { effectiveHostingProfile, resolveHostingProfileForLaunch } from "./hosting-profiles.js";
9
- 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";
9
+ export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, PRESERVE_REASONING_CYCLE, SAMPLING_RANGES, preserveReasoningFromOption, preserveReasoningOption, type CalibrationInfo, type CalibrationState, type ProfileFieldDescriptor, type ProfileWarning, } from "./profile-edit.js";
10
10
  export * from "./schema.js";
11
11
  //# sourceMappingURL=index.d.ts.map
@@ -4,8 +4,8 @@ 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, hasStaleCalibration, } from "./profiles.js";
7
+ export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, getCalibrationForBudget, getLastCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
8
8
  export { effectiveHostingProfile, resolveHostingProfileForLaunch } from "./hosting-profiles.js";
9
- export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, } from "./profile-edit.js";
9
+ export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, PRESERVE_REASONING_CYCLE, SAMPLING_RANGES, preserveReasoningFromOption, preserveReasoningOption, } from "./profile-edit.js";
10
10
  export * from "./schema.js";
11
11
  //# sourceMappingURL=index.js.map
@@ -6,19 +6,88 @@ export interface ProfileFieldDescriptor {
6
6
  key: string;
7
7
  label: string;
8
8
  kind: ProfileFieldKind;
9
+ /**
10
+ * One sentence on what the field does, for the client's tooltip. It lives
11
+ * here rather than in the UI for the same reason the ranges do: a client that
12
+ * wrote its own copy would describe a setting the brain had since changed.
13
+ */
14
+ description?: string;
9
15
  /** For `number`: the increment a stepper should use. */
10
16
  step?: number;
11
17
  min?: number;
12
18
  max?: number;
19
+ /**
20
+ * For `number`: decimal places the value carries. Absent means an integer.
21
+ * A stepper MUST round to this after adding `step`, or repeated presses walk
22
+ * a sampler into 0.30000000000000004 and the profile stores that.
23
+ */
24
+ precision?: number;
13
25
  /** For `cycle`: the values to offer, in order. */
14
26
  options?: (string | number)[];
15
27
  /** Labels for `options`, index-aligned, when the raw value is not presentable. */
16
28
  optionLabels?: string[];
29
+ /**
30
+ * For `cycle`: what each option actually stores, index-aligned. Absent means
31
+ * the option IS the stored value, which is true of every cycle but the
32
+ * tri-states: `options` is limited to strings and numbers on the wire, so a
33
+ * field storing `true`/`false`/`null` needs somewhere to say so. A client
34
+ * matches the current value here first, then falls back to `options`.
35
+ */
36
+ optionValues?: (string | number | boolean | null)[];
17
37
  /** False when this model cannot use the field at all (vision with no projector). */
18
38
  available: boolean;
19
39
  /** Why it is unavailable, for the disabled-state hint. */
20
40
  unavailableReason?: string;
21
41
  }
42
+ /**
43
+ * Sampler ranges. llama.cpp itself bounds almost none of these - it will take a
44
+ * temperature of 50 - so the bounds are the useful range rather than the legal
45
+ * one, which is what a stepper wants. The write path clamps to exactly these,
46
+ * so a value the editor cannot reach is a value the brain will not store.
47
+ */
48
+ export declare const SAMPLING_RANGES: {
49
+ readonly temperature: {
50
+ readonly min: 0;
51
+ readonly max: 2;
52
+ readonly step: 0.05;
53
+ readonly precision: 2;
54
+ };
55
+ readonly topP: {
56
+ readonly min: 0;
57
+ readonly max: 1;
58
+ readonly step: 0.05;
59
+ readonly precision: 2;
60
+ };
61
+ readonly topK: {
62
+ readonly min: 0;
63
+ readonly max: 200;
64
+ readonly step: 1;
65
+ };
66
+ readonly minP: {
67
+ readonly min: 0;
68
+ readonly max: 1;
69
+ readonly step: 0.01;
70
+ readonly precision: 2;
71
+ };
72
+ readonly presencePenalty: {
73
+ readonly min: -2;
74
+ readonly max: 2;
75
+ readonly step: 0.05;
76
+ readonly precision: 2;
77
+ };
78
+ readonly repeatPenalty: {
79
+ readonly min: 0.5;
80
+ readonly max: 2;
81
+ readonly step: 0.01;
82
+ readonly precision: 2;
83
+ };
84
+ };
85
+ /** Tri-state preservation, in the order a cycle should offer it. */
86
+ export declare const PRESERVE_REASONING_CYCLE: readonly ["default", "on", "off"];
87
+ /** The stored value a cycle option maps to. */
88
+ export declare function preserveReasoningFromOption(option: string): boolean | null;
89
+ /** The cycle option a stored value maps to. */
90
+ export declare function preserveReasoningOption(value: boolean | null | undefined): string;
22
91
  /**
23
92
  * The cache types the editor cycles through. `CACHE_TYPE_BYTES` knows more
24
93
  * (f32, bf16, q5_0, q4_1) and a write naming one of those is accepted; these
@@ -42,6 +111,19 @@ export declare const UNRESTRICTED_REASONING_BUDGET = -1;
42
111
  */
43
112
  export declare function formatReasoningBudget(budget: number): string;
44
113
  export declare const MAX_PARALLEL_SLOTS = 16;
114
+ /**
115
+ * Ceiling on `cachedChats`. Generous on purpose: entries are small for a small
116
+ * model and huge for a large one, so the honest guard is the RAM figure the
117
+ * warning shows, not an arbitrary count.
118
+ */
119
+ export declare const MAX_CACHED_CHATS = 64;
120
+ /**
121
+ * What llama.cpp allows by default when no `--cache-ram` is emitted, in bytes.
122
+ * `cachedChats` of 0 emits no flag, so the estimate for the Default option is
123
+ * this fixed figure against the installed RAM - not the model's measured KV
124
+ * cost, which is irrelevant here because the size does not depend on the model.
125
+ */
126
+ export declare const ENGINE_DEFAULT_CACHE_RAM_BYTES: number;
45
127
  export declare const MAX_GPU_LAYERS = 999;
46
128
  export declare const MIN_CONTEXT_SIZE = 1024;
47
129
  export declare const CONTEXT_STEP = 8192;
@@ -54,7 +136,12 @@ export declare function profileFieldDescriptors(model: Model | null, profile?: P
54
136
  /** A note attached to a field, or to the profile as a whole when `field` is null. */
55
137
  export interface ProfileWarning {
56
138
  field: string | null;
57
- severity: "info" | "warn";
139
+ /**
140
+ * `info` renders as muted text, `warn` as yellow. `error` is red: the
141
+ * Cached KVs estimate earns it when the parked state would use at least the
142
+ * machine's whole RAM.
143
+ */
144
+ severity: "info" | "warn" | "error";
58
145
  message: string;
59
146
  /** True when this combination cannot start, as opposed to merely being unwise. */
60
147
  blocksStart: boolean;