turbollm 0.7.0 → 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
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));
|
|
@@ -2311,6 +2472,21 @@ function mlxEntryFor(dir) {
|
|
|
2311
2472
|
if (existsSync9(tc)) hasChatTemplate = readFileSync3(tc, "utf8").includes("chat_template");
|
|
2312
2473
|
} catch {
|
|
2313
2474
|
}
|
|
2475
|
+
let incomplete = false;
|
|
2476
|
+
try {
|
|
2477
|
+
const indexPath = join7(dir, "model.safetensors.index.json");
|
|
2478
|
+
if (existsSync9(indexPath)) {
|
|
2479
|
+
const index = JSON.parse(readFileSync3(indexPath, "utf8"));
|
|
2480
|
+
const shards = new Set(Object.values(index.weight_map ?? {}));
|
|
2481
|
+
for (const shard of shards) {
|
|
2482
|
+
if (!existsSync9(join7(dir, shard))) {
|
|
2483
|
+
incomplete = true;
|
|
2484
|
+
break;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
} catch {
|
|
2489
|
+
}
|
|
2314
2490
|
const expertCount = cfg2.num_local_experts ?? cfg2.num_experts ?? 0;
|
|
2315
2491
|
const bits = cfg2.quantization?.bits;
|
|
2316
2492
|
const quant = bits ? `${bits}bit` : "fp16";
|
|
@@ -2336,7 +2512,7 @@ function mlxEntryFor(dir) {
|
|
|
2336
2512
|
mmprojPath: null,
|
|
2337
2513
|
hasChatTemplate,
|
|
2338
2514
|
embedding: isEmbeddingModel(arch2, basename(dir)),
|
|
2339
|
-
incomplete
|
|
2515
|
+
incomplete,
|
|
2340
2516
|
parseError,
|
|
2341
2517
|
loaded: false,
|
|
2342
2518
|
hasProfile: false,
|
|
@@ -2353,7 +2529,7 @@ function tick() {
|
|
|
2353
2529
|
|
|
2354
2530
|
// src/models/hashes.ts
|
|
2355
2531
|
import { createHash as createHash2 } from "crypto";
|
|
2356
|
-
import { createReadStream, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as
|
|
2532
|
+
import { createReadStream, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2357
2533
|
import { join as join8 } from "path";
|
|
2358
2534
|
var HashStore = class {
|
|
2359
2535
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -2404,7 +2580,7 @@ var HashStore = class {
|
|
|
2404
2580
|
const entries = {};
|
|
2405
2581
|
for (const [k, v] of this.cache) entries[k] = v;
|
|
2406
2582
|
try {
|
|
2407
|
-
|
|
2583
|
+
writeFileSync4(this.path, JSON.stringify({ version: 1, entries }));
|
|
2408
2584
|
} catch {
|
|
2409
2585
|
}
|
|
2410
2586
|
}
|
|
@@ -3011,10 +3187,10 @@ import {
|
|
|
3011
3187
|
mkdirSync as mkdirSync6,
|
|
3012
3188
|
readFileSync as readFileSync5,
|
|
3013
3189
|
renameSync as renameSync2,
|
|
3014
|
-
rmSync as
|
|
3190
|
+
rmSync as rmSync6,
|
|
3015
3191
|
statfsSync,
|
|
3016
3192
|
statSync as statSync3,
|
|
3017
|
-
writeFileSync as
|
|
3193
|
+
writeFileSync as writeFileSync5
|
|
3018
3194
|
} from "fs";
|
|
3019
3195
|
import { basename as basename2, join as join10 } from "path";
|
|
3020
3196
|
import { Readable as Readable2 } from "stream";
|
|
@@ -3125,7 +3301,7 @@ var DownloadManager = class {
|
|
|
3125
3301
|
this.controllers.get(id)?.abort();
|
|
3126
3302
|
this.controllers.delete(id);
|
|
3127
3303
|
if (rec.status !== "done") {
|
|
3128
|
-
|
|
3304
|
+
rmSync6(`${rec.dest}.part`, { force: true });
|
|
3129
3305
|
rec.status = "cancelled";
|
|
3130
3306
|
rec.bytesPerSec = 0;
|
|
3131
3307
|
}
|
|
@@ -3139,7 +3315,7 @@ var DownloadManager = class {
|
|
|
3139
3315
|
if (!rec) return false;
|
|
3140
3316
|
this.controllers.get(id)?.abort();
|
|
3141
3317
|
this.controllers.delete(id);
|
|
3142
|
-
if (rec.status !== "done")
|
|
3318
|
+
if (rec.status !== "done") rmSync6(`${rec.dest}.part`, { force: true });
|
|
3143
3319
|
this.records.delete(id);
|
|
3144
3320
|
this.persist();
|
|
3145
3321
|
this.pump();
|
|
@@ -3204,7 +3380,7 @@ var DownloadManager = class {
|
|
|
3204
3380
|
if (!res.body) throw new DownloadError("download_failed", "Empty response body.");
|
|
3205
3381
|
const resuming = res.status === 206;
|
|
3206
3382
|
if (!resuming && startAt > 0) {
|
|
3207
|
-
|
|
3383
|
+
rmSync6(part, { force: true });
|
|
3208
3384
|
startAt = 0;
|
|
3209
3385
|
rec.received = 0;
|
|
3210
3386
|
}
|
|
@@ -3229,13 +3405,13 @@ var DownloadManager = class {
|
|
|
3229
3405
|
const out = createWriteStream3(part, startAt > 0 ? { flags: "a" } : { flags: "w" });
|
|
3230
3406
|
await pipeline2(body3, out, { signal: ac.signal });
|
|
3231
3407
|
if (rec.total > 0 && rec.received !== rec.total) {
|
|
3232
|
-
|
|
3408
|
+
rmSync6(part, { force: true });
|
|
3233
3409
|
throw new DownloadError("size_mismatch", "Download corrupt \u2014 size did not match. Removed the partial file.");
|
|
3234
3410
|
}
|
|
3235
3411
|
if (verifyHash && rec.sha256) {
|
|
3236
3412
|
const got = hash.digest("hex");
|
|
3237
3413
|
if (got !== rec.sha256) {
|
|
3238
|
-
|
|
3414
|
+
rmSync6(part, { force: true });
|
|
3239
3415
|
throw new DownloadError("checksum_failed", "Checksum failed \u2014 the downloaded file was corrupt.");
|
|
3240
3416
|
}
|
|
3241
3417
|
}
|
|
@@ -3288,7 +3464,7 @@ var DownloadManager = class {
|
|
|
3288
3464
|
}
|
|
3289
3465
|
saveProvenance() {
|
|
3290
3466
|
try {
|
|
3291
|
-
|
|
3467
|
+
writeFileSync5(this.provenancePath, JSON.stringify({ version: 1, entries: this.provenanceList }, null, 2));
|
|
3292
3468
|
} catch {
|
|
3293
3469
|
}
|
|
3294
3470
|
}
|
|
@@ -3310,7 +3486,7 @@ var DownloadManager = class {
|
|
|
3310
3486
|
});
|
|
3311
3487
|
}
|
|
3312
3488
|
try {
|
|
3313
|
-
|
|
3489
|
+
writeFileSync5(this.manifestPath, JSON.stringify({ version: 1, entries }, null, 2));
|
|
3314
3490
|
} catch {
|
|
3315
3491
|
}
|
|
3316
3492
|
}
|
|
@@ -3360,7 +3536,7 @@ function safePathname(u) {
|
|
|
3360
3536
|
|
|
3361
3537
|
// src/bench/bench.ts
|
|
3362
3538
|
import { execFile as execFile6 } from "child_process";
|
|
3363
|
-
import { mkdirSync as mkdirSync7, writeFileSync as
|
|
3539
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
3364
3540
|
import { join as join11 } from "path";
|
|
3365
3541
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3366
3542
|
var READY_TIMEOUT_MS = 12e4;
|
|
@@ -3631,7 +3807,7 @@ var BenchRunner = class {
|
|
|
3631
3807
|
};
|
|
3632
3808
|
const queueDir = join11(this.store.dir(), "telemetry", "queue");
|
|
3633
3809
|
mkdirSync7(queueDir, { recursive: true });
|
|
3634
|
-
|
|
3810
|
+
writeFileSync6(join11(queueDir, `${randomUUID4()}.json`), JSON.stringify(event));
|
|
3635
3811
|
} catch {
|
|
3636
3812
|
}
|
|
3637
3813
|
}
|
|
@@ -3768,16 +3944,15 @@ var ModelRouter = class {
|
|
|
3768
3944
|
const keepN = Math.max(1, this.store.snapshot().gateway.keepN);
|
|
3769
3945
|
const needsNewSlot = entry.embedding || this.chatSlotCount() < keepN;
|
|
3770
3946
|
const targetManager = needsNewSlot ? this.manager.status().state === "stopped" || this.manager.status().state === "error" ? this.manager : new Manager(this.store) : this.evictChatLru();
|
|
3771
|
-
await targetManager.stopAndWait();
|
|
3772
|
-
await this.comfy?.freeComfyUIBeforeLoad();
|
|
3773
3947
|
try {
|
|
3774
|
-
await targetManager.
|
|
3948
|
+
await targetManager.load(opts, {
|
|
3949
|
+
beforeStart: () => this.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve()
|
|
3950
|
+
});
|
|
3775
3951
|
} catch (e) {
|
|
3776
3952
|
return { status: 503, message: `Engine start failed: ${e.message}` };
|
|
3777
3953
|
}
|
|
3778
|
-
const
|
|
3779
|
-
if (
|
|
3780
|
-
const s = targetManager.status();
|
|
3954
|
+
const s = targetManager.status();
|
|
3955
|
+
if (s.state !== "running") {
|
|
3781
3956
|
return { status: 503, message: s.err?.message ?? "Model failed to become ready." };
|
|
3782
3957
|
}
|
|
3783
3958
|
const target = targetManager.target();
|
|
@@ -3869,23 +4044,7 @@ var ModelRouter = class {
|
|
|
3869
4044
|
extraArgs: profileToArgs(profile, entry, engine.capabilities, sys.cores)
|
|
3870
4045
|
};
|
|
3871
4046
|
}
|
|
3872
|
-
/** Poll until the manager's engine process becomes ready or fails.
|
|
3873
|
-
* Mirrors the Manager's internal readiness timeout by engine kind. */
|
|
3874
|
-
async waitReady(manager2, engineKind) {
|
|
3875
|
-
const timeoutMs = engineKind === "vllm" ? 6e5 : 12e4;
|
|
3876
|
-
const deadline = Date.now() + timeoutMs;
|
|
3877
|
-
while (Date.now() < deadline) {
|
|
3878
|
-
const s = manager2.status();
|
|
3879
|
-
if (s.state === "running") return true;
|
|
3880
|
-
if (s.state === "error" || s.state === "stopped") return false;
|
|
3881
|
-
await sleep3(250);
|
|
3882
|
-
}
|
|
3883
|
-
return false;
|
|
3884
|
-
}
|
|
3885
4047
|
};
|
|
3886
|
-
function sleep3(ms) {
|
|
3887
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
3888
|
-
}
|
|
3889
4048
|
|
|
3890
4049
|
// src/tools/builtin.ts
|
|
3891
4050
|
import { runInNewContext } from "vm";
|
|
@@ -4455,7 +4614,7 @@ import { Agent, setGlobalDispatcher } from "undici";
|
|
|
4455
4614
|
|
|
4456
4615
|
// src/api/routes.ts
|
|
4457
4616
|
import { streamSSE } from "hono/streaming";
|
|
4458
|
-
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";
|
|
4459
4618
|
import { basename as basename3, dirname as dirname5, join as join12, resolve, sep } from "path";
|
|
4460
4619
|
|
|
4461
4620
|
// src/comfyui/gate-template.ts
|
|
@@ -4666,6 +4825,23 @@ function catalogEngine(id) {
|
|
|
4666
4825
|
return ALL.find((e) => e.id === id);
|
|
4667
4826
|
}
|
|
4668
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
|
+
|
|
4669
4845
|
// src/api/routes.ts
|
|
4670
4846
|
function err(c, status, code, message) {
|
|
4671
4847
|
return c.json({ error: { code, message } }, status);
|
|
@@ -4948,7 +5124,7 @@ function registerApi(app2, d) {
|
|
|
4948
5124
|
}
|
|
4949
5125
|
let entries;
|
|
4950
5126
|
try {
|
|
4951
|
-
entries =
|
|
5127
|
+
entries = readdirSync5(real, { withFileTypes: true }).filter((d2) => !d2.name.startsWith(".")).map((d2) => {
|
|
4952
5128
|
let isDir = d2.isDirectory();
|
|
4953
5129
|
if (d2.isSymbolicLink()) {
|
|
4954
5130
|
try {
|
|
@@ -5005,13 +5181,7 @@ function registerApi(app2, d) {
|
|
|
5005
5181
|
extraArgs: profileToArgs(profile, entry, active.capabilities, sys.cores)
|
|
5006
5182
|
};
|
|
5007
5183
|
}
|
|
5008
|
-
|
|
5009
|
-
await d.comfy?.freeComfyUIBeforeLoad();
|
|
5010
|
-
try {
|
|
5011
|
-
await d.manager.start(opts2);
|
|
5012
|
-
} catch (e) {
|
|
5013
|
-
return startError(c, e);
|
|
5014
|
-
}
|
|
5184
|
+
void d.manager.load(opts2, { beforeStart: () => d.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve() }).catch((e) => console.warn(`engine load failed: ${e}`));
|
|
5015
5185
|
d.store.update((x) => {
|
|
5016
5186
|
x.lastLoaded = { modelKey: entry.key, engineId: active.id };
|
|
5017
5187
|
});
|
|
@@ -5027,13 +5197,7 @@ function registerApi(app2, d) {
|
|
|
5027
5197
|
}
|
|
5028
5198
|
if (!modelPath) return err(c, 409, "no_such_model", "No model specified. Pick one from the Models screen.");
|
|
5029
5199
|
const opts = { engine: active, model: deriveModel(modelPath, name, extra), modelPath, extraArgs: extra };
|
|
5030
|
-
|
|
5031
|
-
await d.comfy?.freeComfyUIBeforeLoad();
|
|
5032
|
-
try {
|
|
5033
|
-
await d.manager.start(opts);
|
|
5034
|
-
} catch (e) {
|
|
5035
|
-
return startError(c, e);
|
|
5036
|
-
}
|
|
5200
|
+
void d.manager.load(opts, { beforeStart: () => d.comfy?.freeComfyUIBeforeLoad() ?? Promise.resolve() }).catch((e) => console.warn(`engine load failed: ${e}`));
|
|
5037
5201
|
return c.json({ ok: true }, 202);
|
|
5038
5202
|
});
|
|
5039
5203
|
app2.post("/api/v1/engine/stop", (c) => {
|
|
@@ -5074,7 +5238,7 @@ function registerApi(app2, d) {
|
|
|
5074
5238
|
const gateDir = join12(customNodes, "turbollm_gate");
|
|
5075
5239
|
try {
|
|
5076
5240
|
mkdirSync8(gateDir, { recursive: true });
|
|
5077
|
-
|
|
5241
|
+
writeFileSync7(join12(gateDir, "__init__.py"), gateNodeSource(base2));
|
|
5078
5242
|
} catch (e) {
|
|
5079
5243
|
return err(c, 500, "fs_write_failed", `Could not write the gate node: ${e instanceof Error ? e.message : e}`);
|
|
5080
5244
|
}
|
|
@@ -5087,7 +5251,7 @@ function registerApi(app2, d) {
|
|
|
5087
5251
|
const dir = d.store.snapshot().comfyui.gatePath;
|
|
5088
5252
|
if (dir && existsSync11(dir)) {
|
|
5089
5253
|
try {
|
|
5090
|
-
|
|
5254
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
5091
5255
|
} catch (e) {
|
|
5092
5256
|
return err(c, 500, "fs_write_failed", `Could not remove the gate node: ${e instanceof Error ? e.message : e}`);
|
|
5093
5257
|
}
|
|
@@ -5576,21 +5740,6 @@ function overlayModel(e, d, lastTpsMap) {
|
|
|
5576
5740
|
const sourceRepo = provRepo ?? inferRepoFromPath(e.path, snap.modelDirs);
|
|
5577
5741
|
return { ...e, loaded, hasProfile: e.key in profiles, lastTps, liveTps, benchTps, compatibleWithActiveEngine, sourceRepo };
|
|
5578
5742
|
}
|
|
5579
|
-
function inferRepoFromPath(filePath, modelDirs) {
|
|
5580
|
-
const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
5581
|
-
const fp = norm(filePath);
|
|
5582
|
-
const seg = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
5583
|
-
for (const dir of modelDirs) {
|
|
5584
|
-
const root = norm(dir);
|
|
5585
|
-
if (!fp.toLowerCase().startsWith(root.toLowerCase() + "/")) continue;
|
|
5586
|
-
const parts = fp.slice(root.length + 1).split("/");
|
|
5587
|
-
if (parts.length >= 3 && seg.test(parts[0]) && seg.test(parts[1])) {
|
|
5588
|
-
return `${parts[0]}/${parts[1]}`;
|
|
5589
|
-
}
|
|
5590
|
-
return null;
|
|
5591
|
-
}
|
|
5592
|
-
return null;
|
|
5593
|
-
}
|
|
5594
5743
|
function formatMismatchMessage(engineKind, format) {
|
|
5595
5744
|
if (engineKind === "mlx")
|
|
5596
5745
|
return "The active engine is MLX \u2014 pick a safetensors model, or switch to a llama.cpp engine for GGUF.";
|
|
@@ -5710,11 +5859,6 @@ function regErr(c, e) {
|
|
|
5710
5859
|
if (e instanceof ValueError) return err(c, 400, "invalid_config_value", e.message);
|
|
5711
5860
|
return err(c, 500, "internal", e.message);
|
|
5712
5861
|
}
|
|
5713
|
-
function startError(c, e) {
|
|
5714
|
-
if (e instanceof BusyError) return err(c, 409, "engine_already_running", "An engine is already running.");
|
|
5715
|
-
if (e.message === "no_free_port") return err(c, 409, "no_free_port", "No free port for the engine (8081\u20138181 all in use).");
|
|
5716
|
-
return err(c, 500, "engine_start_failed", e.message);
|
|
5717
|
-
}
|
|
5718
5862
|
function deriveModel(modelPath, name, extraArgs) {
|
|
5719
5863
|
let ctx = 0;
|
|
5720
5864
|
for (let i = 0; i + 1 < extraArgs.length; i++) {
|
|
@@ -5863,6 +6007,94 @@ function buildConnectSnippets(cli, base2, apiKey, modelName) {
|
|
|
5863
6007
|
|
|
5864
6008
|
// src/chat/chat-routes.ts
|
|
5865
6009
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
6010
|
+
|
|
6011
|
+
// src/chat/parser.ts
|
|
6012
|
+
var THINK_OPEN = "<think>";
|
|
6013
|
+
var THINK_CLOSE = "</think>";
|
|
6014
|
+
var CHAN_ANALYSIS_OPEN = "<|channel|>analysis<|message|>";
|
|
6015
|
+
var CHAN_CLOSE = "<|end|>";
|
|
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
|
|
5866
6098
|
var inflight = /* @__PURE__ */ new Map();
|
|
5867
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.
|
|
5868
6100
|
|
|
@@ -6126,9 +6358,7 @@ async function runGeneration(d, stream, ctx) {
|
|
|
6126
6358
|
ac.signal.addEventListener("abort", cancelReader, { once: true });
|
|
6127
6359
|
}
|
|
6128
6360
|
let roundContent = "";
|
|
6129
|
-
let
|
|
6130
|
-
let pendingThinkBuf = "";
|
|
6131
|
-
let firstDelta = false;
|
|
6361
|
+
let parseState = initParseState();
|
|
6132
6362
|
let finishReason = "";
|
|
6133
6363
|
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
6134
6364
|
roundLoop: while (true) {
|
|
@@ -6172,8 +6402,8 @@ async function runGeneration(d, stream, ctx) {
|
|
|
6172
6402
|
}
|
|
6173
6403
|
continue;
|
|
6174
6404
|
}
|
|
6175
|
-
|
|
6176
|
-
|
|
6405
|
+
const rc = delta.reasoning_content ?? delta.reasoning;
|
|
6406
|
+
if (rc) {
|
|
6177
6407
|
if (!thinkStart) thinkStart = Date.now();
|
|
6178
6408
|
thinkEnd = Date.now();
|
|
6179
6409
|
fullReasoning += rc;
|
|
@@ -6182,88 +6412,34 @@ async function runGeneration(d, stream, ctx) {
|
|
|
6182
6412
|
}
|
|
6183
6413
|
const raw_content = delta.content ?? "";
|
|
6184
6414
|
if (!raw_content) continue;
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
pendingThinkBuf = pendingThinkBuf.slice(7);
|
|
6194
|
-
toProcess = "";
|
|
6195
|
-
} else if (openIdx > 0) {
|
|
6196
|
-
const before = pendingThinkBuf.slice(0, openIdx);
|
|
6197
|
-
fullContent += before;
|
|
6198
|
-
roundContent += before;
|
|
6199
|
-
firstDelta = true;
|
|
6200
|
-
if (!ttftMs) ttftMs = Date.now() - requestStart;
|
|
6201
|
-
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta: before }) });
|
|
6202
|
-
inThink = true;
|
|
6203
|
-
if (!thinkStart) thinkStart = Date.now();
|
|
6204
|
-
pendingThinkBuf = pendingThinkBuf.slice(openIdx + 7);
|
|
6205
|
-
toProcess = "";
|
|
6206
|
-
} else if (pendingThinkBuf.length > 20) {
|
|
6207
|
-
const flush = pendingThinkBuf;
|
|
6208
|
-
pendingThinkBuf = "";
|
|
6209
|
-
fullContent += flush;
|
|
6210
|
-
roundContent += flush;
|
|
6211
|
-
firstDelta = true;
|
|
6212
|
-
if (!ttftMs) ttftMs = Date.now() - requestStart;
|
|
6213
|
-
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta: flush }) });
|
|
6214
|
-
toProcess = "";
|
|
6215
|
-
} else {
|
|
6216
|
-
toProcess = "";
|
|
6217
|
-
}
|
|
6218
|
-
} else if (inThink) {
|
|
6219
|
-
pendingThinkBuf += toProcess;
|
|
6220
|
-
const closeIdx = pendingThinkBuf.indexOf("</think>");
|
|
6221
|
-
if (closeIdx >= 0) {
|
|
6222
|
-
const thinkChunk = pendingThinkBuf.slice(0, closeIdx);
|
|
6223
|
-
if (thinkChunk) {
|
|
6224
|
-
thinkEnd = Date.now();
|
|
6225
|
-
fullReasoning += thinkChunk;
|
|
6226
|
-
await stream.writeSSE({ event: "reasoning", data: JSON.stringify({ delta: thinkChunk }) });
|
|
6227
|
-
}
|
|
6228
|
-
inThink = false;
|
|
6229
|
-
thinkEnd = Date.now();
|
|
6230
|
-
pendingThinkBuf = pendingThinkBuf.slice(closeIdx + 8);
|
|
6231
|
-
toProcess = "";
|
|
6232
|
-
if (pendingThinkBuf) {
|
|
6233
|
-
fullContent += pendingThinkBuf;
|
|
6234
|
-
roundContent += pendingThinkBuf;
|
|
6235
|
-
firstDelta = true;
|
|
6236
|
-
if (!ttftMs) ttftMs = Date.now() - requestStart;
|
|
6237
|
-
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta: pendingThinkBuf }) });
|
|
6238
|
-
pendingThinkBuf = "";
|
|
6239
|
-
}
|
|
6240
|
-
} else {
|
|
6241
|
-
thinkEnd = Date.now();
|
|
6242
|
-
fullReasoning += pendingThinkBuf;
|
|
6243
|
-
await stream.writeSSE({ event: "reasoning", data: JSON.stringify({ delta: pendingThinkBuf }) });
|
|
6244
|
-
pendingThinkBuf = "";
|
|
6245
|
-
toProcess = "";
|
|
6246
|
-
}
|
|
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 }) });
|
|
6247
6423
|
} else {
|
|
6424
|
+
fullContent += ev.text;
|
|
6425
|
+
roundContent += ev.text;
|
|
6248
6426
|
if (!ttftMs) ttftMs = Date.now() - requestStart;
|
|
6249
|
-
firstDelta = true;
|
|
6250
|
-
fullContent += toProcess;
|
|
6251
|
-
roundContent += toProcess;
|
|
6252
6427
|
d.manager.setLiveGen({ phase: "gen", pct: 0, outputTokens: ++liveOut });
|
|
6253
|
-
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta:
|
|
6254
|
-
toProcess = "";
|
|
6428
|
+
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta: ev.text }) });
|
|
6255
6429
|
}
|
|
6256
6430
|
}
|
|
6257
6431
|
}
|
|
6258
6432
|
}
|
|
6259
6433
|
ac.signal.removeEventListener("abort", cancelReader);
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
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 }) });
|
|
6438
|
+
} else {
|
|
6439
|
+
fullContent += ev.text;
|
|
6440
|
+
roundContent += ev.text;
|
|
6441
|
+
await stream.writeSSE({ event: "delta", data: JSON.stringify({ delta: ev.text }) });
|
|
6442
|
+
}
|
|
6267
6443
|
}
|
|
6268
6444
|
if ((finishReason === "tool_calls" || pendingToolCalls.size > 0) && d.tools && toolIter <= MAX_TOOL_ITER) {
|
|
6269
6445
|
const roundToolCalls = Array.from(pendingToolCalls.values());
|
|
@@ -6358,7 +6534,10 @@ async function runGeneration(d, stream, ctx) {
|
|
|
6358
6534
|
} catch {
|
|
6359
6535
|
}
|
|
6360
6536
|
const finalMsg = db2.getMessage(assistantMsg.id);
|
|
6361
|
-
|
|
6537
|
+
try {
|
|
6538
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ message: finalMsg }) });
|
|
6539
|
+
} catch {
|
|
6540
|
+
}
|
|
6362
6541
|
if (!aborted && conv.title === "New chat" && d.store.snapshot().daemon.autoGenerateTitles) {
|
|
6363
6542
|
setTimeout(() => {
|
|
6364
6543
|
void autoTitle(d, convId, ctx.engineMessages, fullContent, target);
|
|
@@ -6655,7 +6834,8 @@ async function* streamToAnthropic(oaiStream, modelName, msgId, onUsage, onLive)
|
|
|
6655
6834
|
failed = true;
|
|
6656
6835
|
yield sse("error", { error: { type: "api_error", message: "engine stopped" } });
|
|
6657
6836
|
} finally {
|
|
6658
|
-
reader.
|
|
6837
|
+
await reader.cancel().catch(() => {
|
|
6838
|
+
});
|
|
6659
6839
|
}
|
|
6660
6840
|
if (!failed) {
|
|
6661
6841
|
if (inThinking) {
|
|
@@ -6706,6 +6886,15 @@ function cbStop(index) {
|
|
|
6706
6886
|
}
|
|
6707
6887
|
|
|
6708
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
|
+
}
|
|
6709
6898
|
function registerGateway(app2, d) {
|
|
6710
6899
|
app2.post("/v1/messages", async (c) => {
|
|
6711
6900
|
let req;
|
|
@@ -6739,12 +6928,14 @@ function registerGateway(app2, d) {
|
|
|
6739
6928
|
const oaiAlias = engineModelAlias(d.registry.active()?.kind ?? "");
|
|
6740
6929
|
if (oaiAlias) oaiBody.model = oaiAlias;
|
|
6741
6930
|
d.manager.generationStart();
|
|
6931
|
+
const ac = clientAbort(c);
|
|
6742
6932
|
let res;
|
|
6743
6933
|
try {
|
|
6744
6934
|
res = await fetch(`${target}/v1/chat/completions`, {
|
|
6745
6935
|
method: "POST",
|
|
6746
6936
|
headers: { "Content-Type": "application/json" },
|
|
6747
|
-
body: JSON.stringify(oaiBody)
|
|
6937
|
+
body: JSON.stringify(oaiBody),
|
|
6938
|
+
signal: ac.signal
|
|
6748
6939
|
});
|
|
6749
6940
|
} catch (e) {
|
|
6750
6941
|
d.manager.generationEnd();
|
|
@@ -6783,11 +6974,13 @@ function registerGateway(app2, d) {
|
|
|
6783
6974
|
}
|
|
6784
6975
|
);
|
|
6785
6976
|
return streamSSE3(c, async (stream) => {
|
|
6977
|
+
stream.onAbort(() => ac.abort());
|
|
6786
6978
|
try {
|
|
6787
6979
|
for await (const evt of gen) {
|
|
6788
6980
|
await stream.writeSSE({ event: evt.event, data: evt.data });
|
|
6789
6981
|
}
|
|
6790
6982
|
} finally {
|
|
6983
|
+
ac.abort();
|
|
6791
6984
|
d.manager.generationEnd();
|
|
6792
6985
|
}
|
|
6793
6986
|
});
|
|
@@ -6855,7 +7048,8 @@ function registerGateway(app2, d) {
|
|
|
6855
7048
|
const headers = new Headers(c.req.raw.headers);
|
|
6856
7049
|
headers.delete("host");
|
|
6857
7050
|
const maxLimit = d.store.snapshot().modelDefaults.maxTokens ?? 0;
|
|
6858
|
-
const
|
|
7051
|
+
const ac = clientAbort(c);
|
|
7052
|
+
const init = { method: c.req.method, headers, signal: ac.signal };
|
|
6859
7053
|
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
6860
7054
|
if (isChat) {
|
|
6861
7055
|
if (parsedBody && maxLimit > 0) {
|
|
@@ -7083,6 +7277,10 @@ Please upgrade: https://nodejs.org
|
|
|
7083
7277
|
);
|
|
7084
7278
|
process.exit(1);
|
|
7085
7279
|
}
|
|
7280
|
+
process.on("unhandledRejection", (reason) => {
|
|
7281
|
+
if (reason?.name === "AbortError") return;
|
|
7282
|
+
console.warn("unhandledRejection (continuing):", reason);
|
|
7283
|
+
});
|
|
7086
7284
|
var argv = process.argv.slice(2);
|
|
7087
7285
|
function hasFlag(...names) {
|
|
7088
7286
|
return names.some((n) => argv.includes(n));
|
|
@@ -7135,6 +7333,8 @@ var store = ConfigStore.load(argValue("--config", defaultConfigPath()));
|
|
|
7135
7333
|
if (store.brokenBackup()) {
|
|
7136
7334
|
console.warn(`config was reset; previous file backed up at ${store.brokenBackup()}`);
|
|
7137
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`);
|
|
7138
7338
|
var registry = new Registry(store);
|
|
7139
7339
|
var pruned = registry.pruneDeadManagedBuilds();
|
|
7140
7340
|
if (pruned > 0) console.log(`pruned ${pruned} dangling engine build(s)`);
|
|
@@ -7357,12 +7557,11 @@ void (async () => {
|
|
|
7357
7557
|
};
|
|
7358
7558
|
}
|
|
7359
7559
|
if (opts) {
|
|
7360
|
-
|
|
7361
|
-
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}`));
|
|
7362
7561
|
}
|
|
7363
7562
|
})();
|
|
7364
7563
|
var shuttingDown = false;
|
|
7365
|
-
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
7564
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
7366
7565
|
process.on(sig, () => {
|
|
7367
7566
|
if (shuttingDown) return;
|
|
7368
7567
|
shuttingDown = true;
|
|
@@ -7376,3 +7575,9 @@ for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
|
7376
7575
|
setTimeout(() => process.exit(0), 12e3).unref();
|
|
7377
7576
|
});
|
|
7378
7577
|
}
|
|
7578
|
+
process.on("exit", () => {
|
|
7579
|
+
try {
|
|
7580
|
+
killTrackedEnginesSync(store.dir());
|
|
7581
|
+
} catch {
|
|
7582
|
+
}
|
|
7583
|
+
});
|