@otto-code/brain 0.7.5 → 0.8.0
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/bench/context-corpus.js +3 -3
- package/dist/bench/corpus.js +2 -2
- package/dist/bench/curated-repos.js +3 -3
- package/dist/bench/health.d.ts +1 -1
- package/dist/bench/health.js +2 -2
- package/dist/bench/tasks.js +2 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/bench.d.ts +1 -1
- package/dist/commands/bench.js +27 -4
- package/dist/commands/calibrate.d.ts +1 -1
- package/dist/commands/calibrate.js +5 -2
- package/dist/commands/catalog.d.ts +1 -1
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +9 -0
- package/dist/commands/lifecycle.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +9 -4
- package/dist/commands/report.d.ts +1 -1
- package/dist/commands/rescore.d.ts +1 -1
- package/dist/commands/rescore.js +1 -1
- package/dist/commands/runtime.d.ts +1 -1
- package/dist/commands/scan.d.ts +1 -1
- package/dist/commands/scan.js +6 -1
- package/dist/commands/search.d.ts +1 -1
- package/dist/commands/share.js +2 -2
- package/dist/commands/sweep.d.ts +2 -2
- package/dist/commands/sweep.js +22 -8
- package/dist/commands/ui.d.ts +1 -1
- package/dist/commands/ui.js +37 -5
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.js +2 -1
- package/dist/config/otto-home.js +1 -1
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +4 -0
- package/dist/config/profile-edit.d.ts +94 -0
- package/dist/config/profile-edit.js +269 -0
- package/dist/config/profiles.d.ts +2 -2
- package/dist/config/profiles.js +1 -1
- package/dist/config/schema.d.ts +4 -4
- package/dist/config/schema.js +6 -6
- package/dist/config/store.d.ts +1 -1
- package/dist/config/store.js +1 -1
- package/dist/models/download.js +1 -1
- package/dist/models/enrich.d.ts +2 -2
- package/dist/models/index.d.ts +1 -1
- package/dist/models/index.js +1 -1
- package/dist/models/pick.d.ts +9 -0
- package/dist/models/pick.js +24 -1
- package/dist/ops/report.js +28 -28
- package/dist/ops/results.d.ts +128 -9
- package/dist/ops/results.js +77 -5
- package/dist/output/render.js +1 -1
- package/dist/output/types.d.ts +1 -1
- package/dist/runtime/args.d.ts +1 -1
- package/dist/runtime/args.js +1 -1
- package/dist/runtime/managed.js +4 -4
- package/dist/service/activity.d.ts +83 -0
- package/dist/service/activity.js +216 -0
- package/dist/service/host-api.d.ts +132 -0
- package/dist/service/host-api.js +397 -0
- package/dist/service/http-util.d.ts +27 -0
- package/dist/service/http-util.js +72 -0
- package/dist/service/model-selector.d.ts +2 -2
- package/dist/service/model-selector.js +6 -6
- package/dist/service/router.d.ts +28 -4
- package/dist/service/router.js +128 -94
- package/dist/service/scheduler.d.ts +2 -2
- package/dist/service/scheduler.js +1 -1
- package/dist/service/serve.d.ts +8 -1
- package/dist/service/serve.js +69 -16
- package/dist/service/supervisor.d.ts +6 -0
- package/dist/service/supervisor.js +2 -0
- package/dist/service/tailscale.js +1 -1
- package/dist/service/tls.d.ts +4 -4
- package/dist/service/tls.js +3 -3
- package/dist/sysmon.d.ts +20 -4
- package/dist/sysmon.js +42 -18
- package/dist/tui/app.d.ts +12 -2
- package/dist/tui/app.js +48 -23
- package/dist/vram.d.ts +8 -1
- package/dist/vram.js +6 -3
- package/package.json +1 -1
package/dist/config/paths.js
CHANGED
|
@@ -18,6 +18,10 @@ export function resolveBrainPaths(env = process.env) {
|
|
|
18
18
|
modelsDir: path.join(root, "models"),
|
|
19
19
|
runtimesDir: path.join(root, "runtimes"),
|
|
20
20
|
pidFile: path.join(root, "otto-brain.pid"),
|
|
21
|
+
// Which long-running op currently owns the host. A file rather than service
|
|
22
|
+
// state because calibrate/sweep/bench run as their own CLI processes and the
|
|
23
|
+
// service - which is what answers /__host/status - never sees them otherwise.
|
|
24
|
+
activityFile: path.join(root, "otto-brain.activity"),
|
|
21
25
|
logFile: path.join(root, "otto-brain.log"),
|
|
22
26
|
resultsDir: path.join(root, "results"),
|
|
23
27
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Model } from "../types.js";
|
|
2
|
+
import type { Profile, ProfilesStore } from "./schema.js";
|
|
3
|
+
/** How a client should render a field. */
|
|
4
|
+
export type ProfileFieldKind = "number" | "toggle" | "cycle";
|
|
5
|
+
export interface ProfileFieldDescriptor {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
kind: ProfileFieldKind;
|
|
9
|
+
/** For `number`: the increment a stepper should use. */
|
|
10
|
+
step?: number;
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
/** For `cycle`: the values to offer, in order. */
|
|
14
|
+
options?: (string | number)[];
|
|
15
|
+
/** Labels for `options`, index-aligned, when the raw value is not presentable. */
|
|
16
|
+
optionLabels?: string[];
|
|
17
|
+
/** False when this model cannot use the field at all (vision with no projector). */
|
|
18
|
+
available: boolean;
|
|
19
|
+
/** Why it is unavailable, for the disabled-state hint. */
|
|
20
|
+
unavailableReason?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The cache types the editor cycles through. `CACHE_TYPE_BYTES` knows more
|
|
24
|
+
* (f32, bf16, q5_0, q4_1) and a write naming one of those is accepted; these
|
|
25
|
+
* four are the ones worth offering, matching the TUI's cycle.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CACHE_TYPE_CYCLE: string[];
|
|
28
|
+
/**
|
|
29
|
+
* Reasoning budgets worth offering. 0 disables thinking; -1 is unrestricted and
|
|
30
|
+
* is the documented failure mode, so it is last and carries a warning. A sweep
|
|
31
|
+
* may persist any value, so the write path accepts any integer >= -1.
|
|
32
|
+
*/
|
|
33
|
+
export declare const REASONING_BUDGET_CYCLE: number[];
|
|
34
|
+
/** The sentinel llama-server reads as "do not cap thinking at all". */
|
|
35
|
+
export declare const UNRESTRICTED_REASONING_BUDGET = -1;
|
|
36
|
+
/**
|
|
37
|
+
* The user-facing name for a budget. `-1` is never shown raw: it is a sentinel,
|
|
38
|
+
* not a token count, and printing it next to a `0` row invites the reading that
|
|
39
|
+
* one of them means "less thinking" than the other when `-1` means the most
|
|
40
|
+
* possible. "unrestricted" is the one word for it everywhere (TUI, sweep table,
|
|
41
|
+
* warnings) - do not introduce a synonym.
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatReasoningBudget(budget: number): string;
|
|
44
|
+
export declare const MAX_PARALLEL_SLOTS = 16;
|
|
45
|
+
export declare const MAX_GPU_LAYERS = 999;
|
|
46
|
+
export declare const MIN_CONTEXT_SIZE = 1024;
|
|
47
|
+
export declare const CONTEXT_STEP = 8192;
|
|
48
|
+
/** The context ceiling: the model's native window, or a generous bound if unknown. */
|
|
49
|
+
export declare function nativeContextLimit(model: Model | null): number;
|
|
50
|
+
/** The editable fields, resolved against one model's capabilities. */
|
|
51
|
+
export declare function profileFieldDescriptors(model: Model | null): ProfileFieldDescriptor[];
|
|
52
|
+
/** A note attached to a field, or to the profile as a whole when `field` is null. */
|
|
53
|
+
export interface ProfileWarning {
|
|
54
|
+
field: string | null;
|
|
55
|
+
severity: "info" | "warn";
|
|
56
|
+
message: string;
|
|
57
|
+
/** True when this combination cannot start, as opposed to merely being unwise. */
|
|
58
|
+
blocksStart: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** The notes a UI shows beside the fields, and the reason a load would be refused. */
|
|
61
|
+
export declare function profileWarnings(profile: Profile, model: Model | null, store?: ProfilesStore | null): ProfileWarning[];
|
|
62
|
+
/** Where a model's KV bytes/token figure came from. */
|
|
63
|
+
export type CalibrationState = "measured" | "inherited" | "stale" | "theoretical" | "unknown";
|
|
64
|
+
export interface CalibrationInfo {
|
|
65
|
+
state: CalibrationState;
|
|
66
|
+
kvBytesPerToken: number | null;
|
|
67
|
+
measuredAt: string | null;
|
|
68
|
+
/** For `inherited`, the relative the measurement came from. */
|
|
69
|
+
measuredOn: string | null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Classify the calibration backing this profile's budget. `inherited` means the
|
|
73
|
+
* figure came from a relative with the same attention geometry rescaled to this
|
|
74
|
+
* model's layer count, which the UI must never present as measured on this file.
|
|
75
|
+
*/
|
|
76
|
+
export declare function calibrationInfo(store: ProfilesStore, model: Model, profile: Profile): CalibrationInfo;
|
|
77
|
+
export interface SanitizeResult {
|
|
78
|
+
profile: Profile;
|
|
79
|
+
/** Fields the patch named that were clamped, coerced, or dropped. */
|
|
80
|
+
adjustments: string[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Apply an editable patch to a profile, clamping every field to its range and
|
|
84
|
+
* dropping anything the model cannot use.
|
|
85
|
+
*
|
|
86
|
+
* Only the eight editable keys are honoured. `modelPath`/`mmprojPath`/`modelId`
|
|
87
|
+
* are re-derived from the model on every read (`profiles.forModel`), so letting
|
|
88
|
+
* a caller set them would be a lie at best and a path-traversal at worst.
|
|
89
|
+
* `reasoningBudgetMessage`, `batchSize`, `ubatchSize` and `extraArgs` stay
|
|
90
|
+
* CLI-only: they have no measured effect worth exposing and `extraArgs` is
|
|
91
|
+
* arbitrary process arguments.
|
|
92
|
+
*/
|
|
93
|
+
export declare function sanitizeProfilePatch(current: Profile, patch: unknown, model: Model | null): SanitizeResult;
|
|
94
|
+
//# sourceMappingURL=profile-edit.d.ts.map
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editable surface of a per-model profile: which fields a UI may change,
|
|
3
|
+
* what values each accepts, and what warnings a given combination earns.
|
|
4
|
+
*
|
|
5
|
+
* This exists so the ranges live in exactly one place. The TUI enforced them
|
|
6
|
+
* inline in its field table (`tui/app.ts`), which was fine while the TUI was the
|
|
7
|
+
* only editor. Now `/__host/models/:id/profile` accepts writes from Otto's Brain
|
|
8
|
+
* page too, and a validator that disagreed with the editor would either reject
|
|
9
|
+
* something the UI offered or accept something that cannot start.
|
|
10
|
+
*
|
|
11
|
+
* The warnings are the empirical part and must survive edits:
|
|
12
|
+
* - A quantised V cache requires flash attention. Without it llama-server
|
|
13
|
+
* refuses to allocate the cache and the model never reaches ready.
|
|
14
|
+
* - An unrestricted reasoning budget (-1) is the failure this package exists to
|
|
15
|
+
* prevent: thinking models spend an entire token allowance reasoning and
|
|
16
|
+
* return no content at all.
|
|
17
|
+
* - Changing a cache type invalidates a measured calibration, because KV
|
|
18
|
+
* bytes/token is a function of the cache types (see `vram.ts`).
|
|
19
|
+
*/
|
|
20
|
+
import { CACHE_TYPE_BYTES } from "../vram.js";
|
|
21
|
+
import { getCalibration, hasStaleCalibration } from "./profiles.js";
|
|
22
|
+
/**
|
|
23
|
+
* The cache types the editor cycles through. `CACHE_TYPE_BYTES` knows more
|
|
24
|
+
* (f32, bf16, q5_0, q4_1) and a write naming one of those is accepted; these
|
|
25
|
+
* four are the ones worth offering, matching the TUI's cycle.
|
|
26
|
+
*/
|
|
27
|
+
export const CACHE_TYPE_CYCLE = ["q4_0", "q5_1", "q8_0", "f16"];
|
|
28
|
+
/**
|
|
29
|
+
* Reasoning budgets worth offering. 0 disables thinking; -1 is unrestricted and
|
|
30
|
+
* is the documented failure mode, so it is last and carries a warning. A sweep
|
|
31
|
+
* may persist any value, so the write path accepts any integer >= -1.
|
|
32
|
+
*/
|
|
33
|
+
export const REASONING_BUDGET_CYCLE = [0, 512, 1024, 1536, 3072, -1];
|
|
34
|
+
/** The sentinel llama-server reads as "do not cap thinking at all". */
|
|
35
|
+
export const UNRESTRICTED_REASONING_BUDGET = -1;
|
|
36
|
+
/**
|
|
37
|
+
* The user-facing name for a budget. `-1` is never shown raw: it is a sentinel,
|
|
38
|
+
* not a token count, and printing it next to a `0` row invites the reading that
|
|
39
|
+
* one of them means "less thinking" than the other when `-1` means the most
|
|
40
|
+
* possible. "unrestricted" is the one word for it everywhere (TUI, sweep table,
|
|
41
|
+
* warnings) - do not introduce a synonym.
|
|
42
|
+
*/
|
|
43
|
+
export function formatReasoningBudget(budget) {
|
|
44
|
+
return budget === UNRESTRICTED_REASONING_BUDGET ? "unrestricted" : String(budget);
|
|
45
|
+
}
|
|
46
|
+
export const MAX_PARALLEL_SLOTS = 16;
|
|
47
|
+
export const MAX_GPU_LAYERS = 999;
|
|
48
|
+
export const MIN_CONTEXT_SIZE = 1024;
|
|
49
|
+
export const CONTEXT_STEP = 8192;
|
|
50
|
+
/** The context ceiling: the model's native window, or a generous bound if unknown. */
|
|
51
|
+
export function nativeContextLimit(model) {
|
|
52
|
+
const native = model?.metadata?.contextLength;
|
|
53
|
+
return typeof native === "number" && native > 0 ? native : 1000000;
|
|
54
|
+
}
|
|
55
|
+
/** The editable fields, resolved against one model's capabilities. */
|
|
56
|
+
export function profileFieldDescriptors(model) {
|
|
57
|
+
const hasProjector = Boolean(model?.mmprojPath);
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
key: "contextSize",
|
|
61
|
+
label: "Context",
|
|
62
|
+
kind: "number",
|
|
63
|
+
step: CONTEXT_STEP,
|
|
64
|
+
min: MIN_CONTEXT_SIZE,
|
|
65
|
+
max: nativeContextLimit(model),
|
|
66
|
+
available: true,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "cacheTypeK",
|
|
70
|
+
label: "KV cache K",
|
|
71
|
+
kind: "cycle",
|
|
72
|
+
options: CACHE_TYPE_CYCLE,
|
|
73
|
+
available: true,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: "cacheTypeV",
|
|
77
|
+
label: "KV cache V",
|
|
78
|
+
kind: "cycle",
|
|
79
|
+
options: CACHE_TYPE_CYCLE,
|
|
80
|
+
available: true,
|
|
81
|
+
},
|
|
82
|
+
{ key: "flashAttention", label: "Flash attention", kind: "toggle", available: true },
|
|
83
|
+
{
|
|
84
|
+
key: "vision",
|
|
85
|
+
label: "Vision",
|
|
86
|
+
kind: "toggle",
|
|
87
|
+
available: hasProjector,
|
|
88
|
+
...(hasProjector ? {} : { unavailableReason: "no projector" }),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: "reasoningBudget",
|
|
92
|
+
label: "Reasoning budget",
|
|
93
|
+
kind: "cycle",
|
|
94
|
+
options: REASONING_BUDGET_CYCLE,
|
|
95
|
+
optionLabels: ["Thinking Off", "512", "1024", "1536", "3072", "Unrestricted"],
|
|
96
|
+
min: -1,
|
|
97
|
+
available: true,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "gpuLayers",
|
|
101
|
+
label: "GPU layers",
|
|
102
|
+
kind: "number",
|
|
103
|
+
step: 1,
|
|
104
|
+
min: 0,
|
|
105
|
+
max: MAX_GPU_LAYERS,
|
|
106
|
+
available: true,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "parallelSlots",
|
|
110
|
+
label: "Parallel slots",
|
|
111
|
+
kind: "number",
|
|
112
|
+
step: 1,
|
|
113
|
+
min: 1,
|
|
114
|
+
max: MAX_PARALLEL_SLOTS,
|
|
115
|
+
available: true,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
/** Whether a KV cache type is quantised (anything that is not a float type). */
|
|
120
|
+
function isQuantised(cacheType) {
|
|
121
|
+
return /^q/i.test(cacheType);
|
|
122
|
+
}
|
|
123
|
+
/** The notes a UI shows beside the fields, and the reason a load would be refused. */
|
|
124
|
+
export function profileWarnings(profile, model, store) {
|
|
125
|
+
const warnings = [];
|
|
126
|
+
if (!profile.flashAttention && isQuantised(profile.cacheTypeV)) {
|
|
127
|
+
warnings.push({
|
|
128
|
+
field: "flashAttention",
|
|
129
|
+
severity: "warn",
|
|
130
|
+
message: "A quantised V cache requires flash attention.",
|
|
131
|
+
blocksStart: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (profile.reasoningBudget === -1) {
|
|
135
|
+
warnings.push({
|
|
136
|
+
field: "reasoningBudget",
|
|
137
|
+
severity: "warn",
|
|
138
|
+
message: "Unrestricted: a thinking model can spend its whole token allowance reasoning and return no content.",
|
|
139
|
+
blocksStart: false,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (profile.parallelSlots > 1) {
|
|
143
|
+
warnings.push({
|
|
144
|
+
field: "parallelSlots",
|
|
145
|
+
severity: "info",
|
|
146
|
+
message: `${profile.parallelSlots} concurrent requests, sharing one KV pool.`,
|
|
147
|
+
blocksStart: false,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (model && store && hasStaleCalibration(store, model, profile)) {
|
|
151
|
+
warnings.push({
|
|
152
|
+
field: "cacheTypeK",
|
|
153
|
+
severity: "info",
|
|
154
|
+
message: "Cache types changed since the last measurement. Recalibrate for a real budget.",
|
|
155
|
+
blocksStart: false,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return warnings;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Classify the calibration backing this profile's budget. `inherited` means the
|
|
162
|
+
* figure came from a relative with the same attention geometry rescaled to this
|
|
163
|
+
* model's layer count, which the UI must never present as measured on this file.
|
|
164
|
+
*/
|
|
165
|
+
export function calibrationInfo(store, model, profile) {
|
|
166
|
+
const calibration = getCalibration(store, model, profile);
|
|
167
|
+
if (!calibration) {
|
|
168
|
+
return {
|
|
169
|
+
state: hasStaleCalibration(store, model, profile) ? "stale" : "theoretical",
|
|
170
|
+
kvBytesPerToken: null,
|
|
171
|
+
measuredAt: null,
|
|
172
|
+
measuredOn: null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
state: calibration.inherited ? "inherited" : "measured",
|
|
177
|
+
kvBytesPerToken: calibration.kvBytesPerToken,
|
|
178
|
+
measuredAt: calibration.measuredAt ?? null,
|
|
179
|
+
measuredOn: calibration.measuredOn ?? null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function clamp(value, min, max) {
|
|
183
|
+
return Math.max(min, Math.min(max, value));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Apply an editable patch to a profile, clamping every field to its range and
|
|
187
|
+
* dropping anything the model cannot use.
|
|
188
|
+
*
|
|
189
|
+
* Only the eight editable keys are honoured. `modelPath`/`mmprojPath`/`modelId`
|
|
190
|
+
* are re-derived from the model on every read (`profiles.forModel`), so letting
|
|
191
|
+
* a caller set them would be a lie at best and a path-traversal at worst.
|
|
192
|
+
* `reasoningBudgetMessage`, `batchSize`, `ubatchSize` and `extraArgs` stay
|
|
193
|
+
* CLI-only: they have no measured effect worth exposing and `extraArgs` is
|
|
194
|
+
* arbitrary process arguments.
|
|
195
|
+
*/
|
|
196
|
+
export function sanitizeProfilePatch(current, patch, model) {
|
|
197
|
+
const adjustments = [];
|
|
198
|
+
const next = { ...current };
|
|
199
|
+
if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
|
|
200
|
+
throw new Error("profile patch must be an object");
|
|
201
|
+
}
|
|
202
|
+
const p = patch;
|
|
203
|
+
const takeNumber = (key, min, max) => {
|
|
204
|
+
if (!(key in p))
|
|
205
|
+
return;
|
|
206
|
+
const raw = p[key];
|
|
207
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
208
|
+
throw new Error(`${key} must be a number`);
|
|
209
|
+
}
|
|
210
|
+
const rounded = Math.round(raw);
|
|
211
|
+
const clamped = clamp(rounded, min, max);
|
|
212
|
+
if (clamped !== rounded)
|
|
213
|
+
adjustments.push(`${key} clamped to ${clamped}`);
|
|
214
|
+
next[key] = clamped;
|
|
215
|
+
};
|
|
216
|
+
const takeBoolean = (key) => {
|
|
217
|
+
if (!(key in p))
|
|
218
|
+
return;
|
|
219
|
+
if (typeof p[key] !== "boolean")
|
|
220
|
+
throw new Error(`${key} must be a boolean`);
|
|
221
|
+
next[key] = p[key];
|
|
222
|
+
};
|
|
223
|
+
const takeCacheType = (key) => {
|
|
224
|
+
if (!(key in p))
|
|
225
|
+
return;
|
|
226
|
+
const raw = p[key];
|
|
227
|
+
if (typeof raw !== "string")
|
|
228
|
+
throw new Error(`${key} must be a string`);
|
|
229
|
+
const value = raw.toLowerCase();
|
|
230
|
+
if (!(value in CACHE_TYPE_BYTES)) {
|
|
231
|
+
throw new Error(`unknown KV cache type "${raw}"`);
|
|
232
|
+
}
|
|
233
|
+
next[key] = value;
|
|
234
|
+
};
|
|
235
|
+
takeNumber("contextSize", MIN_CONTEXT_SIZE, nativeContextLimit(model));
|
|
236
|
+
takeCacheType("cacheTypeK");
|
|
237
|
+
takeCacheType("cacheTypeV");
|
|
238
|
+
takeBoolean("flashAttention");
|
|
239
|
+
takeNumber("gpuLayers", 0, MAX_GPU_LAYERS);
|
|
240
|
+
takeNumber("parallelSlots", 1, MAX_PARALLEL_SLOTS);
|
|
241
|
+
// -1 (unrestricted) is the floor; anything below it is meaningless to
|
|
242
|
+
// llama-server. There is no upper bound worth inventing: a budget larger than
|
|
243
|
+
// the context is simply never reached.
|
|
244
|
+
if ("reasoningBudget" in p) {
|
|
245
|
+
const raw = p.reasoningBudget;
|
|
246
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
247
|
+
throw new Error("reasoningBudget must be a number");
|
|
248
|
+
}
|
|
249
|
+
const rounded = Math.round(raw);
|
|
250
|
+
const value = rounded < -1 ? -1 : rounded;
|
|
251
|
+
if (value !== rounded)
|
|
252
|
+
adjustments.push(`reasoningBudget clamped to ${value}`);
|
|
253
|
+
next.reasoningBudget = value;
|
|
254
|
+
}
|
|
255
|
+
// Vision is only real when the model actually has a projector paired with it.
|
|
256
|
+
if ("vision" in p) {
|
|
257
|
+
if (typeof p.vision !== "boolean")
|
|
258
|
+
throw new Error("vision must be a boolean");
|
|
259
|
+
if (p.vision && !model?.mmprojPath) {
|
|
260
|
+
adjustments.push("vision ignored: this model has no projector");
|
|
261
|
+
next.vision = false;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
next.vision = p.vision;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { profile: next, adjustments };
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=profile-edit.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-model hosting profiles and the measured-calibration lookup, ported from the
|
|
3
3
|
* original profiles.js. Only settings with a demonstrated effect on stability or
|
|
4
|
-
* throughput are stored
|
|
4
|
+
* throughput are stored - notably `reasoningBudget`, which defaults to -1
|
|
5
5
|
* (unrestricted) in llama-server and in that state makes thinking models spend an
|
|
6
6
|
* entire token allowance reasoning and return no content at all.
|
|
7
7
|
*/
|
|
@@ -24,7 +24,7 @@ export declare function hasStaleCalibration(store: ProfilesStore, model: Model,
|
|
|
24
24
|
* file: two models sharing an architecture and head dimensions cost the same per
|
|
25
25
|
* layer regardless of quantisation or fine-tuning. Storing the measurement *per
|
|
26
26
|
* layer* and keying it without the layer count lets one calibration serve a whole
|
|
27
|
-
* family
|
|
27
|
+
* family - which matters on a library holding a dozen variants of one base, and is
|
|
28
28
|
* necessary because MTP builds carry an extra multi-token-prediction layer (65
|
|
29
29
|
* blocks where the base has 64) and would otherwise never match.
|
|
30
30
|
*/
|
package/dist/config/profiles.js
CHANGED
|
@@ -56,7 +56,7 @@ export function hasStaleCalibration(store, model, profile) {
|
|
|
56
56
|
* file: two models sharing an architecture and head dimensions cost the same per
|
|
57
57
|
* layer regardless of quantisation or fine-tuning. Storing the measurement *per
|
|
58
58
|
* layer* and keying it without the layer count lets one calibration serve a whole
|
|
59
|
-
* family
|
|
59
|
+
* family - which matters on a library holding a dozen variants of one base, and is
|
|
60
60
|
* necessary because MTP builds carry an extra multi-token-prediction layer (65
|
|
61
61
|
* blocks where the base has 64) and would otherwise never match.
|
|
62
62
|
*/
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -562,10 +562,10 @@ export type Auth = z.infer<typeof AuthSchema>;
|
|
|
562
562
|
/**
|
|
563
563
|
* TLS termination, built into the brain so it can be exposed over HTTPS with no
|
|
564
564
|
* relay in front. Four modes:
|
|
565
|
-
* - `off`
|
|
566
|
-
* - `files`
|
|
567
|
-
* - `self-signed`
|
|
568
|
-
* - `tailscale`
|
|
565
|
+
* - `off` - plain HTTP (the default; loopback-only is the safe posture).
|
|
566
|
+
* - `files` - bring your own cert/key (a real cert, or one you manage).
|
|
567
|
+
* - `self-signed` - generate a local keypair on first run, cached under `certDir`.
|
|
568
|
+
* - `tailscale` - issue and auto-renew a real Let's Encrypt cert for this
|
|
569
569
|
* machine's MagicDNS name via `tailscaled` (no cert warnings on
|
|
570
570
|
* the tailnet). `hostname` is auto-detected when null.
|
|
571
571
|
* The service layer enforces that a non-loopback bind still carries auth.
|
package/dist/config/schema.js
CHANGED
|
@@ -86,10 +86,10 @@ export const AuthSchema = z
|
|
|
86
86
|
/**
|
|
87
87
|
* TLS termination, built into the brain so it can be exposed over HTTPS with no
|
|
88
88
|
* relay in front. Four modes:
|
|
89
|
-
* - `off`
|
|
90
|
-
* - `files`
|
|
91
|
-
* - `self-signed`
|
|
92
|
-
* - `tailscale`
|
|
89
|
+
* - `off` - plain HTTP (the default; loopback-only is the safe posture).
|
|
90
|
+
* - `files` - bring your own cert/key (a real cert, or one you manage).
|
|
91
|
+
* - `self-signed` - generate a local keypair on first run, cached under `certDir`.
|
|
92
|
+
* - `tailscale` - issue and auto-renew a real Let's Encrypt cert for this
|
|
93
93
|
* machine's MagicDNS name via `tailscaled` (no cert warnings on
|
|
94
94
|
* the tailnet). `hostname` is auto-detected when null.
|
|
95
95
|
* The service layer enforces that a non-loopback bind still carries auth.
|
|
@@ -147,11 +147,11 @@ export const BrainConfigSchema = z
|
|
|
147
147
|
// refuse completion requests that name a different one, instead of queuing a
|
|
148
148
|
// switch. For hosts that load one model and must not thrash between clients.
|
|
149
149
|
lockModel: z.boolean().default(false),
|
|
150
|
-
// Sharing/control gates (off by default
|
|
150
|
+
// Sharing/control gates (off by default - a brain is not remotely
|
|
151
151
|
// controllable until its owner opts in). `allowRemoteConfig`: a client with
|
|
152
152
|
// the token may CHANGE config over the network (POST /__host/config), not
|
|
153
153
|
// just use/read. `allowInsecureBind`: permit a non-loopback bind with no
|
|
154
|
-
// token (an "open, trusted network" share)
|
|
154
|
+
// token (an "open, trusted network" share) - otherwise the service refuses.
|
|
155
155
|
allowRemoteConfig: z.boolean().default(false),
|
|
156
156
|
allowInsecureBind: z.boolean().default(false),
|
|
157
157
|
defaults: ProfileDefaultsSchema.default({}),
|
package/dist/config/store.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type BrainConfig, type Catalog, type ProfilesStore } from "./schema.js"
|
|
|
3
3
|
export declare function loadProfilesStore(paths?: BrainPaths): ProfilesStore;
|
|
4
4
|
export declare function saveProfilesStore(store: ProfilesStore, paths?: BrainPaths): void;
|
|
5
5
|
export declare function loadCatalog(paths?: BrainPaths): Catalog;
|
|
6
|
-
/** Persisted config only
|
|
6
|
+
/** Persisted config only - no env overrides. Used by writers. */
|
|
7
7
|
export declare function loadPersistedConfig(paths?: BrainPaths): BrainConfig;
|
|
8
8
|
/** Effective config: persisted file with env overrides layered on top. */
|
|
9
9
|
export declare function loadBrainConfig(env?: NodeJS.ProcessEnv, paths?: BrainPaths): BrainConfig;
|
package/dist/config/store.js
CHANGED
|
@@ -53,7 +53,7 @@ export function loadCatalog(paths = resolveBrainPaths()) {
|
|
|
53
53
|
return CatalogSchema.parse({ models: [] });
|
|
54
54
|
}
|
|
55
55
|
// --------------------------------------------------------------------- config
|
|
56
|
-
/** Persisted config only
|
|
56
|
+
/** Persisted config only - no env overrides. Used by writers. */
|
|
57
57
|
export function loadPersistedConfig(paths = resolveBrainPaths()) {
|
|
58
58
|
const current = readJson(paths.configFile, BrainConfigSchema);
|
|
59
59
|
if (current)
|
package/dist/models/download.js
CHANGED
|
@@ -15,7 +15,7 @@ function resolveUrl(repo, file) {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* The GGUF filename to download. Prefer an explicit override, then the catalog
|
|
18
|
-
* entry's `quantFile`, then the basename of the catalog `id`
|
|
18
|
+
* entry's `quantFile`, then the basename of the catalog `id` - which for the
|
|
19
19
|
* seeded catalog is `<hfRepo>/<file>.gguf`, so the id already names the file.
|
|
20
20
|
* Only if none of those yields a `.gguf` do we give up: community repos name
|
|
21
21
|
* files inconsistently, so a bare id with no gguf basename still needs --file.
|
package/dist/models/enrich.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Reconciles scanned models back to their download-catalog entries so a model's
|
|
3
3
|
* coding metadata (useCases, tier, thinking, contextMax) survives a `pull`. The
|
|
4
4
|
* catalog carries this per entry, but once files land on disk scan.ts rebuilds a
|
|
5
|
-
* Model from filename + GGUF header alone, dropping it
|
|
5
|
+
* Model from filename + GGUF header alone, dropping it - this is where it is
|
|
6
6
|
* re-attached. Track B1 of the brain coding-capabilities work.
|
|
7
7
|
*
|
|
8
8
|
* The join key is the hfRepo path. download.ts writes each model to
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Total and best-effort by design: an empty catalog, a model with no match, or a
|
|
16
16
|
* repo carrying several quants all resolve without throwing. Discovery returns
|
|
17
|
-
* things unenriched on absence rather than raising
|
|
17
|
+
* things unenriched on absence rather than raising - the caller decides whether
|
|
18
18
|
* absence matters.
|
|
19
19
|
*/
|
|
20
20
|
import type { Catalog, CatalogModel } from "../config/schema.js";
|
package/dist/models/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BrainConfig } from "../config/schema.js";
|
|
2
2
|
import type { Model } from "../types.js";
|
|
3
3
|
export * from "./scan.js";
|
|
4
|
-
export { pickModel } from "./pick.js";
|
|
4
|
+
export { pickModel, pickAutoModel } from "./pick.js";
|
|
5
5
|
export { resolveModelsDirs, managedModelsDir, type ModelsDir } from "./dirs.js";
|
|
6
6
|
export { pullModel, downloadRepoFiles, type PullOptions, type PullProgress, type DownloadFilesOptions, } from "./download.js";
|
|
7
7
|
export { matchCatalogEntry, enrichWithCatalog } from "./enrich.js";
|
package/dist/models/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveModelsDirs } from "./dirs.js";
|
|
|
6
6
|
import { enrichWithCatalog } from "./enrich.js";
|
|
7
7
|
import { scan } from "./scan.js";
|
|
8
8
|
export * from "./scan.js";
|
|
9
|
-
export { pickModel } from "./pick.js";
|
|
9
|
+
export { pickModel, pickAutoModel } from "./pick.js";
|
|
10
10
|
export { resolveModelsDirs, managedModelsDir } from "./dirs.js";
|
|
11
11
|
export { pullModel, downloadRepoFiles, } from "./download.js";
|
|
12
12
|
export { matchCatalogEntry, enrichWithCatalog } from "./enrich.js";
|
package/dist/models/pick.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { type RankedModel } from "../ops/results.js";
|
|
1
2
|
import type { Model } from "../types.js";
|
|
2
3
|
export declare function pickModel(catalog: Model[], needle: string | undefined): Model;
|
|
4
|
+
/**
|
|
5
|
+
* Choose a model when nothing named one: the best-ranked model from bench
|
|
6
|
+
* history that is still installed, or the first catalog entry when nothing
|
|
7
|
+
* has been benched yet. This is what "Automatic" (a null default model) means
|
|
8
|
+
* in the UI - it must always start something for a non-empty catalog. The
|
|
9
|
+
* VRAM fit check downstream in startService is what can still refuse the pick.
|
|
10
|
+
*/
|
|
11
|
+
export declare function pickAutoModel(catalog: Model[], ranked?: RankedModel[]): Model;
|
|
3
12
|
//# sourceMappingURL=pick.d.ts.map
|
package/dist/models/pick.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolve a model from an exact catalog id or a unique case-insensitive name
|
|
3
|
-
* fragment. An ambiguous fragment is an error that lists the matches
|
|
3
|
+
* fragment. An ambiguous fragment is an error that lists the matches - ported
|
|
4
4
|
* from the original CLI's pickModel, but throwing a CommandError instead of
|
|
5
5
|
* calling process.exit, so the output layer renders it.
|
|
6
6
|
*/
|
|
7
7
|
import { CommandError } from "../output/types.js";
|
|
8
|
+
import { rankModels } from "../ops/results.js";
|
|
8
9
|
export function pickModel(catalog, needle) {
|
|
9
10
|
if (!needle) {
|
|
10
11
|
throw new CommandError({
|
|
@@ -33,4 +34,26 @@ export function pickModel(catalog, needle) {
|
|
|
33
34
|
}
|
|
34
35
|
return matches[0];
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Choose a model when nothing named one: the best-ranked model from bench
|
|
39
|
+
* history that is still installed, or the first catalog entry when nothing
|
|
40
|
+
* has been benched yet. This is what "Automatic" (a null default model) means
|
|
41
|
+
* in the UI - it must always start something for a non-empty catalog. The
|
|
42
|
+
* VRAM fit check downstream in startService is what can still refuse the pick.
|
|
43
|
+
*/
|
|
44
|
+
export function pickAutoModel(catalog, ranked = rankModels()) {
|
|
45
|
+
if (catalog.length === 0) {
|
|
46
|
+
throw new CommandError({
|
|
47
|
+
code: "NO_MODEL",
|
|
48
|
+
message: "no models installed",
|
|
49
|
+
details: "run `otto brain pull <model>` to download one, or point at an LM Studio install",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
for (const entry of ranked) {
|
|
53
|
+
const match = catalog.find((m) => m.id === entry.id);
|
|
54
|
+
if (match)
|
|
55
|
+
return match;
|
|
56
|
+
}
|
|
57
|
+
return catalog[0];
|
|
58
|
+
}
|
|
36
59
|
//# sourceMappingURL=pick.js.map
|