@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.
- package/dist/commands/bench.js +19 -5
- package/dist/commands/catalog.d.ts +1 -0
- package/dist/commands/catalog.js +1 -0
- package/dist/commands/pull.js +14 -33
- package/dist/commands/repo-download.d.ts +14 -0
- package/dist/commands/repo-download.js +29 -0
- package/dist/commands/search.js +6 -14
- package/dist/config/builtin-hosting-profiles.d.ts +8 -0
- package/dist/config/builtin-hosting-profiles.js +32 -0
- package/dist/config/hosting-profiles.d.ts +33 -0
- package/dist/config/hosting-profiles.js +71 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/config/paths.d.ts +2 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/profile-edit.d.ts +4 -2
- package/dist/config/profile-edit.js +94 -4
- package/dist/config/profiles.js +25 -2
- package/dist/config/schema.d.ts +494 -24
- package/dist/config/schema.js +55 -0
- package/dist/config/store.js +12 -2
- package/dist/gguf.d.ts +1 -0
- package/dist/gguf.js +1 -0
- package/dist/models/download.js +5 -0
- package/dist/models/enrich.d.ts +6 -0
- package/dist/models/enrich.js +50 -2
- package/dist/ops/calibrate.d.ts +4 -1
- package/dist/ops/calibrate.js +10 -7
- package/dist/ops/sweep.d.ts +3 -1
- package/dist/ops/sweep.js +3 -3
- package/dist/runtime/args.d.ts +2 -2
- package/dist/runtime/args.js +13 -1
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +10 -0
- package/dist/service/host-api.d.ts +17 -1
- package/dist/service/host-api.js +196 -13
- package/dist/service/router.d.ts +18 -0
- package/dist/service/router.js +89 -4
- package/dist/service/serve.js +184 -12
- package/dist/service/supervisor.d.ts +32 -4
- package/dist/service/supervisor.js +30 -6
- package/dist/tui/app.js +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/vram.js +3 -3
- package/package.json +1 -1
|
@@ -47,25 +47,38 @@ export const MAX_PARALLEL_SLOTS = 16;
|
|
|
47
47
|
export const MAX_GPU_LAYERS = 999;
|
|
48
48
|
export const MIN_CONTEXT_SIZE = 1024;
|
|
49
49
|
export const CONTEXT_STEP = 8192;
|
|
50
|
+
export const CONTEXT_MULTIPLIERS = [1, 2, 4];
|
|
50
51
|
/** The context ceiling: the model's native window, or a generous bound if unknown. */
|
|
51
52
|
export function nativeContextLimit(model) {
|
|
52
53
|
const native = model?.metadata?.contextLength;
|
|
53
54
|
return typeof native === "number" && native > 0 ? native : 1000000;
|
|
54
55
|
}
|
|
56
|
+
export function contextLimit(model, multiplier = 1) {
|
|
57
|
+
return nativeContextLimit(model) * multiplier;
|
|
58
|
+
}
|
|
55
59
|
/** The editable fields, resolved against one model's capabilities. */
|
|
56
|
-
export function profileFieldDescriptors(model) {
|
|
60
|
+
export function profileFieldDescriptors(model, profile) {
|
|
57
61
|
const projector = model?.components?.find((component) => component.role === "vision_projector");
|
|
58
62
|
const hasProjector = model?.components
|
|
59
63
|
? Boolean(projector?.available)
|
|
60
64
|
: Boolean(model?.mmprojPath);
|
|
61
65
|
const fields = [
|
|
66
|
+
{
|
|
67
|
+
key: "contextMultiplier",
|
|
68
|
+
label: "Context multiplier",
|
|
69
|
+
kind: "cycle",
|
|
70
|
+
options: CONTEXT_MULTIPLIERS,
|
|
71
|
+
optionLabels: ["Off", "2× (YaRN)", "4× (YaRN)"],
|
|
72
|
+
available: Boolean(model?.metadata?.contextLength),
|
|
73
|
+
...(model?.metadata?.contextLength ? {} : { unavailableReason: "native context unknown" }),
|
|
74
|
+
},
|
|
62
75
|
{
|
|
63
76
|
key: "contextSize",
|
|
64
77
|
label: "Context",
|
|
65
78
|
kind: "number",
|
|
66
79
|
step: CONTEXT_STEP,
|
|
67
80
|
min: MIN_CONTEXT_SIZE,
|
|
68
|
-
max:
|
|
81
|
+
max: contextLimit(model, profile?.contextMultiplier ?? 1),
|
|
69
82
|
available: true,
|
|
70
83
|
},
|
|
71
84
|
{
|
|
@@ -160,6 +173,17 @@ export function profileWarnings(profile, model, store) {
|
|
|
160
173
|
blocksStart: false,
|
|
161
174
|
});
|
|
162
175
|
}
|
|
176
|
+
if (profile.contextMultiplier > 1) {
|
|
177
|
+
warnings.push({
|
|
178
|
+
field: "contextMultiplier",
|
|
179
|
+
severity: "warn",
|
|
180
|
+
message: `YaRN ×${profile.contextMultiplier} extrapolates beyond the native context. Recalibrate before relying on this profile.`,
|
|
181
|
+
blocksStart: false,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Distinct from `calibrationRequired`, which says this profile has never been
|
|
185
|
+
// measured in its current shape. This says a measurement exists but was taken
|
|
186
|
+
// for other cache types, so it names the reason rather than just the verdict.
|
|
163
187
|
if (model && store && hasStaleCalibration(store, model, profile)) {
|
|
164
188
|
warnings.push({
|
|
165
189
|
field: "cacheTypeK",
|
|
@@ -176,6 +200,17 @@ export function profileWarnings(profile, model, store) {
|
|
|
176
200
|
* model's layer count, which the UI must never present as measured on this file.
|
|
177
201
|
*/
|
|
178
202
|
export function calibrationInfo(store, model, profile) {
|
|
203
|
+
// A historical measurement is invalid as soon as any VRAM-affecting setting
|
|
204
|
+
// changes. Keep the data for comparison, but do not present or use it as the
|
|
205
|
+
// current model budget until a calibration commits the new profile.
|
|
206
|
+
if (profile.calibrationRequired) {
|
|
207
|
+
return {
|
|
208
|
+
state: "theoretical",
|
|
209
|
+
kvBytesPerToken: null,
|
|
210
|
+
measuredAt: null,
|
|
211
|
+
measuredOn: null,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
179
214
|
const calibration = getCalibration(store, model, profile);
|
|
180
215
|
if (!calibration) {
|
|
181
216
|
return {
|
|
@@ -195,6 +230,25 @@ export function calibrationInfo(store, model, profile) {
|
|
|
195
230
|
function clamp(value, min, max) {
|
|
196
231
|
return Math.max(min, Math.min(max, value));
|
|
197
232
|
}
|
|
233
|
+
/** The settings whose value changes what a calibration would measure. */
|
|
234
|
+
const CALIBRATION_INPUTS = [
|
|
235
|
+
"contextMultiplier",
|
|
236
|
+
"contextSize",
|
|
237
|
+
"cacheTypeK",
|
|
238
|
+
"cacheTypeV",
|
|
239
|
+
"flashAttention",
|
|
240
|
+
"gpuLayers",
|
|
241
|
+
"parallelSlots",
|
|
242
|
+
"vision",
|
|
243
|
+
"enabledComponents",
|
|
244
|
+
];
|
|
245
|
+
/** Every calibration input is a scalar except the component id list, which is a set. */
|
|
246
|
+
function sameCalibrationInput(before, after) {
|
|
247
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
248
|
+
return [...before].sort().join("\0") === [...after].sort().join("\0");
|
|
249
|
+
}
|
|
250
|
+
return before === after;
|
|
251
|
+
}
|
|
198
252
|
/**
|
|
199
253
|
* Apply an editable patch to a profile, clamping every field to its range and
|
|
200
254
|
* dropping anything the model cannot use.
|
|
@@ -206,7 +260,7 @@ function clamp(value, min, max) {
|
|
|
206
260
|
* CLI-only: they have no measured effect worth exposing and `extraArgs` is
|
|
207
261
|
* arbitrary process arguments.
|
|
208
262
|
*/
|
|
209
|
-
export function sanitizeProfilePatch(current, patch, model) {
|
|
263
|
+
export function sanitizeProfilePatch(current, patch, model, runtimeBuild = null) {
|
|
210
264
|
const adjustments = [];
|
|
211
265
|
const next = { ...current };
|
|
212
266
|
if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
|
|
@@ -245,7 +299,14 @@ export function sanitizeProfilePatch(current, patch, model) {
|
|
|
245
299
|
}
|
|
246
300
|
next[key] = value;
|
|
247
301
|
};
|
|
248
|
-
|
|
302
|
+
if ("contextMultiplier" in p) {
|
|
303
|
+
const raw = p.contextMultiplier;
|
|
304
|
+
if (typeof raw !== "number" || !CONTEXT_MULTIPLIERS.includes(raw)) {
|
|
305
|
+
throw new Error("contextMultiplier must be one of 1, 2, or 4");
|
|
306
|
+
}
|
|
307
|
+
next.contextMultiplier = raw;
|
|
308
|
+
}
|
|
309
|
+
takeNumber("contextSize", MIN_CONTEXT_SIZE, contextLimit(model, next.contextMultiplier));
|
|
249
310
|
takeCacheType("cacheTypeK");
|
|
250
311
|
takeCacheType("cacheTypeV");
|
|
251
312
|
takeBoolean("flashAttention");
|
|
@@ -283,6 +344,14 @@ export function sanitizeProfilePatch(current, patch, model) {
|
|
|
283
344
|
throw new Error("enabledComponents must be an array of component ids");
|
|
284
345
|
}
|
|
285
346
|
const requested = [...new Set(p.enabledComponents)];
|
|
347
|
+
const requiredBuild = requested
|
|
348
|
+
.map((id) => model?.components?.find((component) => component.id === id))
|
|
349
|
+
.find((component) => component?.minRuntimeBuild !== undefined &&
|
|
350
|
+
(runtimeBuild === null || runtimeBuild < component.minRuntimeBuild))?.minRuntimeBuild;
|
|
351
|
+
if (requiredBuild !== undefined) {
|
|
352
|
+
const active = runtimeBuild === null ? "unknown" : `b${runtimeBuild}`;
|
|
353
|
+
throw new Error(`components require llama.cpp build b${requiredBuild} or newer (active build: ${active})`);
|
|
354
|
+
}
|
|
286
355
|
const available = new Set(model?.components
|
|
287
356
|
?.filter((component) => component.available)
|
|
288
357
|
.map((component) => component.id) ?? []);
|
|
@@ -294,6 +363,27 @@ export function sanitizeProfilePatch(current, patch, model) {
|
|
|
294
363
|
if (model?.components)
|
|
295
364
|
next.vision = Boolean(projector);
|
|
296
365
|
}
|
|
366
|
+
// A partial patch may lower the multiplier without naming contextSize. Clamp
|
|
367
|
+
// the saved value after every field has settled so launch args can never keep
|
|
368
|
+
// an extended context after YaRN has been turned off.
|
|
369
|
+
const contextSize = clamp(next.contextSize, MIN_CONTEXT_SIZE, contextLimit(model, next.contextMultiplier));
|
|
370
|
+
if (contextSize !== next.contextSize) {
|
|
371
|
+
next.contextSize = contextSize;
|
|
372
|
+
adjustments.push(`contextSize clamped to ${contextSize}`);
|
|
373
|
+
}
|
|
374
|
+
// The saved profile carries the calibration verdict. Do not infer it from
|
|
375
|
+
// whether an old measurement happens to exist: a person who changes settings,
|
|
376
|
+
// leaves, and returns must still be told to calibrate this new configuration.
|
|
377
|
+
//
|
|
378
|
+
// Compare values, never key presence. The editor autosaves the whole draft on
|
|
379
|
+
// every edit, so every one of these keys is in `p` every time; keying off
|
|
380
|
+
// presence threw away a real measurement whenever any unrelated field - or a
|
|
381
|
+
// hosting-profile choice, which does not touch VRAM at all - was saved.
|
|
382
|
+
const before = current;
|
|
383
|
+
const after = next;
|
|
384
|
+
if (CALIBRATION_INPUTS.some((key) => !sameCalibrationInput(before[key], after[key]))) {
|
|
385
|
+
next.calibrationRequired = true;
|
|
386
|
+
}
|
|
297
387
|
return { profile: next, adjustments };
|
|
298
388
|
}
|
|
299
389
|
//# sourceMappingURL=profile-edit.js.map
|
package/dist/config/profiles.js
CHANGED
|
@@ -17,6 +17,8 @@ export function defaultProfile(model, defaults) {
|
|
|
17
17
|
// Long context is the point of this hardware; start at the native limit and
|
|
18
18
|
// let the VRAM budget pull it down.
|
|
19
19
|
contextSize: Math.min(nativeContext, contextCap),
|
|
20
|
+
contextMultiplier: 1,
|
|
21
|
+
calibrationRequired: true,
|
|
20
22
|
cacheTypeK: defaults?.cacheTypeK ?? "q8_0",
|
|
21
23
|
cacheTypeV: defaults?.cacheTypeV ?? "q8_0",
|
|
22
24
|
flashAttention: defaults?.flashAttention ?? true, // required for a quantised V cache
|
|
@@ -34,6 +36,13 @@ export function defaultProfile(model, defaults) {
|
|
|
34
36
|
batchSize: null,
|
|
35
37
|
ubatchSize: null,
|
|
36
38
|
extraArgs: [],
|
|
39
|
+
hostingProfileId: null,
|
|
40
|
+
// Inherit, not off: a new model in a family that has a default should use
|
|
41
|
+
// it. With no family default configured this resolves to nothing anyway.
|
|
42
|
+
hostingProfileMode: "inherit",
|
|
43
|
+
chatTemplateFile: null,
|
|
44
|
+
chatTemplateKwargs: {},
|
|
45
|
+
chatSystemAddendum: null,
|
|
37
46
|
};
|
|
38
47
|
}
|
|
39
48
|
/** Stored profile for a model, falling back to computed defaults. */
|
|
@@ -58,6 +67,15 @@ export function forModel(store, model, defaults) {
|
|
|
58
67
|
return {
|
|
59
68
|
...base,
|
|
60
69
|
...stored,
|
|
70
|
+
// COMPAT(hostingProfileMode): added in v0.8.8, remove after 2027-02-12.
|
|
71
|
+
// The first hosting-profile store only had an id, and a non-null id there
|
|
72
|
+
// was an explicit custom choice. Such a profile parses as `inherit` (the
|
|
73
|
+
// schema default), so promote it. Writers hold the inverse invariant - the
|
|
74
|
+
// id is nulled whenever the mode is not `custom` - so a stored id can only
|
|
75
|
+
// mean a legacy record, never a stale leftover of a newer explicit choice.
|
|
76
|
+
hostingProfileMode: stored.hostingProfileId && stored.hostingProfileMode === "inherit"
|
|
77
|
+
? "custom"
|
|
78
|
+
: stored.hostingProfileMode,
|
|
61
79
|
enabledComponents,
|
|
62
80
|
modelPath: model.modelPath,
|
|
63
81
|
mmprojPath: mmproj?.path ?? (model.components ? null : model.mmprojPath),
|
|
@@ -72,12 +90,13 @@ export function put(store, model, profile) {
|
|
|
72
90
|
/** Calibration is keyed by cache types, since those change bytes/token. */
|
|
73
91
|
export function calibrationKey(profile) {
|
|
74
92
|
const components = [...(profile.enabledComponents ?? [])].sort();
|
|
93
|
+
const multiplier = profile.contextMultiplier > 1 ? `:contextMultiplier=${profile.contextMultiplier}` : "";
|
|
75
94
|
// COMPAT(bundleCalibrationKey): added in v0.8.7, remove after 2027-02-11.
|
|
76
95
|
// A main-model-only load remains the historical identity; any enabled bundle
|
|
77
96
|
// artifact gets a distinct key and therefore cannot claim that measurement.
|
|
78
97
|
return components.length
|
|
79
|
-
? `${profile.cacheTypeK}:${profile.cacheTypeV}:components=${components.join(",")}`
|
|
80
|
-
: `${profile.cacheTypeK}:${profile.cacheTypeV}`;
|
|
98
|
+
? `${profile.cacheTypeK}:${profile.cacheTypeV}${multiplier}:components=${components.join(",")}`
|
|
99
|
+
: `${profile.cacheTypeK}:${profile.cacheTypeV}${multiplier}`;
|
|
81
100
|
}
|
|
82
101
|
/**
|
|
83
102
|
* True when this model has a stored calibration, but for different cache types
|
|
@@ -167,6 +186,10 @@ export function putCalibration(store, model, profile, measurement) {
|
|
|
167
186
|
writable: true,
|
|
168
187
|
});
|
|
169
188
|
}
|
|
189
|
+
// Calibration is a durable verdict about the persisted model profile, not a
|
|
190
|
+
// transient UI hint. Every completion path (CLI, TUI, and host job) shares
|
|
191
|
+
// this helper, so clearing it here keeps the state consistent everywhere.
|
|
192
|
+
put(store, model, { ...profile, calibrationRequired: false });
|
|
170
193
|
return store;
|
|
171
194
|
}
|
|
172
195
|
//# sourceMappingURL=profiles.js.map
|