turbollm 0.7.1 → 0.7.2
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/cli.js +359 -195
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -288,9 +288,9 @@ function clampMaxTokens(requested, limit) {
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
// src/engines/manager.ts
|
|
291
|
-
import { execFile as execFile4, spawn } from "child_process";
|
|
292
|
-
import { createWriteStream as createWriteStream2, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2 } from "fs";
|
|
293
|
-
import { createServer } from "net";
|
|
291
|
+
import { execFile as execFile4, spawn, spawnSync } from "child_process";
|
|
292
|
+
import { createWriteStream as createWriteStream2, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync2, rmSync as rmSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
293
|
+
import { createConnection, createServer } from "net";
|
|
294
294
|
import { dirname as dirname2, join as join6 } from "path";
|
|
295
295
|
|
|
296
296
|
// src/engines/mlx.ts
|
|
@@ -773,12 +773,35 @@ var BusyError = class extends Error {
|
|
|
773
773
|
this.name = "BusyError";
|
|
774
774
|
}
|
|
775
775
|
};
|
|
776
|
-
var Manager = class {
|
|
776
|
+
var Manager = class _Manager {
|
|
777
777
|
constructor(store2) {
|
|
778
778
|
this.store = store2;
|
|
779
779
|
setInterval(() => this.watchdogTick(), 6e4).unref();
|
|
780
780
|
}
|
|
781
781
|
store;
|
|
782
|
+
/** Global single-load lock (rules 1 & 2). Shared across EVERY Manager instance —
|
|
783
|
+
* including the gateway keep-N pool's extra slots — so at most ONE model load /
|
|
784
|
+
* reload is ever in flight at a time, no matter who requests it. Holding it
|
|
785
|
+
* through readiness (not just spawn) guarantees two engines never spin up at
|
|
786
|
+
* once and double-allocate VRAM. All load paths funnel through start()/load(),
|
|
787
|
+
* the only entry points that touch the engine — nothing spawns an engine without
|
|
788
|
+
* passing this gate (rule 3). */
|
|
789
|
+
static loadGate = Promise.resolve();
|
|
790
|
+
/** Acquire the global load gate, run `fn` exclusively, then release it. Queued
|
|
791
|
+
* callers run in FIFO order; a thrown fn still releases the gate. */
|
|
792
|
+
static async runExclusive(fn) {
|
|
793
|
+
const prev = _Manager.loadGate;
|
|
794
|
+
let release;
|
|
795
|
+
_Manager.loadGate = new Promise((r) => {
|
|
796
|
+
release = r;
|
|
797
|
+
});
|
|
798
|
+
await prev;
|
|
799
|
+
try {
|
|
800
|
+
return await fn();
|
|
801
|
+
} finally {
|
|
802
|
+
release();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
782
805
|
state = "stopped";
|
|
783
806
|
opts = null;
|
|
784
807
|
port = 0;
|
|
@@ -793,7 +816,41 @@ var Manager = class {
|
|
|
793
816
|
generation = 0;
|
|
794
817
|
liveGen = null;
|
|
795
818
|
session = freshSession();
|
|
819
|
+
/** Load a model, freeing any currently-loaded one first — the single atomic
|
|
820
|
+
* swap entry point (rules 1–3). Runs under the global load gate so the stop +
|
|
821
|
+
* optional pre-start hook (e.g. the ComfyUI VRAM free) + spawn + readiness wait
|
|
822
|
+
* are one indivisible operation; no other load can interleave. Never throws on a
|
|
823
|
+
* model that simply fails to load — callers read status() for the running/error
|
|
824
|
+
* outcome. */
|
|
825
|
+
async load(opts, hooks) {
|
|
826
|
+
await _Manager.runExclusive(async () => {
|
|
827
|
+
if (this.state === "running" || this.state === "starting" || this.state === "stopping") {
|
|
828
|
+
await this.stopAndWait();
|
|
829
|
+
}
|
|
830
|
+
if (hooks?.beforeStart) await hooks.beforeStart();
|
|
831
|
+
await this.startInternal(opts);
|
|
832
|
+
await this.awaitNotStarting();
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
/** Start a model assuming nothing is loaded (throws BusyError otherwise). Held by
|
|
836
|
+
* the global load gate through readiness so concurrent loads can't spin up two
|
|
837
|
+
* engines at once. Most callers want load() (which stops first); this exists for
|
|
838
|
+
* paths that have already ensured the engine is free (bench, ComfyUI reload). */
|
|
796
839
|
async start(opts) {
|
|
840
|
+
await _Manager.runExclusive(async () => {
|
|
841
|
+
await this.startInternal(opts);
|
|
842
|
+
await this.awaitNotStarting();
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
/** Wait until the engine leaves the 'starting' state (→ running or error/stopped),
|
|
846
|
+
* bounded by the engine kind's readiness window plus a small grace. The internal
|
|
847
|
+
* readiness loop flips the state and surfaces errors; this just keeps the load
|
|
848
|
+
* gate held until that resolves. */
|
|
849
|
+
async awaitNotStarting() {
|
|
850
|
+
const deadline = Date.now() + readinessTimeoutMs(this.opts?.engine.kind ?? "llama-server") + 5e3;
|
|
851
|
+
while (this.state === "starting" && Date.now() < deadline) await sleep(200);
|
|
852
|
+
}
|
|
853
|
+
async startInternal(opts) {
|
|
797
854
|
if (this.state === "starting" || this.state === "running" || this.state === "stopping") {
|
|
798
855
|
throw new BusyError();
|
|
799
856
|
}
|
|
@@ -825,6 +882,7 @@ var Manager = class {
|
|
|
825
882
|
this.opts = opts;
|
|
826
883
|
this.port = port2;
|
|
827
884
|
this.pid = child.pid ?? 0;
|
|
885
|
+
if (this.pid) writeEnginePid(this.store.dir(), this.pid, port2);
|
|
828
886
|
this.child = child;
|
|
829
887
|
this.startedAt = Date.now();
|
|
830
888
|
this.errInfo = null;
|
|
@@ -870,14 +928,8 @@ var Manager = class {
|
|
|
870
928
|
}
|
|
871
929
|
async restart() {
|
|
872
930
|
const opts = this.opts;
|
|
873
|
-
const running = this.state === "running" || this.state === "starting";
|
|
874
|
-
const exited = this.exited;
|
|
875
931
|
if (!opts?.modelPath) throw new Error("no_such_model");
|
|
876
|
-
|
|
877
|
-
this.stop();
|
|
878
|
-
await Promise.race([exited, sleep(1e4)]);
|
|
879
|
-
}
|
|
880
|
-
await this.start(opts);
|
|
932
|
+
await this.load(opts);
|
|
881
933
|
}
|
|
882
934
|
status() {
|
|
883
935
|
const st = { state: this.state, err: this.errInfo, port: this.port, pid: this.pid, model: null, loadElapsedMs: 0 };
|
|
@@ -960,6 +1012,7 @@ var Manager = class {
|
|
|
960
1012
|
// ---- internal ----------------------------------------------------------
|
|
961
1013
|
onTerminated(child, code, logStream, errMsg) {
|
|
962
1014
|
if (this.child !== child) return;
|
|
1015
|
+
if (child.pid) clearEnginePid(this.store.dir(), child.pid);
|
|
963
1016
|
const cleanStop = this.state === "stopping" || this.state === "stopped";
|
|
964
1017
|
try {
|
|
965
1018
|
logStream.write(
|
|
@@ -1130,6 +1183,114 @@ function forceKill(child) {
|
|
|
1130
1183
|
child.kill("SIGKILL");
|
|
1131
1184
|
}
|
|
1132
1185
|
}
|
|
1186
|
+
function enginePidDir(dataDir) {
|
|
1187
|
+
return join6(dataDir, "run");
|
|
1188
|
+
}
|
|
1189
|
+
function writeEnginePid(dataDir, pid, port2) {
|
|
1190
|
+
try {
|
|
1191
|
+
const dir = enginePidDir(dataDir);
|
|
1192
|
+
mkdirSync4(dir, { recursive: true });
|
|
1193
|
+
writeFileSync2(join6(dir, `engine-${pid}.pid`), JSON.stringify({ pid, port: port2, owner: process.pid }));
|
|
1194
|
+
} catch {
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function clearEnginePid(dataDir, pid) {
|
|
1198
|
+
try {
|
|
1199
|
+
rmSync4(join6(enginePidDir(dataDir), `engine-${pid}.pid`), { force: true });
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
function pidAlive(pid) {
|
|
1204
|
+
if (!pid) return false;
|
|
1205
|
+
try {
|
|
1206
|
+
process.kill(pid, 0);
|
|
1207
|
+
return true;
|
|
1208
|
+
} catch (e) {
|
|
1209
|
+
return e.code === "EPERM";
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
function readEnginePidFiles(dataDir) {
|
|
1213
|
+
const dir = enginePidDir(dataDir);
|
|
1214
|
+
let names;
|
|
1215
|
+
try {
|
|
1216
|
+
names = readdirSync3(dir).filter((n) => /^engine-\d+\.pid$/.test(n));
|
|
1217
|
+
} catch {
|
|
1218
|
+
return [];
|
|
1219
|
+
}
|
|
1220
|
+
const out = [];
|
|
1221
|
+
for (const name of names) {
|
|
1222
|
+
const file = join6(dir, name);
|
|
1223
|
+
try {
|
|
1224
|
+
const { pid, port: port2, owner } = JSON.parse(readFileSync2(file, "utf8"));
|
|
1225
|
+
if (typeof pid === "number" && pid > 0) {
|
|
1226
|
+
out.push({ pid, port: typeof port2 === "number" ? port2 : 0, owner: typeof owner === "number" ? owner : 0, file });
|
|
1227
|
+
} else rmSync4(file, { force: true });
|
|
1228
|
+
} catch {
|
|
1229
|
+
try {
|
|
1230
|
+
rmSync4(file, { force: true });
|
|
1231
|
+
} catch {
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return out;
|
|
1236
|
+
}
|
|
1237
|
+
function portAlive(port2, timeoutMs = 600) {
|
|
1238
|
+
if (!port2) return Promise.resolve(false);
|
|
1239
|
+
return new Promise((resolve2) => {
|
|
1240
|
+
const sock = createConnection({ host: "127.0.0.1", port: port2 });
|
|
1241
|
+
const done = (alive) => {
|
|
1242
|
+
sock.destroy();
|
|
1243
|
+
resolve2(alive);
|
|
1244
|
+
};
|
|
1245
|
+
sock.setTimeout(timeoutMs);
|
|
1246
|
+
sock.once("connect", () => done(true));
|
|
1247
|
+
sock.once("timeout", () => done(false));
|
|
1248
|
+
sock.once("error", () => done(false));
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
async function reapStaleEngines(dataDir) {
|
|
1252
|
+
let killed = 0;
|
|
1253
|
+
for (const { pid, port: port2, owner, file } of readEnginePidFiles(dataDir)) {
|
|
1254
|
+
if (owner && pidAlive(owner)) continue;
|
|
1255
|
+
if (await portAlive(port2)) {
|
|
1256
|
+
killPidTree(pid);
|
|
1257
|
+
killed++;
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
rmSync4(file, { force: true });
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return killed;
|
|
1265
|
+
}
|
|
1266
|
+
function killTrackedEnginesSync(dataDir) {
|
|
1267
|
+
for (const { pid, owner, file } of readEnginePidFiles(dataDir)) {
|
|
1268
|
+
if (owner !== process.pid) continue;
|
|
1269
|
+
try {
|
|
1270
|
+
if (process.platform === "win32") {
|
|
1271
|
+
spawnSync("taskkill", ["/PID", String(pid), "/F", "/T"]);
|
|
1272
|
+
} else {
|
|
1273
|
+
process.kill(pid, "SIGKILL");
|
|
1274
|
+
}
|
|
1275
|
+
} catch {
|
|
1276
|
+
}
|
|
1277
|
+
try {
|
|
1278
|
+
rmSync4(file, { force: true });
|
|
1279
|
+
} catch {
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function killPidTree(pid) {
|
|
1284
|
+
if (process.platform === "win32") {
|
|
1285
|
+
execFile4("taskkill", ["/PID", String(pid), "/F", "/T"], () => {
|
|
1286
|
+
});
|
|
1287
|
+
} else {
|
|
1288
|
+
try {
|
|
1289
|
+
process.kill(pid, "SIGKILL");
|
|
1290
|
+
} catch {
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1133
1294
|
function readTail(path, n) {
|
|
1134
1295
|
if (!path || !existsSync6(path)) return [];
|
|
1135
1296
|
try {
|
|
@@ -1790,7 +1951,7 @@ function engineModelAlias(engineKind) {
|
|
|
1790
1951
|
}
|
|
1791
1952
|
|
|
1792
1953
|
// src/models/scanner.ts
|
|
1793
|
-
import { existsSync as existsSync9, lstatSync, readdirSync as
|
|
1954
|
+
import { existsSync as existsSync9, lstatSync, readdirSync as readdirSync4, readFileSync as readFileSync3, rmSync as rmSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1794
1955
|
import { basename, dirname as dirname4, join as join7 } from "path";
|
|
1795
1956
|
|
|
1796
1957
|
// src/gguf/gguf.ts
|
|
@@ -2099,7 +2260,7 @@ var Scanner = class {
|
|
|
2099
2260
|
const total = m[3];
|
|
2100
2261
|
let names;
|
|
2101
2262
|
try {
|
|
2102
|
-
names =
|
|
2263
|
+
names = readdirSync4(e.dir);
|
|
2103
2264
|
} catch {
|
|
2104
2265
|
return [e.path];
|
|
2105
2266
|
}
|
|
@@ -2118,9 +2279,9 @@ var Scanner = class {
|
|
|
2118
2279
|
if (!e) throw new ScannerError("no_such_model", "No model with that key.");
|
|
2119
2280
|
const paths = this.filesFor(key);
|
|
2120
2281
|
if (e.format === "mlx") {
|
|
2121
|
-
|
|
2282
|
+
rmSync5(e.path, { recursive: true, force: true });
|
|
2122
2283
|
} else {
|
|
2123
|
-
for (const p of paths)
|
|
2284
|
+
for (const p of paths) rmSync5(p, { force: true });
|
|
2124
2285
|
this.cache.delete(e.path);
|
|
2125
2286
|
}
|
|
2126
2287
|
await this.rescan();
|
|
@@ -2247,7 +2408,7 @@ var Scanner = class {
|
|
|
2247
2408
|
const entries = {};
|
|
2248
2409
|
for (const [k, v] of this.cache) entries[k] = v;
|
|
2249
2410
|
try {
|
|
2250
|
-
|
|
2411
|
+
writeFileSync3(this.cachePath, JSON.stringify({ version: CACHE_VERSION, entries }));
|
|
2251
2412
|
} catch {
|
|
2252
2413
|
}
|
|
2253
2414
|
}
|
|
@@ -2262,7 +2423,7 @@ function isMlxModelDir(names) {
|
|
|
2262
2423
|
function walk(dir, out) {
|
|
2263
2424
|
let names;
|
|
2264
2425
|
try {
|
|
2265
|
-
names =
|
|
2426
|
+
names = readdirSync4(dir);
|
|
2266
2427
|
} catch {
|
|
2267
2428
|
return;
|
|
2268
2429
|
}
|
|
@@ -2299,7 +2460,7 @@ function mlxEntryFor(dir) {
|
|
|
2299
2460
|
let mtimeMs = 0;
|
|
2300
2461
|
let hasChatTemplate = false;
|
|
2301
2462
|
try {
|
|
2302
|
-
for (const n of
|
|
2463
|
+
for (const n of readdirSync4(dir)) {
|
|
2303
2464
|
const lower = n.toLowerCase();
|
|
2304
2465
|
if (lower.endsWith(".safetensors")) {
|
|
2305
2466
|
const st = lstatSync(join7(dir, n));
|
|
@@ -2368,7 +2529,7 @@ function tick() {
|
|
|
2368
2529
|
|
|
2369
2530
|
// src/models/hashes.ts
|
|
2370
2531
|
import { createHash as createHash2 } from "crypto";
|
|
2371
|
-
import { createReadStream, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as
|
|
2532
|
+
import { createReadStream, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2372
2533
|
import { join as join8 } from "path";
|
|
2373
2534
|
var HashStore = class {
|
|
2374
2535
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -2419,7 +2580,7 @@ var HashStore = class {
|
|
|
2419
2580
|
const entries = {};
|
|
2420
2581
|
for (const [k, v] of this.cache) entries[k] = v;
|
|
2421
2582
|
try {
|
|
2422
|
-
|
|
2583
|
+
writeFileSync4(this.path, JSON.stringify({ version: 1, entries }));
|
|
2423
2584
|
} catch {
|
|
2424
2585
|
}
|
|
2425
2586
|
}
|
|
@@ -3026,10 +3187,10 @@ import {
|
|
|
3026
3187
|
mkdirSync as mkdirSync6,
|
|
3027
3188
|
readFileSync as readFileSync5,
|
|
3028
3189
|
renameSync as renameSync2,
|
|
3029
|
-
rmSync as
|
|
3190
|
+
rmSync as rmSync6,
|
|
3030
3191
|
statfsSync,
|
|
3031
3192
|
statSync as statSync3,
|
|
3032
|
-
writeFileSync as
|
|
3193
|
+
writeFileSync as writeFileSync5
|
|
3033
3194
|
} from "fs";
|
|
3034
3195
|
import { basename as basename2, join as join10 } from "path";
|
|
3035
3196
|
import { Readable as Readable2 } from "stream";
|
|
@@ -3140,7 +3301,7 @@ var DownloadManager = class {
|
|
|
3140
3301
|
this.controllers.get(id)?.abort();
|
|
3141
3302
|
this.controllers.delete(id);
|
|
3142
3303
|
if (rec.status !== "done") {
|
|
3143
|
-
|
|
3304
|
+
rmSync6(`${rec.dest}.part`, { force: true });
|
|
3144
3305
|
rec.status = "cancelled";
|
|
3145
3306
|
rec.bytesPerSec = 0;
|
|
3146
3307
|
}
|
|
@@ -3154,7 +3315,7 @@ var DownloadManager = class {
|
|
|
3154
3315
|
if (!rec) return false;
|
|
3155
3316
|
this.controllers.get(id)?.abort();
|
|
3156
3317
|
this.controllers.delete(id);
|
|
3157
|
-
if (rec.status !== "done")
|
|
3318
|
+
if (rec.status !== "done") rmSync6(`${rec.dest}.part`, { force: true });
|
|
3158
3319
|
this.records.delete(id);
|
|
3159
3320
|
this.persist();
|
|
3160
3321
|
this.pump();
|
|
@@ -3219,7 +3380,7 @@ var DownloadManager = class {
|
|
|
3219
3380
|
if (!res.body) throw new DownloadError("download_failed", "Empty response body.");
|
|
3220
3381
|
const resuming = res.status === 206;
|
|
3221
3382
|
if (!resuming && startAt > 0) {
|
|
3222
|
-
|
|
3383
|
+
rmSync6(part, { force: true });
|
|
3223
3384
|
startAt = 0;
|
|
3224
3385
|
rec.received = 0;
|
|
3225
3386
|
}
|
|
@@ -3244,13 +3405,13 @@ var DownloadManager = class {
|
|
|
3244
3405
|
const out = createWriteStream3(part, startAt > 0 ? { flags: "a" } : { flags: "w" });
|
|
3245
3406
|
await pipeline2(body3, out, { signal: ac.signal });
|
|
3246
3407
|
if (rec.total > 0 && rec.received !== rec.total) {
|
|
3247
|
-
|
|
3408
|
+
rmSync6(part, { force: true });
|
|
3248
3409
|
throw new DownloadError("size_mismatch", "Download corrupt \u2014 size did not match. Removed the partial file.");
|
|
3249
3410
|
}
|
|
3250
3411
|
if (verifyHash && rec.sha256) {
|
|
3251
3412
|
const got = hash.digest("hex");
|
|
3252
3413
|
if (got !== rec.sha256) {
|
|
3253
|
-
|
|
3414
|
+
rmSync6(part, { force: true });
|
|
3254
3415
|
throw new DownloadError("checksum_failed", "Checksum failed \u2014 the downloaded file was corrupt.");
|
|
3255
3416
|
}
|
|
3256
3417
|
}
|
|
@@ -3303,7 +3464,7 @@ var DownloadManager = class {
|
|
|
3303
3464
|
}
|
|
3304
3465
|
saveProvenance() {
|
|
3305
3466
|
try {
|
|
3306
|
-
|
|
3467
|
+
writeFileSync5(this.provenancePath, JSON.stringify({ version: 1, entries: this.provenanceList }, null, 2));
|
|
3307
3468
|
} catch {
|
|
3308
3469
|
}
|
|
3309
3470
|
}
|
|
@@ -3325,7 +3486,7 @@ var DownloadManager = class {
|
|
|
3325
3486
|
});
|
|
3326
3487
|
}
|
|
3327
3488
|
try {
|
|
3328
|
-
|
|
3489
|
+
writeFileSync5(this.manifestPath, JSON.stringify({ version: 1, entries }, null, 2));
|
|
3329
3490
|
} catch {
|
|
3330
3491
|
}
|
|
3331
3492
|
}
|
|
@@ -3375,7 +3536,7 @@ function safePathname(u) {
|
|
|
3375
3536
|
|
|
3376
3537
|
// src/bench/bench.ts
|
|
3377
3538
|
import { execFile as execFile6 } from "child_process";
|
|
3378
|
-
import { mkdirSync as mkdirSync7, writeFileSync as
|
|
3539
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
3379
3540
|
import { join as join11 } from "path";
|
|
3380
3541
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3381
3542
|
var READY_TIMEOUT_MS = 12e4;
|
|
@@ -3646,7 +3807,7 @@ var BenchRunner = class {
|
|
|
3646
3807
|
};
|
|
3647
3808
|
const queueDir = join11(this.store.dir(), "telemetry", "queue");
|
|
3648
3809
|
mkdirSync7(queueDir, { recursive: true });
|
|
3649
|
-
|
|
3810
|
+
writeFileSync6(join11(queueDir, `${randomUUID4()}.json`), JSON.stringify(event));
|
|
3650
3811
|
} catch {
|
|
3651
3812
|
}
|
|
3652
3813
|
}
|
|
@@ -3783,16 +3944,15 @@ var ModelRouter = class {
|
|
|
3783
3944
|
const keepN = Math.max(1, this.store.snapshot().gateway.keepN);
|
|
3784
3945
|
const needsNewSlot = entry.embedding || this.chatSlotCount() < keepN;
|
|
3785
3946
|
const targetManager = needsNewSlot ? this.manager.status().state === "stopped" || this.manager.status().state === "error" ? this.manager : new Manager(this.store) : this.evictChatLru();
|
|
3786
|
-
await targetManager.stopAndWait();
|
|
3787
|
-
await this.comfy?.freeComfyUIBeforeLoad();
|
|
3788
3947
|
try {
|
|
3789
|
-
await targetManager.
|
|
3948
|
+
await targetManager.load(opts, {
|
|
3949
|
+
beforeStart: () => this.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve()
|
|
3950
|
+
});
|
|
3790
3951
|
} catch (e) {
|
|
3791
3952
|
return { status: 503, message: `Engine start failed: ${e.message}` };
|
|
3792
3953
|
}
|
|
3793
|
-
const
|
|
3794
|
-
if (
|
|
3795
|
-
const s = targetManager.status();
|
|
3954
|
+
const s = targetManager.status();
|
|
3955
|
+
if (s.state !== "running") {
|
|
3796
3956
|
return { status: 503, message: s.err?.message ?? "Model failed to become ready." };
|
|
3797
3957
|
}
|
|
3798
3958
|
const target = targetManager.target();
|
|
@@ -3884,23 +4044,7 @@ var ModelRouter = class {
|
|
|
3884
4044
|
extraArgs: profileToArgs(profile, entry, engine.capabilities, sys.cores)
|
|
3885
4045
|
};
|
|
3886
4046
|
}
|
|
3887
|
-
/** Poll until the manager's engine process becomes ready or fails.
|
|
3888
|
-
* Mirrors the Manager's internal readiness timeout by engine kind. */
|
|
3889
|
-
async waitReady(manager2, engineKind) {
|
|
3890
|
-
const timeoutMs = engineKind === "vllm" ? 6e5 : 12e4;
|
|
3891
|
-
const deadline = Date.now() + timeoutMs;
|
|
3892
|
-
while (Date.now() < deadline) {
|
|
3893
|
-
const s = manager2.status();
|
|
3894
|
-
if (s.state === "running") return true;
|
|
3895
|
-
if (s.state === "error" || s.state === "stopped") return false;
|
|
3896
|
-
await sleep3(250);
|
|
3897
|
-
}
|
|
3898
|
-
return false;
|
|
3899
|
-
}
|
|
3900
4047
|
};
|
|
3901
|
-
function sleep3(ms) {
|
|
3902
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
3903
|
-
}
|
|
3904
4048
|
|
|
3905
4049
|
// src/tools/builtin.ts
|
|
3906
4050
|
import { runInNewContext } from "vm";
|
|
@@ -4470,7 +4614,7 @@ import { Agent, setGlobalDispatcher } from "undici";
|
|
|
4470
4614
|
|
|
4471
4615
|
// src/api/routes.ts
|
|
4472
4616
|
import { streamSSE } from "hono/streaming";
|
|
4473
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync6, readdirSync as
|
|
4617
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync6, readdirSync as readdirSync5, realpathSync, rmSync as rmSync7, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
4474
4618
|
import { basename as basename3, dirname as dirname5, join as join12, resolve, sep } from "path";
|
|
4475
4619
|
|
|
4476
4620
|
// src/comfyui/gate-template.ts
|
|
@@ -4681,6 +4825,23 @@ function catalogEngine(id) {
|
|
|
4681
4825
|
return ALL.find((e) => e.id === id);
|
|
4682
4826
|
}
|
|
4683
4827
|
|
|
4828
|
+
// src/api/path-utils.ts
|
|
4829
|
+
function inferRepoFromPath(filePath, modelDirs) {
|
|
4830
|
+
const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4831
|
+
const fp = norm(filePath);
|
|
4832
|
+
const seg = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
4833
|
+
for (const dir of modelDirs) {
|
|
4834
|
+
const root = norm(dir);
|
|
4835
|
+
if (!fp.toLowerCase().startsWith(root.toLowerCase() + "/")) continue;
|
|
4836
|
+
const parts = fp.slice(root.length + 1).split("/");
|
|
4837
|
+
if (parts.length >= 2 && seg.test(parts[0]) && seg.test(parts[1])) {
|
|
4838
|
+
return `${parts[0]}/${parts[1]}`;
|
|
4839
|
+
}
|
|
4840
|
+
return null;
|
|
4841
|
+
}
|
|
4842
|
+
return null;
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4684
4845
|
// src/api/routes.ts
|
|
4685
4846
|
function err(c, status, code, message) {
|
|
4686
4847
|
return c.json({ error: { code, message } }, status);
|
|
@@ -4963,7 +5124,7 @@ function registerApi(app2, d) {
|
|
|
4963
5124
|
}
|
|
4964
5125
|
let entries;
|
|
4965
5126
|
try {
|
|
4966
|
-
entries =
|
|
5127
|
+
entries = readdirSync5(real, { withFileTypes: true }).filter((d2) => !d2.name.startsWith(".")).map((d2) => {
|
|
4967
5128
|
let isDir = d2.isDirectory();
|
|
4968
5129
|
if (d2.isSymbolicLink()) {
|
|
4969
5130
|
try {
|
|
@@ -5020,13 +5181,7 @@ function registerApi(app2, d) {
|
|
|
5020
5181
|
extraArgs: profileToArgs(profile, entry, active.capabilities, sys.cores)
|
|
5021
5182
|
};
|
|
5022
5183
|
}
|
|
5023
|
-
|
|
5024
|
-
await d.comfy?.freeComfyUIBeforeLoad();
|
|
5025
|
-
try {
|
|
5026
|
-
await d.manager.start(opts2);
|
|
5027
|
-
} catch (e) {
|
|
5028
|
-
return startError(c, e);
|
|
5029
|
-
}
|
|
5184
|
+
void d.manager.load(opts2, { beforeStart: () => d.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve() }).catch((e) => console.warn(`engine load failed: ${e}`));
|
|
5030
5185
|
d.store.update((x) => {
|
|
5031
5186
|
x.lastLoaded = { modelKey: entry.key, engineId: active.id };
|
|
5032
5187
|
});
|
|
@@ -5042,13 +5197,7 @@ function registerApi(app2, d) {
|
|
|
5042
5197
|
}
|
|
5043
5198
|
if (!modelPath) return err(c, 409, "no_such_model", "No model specified. Pick one from the Models screen.");
|
|
5044
5199
|
const opts = { engine: active, model: deriveModel(modelPath, name, extra), modelPath, extraArgs: extra };
|
|
5045
|
-
|
|
5046
|
-
await d.comfy?.freeComfyUIBeforeLoad();
|
|
5047
|
-
try {
|
|
5048
|
-
await d.manager.start(opts);
|
|
5049
|
-
} catch (e) {
|
|
5050
|
-
return startError(c, e);
|
|
5051
|
-
}
|
|
5200
|
+
void d.manager.load(opts, { beforeStart: () => d.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve() }).catch((e) => console.warn(`engine load failed: ${e}`));
|
|
5052
5201
|
return c.json({ ok: true }, 202);
|
|
5053
5202
|
});
|
|
5054
5203
|
app2.post("/api/v1/engine/stop", (c) => {
|
|
@@ -5089,7 +5238,7 @@ function registerApi(app2, d) {
|
|
|
5089
5238
|
const gateDir = join12(customNodes, "turbollm_gate");
|
|
5090
5239
|
try {
|
|
5091
5240
|
mkdirSync8(gateDir, { recursive: true });
|
|
5092
|
-
|
|
5241
|
+
writeFileSync7(join12(gateDir, "__init__.py"), gateNodeSource(base2));
|
|
5093
5242
|
} catch (e) {
|
|
5094
5243
|
return err(c, 500, "fs_write_failed", `Could not write the gate node: ${e instanceof Error ? e.message : e}`);
|
|
5095
5244
|
}
|
|
@@ -5102,7 +5251,7 @@ function registerApi(app2, d) {
|
|
|
5102
5251
|
const dir = d.store.snapshot().comfyui.gatePath;
|
|
5103
5252
|
if (dir && existsSync11(dir)) {
|
|
5104
5253
|
try {
|
|
5105
|
-
|
|
5254
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
5106
5255
|
} catch (e) {
|
|
5107
5256
|
return err(c, 500, "fs_write_failed", `Could not remove the gate node: ${e instanceof Error ? e.message : e}`);
|
|
5108
5257
|
}
|
|
@@ -5591,21 +5740,6 @@ function overlayModel(e, d, lastTpsMap) {
|
|
|
5591
5740
|
const sourceRepo = provRepo ?? inferRepoFromPath(e.path, snap.modelDirs);
|
|
5592
5741
|
return { ...e, loaded, hasProfile: e.key in profiles, lastTps, liveTps, benchTps, compatibleWithActiveEngine, sourceRepo };
|
|
5593
5742
|
}
|
|
5594
|
-
function inferRepoFromPath(filePath, modelDirs) {
|
|
5595
|
-
const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
5596
|
-
const fp = norm(filePath);
|
|
5597
|
-
const seg = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
5598
|
-
for (const dir of modelDirs) {
|
|
5599
|
-
const root = norm(dir);
|
|
5600
|
-
if (!fp.toLowerCase().startsWith(root.toLowerCase() + "/")) continue;
|
|
5601
|
-
const parts = fp.slice(root.length + 1).split("/");
|
|
5602
|
-
if (parts.length >= 2 && seg.test(parts[0]) && seg.test(parts[1])) {
|
|
5603
|
-
return `${parts[0]}/${parts[1]}`;
|
|
5604
|
-
}
|
|
5605
|
-
return null;
|
|
5606
|
-
}
|
|
5607
|
-
return null;
|
|
5608
|
-
}
|
|
5609
5743
|
function formatMismatchMessage(engineKind, format) {
|
|
5610
5744
|
if (engineKind === "mlx")
|
|
5611
5745
|
return "The active engine is MLX \u2014 pick a safetensors model, or switch to a llama.cpp engine for GGUF.";
|
|
@@ -5725,11 +5859,6 @@ function regErr(c, e) {
|
|
|
5725
5859
|
if (e instanceof ValueError) return err(c, 400, "invalid_config_value", e.message);
|
|
5726
5860
|
return err(c, 500, "internal", e.message);
|
|
5727
5861
|
}
|
|
5728
|
-
function startError(c, e) {
|
|
5729
|
-
if (e instanceof BusyError) return err(c, 409, "engine_already_running", "An engine is already running.");
|
|
5730
|
-
if (e.message === "no_free_port") return err(c, 409, "no_free_port", "No free port for the engine (8081\u20138181 all in use).");
|
|
5731
|
-
return err(c, 500, "engine_start_failed", e.message);
|
|
5732
|
-
}
|
|
5733
5862
|
function deriveModel(modelPath, name, extraArgs) {
|
|
5734
5863
|
let ctx = 0;
|
|
5735
5864
|
for (let i = 0; i + 1 < extraArgs.length; i++) {
|
|
@@ -5878,12 +6007,95 @@ function buildConnectSnippets(cli, base2, apiKey, modelName) {
|
|
|
5878
6007
|
|
|
5879
6008
|
// src/chat/chat-routes.ts
|
|
5880
6009
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
5881
|
-
|
|
6010
|
+
|
|
6011
|
+
// src/chat/parser.ts
|
|
5882
6012
|
var THINK_OPEN = "<think>";
|
|
5883
6013
|
var THINK_CLOSE = "</think>";
|
|
5884
6014
|
var CHAN_ANALYSIS_OPEN = "<|channel|>analysis<|message|>";
|
|
5885
6015
|
var CHAN_CLOSE = "<|end|>";
|
|
5886
6016
|
var CHAN_FINAL_SKIP = "<|start|>assistant<|channel|>final<|message|>";
|
|
6017
|
+
function initParseState() {
|
|
6018
|
+
return { phase: "initial", isChannel: false, buf: "" };
|
|
6019
|
+
}
|
|
6020
|
+
function feedChunk(state, raw) {
|
|
6021
|
+
const events = [];
|
|
6022
|
+
let { phase, isChannel, buf } = state;
|
|
6023
|
+
buf += raw;
|
|
6024
|
+
while (buf.length > 0) {
|
|
6025
|
+
if (phase === "initial") {
|
|
6026
|
+
const thinkIdx = buf.indexOf(THINK_OPEN);
|
|
6027
|
+
const chanIdx = buf.indexOf(CHAN_ANALYSIS_OPEN);
|
|
6028
|
+
const hasThink = thinkIdx >= 0;
|
|
6029
|
+
const hasChan = chanIdx >= 0;
|
|
6030
|
+
const useThink = hasThink && (!hasChan || thinkIdx <= chanIdx);
|
|
6031
|
+
const openIdx = useThink ? thinkIdx : hasChan ? chanIdx : -1;
|
|
6032
|
+
const openTag = useThink ? THINK_OPEN : CHAN_ANALYSIS_OPEN;
|
|
6033
|
+
if (openIdx === 0) {
|
|
6034
|
+
isChannel = !useThink;
|
|
6035
|
+
phase = "reasoning";
|
|
6036
|
+
buf = buf.slice(openTag.length);
|
|
6037
|
+
} else if (openIdx > 0) {
|
|
6038
|
+
events.push({ type: "delta", text: buf.slice(0, openIdx) });
|
|
6039
|
+
isChannel = !useThink;
|
|
6040
|
+
phase = "reasoning";
|
|
6041
|
+
buf = buf.slice(openIdx + openTag.length);
|
|
6042
|
+
} else {
|
|
6043
|
+
const safeLen = buf.length - (CHAN_ANALYSIS_OPEN.length - 1);
|
|
6044
|
+
if (safeLen > 0) {
|
|
6045
|
+
events.push({ type: "delta", text: buf.slice(0, safeLen) });
|
|
6046
|
+
buf = buf.slice(safeLen);
|
|
6047
|
+
phase = "content";
|
|
6048
|
+
} else {
|
|
6049
|
+
break;
|
|
6050
|
+
}
|
|
6051
|
+
}
|
|
6052
|
+
} else if (phase === "reasoning") {
|
|
6053
|
+
const closeTag = isChannel ? CHAN_CLOSE : THINK_CLOSE;
|
|
6054
|
+
const closeIdx = buf.indexOf(closeTag);
|
|
6055
|
+
if (closeIdx >= 0) {
|
|
6056
|
+
if (closeIdx > 0) {
|
|
6057
|
+
events.push({ type: "reasoning", text: buf.slice(0, closeIdx) });
|
|
6058
|
+
}
|
|
6059
|
+
const wasChannel = isChannel;
|
|
6060
|
+
buf = buf.slice(closeIdx + closeTag.length);
|
|
6061
|
+
phase = wasChannel ? "skipFinal" : "content";
|
|
6062
|
+
if (!wasChannel && buf) {
|
|
6063
|
+
events.push({ type: "delta", text: buf });
|
|
6064
|
+
buf = "";
|
|
6065
|
+
}
|
|
6066
|
+
} else if (buf.length >= closeTag.length) {
|
|
6067
|
+
const safe = buf.length - (closeTag.length - 1);
|
|
6068
|
+
events.push({ type: "reasoning", text: buf.slice(0, safe) });
|
|
6069
|
+
buf = buf.slice(safe);
|
|
6070
|
+
} else {
|
|
6071
|
+
break;
|
|
6072
|
+
}
|
|
6073
|
+
} else if (phase === "skipFinal") {
|
|
6074
|
+
if (buf.startsWith(CHAN_FINAL_SKIP)) {
|
|
6075
|
+
buf = buf.slice(CHAN_FINAL_SKIP.length);
|
|
6076
|
+
phase = "content";
|
|
6077
|
+
} else if (CHAN_FINAL_SKIP.startsWith(buf) && buf.length < CHAN_FINAL_SKIP.length) {
|
|
6078
|
+
break;
|
|
6079
|
+
} else {
|
|
6080
|
+
const skipIdx = buf.indexOf(CHAN_FINAL_SKIP);
|
|
6081
|
+
buf = skipIdx >= 0 ? buf.slice(skipIdx + CHAN_FINAL_SKIP.length) : "";
|
|
6082
|
+
phase = "content";
|
|
6083
|
+
}
|
|
6084
|
+
} else {
|
|
6085
|
+
events.push({ type: "delta", text: buf });
|
|
6086
|
+
buf = "";
|
|
6087
|
+
break;
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
return { state: { phase, isChannel, buf }, events };
|
|
6091
|
+
}
|
|
6092
|
+
function flushState(state) {
|
|
6093
|
+
if (!state.buf) return [];
|
|
6094
|
+
return state.phase === "reasoning" ? [{ type: "reasoning", text: state.buf }] : [{ type: "delta", text: state.buf }];
|
|
6095
|
+
}
|
|
6096
|
+
|
|
6097
|
+
// src/chat/chat-routes.ts
|
|
6098
|
+
var inflight = /* @__PURE__ */ new Map();
|
|
5887
6099
|
var EXPERT_SYSTEM_PROMPT = `You are the TurboLLM in-app expert assistant \u2014 a knowledgeable, friendly guide built into TurboLLM, a local-first desktop app for running large language models on the user's own machine.
|
|
5888
6100
|
|
|
5889
6101
|
Your job is to help the user get the most out of TurboLLM:
|
|
@@ -6146,9 +6358,7 @@ async function runGeneration(d, stream, ctx) {
|
|
|
6146
6358
|
ac.signal.addEventListener("abort", cancelReader, { once: true });
|
|
6147
6359
|
}
|
|
6148
6360
|
let roundContent = "";
|
|
6149
|
-
let
|
|
6150
|
-
let parseIsChannel = false;
|
|
6151
|
-
let parseBuf = "";
|
|
6361
|
+
let parseState = initParseState();
|
|
6152
6362
|
let finishReason = "";
|
|
6153
6363
|
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
6154
6364
|
roundLoop: while (true) {
|
|
@@ -6202,108 +6412,33 @@ async function runGeneration(d, stream, ctx) {
|
|
|
6202
6412
|
}
|
|
6203
6413
|
const raw_content = delta.content ?? "";
|
|
6204
6414
|
if (!raw_content) continue;
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
const openIdx = useThink ? thinkIdx : hasChan ? chanIdx : -1;
|
|
6214
|
-
const openTag = useThink ? THINK_OPEN : CHAN_ANALYSIS_OPEN;
|
|
6215
|
-
if (openIdx === 0) {
|
|
6216
|
-
parseIsChannel = !useThink;
|
|
6217
|
-
parsePhase = "reasoning";
|
|
6218
|
-
if (!thinkStart) thinkStart = Date.now();
|
|
6219
|
-
parseBuf = parseBuf.slice(openTag.length);
|
|
6220
|
-
} else if (openIdx > 0) {
|
|
6221
|
-
const before = parseBuf.slice(0, openIdx);
|
|
6222
|
-
fullContent += before;
|
|
6223
|
-
roundContent += before;
|
|
6224
|
-
if (!ttftMs) ttftMs = Date.now() - requestStart;
|
|
6225
|
-
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta: before }) });
|
|
6226
|
-
parseIsChannel = !useThink;
|
|
6227
|
-
parsePhase = "reasoning";
|
|
6228
|
-
if (!thinkStart) thinkStart = Date.now();
|
|
6229
|
-
parseBuf = parseBuf.slice(openIdx + openTag.length);
|
|
6230
|
-
} else {
|
|
6231
|
-
const safeLen = parseBuf.length - (CHAN_ANALYSIS_OPEN.length - 1);
|
|
6232
|
-
if (safeLen > 0) {
|
|
6233
|
-
const flush = parseBuf.slice(0, safeLen);
|
|
6234
|
-
parseBuf = parseBuf.slice(safeLen);
|
|
6235
|
-
fullContent += flush;
|
|
6236
|
-
roundContent += flush;
|
|
6237
|
-
if (!ttftMs) ttftMs = Date.now() - requestStart;
|
|
6238
|
-
d.manager.setLiveGen({ phase: "gen", pct: 0, outputTokens: ++liveOut });
|
|
6239
|
-
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta: flush }) });
|
|
6240
|
-
parsePhase = "content";
|
|
6241
|
-
} else {
|
|
6242
|
-
break;
|
|
6243
|
-
}
|
|
6244
|
-
}
|
|
6245
|
-
} else if (parsePhase === "reasoning") {
|
|
6246
|
-
const closeTag = parseIsChannel ? CHAN_CLOSE : THINK_CLOSE;
|
|
6247
|
-
const closeIdx = parseBuf.indexOf(closeTag);
|
|
6248
|
-
if (closeIdx >= 0) {
|
|
6249
|
-
if (closeIdx > 0) {
|
|
6250
|
-
const chunk2 = parseBuf.slice(0, closeIdx);
|
|
6251
|
-
fullReasoning += chunk2;
|
|
6252
|
-
await stream.writeSSE({ event: "reasoning", data: JSON.stringify({ delta: chunk2 }) });
|
|
6253
|
-
}
|
|
6254
|
-
thinkEnd = Date.now();
|
|
6255
|
-
const wasChannel = parseIsChannel;
|
|
6256
|
-
parseBuf = parseBuf.slice(closeIdx + closeTag.length);
|
|
6257
|
-
parsePhase = wasChannel ? "skipFinal" : "content";
|
|
6258
|
-
if (!wasChannel && parseBuf) {
|
|
6259
|
-
fullContent += parseBuf;
|
|
6260
|
-
roundContent += parseBuf;
|
|
6261
|
-
if (!ttftMs) ttftMs = Date.now() - requestStart;
|
|
6262
|
-
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta: parseBuf }) });
|
|
6263
|
-
parseBuf = "";
|
|
6264
|
-
}
|
|
6265
|
-
} else if (parseBuf.length >= closeTag.length) {
|
|
6266
|
-
const safe = parseBuf.length - (closeTag.length - 1);
|
|
6267
|
-
const chunk2 = parseBuf.slice(0, safe);
|
|
6268
|
-
parseBuf = parseBuf.slice(safe);
|
|
6269
|
-
thinkEnd = Date.now();
|
|
6270
|
-
fullReasoning += chunk2;
|
|
6271
|
-
await stream.writeSSE({ event: "reasoning", data: JSON.stringify({ delta: chunk2 }) });
|
|
6272
|
-
} else {
|
|
6273
|
-
break;
|
|
6274
|
-
}
|
|
6275
|
-
} else if (parsePhase === "skipFinal") {
|
|
6276
|
-
if (parseBuf.startsWith(CHAN_FINAL_SKIP)) {
|
|
6277
|
-
parseBuf = parseBuf.slice(CHAN_FINAL_SKIP.length);
|
|
6278
|
-
parsePhase = "content";
|
|
6279
|
-
} else if (CHAN_FINAL_SKIP.startsWith(parseBuf) && parseBuf.length < CHAN_FINAL_SKIP.length) {
|
|
6280
|
-
break;
|
|
6281
|
-
} else {
|
|
6282
|
-
const skipIdx = parseBuf.indexOf(CHAN_FINAL_SKIP);
|
|
6283
|
-
parseBuf = skipIdx >= 0 ? parseBuf.slice(skipIdx + CHAN_FINAL_SKIP.length) : "";
|
|
6284
|
-
parsePhase = "content";
|
|
6285
|
-
}
|
|
6415
|
+
const { state: nextState, events: parseEvents } = feedChunk(parseState, raw_content);
|
|
6416
|
+
parseState = nextState;
|
|
6417
|
+
for (const ev of parseEvents) {
|
|
6418
|
+
if (ev.type === "reasoning") {
|
|
6419
|
+
if (!thinkStart) thinkStart = Date.now();
|
|
6420
|
+
thinkEnd = Date.now();
|
|
6421
|
+
fullReasoning += ev.text;
|
|
6422
|
+
await stream.writeSSE({ event: "reasoning", data: JSON.stringify({ delta: ev.text }) });
|
|
6286
6423
|
} else {
|
|
6424
|
+
fullContent += ev.text;
|
|
6425
|
+
roundContent += ev.text;
|
|
6287
6426
|
if (!ttftMs) ttftMs = Date.now() - requestStart;
|
|
6288
|
-
fullContent += parseBuf;
|
|
6289
|
-
roundContent += parseBuf;
|
|
6290
6427
|
d.manager.setLiveGen({ phase: "gen", pct: 0, outputTokens: ++liveOut });
|
|
6291
|
-
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta:
|
|
6292
|
-
parseBuf = "";
|
|
6293
|
-
break;
|
|
6428
|
+
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta: ev.text }) });
|
|
6294
6429
|
}
|
|
6295
6430
|
}
|
|
6296
6431
|
}
|
|
6297
6432
|
}
|
|
6298
6433
|
ac.signal.removeEventListener("abort", cancelReader);
|
|
6299
|
-
|
|
6300
|
-
if (
|
|
6301
|
-
fullReasoning +=
|
|
6302
|
-
await stream.writeSSE({ event: "reasoning", data: JSON.stringify({ delta:
|
|
6434
|
+
for (const ev of flushState(parseState)) {
|
|
6435
|
+
if (ev.type === "reasoning") {
|
|
6436
|
+
fullReasoning += ev.text;
|
|
6437
|
+
await stream.writeSSE({ event: "reasoning", data: JSON.stringify({ delta: ev.text }) });
|
|
6303
6438
|
} else {
|
|
6304
|
-
fullContent +=
|
|
6305
|
-
roundContent +=
|
|
6306
|
-
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta:
|
|
6439
|
+
fullContent += ev.text;
|
|
6440
|
+
roundContent += ev.text;
|
|
6441
|
+
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta: ev.text }) });
|
|
6307
6442
|
}
|
|
6308
6443
|
}
|
|
6309
6444
|
if ((finishReason === "tool_calls" || pendingToolCalls.size > 0) && d.tools && toolIter <= MAX_TOOL_ITER) {
|
|
@@ -6399,7 +6534,10 @@ async function runGeneration(d, stream, ctx) {
|
|
|
6399
6534
|
} catch {
|
|
6400
6535
|
}
|
|
6401
6536
|
const finalMsg = db2.getMessage(assistantMsg.id);
|
|
6402
|
-
|
|
6537
|
+
try {
|
|
6538
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ message: finalMsg }) });
|
|
6539
|
+
} catch {
|
|
6540
|
+
}
|
|
6403
6541
|
if (!aborted && conv.title === "New chat" && d.store.snapshot().daemon.autoGenerateTitles) {
|
|
6404
6542
|
setTimeout(() => {
|
|
6405
6543
|
void autoTitle(d, convId, ctx.engineMessages, fullContent, target);
|
|
@@ -6696,7 +6834,8 @@ async function* streamToAnthropic(oaiStream, modelName, msgId, onUsage, onLive)
|
|
|
6696
6834
|
failed = true;
|
|
6697
6835
|
yield sse("error", { error: { type: "api_error", message: "engine stopped" } });
|
|
6698
6836
|
} finally {
|
|
6699
|
-
reader.
|
|
6837
|
+
await reader.cancel().catch(() => {
|
|
6838
|
+
});
|
|
6700
6839
|
}
|
|
6701
6840
|
if (!failed) {
|
|
6702
6841
|
if (inThinking) {
|
|
@@ -6747,6 +6886,15 @@ function cbStop(index) {
|
|
|
6747
6886
|
}
|
|
6748
6887
|
|
|
6749
6888
|
// src/gateway/gateway.ts
|
|
6889
|
+
function clientAbort(c) {
|
|
6890
|
+
const ac = new AbortController();
|
|
6891
|
+
const sig = c.req.raw.signal;
|
|
6892
|
+
if (sig) {
|
|
6893
|
+
if (sig.aborted) ac.abort();
|
|
6894
|
+
else sig.addEventListener("abort", () => ac.abort(), { once: true });
|
|
6895
|
+
}
|
|
6896
|
+
return ac;
|
|
6897
|
+
}
|
|
6750
6898
|
function registerGateway(app2, d) {
|
|
6751
6899
|
app2.post("/v1/messages", async (c) => {
|
|
6752
6900
|
let req;
|
|
@@ -6780,12 +6928,14 @@ function registerGateway(app2, d) {
|
|
|
6780
6928
|
const oaiAlias = engineModelAlias(d.registry.active()?.kind ?? "");
|
|
6781
6929
|
if (oaiAlias) oaiBody.model = oaiAlias;
|
|
6782
6930
|
d.manager.generationStart();
|
|
6931
|
+
const ac = clientAbort(c);
|
|
6783
6932
|
let res;
|
|
6784
6933
|
try {
|
|
6785
6934
|
res = await fetch(`${target}/v1/chat/completions`, {
|
|
6786
6935
|
method: "POST",
|
|
6787
6936
|
headers: { "Content-Type": "application/json" },
|
|
6788
|
-
body: JSON.stringify(oaiBody)
|
|
6937
|
+
body: JSON.stringify(oaiBody),
|
|
6938
|
+
signal: ac.signal
|
|
6789
6939
|
});
|
|
6790
6940
|
} catch (e) {
|
|
6791
6941
|
d.manager.generationEnd();
|
|
@@ -6824,11 +6974,13 @@ function registerGateway(app2, d) {
|
|
|
6824
6974
|
}
|
|
6825
6975
|
);
|
|
6826
6976
|
return streamSSE3(c, async (stream) => {
|
|
6977
|
+
stream.onAbort(() => ac.abort());
|
|
6827
6978
|
try {
|
|
6828
6979
|
for await (const evt of gen) {
|
|
6829
6980
|
await stream.writeSSE({ event: evt.event, data: evt.data });
|
|
6830
6981
|
}
|
|
6831
6982
|
} finally {
|
|
6983
|
+
ac.abort();
|
|
6832
6984
|
d.manager.generationEnd();
|
|
6833
6985
|
}
|
|
6834
6986
|
});
|
|
@@ -6896,7 +7048,8 @@ function registerGateway(app2, d) {
|
|
|
6896
7048
|
const headers = new Headers(c.req.raw.headers);
|
|
6897
7049
|
headers.delete("host");
|
|
6898
7050
|
const maxLimit = d.store.snapshot().modelDefaults.maxTokens ?? 0;
|
|
6899
|
-
const
|
|
7051
|
+
const ac = clientAbort(c);
|
|
7052
|
+
const init = { method: c.req.method, headers, signal: ac.signal };
|
|
6900
7053
|
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
6901
7054
|
if (isChat) {
|
|
6902
7055
|
if (parsedBody && maxLimit > 0) {
|
|
@@ -7124,6 +7277,10 @@ Please upgrade: https://nodejs.org
|
|
|
7124
7277
|
);
|
|
7125
7278
|
process.exit(1);
|
|
7126
7279
|
}
|
|
7280
|
+
process.on("unhandledRejection", (reason) => {
|
|
7281
|
+
if (reason?.name === "AbortError") return;
|
|
7282
|
+
console.warn("unhandledRejection (continuing):", reason);
|
|
7283
|
+
});
|
|
7127
7284
|
var argv = process.argv.slice(2);
|
|
7128
7285
|
function hasFlag(...names) {
|
|
7129
7286
|
return names.some((n) => argv.includes(n));
|
|
@@ -7176,6 +7333,8 @@ var store = ConfigStore.load(argValue("--config", defaultConfigPath()));
|
|
|
7176
7333
|
if (store.brokenBackup()) {
|
|
7177
7334
|
console.warn(`config was reset; previous file backed up at ${store.brokenBackup()}`);
|
|
7178
7335
|
}
|
|
7336
|
+
var reaped = await reapStaleEngines(store.dir()).catch(() => 0);
|
|
7337
|
+
if (reaped > 0) console.log(`reaped ${reaped} orphaned engine process(es) from a previous run`);
|
|
7179
7338
|
var registry = new Registry(store);
|
|
7180
7339
|
var pruned = registry.pruneDeadManagedBuilds();
|
|
7181
7340
|
if (pruned > 0) console.log(`pruned ${pruned} dangling engine build(s)`);
|
|
@@ -7398,12 +7557,11 @@ void (async () => {
|
|
|
7398
7557
|
};
|
|
7399
7558
|
}
|
|
7400
7559
|
if (opts) {
|
|
7401
|
-
|
|
7402
|
-
manager.start(opts).catch((e) => console.warn(`auto-load failed: ${e}`));
|
|
7560
|
+
manager.load(opts, { beforeStart: () => comfy.freeComfyUIBeforeLoad() }).catch((e) => console.warn(`auto-load failed: ${e}`));
|
|
7403
7561
|
}
|
|
7404
7562
|
})();
|
|
7405
7563
|
var shuttingDown = false;
|
|
7406
|
-
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
7564
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
7407
7565
|
process.on(sig, () => {
|
|
7408
7566
|
if (shuttingDown) return;
|
|
7409
7567
|
shuttingDown = true;
|
|
@@ -7417,3 +7575,9 @@ for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
|
7417
7575
|
setTimeout(() => process.exit(0), 12e3).unref();
|
|
7418
7576
|
});
|
|
7419
7577
|
}
|
|
7578
|
+
process.on("exit", () => {
|
|
7579
|
+
try {
|
|
7580
|
+
killTrackedEnginesSync(store.dir());
|
|
7581
|
+
} catch {
|
|
7582
|
+
}
|
|
7583
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbollm",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "TurboLLM — local LLM platform: run any inference engine auto-tuned to your GPU, with a web UI and OpenAI/Anthropic-compatible API. Point Claude Code at your own machine in one command.",
|
|
5
5
|
"license": "FSL-1.1-ALv2",
|
|
6
6
|
"author": "Mohit Soni",
|