@otto-code/brain 0.8.9 → 0.8.12

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 (59) hide show
  1. package/dist/commands/calibrate.js +9 -0
  2. package/dist/commands/catalog.d.ts +1 -0
  3. package/dist/commands/catalog.js +1 -0
  4. package/dist/commands/pull.d.ts +1 -0
  5. package/dist/commands/pull.js +12 -3
  6. package/dist/commands/search.d.ts +1 -0
  7. package/dist/commands/search.js +12 -2
  8. package/dist/config/index.d.ts +1 -1
  9. package/dist/config/index.js +1 -1
  10. package/dist/config/profile-edit.d.ts +88 -1
  11. package/dist/config/profile-edit.js +280 -29
  12. package/dist/config/profiles.js +16 -0
  13. package/dist/config/schema.d.ts +608 -0
  14. package/dist/config/schema.js +58 -0
  15. package/dist/config/store.js +7 -4
  16. package/dist/gguf.d.ts +7 -0
  17. package/dist/gguf.js +15 -2
  18. package/dist/models/download.d.ts +1 -1
  19. package/dist/models/download.js +2 -2
  20. package/dist/models/enrich.d.ts +6 -0
  21. package/dist/models/enrich.js +27 -1
  22. package/dist/models/index.d.ts +1 -1
  23. package/dist/models/index.js +4 -3
  24. package/dist/ops/calibrate.d.ts +38 -3
  25. package/dist/ops/calibrate.js +68 -19
  26. package/dist/ops/report.js +51 -1
  27. package/dist/ops/results.d.ts +57 -11
  28. package/dist/ops/results.js +75 -10
  29. package/dist/ops/sweep.d.ts +38 -1
  30. package/dist/ops/sweep.js +61 -10
  31. package/dist/runtime/args.d.ts +15 -2
  32. package/dist/runtime/args.js +60 -5
  33. package/dist/runtime/managed.js +2 -2
  34. package/dist/service/activity.d.ts +19 -0
  35. package/dist/service/activity.js +47 -4
  36. package/dist/service/host-api.d.ts +25 -4
  37. package/dist/service/host-api.js +82 -16
  38. package/dist/service/log-format.d.ts +18 -0
  39. package/dist/service/log-format.js +32 -0
  40. package/dist/service/router.d.ts +70 -2
  41. package/dist/service/router.js +219 -21
  42. package/dist/service/run-log.d.ts +6 -1
  43. package/dist/service/run-log.js +46 -4
  44. package/dist/service/scheduler.d.ts +227 -24
  45. package/dist/service/scheduler.js +395 -63
  46. package/dist/service/serve.d.ts +4 -0
  47. package/dist/service/serve.js +302 -117
  48. package/dist/service/status-events.d.ts +14 -1
  49. package/dist/service/status-events.js +111 -12
  50. package/dist/service/supervisor.d.ts +9 -7
  51. package/dist/service/supervisor.js +37 -12
  52. package/dist/sysmon.d.ts +15 -0
  53. package/dist/sysmon.js +56 -9
  54. package/dist/tui/app.d.ts +8 -2
  55. package/dist/tui/app.js +65 -17
  56. package/dist/types.d.ts +18 -0
  57. package/dist/vram.d.ts +37 -0
  58. package/dist/vram.js +57 -18
  59. package/package.json +1 -1
@@ -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
+ // A measurement from the previous run is the best prior on bytes/token for
39
+ // this exact profile shape. The sample cap uses it so the high sample never
40
+ // lands at a context that would spill the KV cache to CPU and bias the new
41
+ // slope low. Inherited family calibrations are excluded: they were measured
42
+ // on another file and are not a trustworthy budget input here.
43
+ const prior = getCalibration(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);
@@ -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}`,
@@ -6,6 +6,6 @@ export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
6
6
  export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
7
7
  export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, 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
@@ -6,6 +6,6 @@ export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
6
6
  export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
7
7
  export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, 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;