@otto-code/brain 0.8.10 → 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.
- package/dist/commands/calibrate.js +9 -0
- package/dist/commands/catalog.d.ts +1 -0
- package/dist/commands/catalog.js +1 -0
- package/dist/commands/pull.d.ts +1 -0
- package/dist/commands/pull.js +12 -3
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.js +12 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/profile-edit.d.ts +88 -1
- package/dist/config/profile-edit.js +280 -29
- package/dist/config/profiles.js +16 -0
- package/dist/config/schema.d.ts +608 -0
- package/dist/config/schema.js +58 -0
- package/dist/config/store.js +7 -4
- package/dist/gguf.d.ts +7 -0
- package/dist/gguf.js +15 -2
- package/dist/models/download.d.ts +1 -1
- package/dist/models/download.js +2 -2
- package/dist/models/enrich.d.ts +6 -0
- package/dist/models/enrich.js +27 -1
- package/dist/models/index.d.ts +1 -1
- package/dist/models/index.js +4 -3
- package/dist/ops/calibrate.d.ts +38 -3
- package/dist/ops/calibrate.js +68 -19
- package/dist/ops/report.js +51 -1
- package/dist/ops/results.d.ts +57 -11
- package/dist/ops/results.js +75 -10
- package/dist/ops/sweep.d.ts +38 -1
- package/dist/ops/sweep.js +61 -10
- package/dist/runtime/args.d.ts +15 -2
- package/dist/runtime/args.js +60 -5
- package/dist/runtime/managed.js +2 -2
- package/dist/service/activity.d.ts +19 -0
- package/dist/service/activity.js +47 -4
- package/dist/service/host-api.d.ts +25 -4
- package/dist/service/host-api.js +82 -16
- package/dist/service/log-format.d.ts +18 -0
- package/dist/service/log-format.js +32 -0
- package/dist/service/router.d.ts +70 -2
- package/dist/service/router.js +219 -21
- package/dist/service/run-log.d.ts +6 -1
- package/dist/service/run-log.js +46 -4
- package/dist/service/scheduler.d.ts +227 -24
- package/dist/service/scheduler.js +395 -63
- package/dist/service/serve.d.ts +4 -0
- package/dist/service/serve.js +302 -117
- package/dist/service/status-events.d.ts +14 -1
- package/dist/service/status-events.js +111 -12
- package/dist/service/supervisor.d.ts +9 -7
- package/dist/service/supervisor.js +37 -12
- package/dist/sysmon.d.ts +15 -0
- package/dist/sysmon.js +56 -9
- package/dist/tui/app.d.ts +8 -2
- package/dist/tui/app.js +65 -17
- package/dist/types.d.ts +18 -0
- package/dist/vram.d.ts +37 -0
- package/dist/vram.js +57 -18
- package/package.json +1 -1
|
@@ -17,8 +17,33 @@
|
|
|
17
17
|
* - Changing a cache type invalidates a measured calibration, because KV
|
|
18
18
|
* bytes/token is a function of the cache types (see `vram.ts`).
|
|
19
19
|
*/
|
|
20
|
-
import
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import { CACHE_TYPE_BYTES, formatGiB, promptCacheSize } from "../vram.js";
|
|
21
22
|
import { getCalibration, hasStaleCalibration } from "./profiles.js";
|
|
23
|
+
/**
|
|
24
|
+
* Sampler ranges. llama.cpp itself bounds almost none of these - it will take a
|
|
25
|
+
* temperature of 50 - so the bounds are the useful range rather than the legal
|
|
26
|
+
* one, which is what a stepper wants. The write path clamps to exactly these,
|
|
27
|
+
* so a value the editor cannot reach is a value the brain will not store.
|
|
28
|
+
*/
|
|
29
|
+
export const SAMPLING_RANGES = {
|
|
30
|
+
temperature: { min: 0, max: 2, step: 0.05, precision: 2 },
|
|
31
|
+
topP: { min: 0, max: 1, step: 0.05, precision: 2 },
|
|
32
|
+
topK: { min: 0, max: 200, step: 1 },
|
|
33
|
+
minP: { min: 0, max: 1, step: 0.01, precision: 2 },
|
|
34
|
+
presencePenalty: { min: -2, max: 2, step: 0.05, precision: 2 },
|
|
35
|
+
repeatPenalty: { min: 0.5, max: 2, step: 0.01, precision: 2 },
|
|
36
|
+
};
|
|
37
|
+
/** Tri-state preservation, in the order a cycle should offer it. */
|
|
38
|
+
export const PRESERVE_REASONING_CYCLE = ["default", "on", "off"];
|
|
39
|
+
/** The stored value a cycle option maps to. */
|
|
40
|
+
export function preserveReasoningFromOption(option) {
|
|
41
|
+
return option === "on" ? true : option === "off" ? false : null;
|
|
42
|
+
}
|
|
43
|
+
/** The cycle option a stored value maps to. */
|
|
44
|
+
export function preserveReasoningOption(value) {
|
|
45
|
+
return value === true ? "on" : value === false ? "off" : "default";
|
|
46
|
+
}
|
|
22
47
|
/**
|
|
23
48
|
* The cache types the editor cycles through. `CACHE_TYPE_BYTES` knows more
|
|
24
49
|
* (f32, bf16, q5_0, q4_1) and a write naming one of those is accepted; these
|
|
@@ -44,6 +69,19 @@ export function formatReasoningBudget(budget) {
|
|
|
44
69
|
return budget === UNRESTRICTED_REASONING_BUDGET ? "unrestricted" : String(budget);
|
|
45
70
|
}
|
|
46
71
|
export const MAX_PARALLEL_SLOTS = 16;
|
|
72
|
+
/**
|
|
73
|
+
* Ceiling on `cachedChats`. Generous on purpose: entries are small for a small
|
|
74
|
+
* model and huge for a large one, so the honest guard is the RAM figure the
|
|
75
|
+
* warning shows, not an arbitrary count.
|
|
76
|
+
*/
|
|
77
|
+
export const MAX_CACHED_CHATS = 64;
|
|
78
|
+
/**
|
|
79
|
+
* What llama.cpp allows by default when no `--cache-ram` is emitted, in bytes.
|
|
80
|
+
* `cachedChats` of 0 emits no flag, so the estimate for the Default option is
|
|
81
|
+
* this fixed figure against the installed RAM - not the model's measured KV
|
|
82
|
+
* cost, which is irrelevant here because the size does not depend on the model.
|
|
83
|
+
*/
|
|
84
|
+
export const ENGINE_DEFAULT_CACHE_RAM_BYTES = 8192 * 1024 * 1024;
|
|
47
85
|
export const MAX_GPU_LAYERS = 999;
|
|
48
86
|
export const MIN_CONTEXT_SIZE = 1024;
|
|
49
87
|
export const CONTEXT_STEP = 8192;
|
|
@@ -62,13 +100,25 @@ export function profileFieldDescriptors(model, profile) {
|
|
|
62
100
|
const hasProjector = model?.components
|
|
63
101
|
? Boolean(projector?.available)
|
|
64
102
|
: Boolean(model?.mmprojPath);
|
|
103
|
+
// The template exposes a thinking channel at all - the gate for both reasoning
|
|
104
|
+
// controls. Preservation used to be gated on `reasoningPreservation
|
|
105
|
+
// .templateArgument` instead, which is detected by grepping the chat template
|
|
106
|
+
// for a literal `preserve_thinking`/`preserve_reasoning` kwarg. Almost no
|
|
107
|
+
// template spells it that way: llama.cpp decides the same question by probing
|
|
108
|
+
// the rendered template, so it happily logs "chat template supports preserving
|
|
109
|
+
// reasoning, consider enabling it via --reasoning-preserve" for a model whose
|
|
110
|
+
// toggle Otto was hiding. The flag exists on every build we ship and defaults
|
|
111
|
+
// to the template's own behavior when absent, so offering it wherever there is
|
|
112
|
+
// reasoning to preserve costs nothing and stops hiding a working setting.
|
|
113
|
+
const hasReasoning = Boolean(model?.metadata?.reasoning || model?.reasoningPreservation);
|
|
65
114
|
const fields = [
|
|
66
115
|
{
|
|
67
116
|
key: "contextMultiplier",
|
|
68
117
|
label: "Context multiplier",
|
|
69
118
|
kind: "cycle",
|
|
119
|
+
description: "Stretches the context window past the model's native size with RoPE scaling.",
|
|
70
120
|
options: CONTEXT_MULTIPLIERS,
|
|
71
|
-
optionLabels: ["Off", "2×
|
|
121
|
+
optionLabels: ["Off", "2×", "4×"],
|
|
72
122
|
available: Boolean(model?.metadata?.contextLength),
|
|
73
123
|
...(model?.metadata?.contextLength ? {} : { unavailableReason: "native context unknown" }),
|
|
74
124
|
},
|
|
@@ -76,6 +126,7 @@ export function profileFieldDescriptors(model, profile) {
|
|
|
76
126
|
key: "contextSize",
|
|
77
127
|
label: "Context",
|
|
78
128
|
kind: "number",
|
|
129
|
+
description: "How many tokens of conversation the model can hold at once.",
|
|
79
130
|
step: CONTEXT_STEP,
|
|
80
131
|
min: MIN_CONTEXT_SIZE,
|
|
81
132
|
max: contextLimit(model, profile?.contextMultiplier ?? 1),
|
|
@@ -85,6 +136,7 @@ export function profileFieldDescriptors(model, profile) {
|
|
|
85
136
|
key: "cacheTypeK",
|
|
86
137
|
label: "KV cache K",
|
|
87
138
|
kind: "cycle",
|
|
139
|
+
description: "Precision of the cached keys; lower saves VRAM and costs a little accuracy.",
|
|
88
140
|
options: CACHE_TYPE_CYCLE,
|
|
89
141
|
available: true,
|
|
90
142
|
},
|
|
@@ -92,54 +144,147 @@ export function profileFieldDescriptors(model, profile) {
|
|
|
92
144
|
key: "cacheTypeV",
|
|
93
145
|
label: "KV cache V",
|
|
94
146
|
kind: "cycle",
|
|
147
|
+
description: "Precision of the cached values; lower saves VRAM and costs a little accuracy.",
|
|
95
148
|
options: CACHE_TYPE_CYCLE,
|
|
96
149
|
available: true,
|
|
97
150
|
},
|
|
98
|
-
|
|
151
|
+
// The five KV/context settings that size and split the cache sit together,
|
|
152
|
+
// in the order the math reads them: the window (multiplier, size), its
|
|
153
|
+
// quantisation (K, V), how it is split (slots), and the RAM fallback
|
|
154
|
+
// (cached chats). Flash attention is related but leaves the KV byte total
|
|
155
|
+
// untouched, so it stays with the remaining options below.
|
|
156
|
+
{
|
|
157
|
+
key: "parallelSlots",
|
|
158
|
+
label: "Parallel slots",
|
|
159
|
+
kind: "number",
|
|
160
|
+
description: "How many chats the model serves at once, each taking a share of the context.",
|
|
161
|
+
step: 1,
|
|
162
|
+
min: 1,
|
|
163
|
+
max: MAX_PARALLEL_SLOTS,
|
|
164
|
+
available: true,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
key: "cachedChats",
|
|
168
|
+
label: "Cached KVs",
|
|
169
|
+
kind: "number",
|
|
170
|
+
description: "How many idle chats keep their state in system RAM so returning to one skips a re-read.",
|
|
171
|
+
step: 1,
|
|
172
|
+
min: 0,
|
|
173
|
+
max: MAX_CACHED_CHATS,
|
|
174
|
+
available: true,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: "flashAttention",
|
|
178
|
+
label: "Flash attention",
|
|
179
|
+
kind: "toggle",
|
|
180
|
+
description: "A faster attention kernel, and what a quantised value cache requires.",
|
|
181
|
+
available: true,
|
|
182
|
+
},
|
|
183
|
+
// Bundle models expose the projector in the component section below. Keep
|
|
184
|
+
// the legacy profile field for hand-scanned single-file models, but do not
|
|
185
|
+
// render two controls that write the same vision setting for bundles. It
|
|
186
|
+
// belongs with the other options, right after flash attention.
|
|
187
|
+
...(model?.components
|
|
188
|
+
? []
|
|
189
|
+
: [
|
|
190
|
+
{
|
|
191
|
+
key: "vision",
|
|
192
|
+
label: "Vision",
|
|
193
|
+
kind: "toggle",
|
|
194
|
+
description: "Loads the vision projector so the model can read images.",
|
|
195
|
+
available: hasProjector,
|
|
196
|
+
...(hasProjector
|
|
197
|
+
? {}
|
|
198
|
+
: {
|
|
199
|
+
unavailableReason: projector
|
|
200
|
+
? "download the vision component first"
|
|
201
|
+
: "no projector",
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
]),
|
|
99
205
|
{
|
|
100
206
|
key: "reasoningBudget",
|
|
101
207
|
label: "Reasoning budget",
|
|
102
208
|
kind: "cycle",
|
|
209
|
+
description: "Caps how many tokens the model may think before it is told to answer.",
|
|
103
210
|
options: REASONING_BUDGET_CYCLE,
|
|
104
211
|
optionLabels: ["Thinking Off", "512", "1024", "1536", "3072", "Unrestricted"],
|
|
105
212
|
min: -1,
|
|
106
213
|
available: true,
|
|
107
214
|
},
|
|
215
|
+
// Extends the reasoning group, immediately after the budget it qualifies.
|
|
216
|
+
{
|
|
217
|
+
key: "preserveReasoning",
|
|
218
|
+
label: "Preserve reasoning",
|
|
219
|
+
kind: "cycle",
|
|
220
|
+
description: "Keeps earlier thinking in the history instead of only the latest reply's.",
|
|
221
|
+
options: [...PRESERVE_REASONING_CYCLE],
|
|
222
|
+
optionLabels: ["Template default", "On", "Off"],
|
|
223
|
+
optionValues: PRESERVE_REASONING_CYCLE.map(preserveReasoningFromOption),
|
|
224
|
+
available: hasReasoning,
|
|
225
|
+
...(hasReasoning ? {} : { unavailableReason: "no thinking channel in this template" }),
|
|
226
|
+
},
|
|
108
227
|
{
|
|
109
228
|
key: "gpuLayers",
|
|
110
229
|
label: "GPU layers",
|
|
111
230
|
kind: "number",
|
|
231
|
+
description: "How many layers run on the GPU; any remainder runs on the CPU.",
|
|
112
232
|
step: 1,
|
|
113
233
|
min: 0,
|
|
114
234
|
max: MAX_GPU_LAYERS,
|
|
115
235
|
available: true,
|
|
116
236
|
},
|
|
237
|
+
// Sampling. These change what the model writes rather than what it costs, so
|
|
238
|
+
// they sit after the hosting fields and contribute nothing to the budget.
|
|
117
239
|
{
|
|
118
|
-
key: "
|
|
119
|
-
label: "
|
|
240
|
+
key: "temperature",
|
|
241
|
+
label: "Temperature",
|
|
120
242
|
kind: "number",
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
243
|
+
description: "How adventurous each next-token choice is; lower is more predictable.",
|
|
244
|
+
...SAMPLING_RANGES.temperature,
|
|
245
|
+
available: true,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
key: "topP",
|
|
249
|
+
label: "Top P",
|
|
250
|
+
kind: "number",
|
|
251
|
+
description: "Considers only the likeliest tokens whose probabilities add up to this share.",
|
|
252
|
+
...SAMPLING_RANGES.topP,
|
|
253
|
+
available: true,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
key: "topK",
|
|
257
|
+
label: "Top K",
|
|
258
|
+
kind: "number",
|
|
259
|
+
description: "Considers only this many of the likeliest tokens; 0 turns the limit off.",
|
|
260
|
+
...SAMPLING_RANGES.topK,
|
|
261
|
+
available: true,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
key: "minP",
|
|
265
|
+
label: "Min P",
|
|
266
|
+
kind: "number",
|
|
267
|
+
description: "Drops tokens less likely than this fraction of the best one; 0 turns it off.",
|
|
268
|
+
...SAMPLING_RANGES.minP,
|
|
269
|
+
available: true,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
key: "presencePenalty",
|
|
273
|
+
label: "Presence penalty",
|
|
274
|
+
kind: "number",
|
|
275
|
+
description: "Pushes toward new subjects by penalising tokens already used; 0 turns it off.",
|
|
276
|
+
...SAMPLING_RANGES.presencePenalty,
|
|
277
|
+
available: true,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
key: "repeatPenalty",
|
|
281
|
+
label: "Repetition penalty",
|
|
282
|
+
kind: "number",
|
|
283
|
+
description: "Discourages repeating recent tokens; 1 turns it off.",
|
|
284
|
+
...SAMPLING_RANGES.repeatPenalty,
|
|
124
285
|
available: true,
|
|
125
286
|
},
|
|
126
287
|
];
|
|
127
|
-
// Bundle models expose the projector in the component section below. Keep
|
|
128
|
-
// the legacy profile field for hand-scanned single-file models, but do not
|
|
129
|
-
// render two controls that write the same vision setting for bundles.
|
|
130
|
-
if (!model?.components) {
|
|
131
|
-
fields.splice(4, 0, {
|
|
132
|
-
key: "vision",
|
|
133
|
-
label: "Vision",
|
|
134
|
-
kind: "toggle",
|
|
135
|
-
available: hasProjector,
|
|
136
|
-
...(hasProjector
|
|
137
|
-
? {}
|
|
138
|
-
: {
|
|
139
|
-
unavailableReason: projector ? "download the vision component first" : "no projector",
|
|
140
|
-
}),
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
288
|
return fields;
|
|
144
289
|
}
|
|
145
290
|
/** Whether a KV cache type is quantised (anything that is not a float type). */
|
|
@@ -166,10 +311,47 @@ export function profileWarnings(profile, model, store) {
|
|
|
166
311
|
});
|
|
167
312
|
}
|
|
168
313
|
if (profile.parallelSlots > 1) {
|
|
314
|
+
const perSlot = profile.contextSize > 0 ? Math.floor(profile.contextSize / profile.parallelSlots) : null;
|
|
169
315
|
warnings.push({
|
|
170
316
|
field: "parallelSlots",
|
|
171
317
|
severity: "info",
|
|
172
|
-
message:
|
|
318
|
+
message: perSlot !== null
|
|
319
|
+
? `${profile.parallelSlots} concurrent chats: ~${Math.round(perSlot / 1000)}K context each, all resident.`
|
|
320
|
+
: `${profile.parallelSlots} concurrent requests, sharing one KV pool.`,
|
|
321
|
+
blocksStart: false,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
// The estimate is shown whenever this field is available, including at the
|
|
325
|
+
// Default of 0. That is the one value the user could not otherwise price:
|
|
326
|
+
// 0 emits no flag, and llama.cpp then parks up to its own 8 GiB cache-ram
|
|
327
|
+
// default in system RAM, which is not the same as caching nothing.
|
|
328
|
+
//
|
|
329
|
+
// A count above 0 can only be priced from a measurement: without one the
|
|
330
|
+
// theoretical KV cost runs to multiples of the real one, and naming a figure
|
|
331
|
+
// derived from it would invite reserving several times the RAM actually
|
|
332
|
+
// needed. So the unmeasured count falls back to the calibration request.
|
|
333
|
+
const calibration = model && store ? getCalibration(store, model, profile) : null;
|
|
334
|
+
const cache = model ? promptCacheSize(model, profile, calibration) : null;
|
|
335
|
+
const hasCount = (profile.cachedChats ?? 0) > 0;
|
|
336
|
+
if (hasCount && (!cache || cache.source !== "measured")) {
|
|
337
|
+
warnings.push({
|
|
338
|
+
field: "cachedChats",
|
|
339
|
+
severity: "warn",
|
|
340
|
+
message: "Calibrate this model first: without a measured KV cost the RAM this needs cannot be sized, so llama.cpp's own cache limit stays in effect.",
|
|
341
|
+
blocksStart: false,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const totalBytes = hasCount ? cache.totalBytes : ENGINE_DEFAULT_CACHE_RAM_BYTES;
|
|
346
|
+
const installed = formatGiB(os.totalmem());
|
|
347
|
+
warnings.push({
|
|
348
|
+
field: "cachedChats",
|
|
349
|
+
// Yellow at half the machine's RAM, red once the parked state would use
|
|
350
|
+
// at least all of it.
|
|
351
|
+
severity: totalBytes >= os.totalmem() ? "error" : totalBytes >= os.totalmem() / 2 ? "warn" : "info",
|
|
352
|
+
message: hasCount
|
|
353
|
+
? `${formatGiB(cache.perChatBytes)} each becomes ${formatGiB(totalBytes)} of ${installed}.`
|
|
354
|
+
: `llama.cpp's own limit applies: about ${formatGiB(totalBytes)} of ${installed}.`,
|
|
173
355
|
blocksStart: false,
|
|
174
356
|
});
|
|
175
357
|
}
|
|
@@ -230,15 +412,37 @@ export function calibrationInfo(store, model, profile) {
|
|
|
230
412
|
function clamp(value, min, max) {
|
|
231
413
|
return Math.max(min, Math.min(max, value));
|
|
232
414
|
}
|
|
233
|
-
/**
|
|
415
|
+
/**
|
|
416
|
+
* The settings whose value changes what a calibration would measure - the KV
|
|
417
|
+
* cache system (cache types, attention configuration, loaded components) or the
|
|
418
|
+
* evaluation (the extended RoPE shape). Only these may set
|
|
419
|
+
* `calibrationRequired`.
|
|
420
|
+
*
|
|
421
|
+
* `contextSize` is deliberately absent. A calibration measures bytes *per
|
|
422
|
+
* token* - the context size is the independent variable it varies to get the
|
|
423
|
+
* slope, so the result is by construction the same at any context, and
|
|
424
|
+
* `calibrationKey` does not record it either. Listing it here invalidated the
|
|
425
|
+
* measurement on every context edit, including the one "Fit to VRAM" makes
|
|
426
|
+
* itself: the fit picked a context from the measured figure, the write that
|
|
427
|
+
* saved it dropped back to the (~4x higher) theoretical estimate, and the
|
|
428
|
+
* saved context was then far past the budget it had just been sized against.
|
|
429
|
+
*
|
|
430
|
+
* `gpuLayers` and `parallelSlots` are absent for the same differential reason:
|
|
431
|
+
* they only move weights between devices or split the one KV pool across slots.
|
|
432
|
+
* The calibration is the GPU-delta between two loads at different contexts, so
|
|
433
|
+
* every fixed term - weights wherever they sit, the CUDA context, compute
|
|
434
|
+
* buffers - cancels out of the slope, and a load whose KV split to CPU is
|
|
435
|
+
* rejected as unusable rather than measured low. Neither the bytes/token nor the
|
|
436
|
+
* evaluation changes, so neither may discard a real measurement.
|
|
437
|
+
*
|
|
438
|
+
* `cachedChats` is absent because it spends host RAM, not VRAM, and never
|
|
439
|
+
* reaches the load a calibration measures.
|
|
440
|
+
*/
|
|
234
441
|
const CALIBRATION_INPUTS = [
|
|
235
442
|
"contextMultiplier",
|
|
236
|
-
"contextSize",
|
|
237
443
|
"cacheTypeK",
|
|
238
444
|
"cacheTypeV",
|
|
239
445
|
"flashAttention",
|
|
240
|
-
"gpuLayers",
|
|
241
|
-
"parallelSlots",
|
|
242
446
|
"vision",
|
|
243
447
|
"enabledComponents",
|
|
244
448
|
];
|
|
@@ -287,6 +491,27 @@ export function sanitizeProfilePatch(current, patch, model, runtimeBuild = null)
|
|
|
287
491
|
throw new Error(`${key} must be a boolean`);
|
|
288
492
|
next[key] = p[key];
|
|
289
493
|
};
|
|
494
|
+
/**
|
|
495
|
+
* A sampler value: clamped to its useful range and rounded to the precision
|
|
496
|
+
* the editor advertises. Rounding matters as much as clamping - a stepper that
|
|
497
|
+
* added 0.05 six times sends 0.30000000000000004, and without this the profile
|
|
498
|
+
* would store that and hand it to llama-server on the command line.
|
|
499
|
+
*/
|
|
500
|
+
const takeSampling = (key) => {
|
|
501
|
+
if (!(key in p))
|
|
502
|
+
return;
|
|
503
|
+
const raw = p[key];
|
|
504
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
505
|
+
throw new Error(`${key} must be a number`);
|
|
506
|
+
}
|
|
507
|
+
const range = SAMPLING_RANGES[key];
|
|
508
|
+
const precision = "precision" in range ? range.precision : 0;
|
|
509
|
+
const rounded = Number(raw.toFixed(precision));
|
|
510
|
+
const clamped = clamp(rounded, range.min, range.max);
|
|
511
|
+
if (clamped !== rounded)
|
|
512
|
+
adjustments.push(`${key} clamped to ${clamped}`);
|
|
513
|
+
next[key] = clamped;
|
|
514
|
+
};
|
|
290
515
|
const takeCacheType = (key) => {
|
|
291
516
|
if (!(key in p))
|
|
292
517
|
return;
|
|
@@ -312,6 +537,7 @@ export function sanitizeProfilePatch(current, patch, model, runtimeBuild = null)
|
|
|
312
537
|
takeBoolean("flashAttention");
|
|
313
538
|
takeNumber("gpuLayers", 0, MAX_GPU_LAYERS);
|
|
314
539
|
takeNumber("parallelSlots", 1, MAX_PARALLEL_SLOTS);
|
|
540
|
+
takeNumber("cachedChats", 0, MAX_CACHED_CHATS);
|
|
315
541
|
// -1 (unrestricted) is the floor; anything below it is meaningless to
|
|
316
542
|
// llama-server. There is no upper bound worth inventing: a budget larger than
|
|
317
543
|
// the context is simply never reached.
|
|
@@ -326,6 +552,31 @@ export function sanitizeProfilePatch(current, patch, model, runtimeBuild = null)
|
|
|
326
552
|
adjustments.push(`reasoningBudget clamped to ${value}`);
|
|
327
553
|
next.reasoningBudget = value;
|
|
328
554
|
}
|
|
555
|
+
// Tri-state, and the third state is the point: null means "leave the template's
|
|
556
|
+
// own behavior alone", which is not the same as false ("trim the trace"). The
|
|
557
|
+
// string forms are accepted because the editor renders this as a cycle whose
|
|
558
|
+
// options are strings on the wire.
|
|
559
|
+
if ("preserveReasoning" in p) {
|
|
560
|
+
const raw = p.preserveReasoning;
|
|
561
|
+
if (raw === null || raw === "default") {
|
|
562
|
+
next.preserveReasoning = null;
|
|
563
|
+
}
|
|
564
|
+
else if (typeof raw === "boolean") {
|
|
565
|
+
next.preserveReasoning = raw;
|
|
566
|
+
}
|
|
567
|
+
else if (raw === "on" || raw === "off") {
|
|
568
|
+
next.preserveReasoning = preserveReasoningFromOption(raw);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
throw new Error('preserveReasoning must be a boolean, null, or one of "default"/"on"/"off"');
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
takeSampling("temperature");
|
|
575
|
+
takeSampling("topP");
|
|
576
|
+
takeSampling("topK");
|
|
577
|
+
takeSampling("minP");
|
|
578
|
+
takeSampling("presencePenalty");
|
|
579
|
+
takeSampling("repeatPenalty");
|
|
329
580
|
// Vision is only real when the model actually has a projector paired with it.
|
|
330
581
|
if ("vision" in p) {
|
|
331
582
|
if (typeof p.vision !== "boolean")
|
package/dist/config/profiles.js
CHANGED
|
@@ -32,7 +32,20 @@ export function defaultProfile(model, defaults) {
|
|
|
32
32
|
: Boolean(model?.mmprojPath),
|
|
33
33
|
reasoningBudget: defaults?.reasoningBudget ?? 1536,
|
|
34
34
|
reasoningBudgetMessage: DEFAULT_REASONING_MESSAGE,
|
|
35
|
+
// Null, not false: llama-server's own default is "whatever the template
|
|
36
|
+
// wants", and a profile that has never been touched must not override it.
|
|
37
|
+
// A template that declares a preservation kwarg still gets its own default
|
|
38
|
+
// through that path (see buildArgs).
|
|
39
|
+
preserveReasoning: model?.reasoningPreservation?.default ?? null,
|
|
40
|
+
// llama.cpp's sampler defaults; see the note on ProfileSchema.
|
|
41
|
+
temperature: 0.8,
|
|
42
|
+
topP: 0.95,
|
|
43
|
+
topK: 40,
|
|
44
|
+
minP: 0.05,
|
|
45
|
+
presencePenalty: 0,
|
|
46
|
+
repeatPenalty: 1,
|
|
35
47
|
parallelSlots: defaults?.parallelSlots ?? 1, // one agent at a time: max context per request
|
|
48
|
+
cachedChats: 0, // llama.cpp's own --cache-ram default until the user sizes it
|
|
36
49
|
batchSize: null,
|
|
37
50
|
ubatchSize: null,
|
|
38
51
|
extraArgs: [],
|
|
@@ -67,6 +80,9 @@ export function forModel(store, model, defaults) {
|
|
|
67
80
|
return {
|
|
68
81
|
...base,
|
|
69
82
|
...stored,
|
|
83
|
+
// `preserveReasoning` was optional in the store so adding the Qwen default
|
|
84
|
+
// does not rewrite existing profiles as false merely because they predate it.
|
|
85
|
+
preserveReasoning: stored.preserveReasoning ?? base.preserveReasoning,
|
|
70
86
|
// COMPAT(hostingProfileMode): added in v0.8.8, remove after 2027-02-12.
|
|
71
87
|
// The first hosting-profile store only had an id, and a non-null id there
|
|
72
88
|
// was an explicit custom choice. Such a profile parses as `inherit` (the
|