turbollm 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +454 -125
- package/dist/webdist/assets/index-CpWfZMgy.css +1 -0
- package/dist/webdist/assets/index-DJyXIPFR.js +127 -0
- package/dist/webdist/index.html +2 -2
- package/package.json +1 -1
- package/dist/webdist/assets/index-2qn9bssM.js +0 -104
- package/dist/webdist/assets/index-DWseWCB_.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { spawn as spawn3 } from "child_process";
|
|
5
5
|
import { openSync as openSync3, readFileSync as readFileSync8 } from "fs";
|
|
6
6
|
import { serve } from "@hono/node-server";
|
|
7
|
-
import { dirname as dirname7, join as
|
|
7
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
9
|
|
|
10
10
|
// src/config/config.ts
|
|
@@ -87,7 +87,7 @@ function defaultConfig() {
|
|
|
87
87
|
hf: { token: "" },
|
|
88
88
|
modelDefaults: { ctx: 8192, ngl: 99, imageMaxTokens: 0, maxTokens: 0 },
|
|
89
89
|
featuredOverrideUrl: "",
|
|
90
|
-
comfyui: { enabled: false, gatePath: "" }
|
|
90
|
+
comfyui: { enabled: false, gatePath: "", url: "", reverseGate: false, cachePersist: false }
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
var ConfigStore = class _ConfigStore {
|
|
@@ -217,7 +217,17 @@ function normalize(c) {
|
|
|
217
217
|
c.autoLoadOnStart ??= false;
|
|
218
218
|
c.featuredOverrideUrl ??= "";
|
|
219
219
|
const cu = c.comfyui ?? {};
|
|
220
|
-
c.comfyui = {
|
|
220
|
+
c.comfyui = {
|
|
221
|
+
enabled: !!cu.enabled,
|
|
222
|
+
gatePath: typeof cu.gatePath === "string" ? cu.gatePath : "",
|
|
223
|
+
// Reverse gate (F-011): ComfyUI origin + opt-in toggle. Absent in pre-F-011 configs
|
|
224
|
+
// → '' / false. Reseated here (like the other known fields) so they aren't dropped.
|
|
225
|
+
url: typeof cu.url === "string" ? cu.url : "",
|
|
226
|
+
reverseGate: !!cu.reverseGate,
|
|
227
|
+
// KV prompt-cache persistence (F-014): opt-in. Absent in pre-F-014 configs → false.
|
|
228
|
+
// Reseated like the other known fields so it isn't dropped on every load.
|
|
229
|
+
cachePersist: !!cu.cachePersist
|
|
230
|
+
};
|
|
221
231
|
c.telemetry.level = normalizeTelemetryLevel(c.telemetry.level);
|
|
222
232
|
for (const e of c.engines) {
|
|
223
233
|
e.capabilities ??= { kvTypes: [], flags: [] };
|
|
@@ -244,6 +254,9 @@ function validate(c) {
|
|
|
244
254
|
if (c.activeEngineId && !c.engines.some((e) => e.id === c.activeEngineId)) {
|
|
245
255
|
throw new ValueError("activeEngineId", "unknown engine id");
|
|
246
256
|
}
|
|
257
|
+
if (c.comfyui.url && !/^https?:\/\//i.test(c.comfyui.url)) {
|
|
258
|
+
throw new ValueError("comfyui.url", "must be an http(s):// origin (e.g. http://127.0.0.1:8188)");
|
|
259
|
+
}
|
|
247
260
|
}
|
|
248
261
|
function isAbsolutePath(p) {
|
|
249
262
|
return /^([a-zA-Z]:[\\/]|[\\/])/.test(p);
|
|
@@ -259,9 +272,9 @@ function clampMaxTokens(requested, limit) {
|
|
|
259
272
|
|
|
260
273
|
// src/engines/manager.ts
|
|
261
274
|
import { execFile as execFile4, spawn } from "child_process";
|
|
262
|
-
import { createWriteStream as createWriteStream2, existsSync as
|
|
275
|
+
import { createWriteStream as createWriteStream2, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2 } from "fs";
|
|
263
276
|
import { createServer } from "net";
|
|
264
|
-
import { dirname as dirname2, join as
|
|
277
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
265
278
|
|
|
266
279
|
// src/engines/mlx.ts
|
|
267
280
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync2 } from "fs";
|
|
@@ -557,21 +570,110 @@ function mlxServerCommand(python, model, port2, host2) {
|
|
|
557
570
|
};
|
|
558
571
|
}
|
|
559
572
|
|
|
573
|
+
// src/engines/slot-cache.ts
|
|
574
|
+
import { createHash } from "crypto";
|
|
575
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, rmSync as rmSync3, statSync } from "fs";
|
|
576
|
+
import { join as join4 } from "path";
|
|
577
|
+
var SAVE_CAP_MS = (() => {
|
|
578
|
+
const v = Number(process.env.TURBOLLM_SLOT_CACHE_SAVE_CAP_MS);
|
|
579
|
+
return Number.isFinite(v) && v > 0 ? v : 2500;
|
|
580
|
+
})();
|
|
581
|
+
var TTL_MS = (() => {
|
|
582
|
+
const v = Number(process.env.TURBOLLM_SLOT_CACHE_TTL_MIN);
|
|
583
|
+
return Number.isFinite(v) && v > 0 ? v * 6e4 : 60 * 6e4;
|
|
584
|
+
})();
|
|
585
|
+
function slotCacheDir(dataDir) {
|
|
586
|
+
return join4(dataDir, "slot-cache");
|
|
587
|
+
}
|
|
588
|
+
function slotCacheKey(opts) {
|
|
589
|
+
const material = `${opts.modelPath}\0${opts.extraArgs.join(" ")}\0${opts.engine.version}`;
|
|
590
|
+
const hash = createHash("sha256").update(material).digest("hex").slice(0, 16);
|
|
591
|
+
return `slot-${hash}.bin`;
|
|
592
|
+
}
|
|
593
|
+
function parallelIsOne(extraArgs) {
|
|
594
|
+
const i = extraArgs.indexOf("--parallel");
|
|
595
|
+
if (i === -1) return true;
|
|
596
|
+
return extraArgs[i + 1] === "1";
|
|
597
|
+
}
|
|
598
|
+
function cacheEligible(opts, cfg2) {
|
|
599
|
+
return cfg2.enabled && cfg2.cachePersist && opts.engine.kind === "llama-server" && !opts.model.vision && parallelIsOne(opts.extraArgs);
|
|
600
|
+
}
|
|
601
|
+
function sweepExpired(dir, ttlMs, now) {
|
|
602
|
+
if (!existsSync4(dir)) return;
|
|
603
|
+
let names;
|
|
604
|
+
try {
|
|
605
|
+
names = readdirSync2(dir);
|
|
606
|
+
} catch {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
for (const name of names) {
|
|
610
|
+
if (!/^slot-.*\.bin$/.test(name)) continue;
|
|
611
|
+
const full = join4(dir, name);
|
|
612
|
+
try {
|
|
613
|
+
if (now - statSync(full).mtimeMs > ttlMs) rmSync3(full, { force: true });
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function saveSlot(p) {
|
|
619
|
+
sweepExpired(p.dir, p.ttlMs, p.now);
|
|
620
|
+
try {
|
|
621
|
+
console.log(`[slot-cache] saving the prompt cache before ComfyUI takes the GPU (cap ${p.capMs}ms).`);
|
|
622
|
+
const res = await p.http(`${p.base}/slots/0?action=save`, {
|
|
623
|
+
method: "POST",
|
|
624
|
+
headers: { "content-type": "application/json" },
|
|
625
|
+
body: JSON.stringify({ filename: p.filename }),
|
|
626
|
+
signal: AbortSignal.timeout(p.capMs)
|
|
627
|
+
});
|
|
628
|
+
if (!res.ok) {
|
|
629
|
+
console.warn(`[slot-cache] save returned ${res.status} \u2014 skipping the cache this cycle.`);
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
return true;
|
|
633
|
+
} catch (e) {
|
|
634
|
+
console.warn(`[slot-cache] could not save the prompt cache (${e instanceof Error ? e.message : e}) \u2014 skipping it this cycle.`);
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function restoreSlot(p) {
|
|
639
|
+
try {
|
|
640
|
+
console.log("[slot-cache] restoring the prompt cache after the ComfyUI reload (skipping the re-prefill).");
|
|
641
|
+
const res = await p.http(`${p.base}/slots/0?action=restore`, {
|
|
642
|
+
method: "POST",
|
|
643
|
+
headers: { "content-type": "application/json" },
|
|
644
|
+
body: JSON.stringify({ filename: p.filename }),
|
|
645
|
+
signal: AbortSignal.timeout(3e4)
|
|
646
|
+
});
|
|
647
|
+
if (!res.ok) {
|
|
648
|
+
console.warn(`[slot-cache] restore returned ${res.status} \u2014 the prompt will be re-prefilled normally.`);
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
rmSync3(join4(p.dir, p.filename), { force: true });
|
|
653
|
+
} catch {
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
} catch (e) {
|
|
657
|
+
console.warn(`[slot-cache] could not restore the prompt cache (${e instanceof Error ? e.message : e}) \u2014 re-prefilling normally.`);
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
560
662
|
// src/engines/vllm.ts
|
|
561
|
-
import { existsSync as
|
|
663
|
+
import { existsSync as existsSync5 } from "fs";
|
|
562
664
|
import { execFile as execFile3 } from "child_process";
|
|
563
|
-
import { join as
|
|
665
|
+
import { join as join5 } from "path";
|
|
564
666
|
import { promisify as promisify3 } from "util";
|
|
565
667
|
var execFileP3 = promisify3(execFile3);
|
|
566
668
|
var VLLM_PYTHON = "3.12";
|
|
567
669
|
function venvPython2(envDir) {
|
|
568
|
-
return process.platform === "win32" ?
|
|
670
|
+
return process.platform === "win32" ? join5(envDir, "Scripts", "python.exe") : join5(envDir, "bin", "python");
|
|
569
671
|
}
|
|
570
672
|
async function ensureVllmEnv(root, onProgress) {
|
|
571
673
|
const uv = await ensureUv(root, onProgress);
|
|
572
|
-
const envDir =
|
|
674
|
+
const envDir = join5(root, "vllm", "venv");
|
|
573
675
|
const py = venvPython2(envDir);
|
|
574
|
-
if (!
|
|
676
|
+
if (!existsSync5(py)) {
|
|
575
677
|
onProgress?.({ phase: "extracting", pct: -1 });
|
|
576
678
|
await execFileP3(uv, ["venv", "--python", VLLM_PYTHON, envDir], { cwd: root });
|
|
577
679
|
}
|
|
@@ -591,20 +693,19 @@ async function probeVllm(python) {
|
|
|
591
693
|
);
|
|
592
694
|
return `vllm ${stdout.trim()}`;
|
|
593
695
|
}
|
|
594
|
-
function vllmServerCommand(python, model, port2, host2) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
};
|
|
696
|
+
function vllmServerCommand(python, model, port2, host2, tensorParallelSize = 1) {
|
|
697
|
+
const args = [
|
|
698
|
+
"-m",
|
|
699
|
+
"vllm.entrypoints.openai.api_server",
|
|
700
|
+
"--model",
|
|
701
|
+
model,
|
|
702
|
+
"--host",
|
|
703
|
+
host2,
|
|
704
|
+
"--port",
|
|
705
|
+
String(port2)
|
|
706
|
+
];
|
|
707
|
+
if (tensorParallelSize > 1) args.push("--tensor-parallel-size", String(tensorParallelSize));
|
|
708
|
+
return { cmd: python, args };
|
|
608
709
|
}
|
|
609
710
|
|
|
610
711
|
// src/engines/manager.ts
|
|
@@ -657,14 +758,23 @@ var Manager = class {
|
|
|
657
758
|
if (!opts.engine.binPath) throw new Error("no_active_engine");
|
|
658
759
|
if (!opts.modelPath) throw new Error("no_such_model");
|
|
659
760
|
const port2 = await allocPort();
|
|
660
|
-
const logPath =
|
|
761
|
+
const logPath = join6(this.store.dir(), "logs", `engine-${opts.engine.id}.log`);
|
|
661
762
|
mkdirSync4(dirname2(logPath), { recursive: true });
|
|
662
763
|
const logStream = createWriteStream2(logPath);
|
|
663
764
|
logStream.write(
|
|
664
765
|
`[turbollm] starting engine "${opts.engine.name}" on internal port ${port2} (127.0.0.1 only \u2014 the engine's own port, NOT the TurboLLM app/UI port).
|
|
665
766
|
`
|
|
666
767
|
);
|
|
667
|
-
const
|
|
768
|
+
const cfg2 = this.store.snapshot();
|
|
769
|
+
let slotSavePath;
|
|
770
|
+
if (cfg2.comfyui.enabled && cfg2.comfyui.cachePersist && opts.engine.kind === "llama-server") {
|
|
771
|
+
const flags = opts.engine.capabilities.flags;
|
|
772
|
+
if (flags.length === 0 || flags.includes("--slot-save-path")) {
|
|
773
|
+
slotSavePath = slotCacheDir(this.store.dir());
|
|
774
|
+
mkdirSync4(slotSavePath, { recursive: true });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const { cmd, args } = engineCommand(opts, port2, slotSavePath);
|
|
668
778
|
const child = spawn(cmd, args, { cwd: dirname2(cmd), windowsHide: true });
|
|
669
779
|
child.stdout?.pipe(logStream, { end: false });
|
|
670
780
|
child.stderr?.pipe(logStream, { end: false });
|
|
@@ -870,23 +980,24 @@ var Manager = class {
|
|
|
870
980
|
if (idle) this.stop();
|
|
871
981
|
}
|
|
872
982
|
};
|
|
873
|
-
function engineCommand(opts, port2) {
|
|
983
|
+
function engineCommand(opts, port2, slotSavePath) {
|
|
874
984
|
if (opts.engine.kind === "mlx") {
|
|
875
985
|
return mlxServerCommand(opts.engine.binPath, opts.modelPath, port2, "127.0.0.1");
|
|
876
986
|
}
|
|
877
987
|
if (opts.engine.kind === "vllm") {
|
|
878
|
-
return vllmServerCommand(opts.engine.binPath, opts.modelPath, port2, "127.0.0.1");
|
|
988
|
+
return vllmServerCommand(opts.engine.binPath, opts.modelPath, port2, "127.0.0.1", opts.tensorParallelSize);
|
|
879
989
|
}
|
|
880
|
-
return { cmd: opts.engine.binPath, args: buildArgs(opts, port2) };
|
|
990
|
+
return { cmd: opts.engine.binPath, args: buildArgs(opts, port2, slotSavePath) };
|
|
881
991
|
}
|
|
882
992
|
function readinessTimeoutMs(kind) {
|
|
883
993
|
return kind === "vllm" ? 6e5 : 12e4;
|
|
884
994
|
}
|
|
885
|
-
function buildArgs(opts, port2) {
|
|
995
|
+
function buildArgs(opts, port2, slotSavePath) {
|
|
886
996
|
const args = ["-m", opts.modelPath, "--host", "127.0.0.1", "--port", String(port2)];
|
|
887
997
|
const flags = opts.engine.capabilities.flags;
|
|
888
998
|
if (flags.length === 0 || flags.includes("--metrics")) args.push("--metrics");
|
|
889
999
|
if (flags.includes("--no-webui")) args.push("--no-webui");
|
|
1000
|
+
if (slotSavePath) args.push("--slot-save-path", slotSavePath);
|
|
890
1001
|
args.push(...opts.extraArgs);
|
|
891
1002
|
return args;
|
|
892
1003
|
}
|
|
@@ -945,7 +1056,7 @@ function forceKill(child) {
|
|
|
945
1056
|
}
|
|
946
1057
|
}
|
|
947
1058
|
function readTail(path, n) {
|
|
948
|
-
if (!path || !
|
|
1059
|
+
if (!path || !existsSync6(path)) return [];
|
|
949
1060
|
try {
|
|
950
1061
|
const lines = readFileSync2(path, "utf8").replace(/[\r\n]+$/, "").split("\n").map((l) => l.replace(/\r$/, ""));
|
|
951
1062
|
return lines.length > n ? lines.slice(-n) : lines;
|
|
@@ -958,15 +1069,17 @@ function sleep(ms) {
|
|
|
958
1069
|
}
|
|
959
1070
|
|
|
960
1071
|
// src/engines/comfy-guard.ts
|
|
1072
|
+
var FREE_TIMEOUT_MS = 1e4;
|
|
961
1073
|
var LEASE_MINUTES = (() => {
|
|
962
1074
|
const v = Number(process.env.TURBOLLM_COMFY_LEASE_MIN);
|
|
963
1075
|
return Number.isFinite(v) && v > 0 ? v : 30;
|
|
964
1076
|
})();
|
|
965
1077
|
var ComfyGuard = class {
|
|
966
|
-
constructor(store2, manager2, leaseMinutes = LEASE_MINUTES) {
|
|
1078
|
+
constructor(store2, manager2, leaseMinutes = LEASE_MINUTES, fetchImpl = globalThis.fetch) {
|
|
967
1079
|
this.store = store2;
|
|
968
1080
|
this.manager = manager2;
|
|
969
1081
|
this.leaseMs = Math.max(1, leaseMinutes * 6e4);
|
|
1082
|
+
this.fetchImpl = fetchImpl;
|
|
970
1083
|
}
|
|
971
1084
|
store;
|
|
972
1085
|
manager;
|
|
@@ -977,7 +1090,15 @@ var ComfyGuard = class {
|
|
|
977
1090
|
lastSignalAt = 0;
|
|
978
1091
|
// Serialize concurrent acquire() calls (rapid-fire enqueues) onto one unload.
|
|
979
1092
|
acquiring = null;
|
|
1093
|
+
// Serialize concurrent reverse free-calls (rapid LLM↔image alternation) onto one
|
|
1094
|
+
// POST so we never double-free or race two /free requests at ComfyUI.
|
|
1095
|
+
freeing = null;
|
|
1096
|
+
// KV prompt-cache persistence (F-014): the slot filename we successfully saved THIS
|
|
1097
|
+
// cycle (or null if the save was skipped/failed/ineligible). Gates the restore in
|
|
1098
|
+
// release() — we only restore a cache we actually wrote this acquire.
|
|
1099
|
+
cachedFile = null;
|
|
980
1100
|
leaseMs;
|
|
1101
|
+
fetchImpl;
|
|
981
1102
|
enabled() {
|
|
982
1103
|
return this.store.snapshot().comfyui.enabled;
|
|
983
1104
|
}
|
|
@@ -997,9 +1118,29 @@ var ComfyGuard = class {
|
|
|
997
1118
|
if (this.acquiring) return this.acquiring;
|
|
998
1119
|
this.acquiring = (async () => {
|
|
999
1120
|
const st = this.manager.status();
|
|
1121
|
+
this.cachedFile = null;
|
|
1000
1122
|
if (st.state === "running" || st.state === "starting") {
|
|
1001
1123
|
const opts = this.manager.currentOpts();
|
|
1002
1124
|
if (opts && !this.suspended) this.suspended = opts;
|
|
1125
|
+
if (st.state === "running" && opts) {
|
|
1126
|
+
const cfg2 = this.store.snapshot().comfyui;
|
|
1127
|
+
if (cacheEligible(opts, cfg2)) {
|
|
1128
|
+
const base2 = this.manager.target();
|
|
1129
|
+
if (base2) {
|
|
1130
|
+
const filename = slotCacheKey(opts);
|
|
1131
|
+
const ok = await saveSlot({
|
|
1132
|
+
http: this.fetchImpl,
|
|
1133
|
+
base: base2,
|
|
1134
|
+
dir: slotCacheDir(this.store.dir()),
|
|
1135
|
+
filename,
|
|
1136
|
+
capMs: SAVE_CAP_MS,
|
|
1137
|
+
ttlMs: TTL_MS,
|
|
1138
|
+
now: Date.now()
|
|
1139
|
+
});
|
|
1140
|
+
this.cachedFile = ok ? filename : null;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1003
1144
|
console.log("[comfy-guard] ComfyUI acquired the GPU \u2014 force-unloading the model.");
|
|
1004
1145
|
await this.manager.stopAndWait({ force: true });
|
|
1005
1146
|
}
|
|
@@ -1023,10 +1164,74 @@ var ComfyGuard = class {
|
|
|
1023
1164
|
console.log("[comfy-guard] ComfyUI released the GPU \u2014 reloading the previous model.");
|
|
1024
1165
|
try {
|
|
1025
1166
|
await this.manager.start(opts);
|
|
1167
|
+
if (this.cachedFile) {
|
|
1168
|
+
await this.restoreAfterReady(this.cachedFile);
|
|
1169
|
+
}
|
|
1026
1170
|
} catch (e) {
|
|
1027
1171
|
console.warn(`[comfy-guard] reload after ComfyUI failed: ${e instanceof Error ? e.message : e}`);
|
|
1028
1172
|
}
|
|
1029
1173
|
}
|
|
1174
|
+
this.cachedFile = null;
|
|
1175
|
+
}
|
|
1176
|
+
/** Wait for the just-reloaded engine to reach 'running', then restore the saved KV cache
|
|
1177
|
+
* (F-014). Bounded poll: bails on 'error' or after ~130s (just over the manager's 120s
|
|
1178
|
+
* readiness window). The restore itself is non-fatal — any failure leaves the prefix to
|
|
1179
|
+
* re-prefill normally. */
|
|
1180
|
+
async restoreAfterReady(filename) {
|
|
1181
|
+
const deadline = Date.now() + 13e4;
|
|
1182
|
+
for (; ; ) {
|
|
1183
|
+
const state = this.manager.status().state;
|
|
1184
|
+
if (state === "running") break;
|
|
1185
|
+
if (state === "error" || Date.now() > deadline) return;
|
|
1186
|
+
await this.sleep(500);
|
|
1187
|
+
}
|
|
1188
|
+
const base2 = this.manager.target();
|
|
1189
|
+
if (!base2) return;
|
|
1190
|
+
try {
|
|
1191
|
+
await restoreSlot({ http: this.fetchImpl, base: base2, dir: slotCacheDir(this.store.dir()), filename });
|
|
1192
|
+
} catch {
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
sleep(ms) {
|
|
1196
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1197
|
+
}
|
|
1198
|
+
/** REVERSE gate (F-011): TurboLLM is about to load a model — ask ComfyUI to drop its
|
|
1199
|
+
* VRAM first by calling its native `POST {url}/free`. Symmetric counterpart of the
|
|
1200
|
+
* forward acquire/release gate. Every model-load entry point (the HTTP load route,
|
|
1201
|
+
* the bench load, the startup auto-load) awaits this before `manager.start(...)`.
|
|
1202
|
+
*
|
|
1203
|
+
* Fires ONLY when ComfyUI is idle from our side (`!held`): if WE currently hold the
|
|
1204
|
+
* GPU for ComfyUI a render is in flight, and the forward block already kept this load
|
|
1205
|
+
* out — never interrupt that render. Any failure (ComfyUI down, connection refused,
|
|
1206
|
+
* timeout, non-2xx) is NON-FATAL: we log a warning and return so the load proceeds,
|
|
1207
|
+
* exactly as the gate node treats an unreachable TurboLLM. Concurrent calls (rapid
|
|
1208
|
+
* LLM↔image alternation) collapse onto one in-flight POST so we never double-free. */
|
|
1209
|
+
async freeComfyUIBeforeLoad() {
|
|
1210
|
+
const cfg2 = this.store.snapshot().comfyui;
|
|
1211
|
+
if (!cfg2.enabled || !cfg2.reverseGate || !cfg2.url || this.held) return;
|
|
1212
|
+
if (this.freeing) return this.freeing;
|
|
1213
|
+
this.freeing = (async () => {
|
|
1214
|
+
const url = `${cfg2.url.replace(/\/+$/, "")}/free`;
|
|
1215
|
+
try {
|
|
1216
|
+
console.log("[comfy-guard] TurboLLM is loading a model \u2014 asking ComfyUI to free its VRAM first.");
|
|
1217
|
+
const res = await this.fetchImpl(url, {
|
|
1218
|
+
method: "POST",
|
|
1219
|
+
headers: { "content-type": "application/json" },
|
|
1220
|
+
body: JSON.stringify({ unload_models: true, free_memory: true }),
|
|
1221
|
+
signal: AbortSignal.timeout(FREE_TIMEOUT_MS)
|
|
1222
|
+
});
|
|
1223
|
+
if (!res.ok) {
|
|
1224
|
+
console.warn(`[comfy-guard] ComfyUI /free returned ${res.status} \u2014 loading anyway.`);
|
|
1225
|
+
}
|
|
1226
|
+
} catch (e) {
|
|
1227
|
+
console.warn(`[comfy-guard] could not reach ComfyUI /free (${e instanceof Error ? e.message : e}) \u2014 loading anyway.`);
|
|
1228
|
+
}
|
|
1229
|
+
})();
|
|
1230
|
+
try {
|
|
1231
|
+
await this.freeing;
|
|
1232
|
+
} finally {
|
|
1233
|
+
this.freeing = null;
|
|
1234
|
+
}
|
|
1030
1235
|
}
|
|
1031
1236
|
snapshot() {
|
|
1032
1237
|
const cfg2 = this.store.snapshot().comfyui;
|
|
@@ -1058,12 +1263,12 @@ var ComfyGuard = class {
|
|
|
1058
1263
|
};
|
|
1059
1264
|
|
|
1060
1265
|
// src/engines/registry.ts
|
|
1061
|
-
import { existsSync as
|
|
1266
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1062
1267
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1063
1268
|
|
|
1064
1269
|
// src/engines/probe.ts
|
|
1065
1270
|
import { execFile as execFile5 } from "child_process";
|
|
1066
|
-
import { closeSync, existsSync as
|
|
1271
|
+
import { closeSync, existsSync as existsSync7, openSync, readSync, statSync as statSync2 } from "fs";
|
|
1067
1272
|
import { dirname as dirname3 } from "path";
|
|
1068
1273
|
var ProbeError = class extends Error {
|
|
1069
1274
|
constructor(code, msg) {
|
|
@@ -1093,7 +1298,7 @@ function runCaptured(bin, arg) {
|
|
|
1093
1298
|
});
|
|
1094
1299
|
}
|
|
1095
1300
|
async function probe(bin) {
|
|
1096
|
-
if (!
|
|
1301
|
+
if (!existsSync7(bin) || statSync2(bin).isDirectory()) {
|
|
1097
1302
|
throw new ProbeError("binary_not_found", "Binary not found at that path.");
|
|
1098
1303
|
}
|
|
1099
1304
|
const fmt = detectFormat(bin);
|
|
@@ -1323,7 +1528,7 @@ var Registry = class {
|
|
|
1323
1528
|
pruneDeadManagedBuilds() {
|
|
1324
1529
|
let removed = 0;
|
|
1325
1530
|
for (const e of this.list().engines) {
|
|
1326
|
-
if (isManagedBuild(e.binPath) && !
|
|
1531
|
+
if (isManagedBuild(e.binPath) && !existsSync8(e.binPath)) {
|
|
1327
1532
|
try {
|
|
1328
1533
|
this.remove(e.id);
|
|
1329
1534
|
removed++;
|
|
@@ -1506,8 +1711,8 @@ function engineAcceptsFormat(engineKind, format) {
|
|
|
1506
1711
|
}
|
|
1507
1712
|
|
|
1508
1713
|
// src/models/scanner.ts
|
|
1509
|
-
import { existsSync as
|
|
1510
|
-
import { basename, dirname as dirname4, join as
|
|
1714
|
+
import { existsSync as existsSync9, lstatSync, readdirSync as readdirSync3, readFileSync as readFileSync3, rmSync as rmSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1715
|
+
import { basename, dirname as dirname4, join as join7 } from "path";
|
|
1511
1716
|
|
|
1512
1717
|
// src/gguf/gguf.ts
|
|
1513
1718
|
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2 } from "fs";
|
|
@@ -1772,7 +1977,7 @@ var ScannerError = class extends Error {
|
|
|
1772
1977
|
var Scanner = class {
|
|
1773
1978
|
constructor(store2) {
|
|
1774
1979
|
this.store = store2;
|
|
1775
|
-
this.cachePath =
|
|
1980
|
+
this.cachePath = join7(store2.dir(), "models-cache.json");
|
|
1776
1981
|
this.loadCache();
|
|
1777
1982
|
}
|
|
1778
1983
|
store;
|
|
@@ -1801,14 +2006,14 @@ var Scanner = class {
|
|
|
1801
2006
|
const total = m[3];
|
|
1802
2007
|
let names;
|
|
1803
2008
|
try {
|
|
1804
|
-
names =
|
|
2009
|
+
names = readdirSync3(e.dir);
|
|
1805
2010
|
} catch {
|
|
1806
2011
|
return [e.path];
|
|
1807
2012
|
}
|
|
1808
2013
|
const shards = [];
|
|
1809
2014
|
for (const name of names) {
|
|
1810
2015
|
const sm = name.match(SPLIT_RE);
|
|
1811
|
-
if (sm && sm[1] === prefix && sm[3] === total) shards.push(
|
|
2016
|
+
if (sm && sm[1] === prefix && sm[3] === total) shards.push(join7(e.dir, name));
|
|
1812
2017
|
}
|
|
1813
2018
|
return shards.length > 0 ? shards.sort() : [e.path];
|
|
1814
2019
|
}
|
|
@@ -1820,9 +2025,9 @@ var Scanner = class {
|
|
|
1820
2025
|
if (!e) throw new ScannerError("no_such_model", "No model with that key.");
|
|
1821
2026
|
const paths = this.filesFor(key);
|
|
1822
2027
|
if (e.format === "mlx") {
|
|
1823
|
-
|
|
2028
|
+
rmSync4(e.path, { recursive: true, force: true });
|
|
1824
2029
|
} else {
|
|
1825
|
-
for (const p of paths)
|
|
2030
|
+
for (const p of paths) rmSync4(p, { force: true });
|
|
1826
2031
|
this.cache.delete(e.path);
|
|
1827
2032
|
}
|
|
1828
2033
|
await this.rescan();
|
|
@@ -1836,7 +2041,7 @@ var Scanner = class {
|
|
|
1836
2041
|
const dirs = this.store.snapshot().modelDirs;
|
|
1837
2042
|
const scan = { ggufs: [], mlxDirs: [] };
|
|
1838
2043
|
for (const d of dirs) {
|
|
1839
|
-
if (
|
|
2044
|
+
if (existsSync9(d)) walk(d, scan);
|
|
1840
2045
|
await tick();
|
|
1841
2046
|
}
|
|
1842
2047
|
const gguf = await this.build(scan.ggufs);
|
|
@@ -1962,7 +2167,7 @@ function isMlxModelDir(names) {
|
|
|
1962
2167
|
function walk(dir, out) {
|
|
1963
2168
|
let names;
|
|
1964
2169
|
try {
|
|
1965
|
-
names =
|
|
2170
|
+
names = readdirSync3(dir);
|
|
1966
2171
|
} catch {
|
|
1967
2172
|
return;
|
|
1968
2173
|
}
|
|
@@ -1972,7 +2177,7 @@ function walk(dir, out) {
|
|
|
1972
2177
|
}
|
|
1973
2178
|
for (const name of names) {
|
|
1974
2179
|
if (name === ".git" || name === "node_modules") continue;
|
|
1975
|
-
const full =
|
|
2180
|
+
const full = join7(dir, name);
|
|
1976
2181
|
let st;
|
|
1977
2182
|
try {
|
|
1978
2183
|
st = lstatSync(full);
|
|
@@ -1990,7 +2195,7 @@ function mlxEntryFor(dir) {
|
|
|
1990
2195
|
let cfg2 = {};
|
|
1991
2196
|
let parseError = null;
|
|
1992
2197
|
try {
|
|
1993
|
-
const raw = readFileSync3(
|
|
2198
|
+
const raw = readFileSync3(join7(dir, "config.json"), "utf8").replace(/^/, "");
|
|
1994
2199
|
cfg2 = JSON.parse(raw);
|
|
1995
2200
|
} catch (e) {
|
|
1996
2201
|
parseError = `Could not read config.json: ${e.message}`;
|
|
@@ -1999,16 +2204,16 @@ function mlxEntryFor(dir) {
|
|
|
1999
2204
|
let mtimeMs = 0;
|
|
2000
2205
|
let hasChatTemplate = false;
|
|
2001
2206
|
try {
|
|
2002
|
-
for (const n of
|
|
2207
|
+
for (const n of readdirSync3(dir)) {
|
|
2003
2208
|
const lower = n.toLowerCase();
|
|
2004
2209
|
if (lower.endsWith(".safetensors")) {
|
|
2005
|
-
const st = lstatSync(
|
|
2210
|
+
const st = lstatSync(join7(dir, n));
|
|
2006
2211
|
sizeBytes += st.size;
|
|
2007
2212
|
mtimeMs = Math.max(mtimeMs, st.mtimeMs);
|
|
2008
2213
|
}
|
|
2009
2214
|
}
|
|
2010
|
-
const tc =
|
|
2011
|
-
if (
|
|
2215
|
+
const tc = join7(dir, "tokenizer_config.json");
|
|
2216
|
+
if (existsSync9(tc)) hasChatTemplate = readFileSync3(tc, "utf8").includes("chat_template");
|
|
2012
2217
|
} catch {
|
|
2013
2218
|
}
|
|
2014
2219
|
const expertCount = cfg2.num_local_experts ?? cfg2.num_experts ?? 0;
|
|
@@ -2051,16 +2256,16 @@ function tick() {
|
|
|
2051
2256
|
}
|
|
2052
2257
|
|
|
2053
2258
|
// src/models/hashes.ts
|
|
2054
|
-
import { createHash } from "crypto";
|
|
2259
|
+
import { createHash as createHash2 } from "crypto";
|
|
2055
2260
|
import { createReadStream, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
2056
|
-
import { join as
|
|
2261
|
+
import { join as join8 } from "path";
|
|
2057
2262
|
var HashStore = class {
|
|
2058
2263
|
cache = /* @__PURE__ */ new Map();
|
|
2059
2264
|
pending = /* @__PURE__ */ new Set();
|
|
2060
2265
|
path;
|
|
2061
2266
|
constructor(dataDir) {
|
|
2062
2267
|
mkdirSync5(dataDir, { recursive: true });
|
|
2063
|
-
this.path =
|
|
2268
|
+
this.path = join8(dataDir, "model-hashes.json");
|
|
2064
2269
|
this.load();
|
|
2065
2270
|
}
|
|
2066
2271
|
/** Cached sha256 for a file, only when the cached row still matches size+mtime
|
|
@@ -2078,7 +2283,7 @@ var HashStore = class {
|
|
|
2078
2283
|
}
|
|
2079
2284
|
compute(path, size, mtime) {
|
|
2080
2285
|
return new Promise((resolve2) => {
|
|
2081
|
-
const hash =
|
|
2286
|
+
const hash = createHash2("sha256");
|
|
2082
2287
|
const rs = createReadStream(path);
|
|
2083
2288
|
rs.on("error", () => resolve2());
|
|
2084
2289
|
rs.on("data", (c) => hash.update(c));
|
|
@@ -2110,6 +2315,9 @@ var HashStore = class {
|
|
|
2110
2315
|
};
|
|
2111
2316
|
|
|
2112
2317
|
// src/models/profile.ts
|
|
2318
|
+
function defaultGpu() {
|
|
2319
|
+
return { splitMode: "layer", tensorSplit: [], mainGpu: -1, tensorParallelSize: 1 };
|
|
2320
|
+
}
|
|
2113
2321
|
var HEAD_DIM = 128;
|
|
2114
2322
|
function kvBytesPerElem(t) {
|
|
2115
2323
|
switch (t) {
|
|
@@ -2134,10 +2342,18 @@ function kvBytesPerElem(t) {
|
|
|
2134
2342
|
}
|
|
2135
2343
|
}
|
|
2136
2344
|
function defaultSampling() {
|
|
2137
|
-
return { temp: 0.8, topP: 0.95, topK: 40, minP: 0.05, repeatPenalty: 1, presencePenalty: 0 };
|
|
2345
|
+
return { temp: 0.8, topP: 0.95, topK: 40, minP: 0.05, repeatPenalty: 1, presencePenalty: 0, frequencyPenalty: 0, stop: [] };
|
|
2346
|
+
}
|
|
2347
|
+
function gpuBudgetMb(sys, p) {
|
|
2348
|
+
if (sys.gpus.length === 0) return 0;
|
|
2349
|
+
if (p?.gpu?.splitMode === "none") {
|
|
2350
|
+
const idx = p.gpu.mainGpu >= 0 ? p.gpu.mainGpu : 0;
|
|
2351
|
+
return sys.gpus[idx]?.vramMb ?? sys.gpus[0]?.vramMb ?? 0;
|
|
2352
|
+
}
|
|
2353
|
+
return sys.gpus.reduce((sum, g) => sum + (g.vramMb || 0), 0);
|
|
2138
2354
|
}
|
|
2139
2355
|
function estimateVram(p, m, sys) {
|
|
2140
|
-
const totalVramMb = sys
|
|
2356
|
+
const totalVramMb = gpuBudgetMb(sys, p);
|
|
2141
2357
|
if (totalVramMb === 0) return { estMb: 0, totalVramMb: 0, pct: 0, verdict: "cpu" };
|
|
2142
2358
|
const sizeMb = m.sizeBytes / 1e6;
|
|
2143
2359
|
const blocks = m.blockCount || 1;
|
|
@@ -2182,10 +2398,16 @@ function deriveDefault(m, sys) {
|
|
|
2182
2398
|
mtpHeadPath: "",
|
|
2183
2399
|
draftModelPath: "",
|
|
2184
2400
|
sampling: defaultSampling(),
|
|
2401
|
+
contextOverflow: "shift",
|
|
2402
|
+
nKeep: 0,
|
|
2403
|
+
ropeScalingType: "none",
|
|
2404
|
+
ropeFreqBase: 0,
|
|
2405
|
+
ropeFreqScale: 0,
|
|
2406
|
+
gpu: defaultGpu(),
|
|
2185
2407
|
extraArgs: []
|
|
2186
2408
|
};
|
|
2187
2409
|
if (m.moe && hasGpu && m.blockCount > 0) {
|
|
2188
|
-
const budget = (sys
|
|
2410
|
+
const budget = gpuBudgetMb(sys, base2) * 0.85;
|
|
2189
2411
|
base2.nCpuMoe = m.blockCount;
|
|
2190
2412
|
for (let n = 0; n <= m.blockCount; n += 2) {
|
|
2191
2413
|
if (estimateVram({ ...base2, nCpuMoe: n }, m, sys).estMb <= budget) {
|
|
@@ -2213,13 +2435,24 @@ function resolveProfile(m, sys, saved, overrides, defaults) {
|
|
|
2213
2435
|
...base2,
|
|
2214
2436
|
...saved ?? {},
|
|
2215
2437
|
...overrides ?? {},
|
|
2216
|
-
sampling: { ...base2.sampling, ...saved?.sampling ?? {}, ...overrides?.sampling ?? {} }
|
|
2438
|
+
sampling: { ...base2.sampling, ...saved?.sampling ?? {}, ...overrides?.sampling ?? {} },
|
|
2439
|
+
// gpu is deep-merged like sampling so a partial override (or an old saved profile
|
|
2440
|
+
// missing some fields) keeps the rest of the defaults instead of going undefined.
|
|
2441
|
+
gpu: { ...base2.gpu, ...saved?.gpu ?? {}, ...overrides?.gpu ?? {} }
|
|
2217
2442
|
};
|
|
2218
2443
|
}
|
|
2219
2444
|
function profileToArgs(p, m, caps, cores = 0) {
|
|
2220
2445
|
const has = (flag) => caps.flags.length === 0 || caps.flags.includes(flag);
|
|
2221
2446
|
const a = ["-c", String(p.ctx)];
|
|
2222
2447
|
if (p.ngl > 0) a.push("-ngl", String(p.ngl));
|
|
2448
|
+
const g = p.gpu;
|
|
2449
|
+
if (g) {
|
|
2450
|
+
if (g.splitMode !== "layer" && has("--split-mode")) a.push("--split-mode", g.splitMode);
|
|
2451
|
+
if (g.splitMode !== "none" && g.tensorSplit.length > 0 && has("--tensor-split")) {
|
|
2452
|
+
a.push("--tensor-split", g.tensorSplit.join(","));
|
|
2453
|
+
}
|
|
2454
|
+
if (g.mainGpu >= 0 && has("--main-gpu")) a.push("--main-gpu", String(g.mainGpu));
|
|
2455
|
+
}
|
|
2223
2456
|
if (has("--parallel")) a.push("--parallel", String(p.parallel));
|
|
2224
2457
|
if (p.parallel > 1 && p.kvUnified && has("--kv-unified")) a.push("--kv-unified");
|
|
2225
2458
|
if (m.moe && p.nCpuMoe > 0 && has("--n-cpu-moe")) a.push("--n-cpu-moe", String(p.nCpuMoe));
|
|
@@ -2247,13 +2480,26 @@ function profileToArgs(p, m, caps, cores = 0) {
|
|
|
2247
2480
|
if (specType) a.push("--spec-type", "draft");
|
|
2248
2481
|
a.push("--model-draft", p.draftModelPath, "--draft-max", "16", "--draft-min", "1");
|
|
2249
2482
|
}
|
|
2483
|
+
if (p.sampling.temp !== 0.8 && has("--temp")) a.push("--temp", String(p.sampling.temp));
|
|
2484
|
+
if (p.sampling.topP !== 0.95 && has("--top-p")) a.push("--top-p", String(p.sampling.topP));
|
|
2485
|
+
if (p.sampling.topK !== 40 && has("--top-k")) a.push("--top-k", String(p.sampling.topK));
|
|
2486
|
+
if (p.sampling.minP !== 0.05 && has("--min-p")) a.push("--min-p", String(p.sampling.minP));
|
|
2487
|
+
if (p.sampling.repeatPenalty !== 1 && has("--repeat-penalty")) a.push("--repeat-penalty", String(p.sampling.repeatPenalty));
|
|
2488
|
+
if (p.sampling.presencePenalty !== 0 && has("--presence-penalty")) a.push("--presence-penalty", String(p.sampling.presencePenalty));
|
|
2489
|
+
if (p.sampling.frequencyPenalty !== 0 && has("--frequency-penalty")) a.push("--frequency-penalty", String(p.sampling.frequencyPenalty));
|
|
2490
|
+
if (p.contextOverflow === "keep" && p.nKeep > 0 && has("--n-keep")) a.push("--n-keep", String(p.nKeep));
|
|
2491
|
+
if (p.ropeScalingType !== "none" && has("--rope-scaling")) {
|
|
2492
|
+
a.push("--rope-scaling", p.ropeScalingType);
|
|
2493
|
+
if (p.ropeFreqBase > 0 && has("--rope-freq-base")) a.push("--rope-freq-base", String(p.ropeFreqBase));
|
|
2494
|
+
if (p.ropeFreqScale > 0 && has("--rope-freq-scale")) a.push("--rope-freq-scale", String(p.ropeFreqScale));
|
|
2495
|
+
}
|
|
2250
2496
|
a.push(...p.extraArgs);
|
|
2251
2497
|
return a;
|
|
2252
2498
|
}
|
|
2253
2499
|
|
|
2254
2500
|
// src/chat/db.ts
|
|
2255
2501
|
import { DatabaseSync } from "node:sqlite";
|
|
2256
|
-
import { join as
|
|
2502
|
+
import { join as join9 } from "path";
|
|
2257
2503
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2258
2504
|
function safeJson(s) {
|
|
2259
2505
|
try {
|
|
@@ -2271,7 +2517,7 @@ function rowToMsg(r) {
|
|
|
2271
2517
|
var ConversationStore = class {
|
|
2272
2518
|
db;
|
|
2273
2519
|
constructor(dataDir) {
|
|
2274
|
-
this.db = new DatabaseSync(
|
|
2520
|
+
this.db = new DatabaseSync(join9(dataDir, "turbollm.db"));
|
|
2275
2521
|
this.migrate();
|
|
2276
2522
|
}
|
|
2277
2523
|
migrate() {
|
|
@@ -2614,19 +2860,19 @@ function base(p) {
|
|
|
2614
2860
|
}
|
|
2615
2861
|
|
|
2616
2862
|
// src/downloads/downloads.ts
|
|
2617
|
-
import { createHash as
|
|
2863
|
+
import { createHash as createHash3 } from "crypto";
|
|
2618
2864
|
import {
|
|
2619
2865
|
createWriteStream as createWriteStream3,
|
|
2620
|
-
existsSync as
|
|
2866
|
+
existsSync as existsSync10,
|
|
2621
2867
|
mkdirSync as mkdirSync6,
|
|
2622
2868
|
readFileSync as readFileSync5,
|
|
2623
2869
|
renameSync as renameSync2,
|
|
2624
|
-
rmSync as
|
|
2870
|
+
rmSync as rmSync5,
|
|
2625
2871
|
statfsSync,
|
|
2626
|
-
statSync as
|
|
2872
|
+
statSync as statSync3,
|
|
2627
2873
|
writeFileSync as writeFileSync4
|
|
2628
2874
|
} from "fs";
|
|
2629
|
-
import { basename as basename2, join as
|
|
2875
|
+
import { basename as basename2, join as join10 } from "path";
|
|
2630
2876
|
import { Readable as Readable2 } from "stream";
|
|
2631
2877
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
2632
2878
|
var MAX_CONCURRENT = 2;
|
|
@@ -2644,10 +2890,10 @@ var DownloadManager = class {
|
|
|
2644
2890
|
this.store = store2;
|
|
2645
2891
|
this.onComplete = onComplete;
|
|
2646
2892
|
this.authHeaders = authHeaders;
|
|
2647
|
-
const dir =
|
|
2893
|
+
const dir = join10(store2.dir(), "downloads");
|
|
2648
2894
|
mkdirSync6(dir, { recursive: true });
|
|
2649
|
-
this.manifestPath =
|
|
2650
|
-
this.provenancePath =
|
|
2895
|
+
this.manifestPath = join10(dir, "manifest.json");
|
|
2896
|
+
this.provenancePath = join10(dir, "provenance.json");
|
|
2651
2897
|
this.restore();
|
|
2652
2898
|
this.loadProvenance();
|
|
2653
2899
|
}
|
|
@@ -2710,7 +2956,7 @@ var DownloadManager = class {
|
|
|
2710
2956
|
name: filename,
|
|
2711
2957
|
repo,
|
|
2712
2958
|
url,
|
|
2713
|
-
dest:
|
|
2959
|
+
dest: join10(dir, filename),
|
|
2714
2960
|
total,
|
|
2715
2961
|
received: 0,
|
|
2716
2962
|
status: "queued",
|
|
@@ -2732,7 +2978,7 @@ var DownloadManager = class {
|
|
|
2732
2978
|
this.controllers.get(id)?.abort();
|
|
2733
2979
|
this.controllers.delete(id);
|
|
2734
2980
|
if (rec.status !== "done") {
|
|
2735
|
-
|
|
2981
|
+
rmSync5(`${rec.dest}.part`, { force: true });
|
|
2736
2982
|
rec.status = "cancelled";
|
|
2737
2983
|
rec.bytesPerSec = 0;
|
|
2738
2984
|
}
|
|
@@ -2746,7 +2992,7 @@ var DownloadManager = class {
|
|
|
2746
2992
|
if (!rec) return false;
|
|
2747
2993
|
this.controllers.get(id)?.abort();
|
|
2748
2994
|
this.controllers.delete(id);
|
|
2749
|
-
if (rec.status !== "done")
|
|
2995
|
+
if (rec.status !== "done") rmSync5(`${rec.dest}.part`, { force: true });
|
|
2750
2996
|
this.records.delete(id);
|
|
2751
2997
|
this.persist();
|
|
2752
2998
|
this.pump();
|
|
@@ -2794,9 +3040,9 @@ var DownloadManager = class {
|
|
|
2794
3040
|
const part = `${rec.dest}.part`;
|
|
2795
3041
|
try {
|
|
2796
3042
|
let startAt = 0;
|
|
2797
|
-
if (
|
|
3043
|
+
if (existsSync10(part)) {
|
|
2798
3044
|
try {
|
|
2799
|
-
startAt =
|
|
3045
|
+
startAt = statSync3(part).size;
|
|
2800
3046
|
} catch {
|
|
2801
3047
|
startAt = 0;
|
|
2802
3048
|
}
|
|
@@ -2811,13 +3057,13 @@ var DownloadManager = class {
|
|
|
2811
3057
|
if (!res.body) throw new DownloadError("download_failed", "Empty response body.");
|
|
2812
3058
|
const resuming = res.status === 206;
|
|
2813
3059
|
if (!resuming && startAt > 0) {
|
|
2814
|
-
|
|
3060
|
+
rmSync5(part, { force: true });
|
|
2815
3061
|
startAt = 0;
|
|
2816
3062
|
rec.received = 0;
|
|
2817
3063
|
}
|
|
2818
3064
|
const clen = Number(res.headers.get("content-length") ?? 0);
|
|
2819
3065
|
if (clen > 0) rec.total = resuming ? startAt + clen : clen;
|
|
2820
|
-
const hash = rec.sha256 ?
|
|
3066
|
+
const hash = rec.sha256 ? createHash3("sha256") : null;
|
|
2821
3067
|
const verifyHash = hash !== null && startAt === 0;
|
|
2822
3068
|
let lastTick = Date.now();
|
|
2823
3069
|
let lastBytes = startAt;
|
|
@@ -2836,13 +3082,13 @@ var DownloadManager = class {
|
|
|
2836
3082
|
const out = createWriteStream3(part, startAt > 0 ? { flags: "a" } : { flags: "w" });
|
|
2837
3083
|
await pipeline2(body3, out, { signal: ac.signal });
|
|
2838
3084
|
if (rec.total > 0 && rec.received !== rec.total) {
|
|
2839
|
-
|
|
3085
|
+
rmSync5(part, { force: true });
|
|
2840
3086
|
throw new DownloadError("size_mismatch", "Download corrupt \u2014 size did not match. Removed the partial file.");
|
|
2841
3087
|
}
|
|
2842
3088
|
if (verifyHash && rec.sha256) {
|
|
2843
3089
|
const got = hash.digest("hex");
|
|
2844
3090
|
if (got !== rec.sha256) {
|
|
2845
|
-
|
|
3091
|
+
rmSync5(part, { force: true });
|
|
2846
3092
|
throw new DownloadError("checksum_failed", "Checksum failed \u2014 the downloaded file was corrupt.");
|
|
2847
3093
|
}
|
|
2848
3094
|
}
|
|
@@ -2934,7 +3180,7 @@ var DownloadManager = class {
|
|
|
2934
3180
|
let received = 0;
|
|
2935
3181
|
try {
|
|
2936
3182
|
const p = `${e.dest}.part`;
|
|
2937
|
-
if (
|
|
3183
|
+
if (existsSync10(p)) received = statSync3(p).size;
|
|
2938
3184
|
} catch {
|
|
2939
3185
|
}
|
|
2940
3186
|
this.records.set(e.id, {
|
|
@@ -2968,7 +3214,7 @@ function safePathname(u) {
|
|
|
2968
3214
|
// src/bench/bench.ts
|
|
2969
3215
|
import { execFile as execFile6 } from "child_process";
|
|
2970
3216
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
2971
|
-
import { join as
|
|
3217
|
+
import { join as join11 } from "path";
|
|
2972
3218
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2973
3219
|
var READY_TIMEOUT_MS = 12e4;
|
|
2974
3220
|
var TOTAL_BUDGET_MS = 10 * 6e4;
|
|
@@ -3236,9 +3482,9 @@ var BenchRunner = class {
|
|
|
3236
3482
|
result: { tps: record.tps, ttftMs: record.ttftMs, vramMb: record.vramMb, outcome: "ok" }
|
|
3237
3483
|
}
|
|
3238
3484
|
};
|
|
3239
|
-
const queueDir =
|
|
3485
|
+
const queueDir = join11(this.store.dir(), "telemetry", "queue");
|
|
3240
3486
|
mkdirSync7(queueDir, { recursive: true });
|
|
3241
|
-
writeFileSync5(
|
|
3487
|
+
writeFileSync5(join11(queueDir, `${randomUUID4()}.json`), JSON.stringify(event));
|
|
3242
3488
|
} catch {
|
|
3243
3489
|
}
|
|
3244
3490
|
}
|
|
@@ -3352,18 +3598,18 @@ Install it: ${spec.install}
|
|
|
3352
3598
|
// src/server.ts
|
|
3353
3599
|
import { Hono } from "hono";
|
|
3354
3600
|
import { cors } from "hono/cors";
|
|
3355
|
-
import { existsSync as
|
|
3601
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
|
|
3356
3602
|
import { fileURLToPath } from "url";
|
|
3357
|
-
import { dirname as dirname6, join as
|
|
3603
|
+
import { dirname as dirname6, join as join13, normalize as normalize2 } from "path";
|
|
3358
3604
|
import { Agent, setGlobalDispatcher } from "undici";
|
|
3359
3605
|
|
|
3360
3606
|
// src/api/routes.ts
|
|
3361
3607
|
import { streamSSE } from "hono/streaming";
|
|
3362
|
-
import { existsSync as
|
|
3363
|
-
import { basename as basename3, dirname as dirname5, join as
|
|
3608
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync6, readdirSync as readdirSync4, realpathSync, rmSync as rmSync6, statSync as statSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
3609
|
+
import { basename as basename3, dirname as dirname5, join as join12, resolve, sep } from "path";
|
|
3364
3610
|
|
|
3365
3611
|
// src/comfyui/gate-template.ts
|
|
3366
|
-
var GATE_VERSION =
|
|
3612
|
+
var GATE_VERSION = 2;
|
|
3367
3613
|
function gateNodeSource(baseUrl) {
|
|
3368
3614
|
const base2 = baseUrl.replace(/\/+$/, "");
|
|
3369
3615
|
const baseLit = JSON.stringify(base2);
|
|
@@ -3420,6 +3666,19 @@ def _acquire_blocking() -> None:
|
|
|
3420
3666
|
_post("/api/v1/comfyui/acquire", ACQUIRE_TIMEOUT_SEC)
|
|
3421
3667
|
|
|
3422
3668
|
|
|
3669
|
+
def _free_self() -> None:
|
|
3670
|
+
"""Unload ComfyUI's own models and clear the CUDA cache so VRAM is available
|
|
3671
|
+
for TurboLLM to reload its model. Called on the release edge (queue drained),
|
|
3672
|
+
before notifying TurboLLM. In-process \u2014 no HTTP roundtrip."""
|
|
3673
|
+
try:
|
|
3674
|
+
import comfy.model_management as mm # type: ignore[import]
|
|
3675
|
+
mm.unload_all_models()
|
|
3676
|
+
mm.soft_empty_cache()
|
|
3677
|
+
_log("freed ComfyUI model cache.")
|
|
3678
|
+
except Exception as e: # noqa: BLE001
|
|
3679
|
+
_log(f"could not free ComfyUI models ({e}); continuing.")
|
|
3680
|
+
|
|
3681
|
+
|
|
3423
3682
|
def _release() -> None:
|
|
3424
3683
|
_post("/api/v1/comfyui/release", RELEASE_TIMEOUT_SEC)
|
|
3425
3684
|
|
|
@@ -3469,6 +3728,7 @@ def _install_hooks() -> None:
|
|
|
3469
3728
|
_busy = False
|
|
3470
3729
|
do_release = True
|
|
3471
3730
|
if do_release:
|
|
3731
|
+
_free_self()
|
|
3472
3732
|
_release()
|
|
3473
3733
|
return result
|
|
3474
3734
|
|
|
@@ -3485,7 +3745,7 @@ __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
|
|
3485
3745
|
}
|
|
3486
3746
|
|
|
3487
3747
|
// src/api/routes.ts
|
|
3488
|
-
import { createHash as
|
|
3748
|
+
import { createHash as createHash4, randomBytes, randomUUID as randomUUID5 } from "crypto";
|
|
3489
3749
|
import { homedir as homedir2, networkInterfaces } from "os";
|
|
3490
3750
|
|
|
3491
3751
|
// src/engines/catalog.ts
|
|
@@ -3594,7 +3854,22 @@ function registerApi(app2, d) {
|
|
|
3594
3854
|
downloads: { active: d.downloads.activeCount() },
|
|
3595
3855
|
engineProvision: d.provision.get(),
|
|
3596
3856
|
// ComfyUI GPU coordination: lets the UI explain a paused/unloaded engine.
|
|
3597
|
-
|
|
3857
|
+
// Also expose the installed gate node version so the UI can prompt an upgrade.
|
|
3858
|
+
comfyui: (() => {
|
|
3859
|
+
const snap = d.comfy?.snapshot() ?? null;
|
|
3860
|
+
if (!snap) return null;
|
|
3861
|
+
const gatePath = d.store.snapshot().comfyui.gatePath;
|
|
3862
|
+
let installedVersion = null;
|
|
3863
|
+
if (gatePath) {
|
|
3864
|
+
try {
|
|
3865
|
+
const src = readFileSync6(join12(gatePath, "__init__.py"), "utf-8");
|
|
3866
|
+
const m = src.match(/^GATE_VERSION\s*=\s*(\d+)/m);
|
|
3867
|
+
if (m) installedVersion = Number(m[1]);
|
|
3868
|
+
} catch {
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
return { ...snap, installedVersion, currentVersion: GATE_VERSION };
|
|
3872
|
+
})(),
|
|
3598
3873
|
telemetryLevel: d.store.snapshot().telemetry.level,
|
|
3599
3874
|
uptimeSec: Math.floor((Date.now() - d.startedAt) / 1e3)
|
|
3600
3875
|
});
|
|
@@ -3608,7 +3883,7 @@ function registerApi(app2, d) {
|
|
|
3608
3883
|
const sys = getSysInfo();
|
|
3609
3884
|
const vendor = primaryVendor(sys);
|
|
3610
3885
|
const recommended = recommendBackendId(vendor, sys.gpus.length > 0);
|
|
3611
|
-
const root =
|
|
3886
|
+
const root = join12(d.store.dir(), "engines");
|
|
3612
3887
|
const active = d.registry.active();
|
|
3613
3888
|
const regEngines = d.registry.list().engines;
|
|
3614
3889
|
const backends = availableBackends().map((b) => {
|
|
@@ -3637,7 +3912,7 @@ function registerApi(app2, d) {
|
|
|
3637
3912
|
const def = availableBackends().find((x) => x.id === b.backend);
|
|
3638
3913
|
if (!def) return err(c, 400, "invalid_config_value", "Unknown backend for this platform.");
|
|
3639
3914
|
if (d.provision.get().active) return err(c, 409, "engine_already_running", "Another engine download is already in progress.");
|
|
3640
|
-
const root =
|
|
3915
|
+
const root = join12(d.store.dir(), "engines");
|
|
3641
3916
|
const ac = new AbortController();
|
|
3642
3917
|
provisionAbort = ac;
|
|
3643
3918
|
void (async () => {
|
|
@@ -3673,7 +3948,7 @@ function registerApi(app2, d) {
|
|
|
3673
3948
|
app2.delete("/api/v1/engines/backends/:id", async (c) => {
|
|
3674
3949
|
const def = availableBackends().find((x) => x.id === c.req.param("id"));
|
|
3675
3950
|
if (!def) return err(c, 400, "invalid_config_value", "Unknown backend for this platform.");
|
|
3676
|
-
const root =
|
|
3951
|
+
const root = join12(d.store.dir(), "engines");
|
|
3677
3952
|
const bin = installedBackendServer(root, def.id);
|
|
3678
3953
|
const eng = bin ? d.registry.list().engines.find((e) => e.binPath === bin) : void 0;
|
|
3679
3954
|
if (eng && d.registry.active()?.id === eng.id) await d.manager.stopAndWait();
|
|
@@ -3686,7 +3961,7 @@ function registerApi(app2, d) {
|
|
|
3686
3961
|
return err(c, 409, "unsupported_platform", "MLX is only available on macOS (Apple Silicon).");
|
|
3687
3962
|
}
|
|
3688
3963
|
if (d.provision.get().active) return err(c, 409, "engine_already_running", "Another engine download is already in progress.");
|
|
3689
|
-
const root =
|
|
3964
|
+
const root = join12(d.store.dir(), "engines");
|
|
3690
3965
|
void (async () => {
|
|
3691
3966
|
try {
|
|
3692
3967
|
d.provision.start("mlx");
|
|
@@ -3715,7 +3990,7 @@ function registerApi(app2, d) {
|
|
|
3715
3990
|
});
|
|
3716
3991
|
app2.post("/api/v1/engines/vllm", (c) => {
|
|
3717
3992
|
if (d.provision.get().active) return err(c, 409, "engine_already_running", "Another engine download is already in progress.");
|
|
3718
|
-
const root =
|
|
3993
|
+
const root = join12(d.store.dir(), "engines");
|
|
3719
3994
|
void (async () => {
|
|
3720
3995
|
try {
|
|
3721
3996
|
d.provision.start("vllm");
|
|
@@ -3736,7 +4011,7 @@ function registerApi(app2, d) {
|
|
|
3736
4011
|
return err(c, 409, "unsupported_platform", "TurboQuant has no prebuilt binary for this operating system yet.");
|
|
3737
4012
|
}
|
|
3738
4013
|
if (d.provision.get().active) return err(c, 409, "engine_already_running", "Another engine download is already in progress.");
|
|
3739
|
-
const root =
|
|
4014
|
+
const root = join12(d.store.dir(), "engines");
|
|
3740
4015
|
void (async () => {
|
|
3741
4016
|
try {
|
|
3742
4017
|
d.provision.start("turboquant");
|
|
@@ -3822,16 +4097,16 @@ function registerApi(app2, d) {
|
|
|
3822
4097
|
}
|
|
3823
4098
|
let entries;
|
|
3824
4099
|
try {
|
|
3825
|
-
entries =
|
|
4100
|
+
entries = readdirSync4(real, { withFileTypes: true }).filter((d2) => !d2.name.startsWith(".")).map((d2) => {
|
|
3826
4101
|
let isDir = d2.isDirectory();
|
|
3827
4102
|
if (d2.isSymbolicLink()) {
|
|
3828
4103
|
try {
|
|
3829
|
-
isDir =
|
|
4104
|
+
isDir = statSync4(join12(real, d2.name)).isDirectory();
|
|
3830
4105
|
} catch {
|
|
3831
4106
|
isDir = false;
|
|
3832
4107
|
}
|
|
3833
4108
|
}
|
|
3834
|
-
return { name: d2.name, path:
|
|
4109
|
+
return { name: d2.name, path: join12(real, d2.name), isDir };
|
|
3835
4110
|
}).sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1);
|
|
3836
4111
|
} catch {
|
|
3837
4112
|
return err(c, 400, "fs_read_failed", "Could not read that folder (permission denied or not a directory).");
|
|
@@ -3859,11 +4134,13 @@ function registerApi(app2, d) {
|
|
|
3859
4134
|
}
|
|
3860
4135
|
let opts2;
|
|
3861
4136
|
if (entry.format !== "gguf") {
|
|
4137
|
+
const savedGpu = cfg2.modelProfiles[entry.key]?.gpu;
|
|
3862
4138
|
opts2 = {
|
|
3863
4139
|
engine: active,
|
|
3864
4140
|
model: { key: entry.key, name: entry.name, quant: entry.quant, ctx: entry.nativeCtx, vision: false },
|
|
3865
4141
|
modelPath: entry.path,
|
|
3866
|
-
extraArgs: []
|
|
4142
|
+
extraArgs: [],
|
|
4143
|
+
tensorParallelSize: savedGpu?.tensorParallelSize
|
|
3867
4144
|
};
|
|
3868
4145
|
} else {
|
|
3869
4146
|
const saved = cfg2.modelProfiles[entry.key];
|
|
@@ -3876,6 +4153,7 @@ function registerApi(app2, d) {
|
|
|
3876
4153
|
};
|
|
3877
4154
|
}
|
|
3878
4155
|
await d.manager.stopAndWait();
|
|
4156
|
+
await d.comfy?.freeComfyUIBeforeLoad();
|
|
3879
4157
|
try {
|
|
3880
4158
|
await d.manager.start(opts2);
|
|
3881
4159
|
} catch (e) {
|
|
@@ -3897,6 +4175,7 @@ function registerApi(app2, d) {
|
|
|
3897
4175
|
if (!modelPath) return err(c, 409, "no_such_model", "No model specified. Pick one from the Models screen.");
|
|
3898
4176
|
const opts = { engine: active, model: deriveModel(modelPath, name, extra), modelPath, extraArgs: extra };
|
|
3899
4177
|
await d.manager.stopAndWait();
|
|
4178
|
+
await d.comfy?.freeComfyUIBeforeLoad();
|
|
3900
4179
|
try {
|
|
3901
4180
|
await d.manager.start(opts);
|
|
3902
4181
|
} catch (e) {
|
|
@@ -3927,22 +4206,22 @@ function registerApi(app2, d) {
|
|
|
3927
4206
|
const raw = (b.path ?? "").trim();
|
|
3928
4207
|
if (!raw) return err(c, 400, "invalid_config_value", "Enter the path to your ComfyUI folder.");
|
|
3929
4208
|
const root = resolve(raw);
|
|
3930
|
-
if (!
|
|
4209
|
+
if (!existsSync11(root) || !statSync4(root).isDirectory()) {
|
|
3931
4210
|
return err(c, 400, "invalid_config_value", "That folder does not exist.");
|
|
3932
4211
|
}
|
|
3933
4212
|
let customNodes;
|
|
3934
4213
|
if (basename3(root).toLowerCase() === "custom_nodes") customNodes = root;
|
|
3935
|
-
else if (
|
|
4214
|
+
else if (existsSync11(join12(root, "custom_nodes"))) customNodes = join12(root, "custom_nodes");
|
|
3936
4215
|
else {
|
|
3937
4216
|
return err(c, 400, "invalid_config_value", "No 'custom_nodes' folder here. Point me at your ComfyUI folder or its custom_nodes folder.");
|
|
3938
4217
|
}
|
|
3939
4218
|
const reqUrl = new URL(c.req.url);
|
|
3940
4219
|
const port2 = reqUrl.port || (reqUrl.protocol === "https:" ? "443" : "80");
|
|
3941
4220
|
const base2 = `http://127.0.0.1:${port2}`;
|
|
3942
|
-
const gateDir =
|
|
4221
|
+
const gateDir = join12(customNodes, "turbollm_gate");
|
|
3943
4222
|
try {
|
|
3944
4223
|
mkdirSync8(gateDir, { recursive: true });
|
|
3945
|
-
writeFileSync6(
|
|
4224
|
+
writeFileSync6(join12(gateDir, "__init__.py"), gateNodeSource(base2));
|
|
3946
4225
|
} catch (e) {
|
|
3947
4226
|
return err(c, 500, "fs_write_failed", `Could not write the gate node: ${e instanceof Error ? e.message : e}`);
|
|
3948
4227
|
}
|
|
@@ -3953,9 +4232,9 @@ function registerApi(app2, d) {
|
|
|
3953
4232
|
});
|
|
3954
4233
|
app2.post("/api/v1/comfyui/uninstall", (c) => {
|
|
3955
4234
|
const dir = d.store.snapshot().comfyui.gatePath;
|
|
3956
|
-
if (dir &&
|
|
4235
|
+
if (dir && existsSync11(dir)) {
|
|
3957
4236
|
try {
|
|
3958
|
-
|
|
4237
|
+
rmSync6(dir, { recursive: true, force: true });
|
|
3959
4238
|
} catch (e) {
|
|
3960
4239
|
return err(c, 500, "fs_write_failed", `Could not remove the gate node: ${e instanceof Error ? e.message : e}`);
|
|
3961
4240
|
}
|
|
@@ -3988,7 +4267,7 @@ function registerApi(app2, d) {
|
|
|
3988
4267
|
});
|
|
3989
4268
|
while (!aborted) {
|
|
3990
4269
|
const path = d.manager.logPath();
|
|
3991
|
-
if (path &&
|
|
4270
|
+
if (path && existsSync11(path)) {
|
|
3992
4271
|
const lines = readFileSync6(path, "utf8").split("\n");
|
|
3993
4272
|
for (; sent < lines.length - 1; sent++) {
|
|
3994
4273
|
await stream.writeSSE({ event: "line", data: JSON.stringify({ line: lines[sent].replace(/\r$/, "") }) });
|
|
@@ -4004,6 +4283,7 @@ function registerApi(app2, d) {
|
|
|
4004
4283
|
const key = (b.modelKey ?? "").trim();
|
|
4005
4284
|
if (!key) return err(c, 400, "invalid_config_value", "modelKey is required.");
|
|
4006
4285
|
if (d.comfy?.isBlocked()) return err(c, 409, "comfyui_busy", "ComfyUI is rendering \u2014 benchmarking is paused until its queue finishes.");
|
|
4286
|
+
await d.comfy?.freeComfyUIBeforeLoad();
|
|
4007
4287
|
try {
|
|
4008
4288
|
d.bench.start(key, b.base && typeof b.base === "object" ? b.base : void 0);
|
|
4009
4289
|
return c.json({ accepted: true }, 202);
|
|
@@ -4048,7 +4328,7 @@ function registerApi(app2, d) {
|
|
|
4048
4328
|
const snap = d.store.snapshot();
|
|
4049
4329
|
const saved = snap.modelProfiles[e.key];
|
|
4050
4330
|
const profile = resolveProfile(e, sys, saved, void 0, snap.modelDefaults);
|
|
4051
|
-
return c.json({ ...overlayModel(e, d), profile, vramFit: estimateVram(profile, e, sys), gpu: sys.gpus[0] ?? null, cores: sys.cores });
|
|
4331
|
+
return c.json({ ...overlayModel(e, d), profile, vramFit: estimateVram(profile, e, sys), gpu: sys.gpus[0] ?? null, gpus: sys.gpus, cores: sys.cores });
|
|
4052
4332
|
});
|
|
4053
4333
|
app2.put("/api/v1/models/:key/profile", async (c) => {
|
|
4054
4334
|
const key = decodeURIComponent(c.req.param("key"));
|
|
@@ -4058,6 +4338,21 @@ function registerApi(app2, d) {
|
|
|
4058
4338
|
if (!p || typeof p.ctx !== "number" || p.ctx < 256) {
|
|
4059
4339
|
return err(c, 400, "invalid_profile_value", "ctx must be at least 256.");
|
|
4060
4340
|
}
|
|
4341
|
+
if (p.gpu) {
|
|
4342
|
+
const g = p.gpu;
|
|
4343
|
+
if (!["layer", "row", "none"].includes(g.splitMode)) {
|
|
4344
|
+
return err(c, 400, "invalid_profile_value", "gpu.splitMode must be layer, row, or none.");
|
|
4345
|
+
}
|
|
4346
|
+
if (!Array.isArray(g.tensorSplit) || g.tensorSplit.some((n) => typeof n !== "number" || !(n >= 0))) {
|
|
4347
|
+
return err(c, 400, "invalid_profile_value", "gpu.tensorSplit must be an array of non-negative numbers.");
|
|
4348
|
+
}
|
|
4349
|
+
if (!Number.isInteger(g.mainGpu) || g.mainGpu < -1) {
|
|
4350
|
+
return err(c, 400, "invalid_profile_value", "gpu.mainGpu must be an integer \u2265 -1.");
|
|
4351
|
+
}
|
|
4352
|
+
if (!Number.isInteger(g.tensorParallelSize) || g.tensorParallelSize < 1) {
|
|
4353
|
+
return err(c, 400, "invalid_profile_value", "gpu.tensorParallelSize must be an integer \u2265 1.");
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4061
4356
|
d.store.update((cfg2) => {
|
|
4062
4357
|
cfg2.modelProfiles[key] = p;
|
|
4063
4358
|
});
|
|
@@ -4126,6 +4421,14 @@ function registerApi(app2, d) {
|
|
|
4126
4421
|
}
|
|
4127
4422
|
const cuUpdates = {};
|
|
4128
4423
|
if (b.comfyui?.enabled !== void 0) cuUpdates.enabled = !!b.comfyui.enabled;
|
|
4424
|
+
if (b.comfyui?.reverseGate !== void 0) cuUpdates.reverseGate = !!b.comfyui.reverseGate;
|
|
4425
|
+
if (b.comfyui?.url !== void 0) {
|
|
4426
|
+
const u = b.comfyui.url.trim();
|
|
4427
|
+
if (u && !/^https?:\/\//i.test(u)) {
|
|
4428
|
+
return err(c, 400, "invalid_config_value", "comfyui.url must be an http(s):// origin (e.g. http://127.0.0.1:8188).");
|
|
4429
|
+
}
|
|
4430
|
+
cuUpdates.url = u;
|
|
4431
|
+
}
|
|
4129
4432
|
const before = d.store.snapshot().daemon;
|
|
4130
4433
|
d.store.update((cfg2) => {
|
|
4131
4434
|
Object.assign(cfg2.daemon, updates);
|
|
@@ -4164,7 +4467,7 @@ function registerApi(app2, d) {
|
|
|
4164
4467
|
const b = await body(c);
|
|
4165
4468
|
const dir = (b.dir ?? "").trim();
|
|
4166
4469
|
if (!dir || !/^([a-zA-Z]:[\\/]|[\\/])/.test(dir)) return err(c, 400, "invalid_config_value", "Path must be absolute.");
|
|
4167
|
-
if (!
|
|
4470
|
+
if (!existsSync11(dir)) return err(c, 400, "invalid_config_value", "That folder does not exist.");
|
|
4168
4471
|
try {
|
|
4169
4472
|
d.store.update((cfg2) => {
|
|
4170
4473
|
if (!cfg2.modelDirs.includes(dir)) cfg2.modelDirs.push(dir);
|
|
@@ -4498,7 +4801,7 @@ function cleanModelName(p) {
|
|
|
4498
4801
|
return basename3(p).replace(/\.gguf$/i, "");
|
|
4499
4802
|
}
|
|
4500
4803
|
function readTail2(path, n) {
|
|
4501
|
-
if (!path || !
|
|
4804
|
+
if (!path || !existsSync11(path)) return [];
|
|
4502
4805
|
try {
|
|
4503
4806
|
const lines = readFileSync6(path, "utf8").replace(/[\r\n]+$/, "").split("\n").map((l) => l.replace(/\r$/, ""));
|
|
4504
4807
|
return lines.length > n ? lines.slice(-n) : lines;
|
|
@@ -4512,7 +4815,7 @@ function generateApiKey() {
|
|
|
4512
4815
|
let key = "";
|
|
4513
4816
|
for (let i = 0; i < 40; i++) key += charset[buf[i] % 62];
|
|
4514
4817
|
const full = `tllm-${key}`;
|
|
4515
|
-
const hash =
|
|
4818
|
+
const hash = createHash4("sha256").update(full).digest("hex");
|
|
4516
4819
|
return { full, hash, prefix: full.slice(0, 12) };
|
|
4517
4820
|
}
|
|
4518
4821
|
function realHome() {
|
|
@@ -4819,17 +5122,33 @@ ${content}` : b.docContext : content;
|
|
|
4819
5122
|
async function runGeneration(d, stream, ctx) {
|
|
4820
5123
|
const { db: db2 } = d;
|
|
4821
5124
|
const { convId, conv, engineMessages, assistantMsg, ms, target, ac, disableThinking } = ctx;
|
|
4822
|
-
const
|
|
4823
|
-
const
|
|
4824
|
-
|
|
5125
|
+
const convS = conv.sampling ?? {};
|
|
5126
|
+
const SAMPLING_KEYS = {
|
|
5127
|
+
temp: "temperature",
|
|
5128
|
+
topP: "top_p",
|
|
5129
|
+
topK: "top_k",
|
|
5130
|
+
minP: "min_p",
|
|
5131
|
+
repeatPenalty: "repeat_penalty",
|
|
5132
|
+
presencePenalty: "presence_penalty",
|
|
5133
|
+
frequencyPenalty: "frequency_penalty"
|
|
5134
|
+
};
|
|
5135
|
+
const samplingOverride = {};
|
|
5136
|
+
for (const [camel, snake] of Object.entries(SAMPLING_KEYS)) {
|
|
5137
|
+
if (camel in convS) samplingOverride[snake] = convS[camel];
|
|
5138
|
+
}
|
|
5139
|
+
for (const [k, v] of Object.entries(convS)) {
|
|
5140
|
+
if (!(k in SAMPLING_KEYS) && k !== "stop") samplingOverride[k] = v;
|
|
5141
|
+
}
|
|
4825
5142
|
const reqBody = {
|
|
4826
5143
|
model: ms.model.key,
|
|
4827
5144
|
messages: engineMessages,
|
|
4828
5145
|
stream: true,
|
|
4829
5146
|
stream_options: { include_usage: true },
|
|
4830
5147
|
return_progress: true,
|
|
4831
|
-
...
|
|
5148
|
+
...samplingOverride
|
|
4832
5149
|
};
|
|
5150
|
+
const stopStrings = convS.stop;
|
|
5151
|
+
if (stopStrings?.length) reqBody.stop = stopStrings;
|
|
4833
5152
|
const maxLimit = d.store.snapshot().modelDefaults.maxTokens ?? 0;
|
|
4834
5153
|
const cappedMax = clampMaxTokens(reqBody.max_tokens, maxLimit);
|
|
4835
5154
|
if (cappedMax != null) reqBody.max_tokens = cappedMax;
|
|
@@ -4867,6 +5186,12 @@ async function runGeneration(d, stream, ctx) {
|
|
|
4867
5186
|
const reader = res.body.getReader();
|
|
4868
5187
|
const decoder = new TextDecoder();
|
|
4869
5188
|
let buf = "";
|
|
5189
|
+
const cancelReader = () => void reader.cancel();
|
|
5190
|
+
if (ac.signal.aborted) {
|
|
5191
|
+
cancelReader();
|
|
5192
|
+
} else {
|
|
5193
|
+
ac.signal.addEventListener("abort", cancelReader, { once: true });
|
|
5194
|
+
}
|
|
4870
5195
|
outer: while (true) {
|
|
4871
5196
|
const { done, value } = await reader.read();
|
|
4872
5197
|
if (done) break;
|
|
@@ -4978,6 +5303,7 @@ async function runGeneration(d, stream, ctx) {
|
|
|
4978
5303
|
}
|
|
4979
5304
|
}
|
|
4980
5305
|
}
|
|
5306
|
+
ac.signal.removeEventListener("abort", cancelReader);
|
|
4981
5307
|
if (pendingThinkBuf && inThink) {
|
|
4982
5308
|
fullReasoning += pendingThinkBuf;
|
|
4983
5309
|
await stream.writeSSE({ event: "reasoning", data: JSON.stringify({ delta: pendingThinkBuf }) });
|
|
@@ -5629,11 +5955,11 @@ async function recordOpenAiStreamUsage(d, body3) {
|
|
|
5629
5955
|
}
|
|
5630
5956
|
|
|
5631
5957
|
// src/auth.ts
|
|
5632
|
-
import { createHash as
|
|
5958
|
+
import { createHash as createHash5 } from "crypto";
|
|
5633
5959
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
5634
5960
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
|
|
5635
5961
|
function hashKey(raw) {
|
|
5636
|
-
return
|
|
5962
|
+
return createHash5("sha256").update(raw).digest("hex");
|
|
5637
5963
|
}
|
|
5638
5964
|
function presentedKey(c) {
|
|
5639
5965
|
const direct = c.req.header("X-TurboLLM-Auth") ?? c.req.header("x-api-key");
|
|
@@ -5691,7 +6017,7 @@ function lanAuth(d) {
|
|
|
5691
6017
|
|
|
5692
6018
|
// src/server.ts
|
|
5693
6019
|
setGlobalDispatcher(new Agent({ keepAliveMaxTimeout: 6e4, connections: 10 }));
|
|
5694
|
-
var WEB_ROOT =
|
|
6020
|
+
var WEB_ROOT = join13(dirname6(fileURLToPath(import.meta.url)), "webdist");
|
|
5695
6021
|
function createApp(d) {
|
|
5696
6022
|
const app2 = new Hono();
|
|
5697
6023
|
app2.use(
|
|
@@ -5716,11 +6042,11 @@ function createApp(d) {
|
|
|
5716
6042
|
if (path.startsWith("api/") || path.startsWith("v1/")) {
|
|
5717
6043
|
return c.json({ error: { code: "not_found", message: "Unknown endpoint." } }, 404);
|
|
5718
6044
|
}
|
|
5719
|
-
let file = normalize2(
|
|
5720
|
-
if (!file.startsWith(WEB_ROOT) || !
|
|
5721
|
-
file =
|
|
6045
|
+
let file = normalize2(join13(WEB_ROOT, path || "index.html"));
|
|
6046
|
+
if (!file.startsWith(WEB_ROOT) || !existsSync12(file) || statSync5(file).isDirectory()) {
|
|
6047
|
+
file = join13(WEB_ROOT, "index.html");
|
|
5722
6048
|
}
|
|
5723
|
-
if (!
|
|
6049
|
+
if (!existsSync12(file)) return c.text("web ui not built \u2014 run `npm run build:web`", 500);
|
|
5724
6050
|
return new Response(readFileSync7(file), { status: 200, headers: { "Content-Type": contentType(file) } });
|
|
5725
6051
|
});
|
|
5726
6052
|
return app2;
|
|
@@ -5748,7 +6074,7 @@ function contentType(file) {
|
|
|
5748
6074
|
// src/cli.ts
|
|
5749
6075
|
var version = "0.1.1";
|
|
5750
6076
|
try {
|
|
5751
|
-
const pkgPath =
|
|
6077
|
+
const pkgPath = join14(dirname7(fileURLToPath2(import.meta.url)), "..", "package.json");
|
|
5752
6078
|
version = JSON.parse(readFileSync8(pkgPath, "utf8")).version ?? version;
|
|
5753
6079
|
} catch {
|
|
5754
6080
|
}
|
|
@@ -5818,7 +6144,7 @@ var registry = new Registry(store);
|
|
|
5818
6144
|
var pruned = registry.pruneDeadManagedBuilds();
|
|
5819
6145
|
if (pruned > 0) console.log(`pruned ${pruned} dangling engine build(s)`);
|
|
5820
6146
|
var provision = new ProvisionState();
|
|
5821
|
-
var enginesDir =
|
|
6147
|
+
var enginesDir = join14(store.dir(), "engines");
|
|
5822
6148
|
void seedDefaultEngines(registry, enginesDir, provision).then(() => registry.ensureProbed());
|
|
5823
6149
|
var manager = new Manager(store);
|
|
5824
6150
|
var scanner = new Scanner(store);
|
|
@@ -5944,7 +6270,7 @@ var restarting = false;
|
|
|
5944
6270
|
function spawnReplacement() {
|
|
5945
6271
|
let out = "ignore";
|
|
5946
6272
|
try {
|
|
5947
|
-
out = openSync3(
|
|
6273
|
+
out = openSync3(join14(store.dir(), "restart.log"), "a");
|
|
5948
6274
|
} catch {
|
|
5949
6275
|
out = "ignore";
|
|
5950
6276
|
}
|
|
@@ -6029,7 +6355,10 @@ void (async () => {
|
|
|
6029
6355
|
extraArgs: cfg.devModel.extraArgs
|
|
6030
6356
|
};
|
|
6031
6357
|
}
|
|
6032
|
-
if (opts)
|
|
6358
|
+
if (opts) {
|
|
6359
|
+
await comfy.freeComfyUIBeforeLoad();
|
|
6360
|
+
manager.start(opts).catch((e) => console.warn(`auto-load failed: ${e}`));
|
|
6361
|
+
}
|
|
6033
6362
|
})();
|
|
6034
6363
|
var shuttingDown = false;
|
|
6035
6364
|
for (const sig of ["SIGINT", "SIGTERM"]) {
|