@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.
- package/dist/commands/bench.js +2 -2
- package/dist/commands/calibrate.js +11 -2
- 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 +2 -2
- package/dist/config/index.js +2 -2
- package/dist/config/profile-edit.d.ts +88 -1
- package/dist/config/profile-edit.js +294 -43
- package/dist/config/profiles.d.ts +19 -3
- package/dist/config/profiles.js +52 -4
- package/dist/config/schema.d.ts +616 -0
- package/dist/config/schema.js +65 -3
- 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/archive.d.ts +14 -1
- package/dist/ops/archive.js +9 -5
- 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 +77 -11
- package/dist/ops/results.js +84 -14
- 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 +28 -4
- package/dist/service/host-api.js +109 -28
- package/dist/service/log-format.d.ts +18 -0
- package/dist/service/log-format.js +32 -0
- package/dist/service/process-pool.d.ts +45 -0
- package/dist/service/process-pool.js +271 -0
- package/dist/service/router.d.ts +74 -3
- package/dist/service/router.js +277 -51
- package/dist/service/run-log.d.ts +6 -1
- package/dist/service/run-log.js +46 -4
- package/dist/service/scheduler.d.ts +250 -31
- package/dist/service/scheduler.js +408 -63
- package/dist/service/serve.d.ts +4 -0
- package/dist/service/serve.js +376 -142
- package/dist/service/status-events.d.ts +14 -1
- package/dist/service/status-events.js +112 -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 +83 -26
- 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
|
@@ -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
|
package/dist/service/router.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|