@otto-code/brain 0.8.10 → 0.8.13

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 (65) hide show
  1. package/dist/commands/bench.js +2 -2
  2. package/dist/commands/calibrate.js +11 -2
  3. package/dist/commands/catalog.d.ts +1 -0
  4. package/dist/commands/catalog.js +1 -0
  5. package/dist/commands/pull.d.ts +1 -0
  6. package/dist/commands/pull.js +12 -3
  7. package/dist/commands/search.d.ts +1 -0
  8. package/dist/commands/search.js +12 -2
  9. package/dist/config/index.d.ts +2 -2
  10. package/dist/config/index.js +2 -2
  11. package/dist/config/profile-edit.d.ts +88 -1
  12. package/dist/config/profile-edit.js +294 -43
  13. package/dist/config/profiles.d.ts +19 -3
  14. package/dist/config/profiles.js +52 -4
  15. package/dist/config/schema.d.ts +616 -0
  16. package/dist/config/schema.js +65 -3
  17. package/dist/config/store.js +7 -4
  18. package/dist/gguf.d.ts +7 -0
  19. package/dist/gguf.js +15 -2
  20. package/dist/models/download.d.ts +1 -1
  21. package/dist/models/download.js +2 -2
  22. package/dist/models/enrich.d.ts +6 -0
  23. package/dist/models/enrich.js +27 -1
  24. package/dist/models/index.d.ts +1 -1
  25. package/dist/models/index.js +4 -3
  26. package/dist/ops/archive.d.ts +14 -1
  27. package/dist/ops/archive.js +9 -5
  28. package/dist/ops/calibrate.d.ts +38 -3
  29. package/dist/ops/calibrate.js +68 -19
  30. package/dist/ops/report.js +51 -1
  31. package/dist/ops/results.d.ts +77 -11
  32. package/dist/ops/results.js +84 -14
  33. package/dist/ops/sweep.d.ts +38 -1
  34. package/dist/ops/sweep.js +61 -10
  35. package/dist/runtime/args.d.ts +15 -2
  36. package/dist/runtime/args.js +60 -5
  37. package/dist/runtime/managed.js +2 -2
  38. package/dist/service/activity.d.ts +19 -0
  39. package/dist/service/activity.js +47 -4
  40. package/dist/service/host-api.d.ts +28 -4
  41. package/dist/service/host-api.js +109 -28
  42. package/dist/service/log-format.d.ts +18 -0
  43. package/dist/service/log-format.js +32 -0
  44. package/dist/service/process-pool.d.ts +45 -0
  45. package/dist/service/process-pool.js +271 -0
  46. package/dist/service/router.d.ts +74 -3
  47. package/dist/service/router.js +277 -51
  48. package/dist/service/run-log.d.ts +6 -1
  49. package/dist/service/run-log.js +46 -4
  50. package/dist/service/scheduler.d.ts +250 -31
  51. package/dist/service/scheduler.js +408 -63
  52. package/dist/service/serve.d.ts +4 -0
  53. package/dist/service/serve.js +376 -142
  54. package/dist/service/status-events.d.ts +14 -1
  55. package/dist/service/status-events.js +112 -12
  56. package/dist/service/supervisor.d.ts +9 -7
  57. package/dist/service/supervisor.js +37 -12
  58. package/dist/sysmon.d.ts +15 -0
  59. package/dist/sysmon.js +56 -9
  60. package/dist/tui/app.d.ts +8 -2
  61. package/dist/tui/app.js +83 -26
  62. package/dist/types.d.ts +18 -0
  63. package/dist/vram.d.ts +37 -0
  64. package/dist/vram.js +57 -18
  65. package/package.json +1 -1
@@ -0,0 +1,45 @@
1
+ import type { Model } from "../types.js";
2
+ import type { ModelScheduler, SchedulerStats, SchedulerSubmitOptions } from "./scheduler.js";
3
+ import { Scheduler } from "./scheduler.js";
4
+ import type { Supervisor } from "./supervisor.js";
5
+ export interface ModelProcessPoolOptions {
6
+ initialSupervisor: Supervisor;
7
+ maxModels: number;
8
+ createSupervisor: (index: number) => Supervisor;
9
+ createScheduler: (supervisor: Supervisor, loadModel: (model: Model) => Promise<void>, onChange: () => void) => Scheduler<Supervisor>;
10
+ /** Returns the complete VRAM reservation for the process after it is ready. */
11
+ loadModel: (supervisor: Supervisor, model: Model, reservedElsewhereBytes: number) => Promise<number>;
12
+ onChange?: (() => void) | null;
13
+ logger?: ((message: string) => void) | null;
14
+ }
15
+ /**
16
+ * Global admission and eviction boundary for independently hosted models.
17
+ *
18
+ * Each resident model owns one Supervisor/Scheduler pair and therefore one
19
+ * llama-server process, port, KV pool, and lifecycle. Requests for a resident
20
+ * model go directly to that process. An unloaded model claims a free process
21
+ * slot, or evicts the least-recently-used idle process when the configured
22
+ * model limit is full. Busy processes are never evicted; the request remains
23
+ * queued until one becomes idle.
24
+ */
25
+ export declare class ModelProcessPool implements ModelScheduler<Supervisor> {
26
+ #private;
27
+ constructor(options: ModelProcessPoolOptions);
28
+ get maxModels(): number;
29
+ submit(model: Model, run: (supervisor: Supervisor) => Promise<unknown>, options?: SchedulerSubmitOptions): Promise<unknown>;
30
+ /** Load a model and leave it resident without consuming an inference slot. */
31
+ preload(model: Model): Promise<void>;
32
+ /** Apply the host-owned process limit and retire excess idle processes. */
33
+ configure(maxModels: number): Promise<void>;
34
+ supervisorFor(modelId: string): Supervisor | null;
35
+ supervisors(): Supervisor[];
36
+ /** Complete statuses for every process that currently owns a model. */
37
+ residentSupervisors(): Supervisor[];
38
+ reservationFor(modelId: string): number;
39
+ unload(modelId?: string | null): Promise<void>;
40
+ stop(): Promise<void>;
41
+ forgetSlots(): void;
42
+ stats(): SchedulerStats;
43
+ }
44
+ export declare function normalizeModelLimit(value: number): number;
45
+ //# sourceMappingURL=process-pool.d.ts.map
@@ -0,0 +1,271 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _ModelProcessPool_instances, _ModelProcessPool_createSupervisor, _ModelProcessPool_createScheduler, _ModelProcessPool_loadModel, _ModelProcessPool_onChange, _ModelProcessPool_logger, _ModelProcessPool_slots, _ModelProcessPool_queue, _ModelProcessPool_maxModels, _ModelProcessPool_busy, _ModelProcessPool_dirty, _ModelProcessPool_clock, _ModelProcessPool_makeSlot, _ModelProcessPool_announce, _ModelProcessPool_dispatch, _ModelProcessPool_pass, _ModelProcessPool_slotFor, _ModelProcessPool_nextSlotIndex, _ModelProcessPool_releaseSlot, _ModelProcessPool_trimIdleSlots;
13
+ /**
14
+ * Global admission and eviction boundary for independently hosted models.
15
+ *
16
+ * Each resident model owns one Supervisor/Scheduler pair and therefore one
17
+ * llama-server process, port, KV pool, and lifecycle. Requests for a resident
18
+ * model go directly to that process. An unloaded model claims a free process
19
+ * slot, or evicts the least-recently-used idle process when the configured
20
+ * model limit is full. Busy processes are never evicted; the request remains
21
+ * queued until one becomes idle.
22
+ */
23
+ export class ModelProcessPool {
24
+ constructor(options) {
25
+ _ModelProcessPool_instances.add(this);
26
+ _ModelProcessPool_createSupervisor.set(this, void 0);
27
+ _ModelProcessPool_createScheduler.set(this, void 0);
28
+ _ModelProcessPool_loadModel.set(this, void 0);
29
+ _ModelProcessPool_onChange.set(this, void 0);
30
+ _ModelProcessPool_logger.set(this, void 0);
31
+ _ModelProcessPool_slots.set(this, []);
32
+ _ModelProcessPool_queue.set(this, []);
33
+ _ModelProcessPool_maxModels.set(this, void 0);
34
+ _ModelProcessPool_busy.set(this, false);
35
+ _ModelProcessPool_dirty.set(this, false);
36
+ _ModelProcessPool_clock.set(this, 0);
37
+ __classPrivateFieldSet(this, _ModelProcessPool_createSupervisor, options.createSupervisor, "f");
38
+ __classPrivateFieldSet(this, _ModelProcessPool_createScheduler, options.createScheduler, "f");
39
+ __classPrivateFieldSet(this, _ModelProcessPool_loadModel, options.loadModel, "f");
40
+ __classPrivateFieldSet(this, _ModelProcessPool_onChange, options.onChange ?? null, "f");
41
+ __classPrivateFieldSet(this, _ModelProcessPool_logger, options.logger ?? null, "f");
42
+ __classPrivateFieldSet(this, _ModelProcessPool_maxModels, normalizeModelLimit(options.maxModels), "f");
43
+ __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").push(__classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_makeSlot).call(this, 0, options.initialSupervisor));
44
+ }
45
+ get maxModels() {
46
+ return __classPrivateFieldGet(this, _ModelProcessPool_maxModels, "f");
47
+ }
48
+ submit(model, run, options = {}) {
49
+ return new Promise((resolve, reject) => {
50
+ __classPrivateFieldGet(this, _ModelProcessPool_queue, "f").push({ model, run, options, resolve, reject });
51
+ __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_announce).call(this);
52
+ queueMicrotask(() => void __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_dispatch).call(this));
53
+ });
54
+ }
55
+ /** Load a model and leave it resident without consuming an inference slot. */
56
+ async preload(model) {
57
+ await this.submit(model, async () => undefined);
58
+ }
59
+ /** Apply the host-owned process limit and retire excess idle processes. */
60
+ async configure(maxModels) {
61
+ __classPrivateFieldSet(this, _ModelProcessPool_maxModels, normalizeModelLimit(maxModels), "f");
62
+ await __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_trimIdleSlots).call(this);
63
+ __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_announce).call(this);
64
+ void __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_dispatch).call(this);
65
+ }
66
+ supervisorFor(modelId) {
67
+ return (__classPrivateFieldGet(this, _ModelProcessPool_slots, "f").find((slot) => slot.assignedModelId === modelId ||
68
+ (slot.supervisor.model?.id === modelId && slot.supervisor.state !== "stopped"))?.supervisor ?? null);
69
+ }
70
+ supervisors() {
71
+ return __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").map((slot) => slot.supervisor);
72
+ }
73
+ /** Complete statuses for every process that currently owns a model. */
74
+ residentSupervisors() {
75
+ return __classPrivateFieldGet(this, _ModelProcessPool_slots, "f")
76
+ .filter((slot) => slot.assignedModelId !== null && slot.supervisor.state !== "stopped")
77
+ .map((slot) => slot.supervisor);
78
+ }
79
+ reservationFor(modelId) {
80
+ return __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").find((slot) => slot.assignedModelId === modelId)?.reservationBytes ?? 0;
81
+ }
82
+ async unload(modelId) {
83
+ const targets = modelId
84
+ ? __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").filter((slot) => slot.assignedModelId === modelId)
85
+ : [...__classPrivateFieldGet(this, _ModelProcessPool_slots, "f")];
86
+ for (const slot of targets) {
87
+ if (!slot.scheduler.isIdle || slot.pending > 0) {
88
+ throw new Error(modelId ? "the model is still serving requests" : "a model is still serving requests");
89
+ }
90
+ }
91
+ await Promise.all(targets.map((slot) => __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_releaseSlot).call(this, slot)));
92
+ await __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_trimIdleSlots).call(this);
93
+ __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_announce).call(this);
94
+ void __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_dispatch).call(this);
95
+ }
96
+ async stop() {
97
+ const error = new Error("Brain process pool stopped");
98
+ for (const job of __classPrivateFieldGet(this, _ModelProcessPool_queue, "f").splice(0))
99
+ job.reject(error);
100
+ await Promise.all(__classPrivateFieldGet(this, _ModelProcessPool_slots, "f").map((slot) => slot.supervisor.stop()));
101
+ __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_announce).call(this);
102
+ }
103
+ forgetSlots() {
104
+ for (const slot of __classPrivateFieldGet(this, _ModelProcessPool_slots, "f"))
105
+ slot.scheduler.forgetSlots();
106
+ }
107
+ stats() {
108
+ const waiting = {};
109
+ const waitingModelIds = {};
110
+ let queued = 0;
111
+ let lastTurn = null;
112
+ let active = null;
113
+ for (const job of __classPrivateFieldGet(this, _ModelProcessPool_queue, "f")) {
114
+ queued += 1;
115
+ waiting[job.model.displayName] = (waiting[job.model.displayName] ?? 0) + 1;
116
+ waitingModelIds[job.model.id] = (waitingModelIds[job.model.id] ?? 0) + 1;
117
+ }
118
+ for (const slot of __classPrivateFieldGet(this, _ModelProcessPool_slots, "f")) {
119
+ const stats = slot.scheduler.stats();
120
+ queued += stats.queued;
121
+ for (const [name, count] of Object.entries(stats.waiting)) {
122
+ waiting[name] = (waiting[name] ?? 0) + count;
123
+ }
124
+ for (const [id, count] of Object.entries(stats.waitingModelIds)) {
125
+ waitingModelIds[id] = (waitingModelIds[id] ?? 0) + count;
126
+ }
127
+ lastTurn = stats.lastTurn ?? lastTurn;
128
+ active = active ?? stats.active;
129
+ }
130
+ return { queued, waiting, waitingModelIds, lastTurn, active };
131
+ }
132
+ }
133
+ _ModelProcessPool_createSupervisor = new WeakMap(), _ModelProcessPool_createScheduler = new WeakMap(), _ModelProcessPool_loadModel = new WeakMap(), _ModelProcessPool_onChange = new WeakMap(), _ModelProcessPool_logger = new WeakMap(), _ModelProcessPool_slots = new WeakMap(), _ModelProcessPool_queue = new WeakMap(), _ModelProcessPool_maxModels = new WeakMap(), _ModelProcessPool_busy = new WeakMap(), _ModelProcessPool_dirty = new WeakMap(), _ModelProcessPool_clock = new WeakMap(), _ModelProcessPool_instances = new WeakSet(), _ModelProcessPool_makeSlot = function _ModelProcessPool_makeSlot(index, supervisor) {
134
+ var _a;
135
+ const slot = {
136
+ index,
137
+ supervisor,
138
+ scheduler: null,
139
+ assignedModelId: supervisor.model?.id ?? null,
140
+ pending: 0,
141
+ reservationBytes: 0,
142
+ lastUsedAt: __classPrivateFieldSet(this, _ModelProcessPool_clock, (_a = __classPrivateFieldGet(this, _ModelProcessPool_clock, "f"), ++_a), "f"),
143
+ };
144
+ slot.scheduler = __classPrivateFieldGet(this, _ModelProcessPool_createScheduler, "f").call(this, supervisor, async (model) => {
145
+ var _a;
146
+ const reservedElsewhereBytes = __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").reduce((total, candidate) => (candidate === slot ? total : total + candidate.reservationBytes), 0);
147
+ slot.reservationBytes = await __classPrivateFieldGet(this, _ModelProcessPool_loadModel, "f").call(this, supervisor, model, reservedElsewhereBytes);
148
+ slot.assignedModelId = model.id;
149
+ slot.lastUsedAt = __classPrivateFieldSet(this, _ModelProcessPool_clock, (_a = __classPrivateFieldGet(this, _ModelProcessPool_clock, "f"), ++_a), "f");
150
+ __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_announce).call(this);
151
+ }, () => {
152
+ __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_announce).call(this);
153
+ void __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_dispatch).call(this);
154
+ });
155
+ return slot;
156
+ }, _ModelProcessPool_announce = function _ModelProcessPool_announce() {
157
+ try {
158
+ __classPrivateFieldGet(this, _ModelProcessPool_onChange, "f")?.call(this);
159
+ }
160
+ catch {
161
+ // Status observers are not allowed to fail admission or eviction.
162
+ }
163
+ }, _ModelProcessPool_dispatch = async function _ModelProcessPool_dispatch() {
164
+ if (__classPrivateFieldGet(this, _ModelProcessPool_busy, "f")) {
165
+ __classPrivateFieldSet(this, _ModelProcessPool_dirty, true, "f");
166
+ return;
167
+ }
168
+ __classPrivateFieldSet(this, _ModelProcessPool_busy, true, "f");
169
+ try {
170
+ do {
171
+ __classPrivateFieldSet(this, _ModelProcessPool_dirty, false, "f");
172
+ await __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_pass).call(this);
173
+ } while (__classPrivateFieldGet(this, _ModelProcessPool_dirty, "f"));
174
+ }
175
+ finally {
176
+ __classPrivateFieldSet(this, _ModelProcessPool_busy, false, "f");
177
+ }
178
+ }, _ModelProcessPool_pass = async function _ModelProcessPool_pass() {
179
+ var _a;
180
+ await __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_trimIdleSlots).call(this);
181
+ for (;;) {
182
+ let selectedIndex = -1;
183
+ let selectedSlot = null;
184
+ for (let index = 0; index < __classPrivateFieldGet(this, _ModelProcessPool_queue, "f").length; index += 1) {
185
+ const candidate = __classPrivateFieldGet(this, _ModelProcessPool_queue, "f")[index];
186
+ const slot = await __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_slotFor).call(this, candidate.model);
187
+ if (!slot)
188
+ continue;
189
+ selectedIndex = index;
190
+ selectedSlot = slot;
191
+ break;
192
+ }
193
+ if (selectedIndex < 0 || !selectedSlot)
194
+ return;
195
+ const [job] = __classPrivateFieldGet(this, _ModelProcessPool_queue, "f").splice(selectedIndex, 1);
196
+ if (!job)
197
+ return;
198
+ const slot = selectedSlot;
199
+ slot.pending += 1;
200
+ slot.lastUsedAt = __classPrivateFieldSet(this, _ModelProcessPool_clock, (_a = __classPrivateFieldGet(this, _ModelProcessPool_clock, "f"), ++_a), "f");
201
+ __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_announce).call(this);
202
+ void slot.scheduler
203
+ .submit(job.model, job.run, job.options)
204
+ .then(job.resolve, job.reject)
205
+ .finally(() => {
206
+ var _a;
207
+ slot.pending = Math.max(0, slot.pending - 1);
208
+ slot.lastUsedAt = __classPrivateFieldSet(this, _ModelProcessPool_clock, (_a = __classPrivateFieldGet(this, _ModelProcessPool_clock, "f"), ++_a), "f");
209
+ if (slot.supervisor.state === "failed" && slot.pending === 0) {
210
+ void __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_releaseSlot).call(this, slot).finally(() => void __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_dispatch).call(this));
211
+ }
212
+ __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_announce).call(this);
213
+ void __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_dispatch).call(this);
214
+ });
215
+ }
216
+ }, _ModelProcessPool_slotFor = async function _ModelProcessPool_slotFor(model) {
217
+ const resident = __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").find((slot) => slot.assignedModelId === model.id);
218
+ if (resident)
219
+ return resident;
220
+ let empty = __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").find((slot) => slot.assignedModelId === null && slot.scheduler.isIdle && slot.pending === 0);
221
+ if (!empty && __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").length < __classPrivateFieldGet(this, _ModelProcessPool_maxModels, "f")) {
222
+ const index = __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_nextSlotIndex).call(this);
223
+ empty = __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_makeSlot).call(this, index, __classPrivateFieldGet(this, _ModelProcessPool_createSupervisor, "f").call(this, index));
224
+ __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").push(empty);
225
+ }
226
+ if (empty) {
227
+ empty.assignedModelId = model.id;
228
+ return empty;
229
+ }
230
+ const evictable = __classPrivateFieldGet(this, _ModelProcessPool_slots, "f")
231
+ .filter((slot) => slot.scheduler.isIdle && slot.pending === 0)
232
+ .sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
233
+ if (!evictable)
234
+ return null;
235
+ __classPrivateFieldGet(this, _ModelProcessPool_logger, "f")?.call(this, `evicting ${evictable.supervisor.model?.displayName ?? "idle model"} for ${model.displayName}`);
236
+ await __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_releaseSlot).call(this, evictable);
237
+ evictable.assignedModelId = model.id;
238
+ return evictable;
239
+ }, _ModelProcessPool_nextSlotIndex = function _ModelProcessPool_nextSlotIndex() {
240
+ const used = new Set(__classPrivateFieldGet(this, _ModelProcessPool_slots, "f").map((slot) => slot.index));
241
+ let index = 0;
242
+ while (used.has(index))
243
+ index += 1;
244
+ return index;
245
+ }, _ModelProcessPool_releaseSlot = async function _ModelProcessPool_releaseSlot(slot) {
246
+ var _a;
247
+ await slot.supervisor.stop();
248
+ slot.scheduler.forgetSlots();
249
+ slot.assignedModelId = null;
250
+ slot.reservationBytes = 0;
251
+ slot.lastUsedAt = __classPrivateFieldSet(this, _ModelProcessPool_clock, (_a = __classPrivateFieldGet(this, _ModelProcessPool_clock, "f"), ++_a), "f");
252
+ }, _ModelProcessPool_trimIdleSlots = async function _ModelProcessPool_trimIdleSlots() {
253
+ if (__classPrivateFieldGet(this, _ModelProcessPool_slots, "f").length <= __classPrivateFieldGet(this, _ModelProcessPool_maxModels, "f"))
254
+ return;
255
+ const removable = __classPrivateFieldGet(this, _ModelProcessPool_slots, "f")
256
+ .filter((slot) => slot.scheduler.isIdle && slot.pending === 0)
257
+ .sort((left, right) => left.lastUsedAt - right.lastUsedAt);
258
+ while (__classPrivateFieldGet(this, _ModelProcessPool_slots, "f").length > __classPrivateFieldGet(this, _ModelProcessPool_maxModels, "f") && removable.length > 0) {
259
+ const slot = removable.shift();
260
+ await __classPrivateFieldGet(this, _ModelProcessPool_instances, "m", _ModelProcessPool_releaseSlot).call(this, slot);
261
+ const index = __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").indexOf(slot);
262
+ if (index >= 0)
263
+ __classPrivateFieldGet(this, _ModelProcessPool_slots, "f").splice(index, 1);
264
+ }
265
+ };
266
+ export function normalizeModelLimit(value) {
267
+ if (!Number.isFinite(value))
268
+ return 1;
269
+ return Math.max(1, Math.min(16, Math.floor(value)));
270
+ }
271
+ //# sourceMappingURL=process-pool.js.map
@@ -1,4 +1,5 @@
1
1
  import http from "node:http";
2
+ import { type ModelScheduler } from "./scheduler.js";
2
3
  import type { Supervisor } from "./supervisor.js";
3
4
  import { type RankedModel } from "../ops/results.js";
4
5
  import type { GpuInfo, Model } from "../types.js";
@@ -6,8 +7,9 @@ import type { Profile } from "../config/schema.js";
6
7
  import { type HostApi } from "./host-api.js";
7
8
  import type { BrainStatusPublisher } from "./status-events.js";
8
9
  type Verdict = "ok" | "reasoning-only" | "truncated" | "failed";
9
- /** A logger sink; only `warn` is used by the router. */
10
+ /** Optional durable operational-log sink for completion lifecycle events. */
10
11
  export interface Logger {
12
+ info?(message: string): void;
11
13
  warn(message: string): void;
12
14
  }
13
15
  /** A source of the catalog: a getter, a snapshot array, or nothing. */
@@ -88,6 +90,8 @@ export interface ModelEntry {
88
90
  reasoning: boolean;
89
91
  /** Optional per-model values accepted by the OpenAI-compatible endpoint. */
90
92
  reasoning_efforts?: string[];
93
+ /** Optional model-native default among `reasoning_efforts`. */
94
+ reasoning_effort_default?: string;
91
95
  loaded_context_length?: number;
92
96
  }
93
97
  /**
@@ -104,7 +108,69 @@ export declare function describeModel(model: Model | null, options?: DescribeOpt
104
108
  * the supervisor is running marked 'loaded'. Falls back to just the running
105
109
  * model when no catalog provider is wired in.
106
110
  */
107
- export declare function buildModelList(supervisor: Supervisor, getCatalog: GetCatalog): ModelEntry[];
111
+ export declare function buildModelList(supervisor: Supervisor, getCatalog: GetCatalog, scheduler?: ModelScheduler<Supervisor> | null): ModelEntry[];
112
+ /**
113
+ * Map an OpenAI-compatible effort request onto a model's own chat-template
114
+ * arguments. llama.cpp does not know every model's dialect: Qwen3.8 calls the
115
+ * controls `enable_thinking` and `reasoning_effort`, for example. Only catalog
116
+ * entries that declare these names are rewritten, so generic models and GPT-OSS
117
+ * keep their existing server-native request handling.
118
+ */
119
+ export declare function applyModelReasoningTemplate(body: Buffer, model: Model): Buffer;
120
+ /**
121
+ * Pin the request to one llama-server slot by adding the engine's own
122
+ * `id_slot` field to the request body (host API v3).
123
+ *
124
+ * Why pin instead of guess: the OpenAI-compatible stream chunks never carry the
125
+ * slot id, so without a pin the router can only correlate a request to a slot
126
+ * by elimination, and with several concurrent requests that guess is exactly
127
+ * the lie the Overview panel used to tell. llama-server honors the pin on every
128
+ * completion endpoint: if the named slot is free the task lands there, and if
129
+ * it is busy the engine DEFERS the task internally - it never reassigns the
130
+ * task elsewhere and never fails the request - so the slot this router names is
131
+ * always the slot the request ends up on (possibly after waiting on it).
132
+ *
133
+ * `null` returns the body untouched: no pin, and therefore no join data for
134
+ * this request, which is the same degraded state as an older brain. A body this
135
+ * cannot parse is forwarded exactly as-is - an unfamiliar request must reach
136
+ * llama-server and get llama-server's own answer, not a 400 invented here.
137
+ */
138
+ export declare function pinSlot(body: Buffer, slotId: number | null): Buffer;
139
+ /**
140
+ * Wipe one llama-server slot's retained KV state, and RESOLVE only once the
141
+ * engine has acknowledged the wipe.
142
+ *
143
+ * This is the engine-side half of the scheduler's OWNERSHIP fix. The engine
144
+ * never clears a released slot's prompt, so a slot handed to a different chat
145
+ * would keep the previous chat's KV and bleed its topics into the new chat's
146
+ * thinking. The router erases the slot the moment the scheduler hands it off;
147
+ * the engine's task queue runs in arrival order, so resolving on the
148
+ * acknowledgment is what guarantees the clean state sits in the queue ahead of
149
+ * the completion the scheduler posts right after.
150
+ *
151
+ * The route is `POST /slots?action=erase&id_slot=N` - llama.cpp's own slot
152
+ * action. It answers 200 `{id, id_slot, n_erased}` on success and a
153
+ * `NOT_SUPPORTED` error when the server was not launched with a slot-save path;
154
+ * either way this resolves (never rejects), because an erase that cannot be
155
+ * performed degrades to the old behavior rather than failing the completion.
156
+ *
157
+ * NOTE: `action` and `id_slot` MUST travel in the query string, not the JSON
158
+ * body. llama-server's `POST /slots` handler reads both via `req.get_param()`,
159
+ * which is built only from query + path params (b10441 tools/server/server-http.cpp,
160
+ * `server_http_req::params` = "path_params + query_params"; the body is a
161
+ * separate field the handler never parses for this route). A body-only request
162
+ * reaches `std::stoi("")` and answers 400 "Invalid slot ID" - the erase then
163
+ * silently no-ops and the bleed survives. The body must stay empty for the
164
+ * same reason `handle_slots_erase` ignores it entirely.
165
+ */
166
+ export declare function eraseSlot(host: string, port: number, slotId: number): Promise<void>;
167
+ /**
168
+ * The eraser the scheduler needs, bound to one engine endpoint. Extracted so
169
+ * the router (which builds its own scheduler) and the service (which builds a
170
+ * shared one and passes it in) hand the scheduler the SAME transport rather
171
+ * than each spelling the request.
172
+ */
173
+ export declare function createSlotEraser(host: string, port: number): (slotId: number) => Promise<void>;
108
174
  /** Which shape a completion path uses to carry its system turn. */
109
175
  export type CompletionShape = "anthropic" | "openai";
110
176
  export declare function completionShape(url: string | null | undefined): CompletionShape;
@@ -141,6 +207,7 @@ export declare function decideModelGate(params: {
141
207
  lockModel: boolean;
142
208
  requestedName: string | null;
143
209
  pinned: Model | null;
210
+ pinnedModels?: Model[];
144
211
  resolved: Model | null;
145
212
  }): ModelGateResult;
146
213
  export interface RouterOptions {
@@ -161,6 +228,8 @@ export interface RouterOptions {
161
228
  getLockModel?: () => boolean;
162
229
  /** Live: the configured default model, the pin target before one is resident. */
163
230
  getDefaultModel?: () => string | null;
231
+ /** Live: the complete model set served while locking is enabled. */
232
+ getLockedModels?: () => string[];
164
233
  /**
165
234
  * Apply an editable config patch (write config.json, live-switch the model,
166
235
  * update the lock), for POST /__host/config. Absent = the write endpoint is
@@ -195,7 +264,9 @@ export interface RouterOptions {
195
264
  * can never disagree. Absent means this brain does not advertise events.
196
265
  */
197
266
  statusEvents?: BrainStatusPublisher | null;
267
+ /** A service shares this scheduler with host-owned model operations. */
268
+ scheduler?: ModelScheduler<Supervisor> | null;
198
269
  }
199
- export declare function createRouter({ supervisor, telemetry, logger, getCatalog, loadModel, loadRanking, queryGpuInfo, version, getConfig, getEvals, getLockModel, getDefaultModel, applyConfigPatch, getAllowConfigWrite, hostApi, getResources, statusEvents, }: RouterOptions): (req: http.IncomingMessage, res: http.ServerResponse) => void;
270
+ export declare function createRouter({ supervisor, telemetry, logger, getCatalog, loadModel, loadRanking, queryGpuInfo, version, getConfig, getEvals, getLockModel, getDefaultModel, getLockedModels, applyConfigPatch, getAllowConfigWrite, hostApi, getResources, statusEvents, scheduler: suppliedScheduler, }: RouterOptions): (req: http.IncomingMessage, res: http.ServerResponse) => void;
200
271
  export {};
201
272
  //# sourceMappingURL=router.d.ts.map