@otto-code/brain 0.8.8 → 0.8.10

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 (45) hide show
  1. package/dist/commands/bench.js +19 -5
  2. package/dist/commands/catalog.d.ts +1 -0
  3. package/dist/commands/catalog.js +1 -0
  4. package/dist/commands/pull.js +14 -33
  5. package/dist/commands/repo-download.d.ts +14 -0
  6. package/dist/commands/repo-download.js +29 -0
  7. package/dist/commands/search.js +6 -14
  8. package/dist/config/builtin-hosting-profiles.d.ts +8 -0
  9. package/dist/config/builtin-hosting-profiles.js +32 -0
  10. package/dist/config/hosting-profiles.d.ts +33 -0
  11. package/dist/config/hosting-profiles.js +71 -0
  12. package/dist/config/index.d.ts +1 -0
  13. package/dist/config/index.js +1 -0
  14. package/dist/config/paths.d.ts +2 -0
  15. package/dist/config/paths.js +1 -0
  16. package/dist/config/profile-edit.d.ts +4 -2
  17. package/dist/config/profile-edit.js +94 -4
  18. package/dist/config/profiles.js +25 -2
  19. package/dist/config/schema.d.ts +494 -24
  20. package/dist/config/schema.js +55 -0
  21. package/dist/config/store.js +12 -2
  22. package/dist/gguf.d.ts +1 -0
  23. package/dist/gguf.js +1 -0
  24. package/dist/models/download.js +5 -0
  25. package/dist/models/enrich.d.ts +6 -0
  26. package/dist/models/enrich.js +50 -2
  27. package/dist/ops/calibrate.d.ts +4 -1
  28. package/dist/ops/calibrate.js +10 -7
  29. package/dist/ops/sweep.d.ts +3 -1
  30. package/dist/ops/sweep.js +3 -3
  31. package/dist/runtime/args.d.ts +2 -2
  32. package/dist/runtime/args.js +13 -1
  33. package/dist/runtime/index.d.ts +7 -0
  34. package/dist/runtime/index.js +10 -0
  35. package/dist/service/host-api.d.ts +17 -1
  36. package/dist/service/host-api.js +196 -13
  37. package/dist/service/router.d.ts +18 -0
  38. package/dist/service/router.js +89 -4
  39. package/dist/service/serve.js +184 -12
  40. package/dist/service/supervisor.d.ts +32 -4
  41. package/dist/service/supervisor.js +30 -6
  42. package/dist/tui/app.js +1 -1
  43. package/dist/types.d.ts +4 -0
  44. package/dist/vram.js +3 -3
  45. package/package.json +1 -1
@@ -25,11 +25,54 @@ export const ProfileSchema = z
25
25
  reasoningBudget: z.number().default(1536),
26
26
  reasoningBudgetMessage: z.string().default(DEFAULT_REASONING_MESSAGE),
27
27
  parallelSlots: z.number().default(1),
28
+ /** RoPE extension factor; 1 keeps the GGUF-native context window. */
29
+ contextMultiplier: z.number().default(1),
30
+ /** Cleared only by a successful calibration of this saved model profile. */
31
+ calibrationRequired: z.boolean().default(true),
28
32
  batchSize: z.number().nullable().default(null),
29
33
  ubatchSize: z.number().nullable().default(null),
30
34
  extraArgs: z.array(z.string()).default([]),
35
+ /** The selected profile id when `hostingProfileMode` is `custom`. */
36
+ hostingProfileId: z.string().nullable().default(null),
37
+ /**
38
+ * Whether this model inherits its family's profile, disables profiles, or
39
+ * selects one itself. `inherit` is the default because it is what a profile
40
+ * stored before this field existed meant: the family default applied to
41
+ * every model in the family that had not overridden it. Defaulting to `off`
42
+ * would silently drop that default on every profile written by an older
43
+ * Brain. With no family default set, `inherit` resolves to nothing, which
44
+ * is exactly what `off` does, so the safe default costs nothing.
45
+ */
46
+ hostingProfileMode: z.enum(["inherit", "off", "custom"]).default("inherit"),
47
+ /** Derived at load time from the selected Brain-owned hosting profile. */
48
+ chatTemplateFile: z.string().nullable().default(null),
49
+ /** Derived at load time; passed directly to llama-server's Jinja engine. */
50
+ chatTemplateKwargs: z.record(z.unknown()).default({}),
51
+ /**
52
+ * Derived at load time from the selected hosting profile's system-prompt
53
+ * addendum. The router injects it per request rather than the launcher
54
+ * baking it into a CLI flag; see `hosting-profiles.ts` for why.
55
+ */
56
+ chatSystemAddendum: z.string().nullable().default(null),
31
57
  })
32
58
  .passthrough();
59
+ /**
60
+ * A named, Brain-owned inference composition. The template is intentionally
61
+ * text, rather than a user-owned path: remote Brains must be able to apply the
62
+ * same profile and no client is allowed to make llama-server read arbitrary
63
+ * files from its host.
64
+ */
65
+ export const HostingProfileSchema = z
66
+ .object({
67
+ id: z.string(),
68
+ name: z.string(),
69
+ family: z.string(),
70
+ description: z.string().default(""),
71
+ template: z.string().nullable().default(null),
72
+ systemPromptAddendum: z.string().nullable().default(null),
73
+ templateKwargs: z.record(z.unknown()).default({}),
74
+ })
75
+ .strict();
33
76
  export const CalibrationSampleSchema = z
34
77
  .object({
35
78
  contextSize: z.number(),
@@ -67,6 +110,16 @@ export const ProfilesStoreSchema = z
67
110
  profiles: z.record(ProfileSchema).default({}),
68
111
  calibrations: z.record(z.record(CalibrationSchema)).default({}),
69
112
  geometryCalibrations: z.record(GeometryCalibrationSchema).default({}),
113
+ /** Named reusable profiles, scoped to this Brain rather than any client. */
114
+ hostingProfiles: z.record(HostingProfileSchema).default({}),
115
+ /** Optional family default; individual model profiles may override it. */
116
+ familyHostingProfileIds: z.record(z.string().nullable()).default({}),
117
+ /**
118
+ * A saved edit made while that model was resident. It stays visible when
119
+ * the user leaves and revisits the model settings, and clears only after a
120
+ * successful fresh llama-server load of that model.
121
+ */
122
+ pendingReloadModelIds: z.record(z.boolean()).default({}),
70
123
  lastModelId: z.string().nullable().default(null),
71
124
  })
72
125
  .passthrough();
@@ -169,6 +222,8 @@ export const CatalogModelSchema = z
169
222
  /** Retired Otto-curated ids this canonical catalog entry replaces. */
170
223
  replaces: z.array(z.string()).optional(),
171
224
  name: z.string(),
225
+ /** Stable UI family identity. Otto clients resolve this to a monochrome glyph. */
226
+ family: z.string().optional(),
172
227
  publisher: z.string().optional(),
173
228
  hfRepo: z.string(),
174
229
  quant: z.string(),
@@ -10,6 +10,7 @@ import { writePrivateFileAtomicSync } from "./private-files.js";
10
10
  import { packageRoot, resolveBrainPaths } from "./paths.js";
11
11
  import { BrainConfigSchema, CatalogSchema, ProfilesStoreSchema, } from "./schema.js";
12
12
  import { applyEnvOverrides } from "./env.js";
13
+ import { seedBuiltinHostingProfiles } from "./builtin-hosting-profiles.js";
13
14
  function readJson(file, schema) {
14
15
  if (!existsSync(file))
15
16
  return null;
@@ -26,15 +27,24 @@ function writeJson(file, data) {
26
27
  // ------------------------------------------------------------------- profiles
27
28
  export function loadProfilesStore(paths = resolveBrainPaths()) {
28
29
  const current = readJson(paths.profilesFile, ProfilesStoreSchema);
29
- if (current)
30
+ if (current) {
31
+ if (seedBuiltinHostingProfiles(current))
32
+ writeJson(paths.profilesFile, current);
30
33
  return current;
34
+ }
31
35
  const legacy = path.join(packageRoot(), "config", "profiles.json");
32
36
  const migrated = readJson(legacy, ProfilesStoreSchema);
33
37
  if (migrated) {
38
+ seedBuiltinHostingProfiles(migrated);
34
39
  writeJson(paths.profilesFile, migrated);
35
40
  return migrated;
36
41
  }
37
- return ProfilesStoreSchema.parse({});
42
+ const seeded = ProfilesStoreSchema.parse({});
43
+ seedBuiltinHostingProfiles(seeded);
44
+ // A read-only command must not create profiles.json. In-memory seeding keeps
45
+ // built-ins available; the first real profile save persists the whole store.
46
+ // This avoids racing a running service that later writes its startup snapshot.
47
+ return seeded;
38
48
  }
39
49
  export function saveProfilesStore(store, paths = resolveBrainPaths()) {
40
50
  writeJson(paths.profilesFile, store);
package/dist/gguf.d.ts CHANGED
@@ -35,6 +35,7 @@ interface GgufSummary {
35
35
  tensorCount: number;
36
36
  arch: string;
37
37
  name: string | null;
38
+ basename: string | null;
38
39
  sizeLabel: string | null;
39
40
  fileType: number | null;
40
41
  contextLength: number | null;
package/dist/gguf.js CHANGED
@@ -210,6 +210,7 @@ export function summarize(file) {
210
210
  tensorCount,
211
211
  arch,
212
212
  name: meta["general.name"] || null,
213
+ basename: meta["general.basename"] || null,
213
214
  sizeLabel: meta["general.size_label"] || null,
214
215
  fileType: (meta["general.file_type"] ?? null),
215
216
  contextLength: (get("context_length") ?? null),
@@ -98,6 +98,11 @@ async function streamRepoFile(url, destPath, label, token, onProgress, received)
98
98
  mkdirSync(path.dirname(destPath), { recursive: true });
99
99
  if (existsSync(destPath)) {
100
100
  clearPartial(tmp);
101
+ // A bundle plan counts every selected artifact. An artifact that is
102
+ // already present is complete work, not zero work, otherwise the progress
103
+ // denominator includes it while the numerator never can.
104
+ received.bytes += statSync(destPath).size;
105
+ onProgress?.({ file: label, receivedBytes: received.bytes });
101
106
  return false;
102
107
  }
103
108
  let partial = resumablePartial(tmp);
@@ -1,5 +1,11 @@
1
1
  import type { Catalog, CatalogModel } from "../config/schema.js";
2
2
  import type { Model } from "../types.js";
3
+ /**
4
+ * Normalize a GGUF identity into the catalog's hosting-profile vocabulary.
5
+ * Architecture is a stable structural field; names are only fallbacks for
6
+ * headers whose architecture is absent or unrecognised.
7
+ */
8
+ export declare function familyFromGgufMetadata(model: Model): string | undefined;
3
9
  /**
4
10
  * Find the catalog entry a scanned model belongs to, or null. A model matches
5
11
  * when its id path sits directly under the entry's hfRepo directory. When a repo
@@ -19,6 +19,50 @@
19
19
  */
20
20
  import fs from "node:fs";
21
21
  import path from "node:path";
22
+ /**
23
+ * Hosting-profile families deliberately use a small, curated vocabulary. It is
24
+ * a mix of publisher and model-line identities, so GGUF architecture names
25
+ * must be folded into the existing buckets rather than exposed directly.
26
+ */
27
+ const FAMILY_BY_GGUF_IDENTIFIER = {
28
+ chatglm: "chatglm",
29
+ deepseek: "deepseek",
30
+ gemma: "gemma",
31
+ gptoss: "openai",
32
+ llama: "meta",
33
+ meta: "meta",
34
+ microsoft: "microsoft",
35
+ mistral: "mistral",
36
+ mixtral: "mistral",
37
+ nemotron: "nvidia",
38
+ nvidia: "nvidia",
39
+ openai: "openai",
40
+ phi: "microsoft",
41
+ qwen: "qwen",
42
+ };
43
+ /**
44
+ * Normalize a GGUF identity into the catalog's hosting-profile vocabulary.
45
+ * Architecture is a stable structural field; names are only fallbacks for
46
+ * headers whose architecture is absent or unrecognised.
47
+ */
48
+ export function familyFromGgufMetadata(model) {
49
+ const metadata = model.metadata;
50
+ if (!metadata)
51
+ return undefined;
52
+ for (const value of [metadata.arch, metadata.basename, metadata.name]) {
53
+ if (typeof value !== "string")
54
+ continue;
55
+ const identifier = value
56
+ .trim()
57
+ .toLowerCase()
58
+ .replace(/[^a-z0-9]/gu, "");
59
+ for (const [prefix, family] of Object.entries(FAMILY_BY_GGUF_IDENTIFIER)) {
60
+ if (identifier.startsWith(prefix))
61
+ return family;
62
+ }
63
+ }
64
+ return undefined;
65
+ }
22
66
  /** Normalize a repo/id path: forward slashes, lowercased, trailing slashes trimmed. */
23
67
  function normalizePath(value) {
24
68
  const normalized = value.replaceAll("\\", "/");
@@ -76,14 +120,18 @@ export function matchCatalogEntry(model, catalog) {
76
120
  export function enrichWithCatalog(models, catalog) {
77
121
  return models.map((model) => {
78
122
  const entry = matchCatalogEntry(model, catalog);
79
- if (!entry)
80
- return enrichDiscoveredProjector(model);
123
+ if (!entry) {
124
+ const enriched = enrichDiscoveredProjector(model);
125
+ const family = familyFromGgufMetadata(enriched);
126
+ return family ? { ...enriched, family } : enriched;
127
+ }
81
128
  const components = resolveComponents(model, entry);
82
129
  const projector = components?.find((component) => component.role === "vision_projector");
83
130
  return {
84
131
  ...model,
85
132
  catalogId: entry.id,
86
133
  catalogHfRepo: entry.hfRepo,
134
+ family: entry.family,
87
135
  components,
88
136
  // A manifest is authoritative. Do not pair a random same-directory
89
137
  // projector when the catalog declares the exact companion artifact.
@@ -1,3 +1,4 @@
1
+ import { Supervisor } from "../service/supervisor.js";
1
2
  import type { Model, Runtime } from "../types.js";
2
3
  import type { Profile } from "../config/schema.js";
3
4
  /**
@@ -31,6 +32,8 @@ export interface CalibrateOptions {
31
32
  profile: Profile;
32
33
  samples?: number[];
33
34
  internalPort?: number;
35
+ /** Reuse the host's resident supervisor instead of creating a sidecar server. */
36
+ supervisor?: Supervisor;
34
37
  onProgress?: (event: CalibrateProgress) => void;
35
38
  }
36
39
  /** The measured KV-cache profile calibration produces. */
@@ -45,5 +48,5 @@ export interface CalibrationMeasurement {
45
48
  vision: boolean;
46
49
  measuredAt: string;
47
50
  }
48
- export declare function calibrate({ runtime, model, profile, samples, internalPort, onProgress, }: CalibrateOptions): Promise<CalibrationMeasurement>;
51
+ export declare function calibrate({ runtime, model, profile, samples, internalPort, supervisor: optionsSupervisor, onProgress, }: CalibrateOptions): Promise<CalibrationMeasurement>;
49
52
  //# sourceMappingURL=calibrate.d.ts.map
@@ -12,21 +12,24 @@ import { DEFAULT_INTERNAL_PORT, Supervisor } from "../service/supervisor.js";
12
12
  * buffers) cancels out of the difference.
13
13
  */
14
14
  export const DEFAULT_SAMPLES = [8192, 65536];
15
- export async function calibrate({ runtime, model, profile, samples = DEFAULT_SAMPLES, internalPort = DEFAULT_INTERNAL_PORT + 1, onProgress = () => { }, }) {
16
- if (samples.length < 2)
15
+ export async function calibrate({ runtime, model, profile, samples, internalPort = DEFAULT_INTERNAL_PORT + 1, supervisor: optionsSupervisor, onProgress = () => { }, }) {
16
+ const nativeContext = model.metadata?.contextLength ?? null;
17
+ const effectiveSamples = samples ??
18
+ (nativeContext ? [8192, nativeContext * profile.contextMultiplier] : DEFAULT_SAMPLES);
19
+ if (effectiveSamples.length < 2)
17
20
  throw new Error("calibration needs at least two context sizes");
18
- const native = model.metadata?.contextLength || Math.max(...samples);
21
+ const native = (nativeContext || Math.max(...effectiveSamples)) * profile.contextMultiplier;
19
22
  const points = [];
20
- for (const contextSize of samples) {
23
+ for (const contextSize of effectiveSamples) {
21
24
  if (contextSize > native) {
22
- onProgress({ phase: "skip", contextSize, reason: `exceeds native context ${native}` });
25
+ onProgress({ phase: "skip", contextSize, reason: `exceeds configured context ${native}` });
23
26
  continue;
24
27
  }
25
- const supervisor = new Supervisor({ runtime, internalPort });
28
+ const supervisor = optionsSupervisor ?? new Supervisor({ runtime, internalPort });
26
29
  onProgress({ phase: "loading", contextSize });
27
30
  const baseline = await usedBytes();
28
31
  try {
29
- await supervisor.start(model, { ...profile, contextSize });
32
+ await supervisor.start(model, { ...profile, contextSize }, { preserveLogs: Boolean(optionsSupervisor) });
30
33
  const used = supervisor.vramAtReadyBytes ?? (await usedBytes());
31
34
  const delta = Number(used) - Number(supervisor.vramBaselineBytes ?? baseline);
32
35
  points.push({ contextSize, deltaBytes: delta, loadSeconds: supervisor.loadSeconds });
@@ -70,8 +70,10 @@ export interface SweepOptions {
70
70
  maxTokens?: number;
71
71
  temperature?: number;
72
72
  internalPort?: number;
73
+ /** Reuse the host's resident supervisor instead of creating a sidecar server. */
74
+ supervisor?: Supervisor;
73
75
  onProgress?: (event: SweepProgress) => void;
74
76
  }
75
- export declare function sweep({ runtime, model, profile, budgets, maxTokens, temperature, internalPort, onProgress, }: SweepOptions): Promise<SweepReport>;
77
+ export declare function sweep({ runtime, model, profile, budgets, maxTokens, temperature, internalPort, supervisor: optionsSupervisor, onProgress, }: SweepOptions): Promise<SweepReport>;
76
78
  export {};
77
79
  //# sourceMappingURL=sweep.d.ts.map
package/dist/ops/sweep.js CHANGED
@@ -81,13 +81,13 @@ export async function runTrial({ supervisor, maxTokens, temperature, }) {
81
81
  contentPerSecond: elapsedSeconds > 0 ? content.length / elapsedSeconds : 0,
82
82
  };
83
83
  }
84
- export async function sweep({ runtime, model, profile, budgets = DEFAULT_BUDGETS, maxTokens = 8192, temperature = 0.7, internalPort = DEFAULT_INTERNAL_PORT + 2, onProgress = () => { }, }) {
84
+ export async function sweep({ runtime, model, profile, budgets = DEFAULT_BUDGETS, maxTokens = 8192, temperature = 0.7, internalPort = DEFAULT_INTERNAL_PORT + 2, supervisor: optionsSupervisor, onProgress = () => { }, }) {
85
85
  const results = [];
86
86
  for (const budget of budgets) {
87
- const supervisor = new Supervisor({ runtime, internalPort });
87
+ const supervisor = optionsSupervisor ?? new Supervisor({ runtime, internalPort });
88
88
  onProgress({ phase: "loading", budget });
89
89
  try {
90
- await supervisor.start(model, { ...profile, reasoningBudget: budget });
90
+ await supervisor.start(model, { ...profile, reasoningBudget: budget }, { preserveLogs: Boolean(optionsSupervisor) });
91
91
  onProgress({ phase: "generating", budget });
92
92
  const trial = await runTrial({ supervisor, maxTokens, temperature });
93
93
  results.push({ budget, ...trial, error: null });
@@ -4,7 +4,7 @@
4
4
  * managed one, since both resolve to a `Runtime` (exe + optional vendorDir).
5
5
  */
6
6
  import type { Profile } from "../config/schema.js";
7
- import type { Runtime } from "../types.js";
7
+ import type { Model, Runtime } from "../types.js";
8
8
  export interface ServeTarget {
9
9
  port: number;
10
10
  host?: string;
@@ -28,7 +28,7 @@ export declare function buildEnv(runtime: Runtime, baseEnv?: NodeJS.ProcessEnv,
28
28
  * Only settings that demonstrably matter for stable local inference are emitted -
29
29
  * no experimental sampler knobs.
30
30
  */
31
- export declare function buildArgs(profile: Profile, { port, host }: ServeTarget): string[];
31
+ export declare function buildArgs(profile: Profile, { port, host }: ServeTarget, model?: Model): string[];
32
32
  /** The same command as a copy-pasteable shell line, for the TUI to display. */
33
33
  export declare function formatCommand(runtime: Runtime, args: string[]): string;
34
34
  //# sourceMappingURL=args.d.ts.map
@@ -29,7 +29,7 @@ export function buildEnv(runtime, baseEnv = process.env, platform = process.plat
29
29
  * Only settings that demonstrably matter for stable local inference are emitted -
30
30
  * no experimental sampler knobs.
31
31
  */
32
- export function buildArgs(profile, { port, host = "127.0.0.1" }) {
32
+ export function buildArgs(profile, { port, host = "127.0.0.1" }, model) {
33
33
  const args = [
34
34
  "-m",
35
35
  profile.modelPath ?? "",
@@ -66,10 +66,22 @@ export function buildArgs(profile, { port, host = "127.0.0.1" }) {
66
66
  }
67
67
  if (profile.parallelSlots)
68
68
  args.push("--parallel", String(profile.parallelSlots));
69
+ if (profile.contextMultiplier > 1) {
70
+ const nativeContext = model?.metadata?.contextLength;
71
+ args.push("--rope-scaling", "yarn", "--rope-scale", String(profile.contextMultiplier), ...(nativeContext ? ["--yarn-orig-ctx", String(nativeContext)] : []), ...(model?.metadata?.arch
72
+ ? ["--override-kv", `${model.metadata.arch}.context_length=int:${profile.contextSize}`]
73
+ : []));
74
+ }
69
75
  if (profile.batchSize)
70
76
  args.push("-b", String(profile.batchSize));
71
77
  if (profile.ubatchSize)
72
78
  args.push("-ub", String(profile.ubatchSize));
79
+ if (profile.chatTemplateFile) {
80
+ args.push("--chat-template-file", profile.chatTemplateFile);
81
+ if (Object.keys(profile.chatTemplateKwargs ?? {}).length > 0) {
82
+ args.push("--chat-template-kwargs", JSON.stringify(profile.chatTemplateKwargs));
83
+ }
84
+ }
73
85
  if (profile.extraArgs && profile.extraArgs.length)
74
86
  args.push(...profile.extraArgs);
75
87
  return args;
@@ -14,6 +14,13 @@ export declare function listAllRuntimes(env?: NodeJS.ProcessEnv): Runtime[];
14
14
  export declare function probeNvidiaGpu(): Promise<boolean>;
15
15
  /** The runtime to use given config, or null when none is available. */
16
16
  export declare function resolveRuntime(config: BrainConfig, env?: NodeJS.ProcessEnv): Runtime | null;
17
+ /**
18
+ * The numeric llama.cpp build carried by a resolved runtime, when its source
19
+ * identifies one. LM Studio and explicit overrides need not expose their
20
+ * upstream build, so callers must treat null as incompatible with a component
21
+ * that declares a minimum build rather than guessing compatibility.
22
+ */
23
+ export declare function runtimeBuild(runtime: Runtime | null | undefined): number | null;
17
24
  /** Ensure a runtime exists, downloading the default managed build if none does. */
18
25
  export declare function ensureRuntime(config: BrainConfig, env?: NodeJS.ProcessEnv, onProgress?: (progress: InstallProgress) => void, target?: RuntimeTarget): Promise<Runtime>;
19
26
  //# sourceMappingURL=index.d.ts.map
@@ -44,6 +44,16 @@ export function resolveRuntime(config, env = process.env) {
44
44
  return lmstudio[0] ?? null;
45
45
  return managed[0] ?? lmstudio[0] ?? null; // auto
46
46
  }
47
+ /**
48
+ * The numeric llama.cpp build carried by a resolved runtime, when its source
49
+ * identifies one. LM Studio and explicit overrides need not expose their
50
+ * upstream build, so callers must treat null as incompatible with a component
51
+ * that declares a minimum build rather than guessing compatibility.
52
+ */
53
+ export function runtimeBuild(runtime) {
54
+ const match = /^b(\d+)$/iu.exec(runtime?.version ?? "");
55
+ return match ? Number(match[1]) : null;
56
+ }
47
57
  /** Ensure a runtime exists, downloading the default managed build if none does. */
48
58
  export async function ensureRuntime(config, env = process.env, onProgress, target = {}) {
49
59
  const existing = resolveRuntime(config, env);
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import type http from "node:http";
22
22
  import { calibrationInfo, profileWarnings } from "../config/profile-edit.js";
23
- import type { Profile, ProfileDefaults, ProfilesStore } from "../config/schema.js";
23
+ import { type Profile, type ProfileDefaults, type ProfilesStore } from "../config/schema.js";
24
24
  import { planDelete } from "../models/manage.js";
25
25
  import type { RankedModel } from "../ops/results.js";
26
26
  import type { GpuInfo, Model } from "../types.js";
@@ -130,6 +130,8 @@ export interface HostApiDeps {
130
130
  export interface InventoryRow {
131
131
  id: string;
132
132
  displayName: string;
133
+ /** Curated model-family identity for the Otto Brain client glyph. */
134
+ family: string | null;
133
135
  publisher: string | null;
134
136
  quant: string | null;
135
137
  sizeBytes: number;
@@ -155,6 +157,19 @@ export interface InventoryRow {
155
157
  warnings: ReturnType<typeof profileWarnings>;
156
158
  components: NonNullable<Model["components"]> | null;
157
159
  }
160
+ /**
161
+ * Apply the hosting-profile half of a profile patch, mutating both `profile`
162
+ * (this model's selection) and `store` (the shared library and family default).
163
+ *
164
+ * Separate from `sanitizeProfilePatch` because these keys are not profile
165
+ * fields: three of the five write to the store rather than the profile.
166
+ * Exported for testing - the ordering and the cross-profile cleanup are the
167
+ * parts worth pinning down, and they are unreachable through the HTTP surface
168
+ * without standing up a service.
169
+ *
170
+ * Throws on any invalid input; the caller turns that into a 400.
171
+ */
172
+ export declare function applyHostingProfilePatch(store: ProfilesStore, model: Model, profile: Profile, patch: Record<string, unknown>, onDelete?: ((id: string) => void) | undefined): void;
158
173
  /**
159
174
  * Join one model's scan row, GGUF metadata, saved profile, calibration, VRAM
160
175
  * budget and benchmark score into the single shape the Models tab renders.
@@ -169,6 +184,7 @@ export declare function buildInventoryRow(params: {
169
184
  gpu: GpuInfo | null;
170
185
  ranking: RankedModel[];
171
186
  supervisor: Supervisor;
187
+ runtimeBuild?: number | null;
172
188
  }): InventoryRow;
173
189
  export interface HostApi {
174
190
  /** Returns true when it answered the request, false to fall through. */