turbollm 0.2.0 → 0.3.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
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) {
|
|
@@ -2136,8 +2344,16 @@ function kvBytesPerElem(t) {
|
|
|
2136
2344
|
function defaultSampling() {
|
|
2137
2345
|
return { temp: 0.8, topP: 0.95, topK: 40, minP: 0.05, repeatPenalty: 1, presencePenalty: 0 };
|
|
2138
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);
|
|
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,11 @@ function deriveDefault(m, sys) {
|
|
|
2182
2398
|
mtpHeadPath: "",
|
|
2183
2399
|
draftModelPath: "",
|
|
2184
2400
|
sampling: defaultSampling(),
|
|
2401
|
+
gpu: defaultGpu(),
|
|
2185
2402
|
extraArgs: []
|
|
2186
2403
|
};
|
|
2187
2404
|
if (m.moe && hasGpu && m.blockCount > 0) {
|
|
2188
|
-
const budget = (sys
|
|
2405
|
+
const budget = gpuBudgetMb(sys, base2) * 0.85;
|
|
2189
2406
|
base2.nCpuMoe = m.blockCount;
|
|
2190
2407
|
for (let n = 0; n <= m.blockCount; n += 2) {
|
|
2191
2408
|
if (estimateVram({ ...base2, nCpuMoe: n }, m, sys).estMb <= budget) {
|
|
@@ -2213,13 +2430,24 @@ function resolveProfile(m, sys, saved, overrides, defaults) {
|
|
|
2213
2430
|
...base2,
|
|
2214
2431
|
...saved ?? {},
|
|
2215
2432
|
...overrides ?? {},
|
|
2216
|
-
sampling: { ...base2.sampling, ...saved?.sampling ?? {}, ...overrides?.sampling ?? {} }
|
|
2433
|
+
sampling: { ...base2.sampling, ...saved?.sampling ?? {}, ...overrides?.sampling ?? {} },
|
|
2434
|
+
// gpu is deep-merged like sampling so a partial override (or an old saved profile
|
|
2435
|
+
// missing some fields) keeps the rest of the defaults instead of going undefined.
|
|
2436
|
+
gpu: { ...base2.gpu, ...saved?.gpu ?? {}, ...overrides?.gpu ?? {} }
|
|
2217
2437
|
};
|
|
2218
2438
|
}
|
|
2219
2439
|
function profileToArgs(p, m, caps, cores = 0) {
|
|
2220
2440
|
const has = (flag) => caps.flags.length === 0 || caps.flags.includes(flag);
|
|
2221
2441
|
const a = ["-c", String(p.ctx)];
|
|
2222
2442
|
if (p.ngl > 0) a.push("-ngl", String(p.ngl));
|
|
2443
|
+
const g = p.gpu;
|
|
2444
|
+
if (g) {
|
|
2445
|
+
if (g.splitMode !== "layer" && has("--split-mode")) a.push("--split-mode", g.splitMode);
|
|
2446
|
+
if (g.splitMode !== "none" && g.tensorSplit.length > 0 && has("--tensor-split")) {
|
|
2447
|
+
a.push("--tensor-split", g.tensorSplit.join(","));
|
|
2448
|
+
}
|
|
2449
|
+
if (g.mainGpu >= 0 && has("--main-gpu")) a.push("--main-gpu", String(g.mainGpu));
|
|
2450
|
+
}
|
|
2223
2451
|
if (has("--parallel")) a.push("--parallel", String(p.parallel));
|
|
2224
2452
|
if (p.parallel > 1 && p.kvUnified && has("--kv-unified")) a.push("--kv-unified");
|
|
2225
2453
|
if (m.moe && p.nCpuMoe > 0 && has("--n-cpu-moe")) a.push("--n-cpu-moe", String(p.nCpuMoe));
|
|
@@ -2253,7 +2481,7 @@ function profileToArgs(p, m, caps, cores = 0) {
|
|
|
2253
2481
|
|
|
2254
2482
|
// src/chat/db.ts
|
|
2255
2483
|
import { DatabaseSync } from "node:sqlite";
|
|
2256
|
-
import { join as
|
|
2484
|
+
import { join as join9 } from "path";
|
|
2257
2485
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2258
2486
|
function safeJson(s) {
|
|
2259
2487
|
try {
|
|
@@ -2271,7 +2499,7 @@ function rowToMsg(r) {
|
|
|
2271
2499
|
var ConversationStore = class {
|
|
2272
2500
|
db;
|
|
2273
2501
|
constructor(dataDir) {
|
|
2274
|
-
this.db = new DatabaseSync(
|
|
2502
|
+
this.db = new DatabaseSync(join9(dataDir, "turbollm.db"));
|
|
2275
2503
|
this.migrate();
|
|
2276
2504
|
}
|
|
2277
2505
|
migrate() {
|
|
@@ -2614,19 +2842,19 @@ function base(p) {
|
|
|
2614
2842
|
}
|
|
2615
2843
|
|
|
2616
2844
|
// src/downloads/downloads.ts
|
|
2617
|
-
import { createHash as
|
|
2845
|
+
import { createHash as createHash3 } from "crypto";
|
|
2618
2846
|
import {
|
|
2619
2847
|
createWriteStream as createWriteStream3,
|
|
2620
|
-
existsSync as
|
|
2848
|
+
existsSync as existsSync10,
|
|
2621
2849
|
mkdirSync as mkdirSync6,
|
|
2622
2850
|
readFileSync as readFileSync5,
|
|
2623
2851
|
renameSync as renameSync2,
|
|
2624
|
-
rmSync as
|
|
2852
|
+
rmSync as rmSync5,
|
|
2625
2853
|
statfsSync,
|
|
2626
|
-
statSync as
|
|
2854
|
+
statSync as statSync3,
|
|
2627
2855
|
writeFileSync as writeFileSync4
|
|
2628
2856
|
} from "fs";
|
|
2629
|
-
import { basename as basename2, join as
|
|
2857
|
+
import { basename as basename2, join as join10 } from "path";
|
|
2630
2858
|
import { Readable as Readable2 } from "stream";
|
|
2631
2859
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
2632
2860
|
var MAX_CONCURRENT = 2;
|
|
@@ -2644,10 +2872,10 @@ var DownloadManager = class {
|
|
|
2644
2872
|
this.store = store2;
|
|
2645
2873
|
this.onComplete = onComplete;
|
|
2646
2874
|
this.authHeaders = authHeaders;
|
|
2647
|
-
const dir =
|
|
2875
|
+
const dir = join10(store2.dir(), "downloads");
|
|
2648
2876
|
mkdirSync6(dir, { recursive: true });
|
|
2649
|
-
this.manifestPath =
|
|
2650
|
-
this.provenancePath =
|
|
2877
|
+
this.manifestPath = join10(dir, "manifest.json");
|
|
2878
|
+
this.provenancePath = join10(dir, "provenance.json");
|
|
2651
2879
|
this.restore();
|
|
2652
2880
|
this.loadProvenance();
|
|
2653
2881
|
}
|
|
@@ -2710,7 +2938,7 @@ var DownloadManager = class {
|
|
|
2710
2938
|
name: filename,
|
|
2711
2939
|
repo,
|
|
2712
2940
|
url,
|
|
2713
|
-
dest:
|
|
2941
|
+
dest: join10(dir, filename),
|
|
2714
2942
|
total,
|
|
2715
2943
|
received: 0,
|
|
2716
2944
|
status: "queued",
|
|
@@ -2732,7 +2960,7 @@ var DownloadManager = class {
|
|
|
2732
2960
|
this.controllers.get(id)?.abort();
|
|
2733
2961
|
this.controllers.delete(id);
|
|
2734
2962
|
if (rec.status !== "done") {
|
|
2735
|
-
|
|
2963
|
+
rmSync5(`${rec.dest}.part`, { force: true });
|
|
2736
2964
|
rec.status = "cancelled";
|
|
2737
2965
|
rec.bytesPerSec = 0;
|
|
2738
2966
|
}
|
|
@@ -2746,7 +2974,7 @@ var DownloadManager = class {
|
|
|
2746
2974
|
if (!rec) return false;
|
|
2747
2975
|
this.controllers.get(id)?.abort();
|
|
2748
2976
|
this.controllers.delete(id);
|
|
2749
|
-
if (rec.status !== "done")
|
|
2977
|
+
if (rec.status !== "done") rmSync5(`${rec.dest}.part`, { force: true });
|
|
2750
2978
|
this.records.delete(id);
|
|
2751
2979
|
this.persist();
|
|
2752
2980
|
this.pump();
|
|
@@ -2794,9 +3022,9 @@ var DownloadManager = class {
|
|
|
2794
3022
|
const part = `${rec.dest}.part`;
|
|
2795
3023
|
try {
|
|
2796
3024
|
let startAt = 0;
|
|
2797
|
-
if (
|
|
3025
|
+
if (existsSync10(part)) {
|
|
2798
3026
|
try {
|
|
2799
|
-
startAt =
|
|
3027
|
+
startAt = statSync3(part).size;
|
|
2800
3028
|
} catch {
|
|
2801
3029
|
startAt = 0;
|
|
2802
3030
|
}
|
|
@@ -2811,13 +3039,13 @@ var DownloadManager = class {
|
|
|
2811
3039
|
if (!res.body) throw new DownloadError("download_failed", "Empty response body.");
|
|
2812
3040
|
const resuming = res.status === 206;
|
|
2813
3041
|
if (!resuming && startAt > 0) {
|
|
2814
|
-
|
|
3042
|
+
rmSync5(part, { force: true });
|
|
2815
3043
|
startAt = 0;
|
|
2816
3044
|
rec.received = 0;
|
|
2817
3045
|
}
|
|
2818
3046
|
const clen = Number(res.headers.get("content-length") ?? 0);
|
|
2819
3047
|
if (clen > 0) rec.total = resuming ? startAt + clen : clen;
|
|
2820
|
-
const hash = rec.sha256 ?
|
|
3048
|
+
const hash = rec.sha256 ? createHash3("sha256") : null;
|
|
2821
3049
|
const verifyHash = hash !== null && startAt === 0;
|
|
2822
3050
|
let lastTick = Date.now();
|
|
2823
3051
|
let lastBytes = startAt;
|
|
@@ -2836,13 +3064,13 @@ var DownloadManager = class {
|
|
|
2836
3064
|
const out = createWriteStream3(part, startAt > 0 ? { flags: "a" } : { flags: "w" });
|
|
2837
3065
|
await pipeline2(body3, out, { signal: ac.signal });
|
|
2838
3066
|
if (rec.total > 0 && rec.received !== rec.total) {
|
|
2839
|
-
|
|
3067
|
+
rmSync5(part, { force: true });
|
|
2840
3068
|
throw new DownloadError("size_mismatch", "Download corrupt \u2014 size did not match. Removed the partial file.");
|
|
2841
3069
|
}
|
|
2842
3070
|
if (verifyHash && rec.sha256) {
|
|
2843
3071
|
const got = hash.digest("hex");
|
|
2844
3072
|
if (got !== rec.sha256) {
|
|
2845
|
-
|
|
3073
|
+
rmSync5(part, { force: true });
|
|
2846
3074
|
throw new DownloadError("checksum_failed", "Checksum failed \u2014 the downloaded file was corrupt.");
|
|
2847
3075
|
}
|
|
2848
3076
|
}
|
|
@@ -2934,7 +3162,7 @@ var DownloadManager = class {
|
|
|
2934
3162
|
let received = 0;
|
|
2935
3163
|
try {
|
|
2936
3164
|
const p = `${e.dest}.part`;
|
|
2937
|
-
if (
|
|
3165
|
+
if (existsSync10(p)) received = statSync3(p).size;
|
|
2938
3166
|
} catch {
|
|
2939
3167
|
}
|
|
2940
3168
|
this.records.set(e.id, {
|
|
@@ -2968,7 +3196,7 @@ function safePathname(u) {
|
|
|
2968
3196
|
// src/bench/bench.ts
|
|
2969
3197
|
import { execFile as execFile6 } from "child_process";
|
|
2970
3198
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
2971
|
-
import { join as
|
|
3199
|
+
import { join as join11 } from "path";
|
|
2972
3200
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2973
3201
|
var READY_TIMEOUT_MS = 12e4;
|
|
2974
3202
|
var TOTAL_BUDGET_MS = 10 * 6e4;
|
|
@@ -3236,9 +3464,9 @@ var BenchRunner = class {
|
|
|
3236
3464
|
result: { tps: record.tps, ttftMs: record.ttftMs, vramMb: record.vramMb, outcome: "ok" }
|
|
3237
3465
|
}
|
|
3238
3466
|
};
|
|
3239
|
-
const queueDir =
|
|
3467
|
+
const queueDir = join11(this.store.dir(), "telemetry", "queue");
|
|
3240
3468
|
mkdirSync7(queueDir, { recursive: true });
|
|
3241
|
-
writeFileSync5(
|
|
3469
|
+
writeFileSync5(join11(queueDir, `${randomUUID4()}.json`), JSON.stringify(event));
|
|
3242
3470
|
} catch {
|
|
3243
3471
|
}
|
|
3244
3472
|
}
|
|
@@ -3352,15 +3580,15 @@ Install it: ${spec.install}
|
|
|
3352
3580
|
// src/server.ts
|
|
3353
3581
|
import { Hono } from "hono";
|
|
3354
3582
|
import { cors } from "hono/cors";
|
|
3355
|
-
import { existsSync as
|
|
3583
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
|
|
3356
3584
|
import { fileURLToPath } from "url";
|
|
3357
|
-
import { dirname as dirname6, join as
|
|
3585
|
+
import { dirname as dirname6, join as join13, normalize as normalize2 } from "path";
|
|
3358
3586
|
import { Agent, setGlobalDispatcher } from "undici";
|
|
3359
3587
|
|
|
3360
3588
|
// src/api/routes.ts
|
|
3361
3589
|
import { streamSSE } from "hono/streaming";
|
|
3362
|
-
import { existsSync as
|
|
3363
|
-
import { basename as basename3, dirname as dirname5, join as
|
|
3590
|
+
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";
|
|
3591
|
+
import { basename as basename3, dirname as dirname5, join as join12, resolve, sep } from "path";
|
|
3364
3592
|
|
|
3365
3593
|
// src/comfyui/gate-template.ts
|
|
3366
3594
|
var GATE_VERSION = 1;
|
|
@@ -3485,7 +3713,7 @@ __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
|
|
3485
3713
|
}
|
|
3486
3714
|
|
|
3487
3715
|
// src/api/routes.ts
|
|
3488
|
-
import { createHash as
|
|
3716
|
+
import { createHash as createHash4, randomBytes, randomUUID as randomUUID5 } from "crypto";
|
|
3489
3717
|
import { homedir as homedir2, networkInterfaces } from "os";
|
|
3490
3718
|
|
|
3491
3719
|
// src/engines/catalog.ts
|
|
@@ -3608,7 +3836,7 @@ function registerApi(app2, d) {
|
|
|
3608
3836
|
const sys = getSysInfo();
|
|
3609
3837
|
const vendor = primaryVendor(sys);
|
|
3610
3838
|
const recommended = recommendBackendId(vendor, sys.gpus.length > 0);
|
|
3611
|
-
const root =
|
|
3839
|
+
const root = join12(d.store.dir(), "engines");
|
|
3612
3840
|
const active = d.registry.active();
|
|
3613
3841
|
const regEngines = d.registry.list().engines;
|
|
3614
3842
|
const backends = availableBackends().map((b) => {
|
|
@@ -3637,7 +3865,7 @@ function registerApi(app2, d) {
|
|
|
3637
3865
|
const def = availableBackends().find((x) => x.id === b.backend);
|
|
3638
3866
|
if (!def) return err(c, 400, "invalid_config_value", "Unknown backend for this platform.");
|
|
3639
3867
|
if (d.provision.get().active) return err(c, 409, "engine_already_running", "Another engine download is already in progress.");
|
|
3640
|
-
const root =
|
|
3868
|
+
const root = join12(d.store.dir(), "engines");
|
|
3641
3869
|
const ac = new AbortController();
|
|
3642
3870
|
provisionAbort = ac;
|
|
3643
3871
|
void (async () => {
|
|
@@ -3673,7 +3901,7 @@ function registerApi(app2, d) {
|
|
|
3673
3901
|
app2.delete("/api/v1/engines/backends/:id", async (c) => {
|
|
3674
3902
|
const def = availableBackends().find((x) => x.id === c.req.param("id"));
|
|
3675
3903
|
if (!def) return err(c, 400, "invalid_config_value", "Unknown backend for this platform.");
|
|
3676
|
-
const root =
|
|
3904
|
+
const root = join12(d.store.dir(), "engines");
|
|
3677
3905
|
const bin = installedBackendServer(root, def.id);
|
|
3678
3906
|
const eng = bin ? d.registry.list().engines.find((e) => e.binPath === bin) : void 0;
|
|
3679
3907
|
if (eng && d.registry.active()?.id === eng.id) await d.manager.stopAndWait();
|
|
@@ -3686,7 +3914,7 @@ function registerApi(app2, d) {
|
|
|
3686
3914
|
return err(c, 409, "unsupported_platform", "MLX is only available on macOS (Apple Silicon).");
|
|
3687
3915
|
}
|
|
3688
3916
|
if (d.provision.get().active) return err(c, 409, "engine_already_running", "Another engine download is already in progress.");
|
|
3689
|
-
const root =
|
|
3917
|
+
const root = join12(d.store.dir(), "engines");
|
|
3690
3918
|
void (async () => {
|
|
3691
3919
|
try {
|
|
3692
3920
|
d.provision.start("mlx");
|
|
@@ -3715,7 +3943,7 @@ function registerApi(app2, d) {
|
|
|
3715
3943
|
});
|
|
3716
3944
|
app2.post("/api/v1/engines/vllm", (c) => {
|
|
3717
3945
|
if (d.provision.get().active) return err(c, 409, "engine_already_running", "Another engine download is already in progress.");
|
|
3718
|
-
const root =
|
|
3946
|
+
const root = join12(d.store.dir(), "engines");
|
|
3719
3947
|
void (async () => {
|
|
3720
3948
|
try {
|
|
3721
3949
|
d.provision.start("vllm");
|
|
@@ -3736,7 +3964,7 @@ function registerApi(app2, d) {
|
|
|
3736
3964
|
return err(c, 409, "unsupported_platform", "TurboQuant has no prebuilt binary for this operating system yet.");
|
|
3737
3965
|
}
|
|
3738
3966
|
if (d.provision.get().active) return err(c, 409, "engine_already_running", "Another engine download is already in progress.");
|
|
3739
|
-
const root =
|
|
3967
|
+
const root = join12(d.store.dir(), "engines");
|
|
3740
3968
|
void (async () => {
|
|
3741
3969
|
try {
|
|
3742
3970
|
d.provision.start("turboquant");
|
|
@@ -3822,16 +4050,16 @@ function registerApi(app2, d) {
|
|
|
3822
4050
|
}
|
|
3823
4051
|
let entries;
|
|
3824
4052
|
try {
|
|
3825
|
-
entries =
|
|
4053
|
+
entries = readdirSync4(real, { withFileTypes: true }).filter((d2) => !d2.name.startsWith(".")).map((d2) => {
|
|
3826
4054
|
let isDir = d2.isDirectory();
|
|
3827
4055
|
if (d2.isSymbolicLink()) {
|
|
3828
4056
|
try {
|
|
3829
|
-
isDir =
|
|
4057
|
+
isDir = statSync4(join12(real, d2.name)).isDirectory();
|
|
3830
4058
|
} catch {
|
|
3831
4059
|
isDir = false;
|
|
3832
4060
|
}
|
|
3833
4061
|
}
|
|
3834
|
-
return { name: d2.name, path:
|
|
4062
|
+
return { name: d2.name, path: join12(real, d2.name), isDir };
|
|
3835
4063
|
}).sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1);
|
|
3836
4064
|
} catch {
|
|
3837
4065
|
return err(c, 400, "fs_read_failed", "Could not read that folder (permission denied or not a directory).");
|
|
@@ -3859,11 +4087,13 @@ function registerApi(app2, d) {
|
|
|
3859
4087
|
}
|
|
3860
4088
|
let opts2;
|
|
3861
4089
|
if (entry.format !== "gguf") {
|
|
4090
|
+
const savedGpu = cfg2.modelProfiles[entry.key]?.gpu;
|
|
3862
4091
|
opts2 = {
|
|
3863
4092
|
engine: active,
|
|
3864
4093
|
model: { key: entry.key, name: entry.name, quant: entry.quant, ctx: entry.nativeCtx, vision: false },
|
|
3865
4094
|
modelPath: entry.path,
|
|
3866
|
-
extraArgs: []
|
|
4095
|
+
extraArgs: [],
|
|
4096
|
+
tensorParallelSize: savedGpu?.tensorParallelSize
|
|
3867
4097
|
};
|
|
3868
4098
|
} else {
|
|
3869
4099
|
const saved = cfg2.modelProfiles[entry.key];
|
|
@@ -3876,6 +4106,7 @@ function registerApi(app2, d) {
|
|
|
3876
4106
|
};
|
|
3877
4107
|
}
|
|
3878
4108
|
await d.manager.stopAndWait();
|
|
4109
|
+
await d.comfy?.freeComfyUIBeforeLoad();
|
|
3879
4110
|
try {
|
|
3880
4111
|
await d.manager.start(opts2);
|
|
3881
4112
|
} catch (e) {
|
|
@@ -3897,6 +4128,7 @@ function registerApi(app2, d) {
|
|
|
3897
4128
|
if (!modelPath) return err(c, 409, "no_such_model", "No model specified. Pick one from the Models screen.");
|
|
3898
4129
|
const opts = { engine: active, model: deriveModel(modelPath, name, extra), modelPath, extraArgs: extra };
|
|
3899
4130
|
await d.manager.stopAndWait();
|
|
4131
|
+
await d.comfy?.freeComfyUIBeforeLoad();
|
|
3900
4132
|
try {
|
|
3901
4133
|
await d.manager.start(opts);
|
|
3902
4134
|
} catch (e) {
|
|
@@ -3927,22 +4159,22 @@ function registerApi(app2, d) {
|
|
|
3927
4159
|
const raw = (b.path ?? "").trim();
|
|
3928
4160
|
if (!raw) return err(c, 400, "invalid_config_value", "Enter the path to your ComfyUI folder.");
|
|
3929
4161
|
const root = resolve(raw);
|
|
3930
|
-
if (!
|
|
4162
|
+
if (!existsSync11(root) || !statSync4(root).isDirectory()) {
|
|
3931
4163
|
return err(c, 400, "invalid_config_value", "That folder does not exist.");
|
|
3932
4164
|
}
|
|
3933
4165
|
let customNodes;
|
|
3934
4166
|
if (basename3(root).toLowerCase() === "custom_nodes") customNodes = root;
|
|
3935
|
-
else if (
|
|
4167
|
+
else if (existsSync11(join12(root, "custom_nodes"))) customNodes = join12(root, "custom_nodes");
|
|
3936
4168
|
else {
|
|
3937
4169
|
return err(c, 400, "invalid_config_value", "No 'custom_nodes' folder here. Point me at your ComfyUI folder or its custom_nodes folder.");
|
|
3938
4170
|
}
|
|
3939
4171
|
const reqUrl = new URL(c.req.url);
|
|
3940
4172
|
const port2 = reqUrl.port || (reqUrl.protocol === "https:" ? "443" : "80");
|
|
3941
4173
|
const base2 = `http://127.0.0.1:${port2}`;
|
|
3942
|
-
const gateDir =
|
|
4174
|
+
const gateDir = join12(customNodes, "turbollm_gate");
|
|
3943
4175
|
try {
|
|
3944
4176
|
mkdirSync8(gateDir, { recursive: true });
|
|
3945
|
-
writeFileSync6(
|
|
4177
|
+
writeFileSync6(join12(gateDir, "__init__.py"), gateNodeSource(base2));
|
|
3946
4178
|
} catch (e) {
|
|
3947
4179
|
return err(c, 500, "fs_write_failed", `Could not write the gate node: ${e instanceof Error ? e.message : e}`);
|
|
3948
4180
|
}
|
|
@@ -3953,9 +4185,9 @@ function registerApi(app2, d) {
|
|
|
3953
4185
|
});
|
|
3954
4186
|
app2.post("/api/v1/comfyui/uninstall", (c) => {
|
|
3955
4187
|
const dir = d.store.snapshot().comfyui.gatePath;
|
|
3956
|
-
if (dir &&
|
|
4188
|
+
if (dir && existsSync11(dir)) {
|
|
3957
4189
|
try {
|
|
3958
|
-
|
|
4190
|
+
rmSync6(dir, { recursive: true, force: true });
|
|
3959
4191
|
} catch (e) {
|
|
3960
4192
|
return err(c, 500, "fs_write_failed", `Could not remove the gate node: ${e instanceof Error ? e.message : e}`);
|
|
3961
4193
|
}
|
|
@@ -3988,7 +4220,7 @@ function registerApi(app2, d) {
|
|
|
3988
4220
|
});
|
|
3989
4221
|
while (!aborted) {
|
|
3990
4222
|
const path = d.manager.logPath();
|
|
3991
|
-
if (path &&
|
|
4223
|
+
if (path && existsSync11(path)) {
|
|
3992
4224
|
const lines = readFileSync6(path, "utf8").split("\n");
|
|
3993
4225
|
for (; sent < lines.length - 1; sent++) {
|
|
3994
4226
|
await stream.writeSSE({ event: "line", data: JSON.stringify({ line: lines[sent].replace(/\r$/, "") }) });
|
|
@@ -4004,6 +4236,7 @@ function registerApi(app2, d) {
|
|
|
4004
4236
|
const key = (b.modelKey ?? "").trim();
|
|
4005
4237
|
if (!key) return err(c, 400, "invalid_config_value", "modelKey is required.");
|
|
4006
4238
|
if (d.comfy?.isBlocked()) return err(c, 409, "comfyui_busy", "ComfyUI is rendering \u2014 benchmarking is paused until its queue finishes.");
|
|
4239
|
+
await d.comfy?.freeComfyUIBeforeLoad();
|
|
4007
4240
|
try {
|
|
4008
4241
|
d.bench.start(key, b.base && typeof b.base === "object" ? b.base : void 0);
|
|
4009
4242
|
return c.json({ accepted: true }, 202);
|
|
@@ -4048,7 +4281,7 @@ function registerApi(app2, d) {
|
|
|
4048
4281
|
const snap = d.store.snapshot();
|
|
4049
4282
|
const saved = snap.modelProfiles[e.key];
|
|
4050
4283
|
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 });
|
|
4284
|
+
return c.json({ ...overlayModel(e, d), profile, vramFit: estimateVram(profile, e, sys), gpu: sys.gpus[0] ?? null, gpus: sys.gpus, cores: sys.cores });
|
|
4052
4285
|
});
|
|
4053
4286
|
app2.put("/api/v1/models/:key/profile", async (c) => {
|
|
4054
4287
|
const key = decodeURIComponent(c.req.param("key"));
|
|
@@ -4058,6 +4291,21 @@ function registerApi(app2, d) {
|
|
|
4058
4291
|
if (!p || typeof p.ctx !== "number" || p.ctx < 256) {
|
|
4059
4292
|
return err(c, 400, "invalid_profile_value", "ctx must be at least 256.");
|
|
4060
4293
|
}
|
|
4294
|
+
if (p.gpu) {
|
|
4295
|
+
const g = p.gpu;
|
|
4296
|
+
if (!["layer", "row", "none"].includes(g.splitMode)) {
|
|
4297
|
+
return err(c, 400, "invalid_profile_value", "gpu.splitMode must be layer, row, or none.");
|
|
4298
|
+
}
|
|
4299
|
+
if (!Array.isArray(g.tensorSplit) || g.tensorSplit.some((n) => typeof n !== "number" || !(n >= 0))) {
|
|
4300
|
+
return err(c, 400, "invalid_profile_value", "gpu.tensorSplit must be an array of non-negative numbers.");
|
|
4301
|
+
}
|
|
4302
|
+
if (!Number.isInteger(g.mainGpu) || g.mainGpu < -1) {
|
|
4303
|
+
return err(c, 400, "invalid_profile_value", "gpu.mainGpu must be an integer \u2265 -1.");
|
|
4304
|
+
}
|
|
4305
|
+
if (!Number.isInteger(g.tensorParallelSize) || g.tensorParallelSize < 1) {
|
|
4306
|
+
return err(c, 400, "invalid_profile_value", "gpu.tensorParallelSize must be an integer \u2265 1.");
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4061
4309
|
d.store.update((cfg2) => {
|
|
4062
4310
|
cfg2.modelProfiles[key] = p;
|
|
4063
4311
|
});
|
|
@@ -4126,6 +4374,14 @@ function registerApi(app2, d) {
|
|
|
4126
4374
|
}
|
|
4127
4375
|
const cuUpdates = {};
|
|
4128
4376
|
if (b.comfyui?.enabled !== void 0) cuUpdates.enabled = !!b.comfyui.enabled;
|
|
4377
|
+
if (b.comfyui?.reverseGate !== void 0) cuUpdates.reverseGate = !!b.comfyui.reverseGate;
|
|
4378
|
+
if (b.comfyui?.url !== void 0) {
|
|
4379
|
+
const u = b.comfyui.url.trim();
|
|
4380
|
+
if (u && !/^https?:\/\//i.test(u)) {
|
|
4381
|
+
return err(c, 400, "invalid_config_value", "comfyui.url must be an http(s):// origin (e.g. http://127.0.0.1:8188).");
|
|
4382
|
+
}
|
|
4383
|
+
cuUpdates.url = u;
|
|
4384
|
+
}
|
|
4129
4385
|
const before = d.store.snapshot().daemon;
|
|
4130
4386
|
d.store.update((cfg2) => {
|
|
4131
4387
|
Object.assign(cfg2.daemon, updates);
|
|
@@ -4164,7 +4420,7 @@ function registerApi(app2, d) {
|
|
|
4164
4420
|
const b = await body(c);
|
|
4165
4421
|
const dir = (b.dir ?? "").trim();
|
|
4166
4422
|
if (!dir || !/^([a-zA-Z]:[\\/]|[\\/])/.test(dir)) return err(c, 400, "invalid_config_value", "Path must be absolute.");
|
|
4167
|
-
if (!
|
|
4423
|
+
if (!existsSync11(dir)) return err(c, 400, "invalid_config_value", "That folder does not exist.");
|
|
4168
4424
|
try {
|
|
4169
4425
|
d.store.update((cfg2) => {
|
|
4170
4426
|
if (!cfg2.modelDirs.includes(dir)) cfg2.modelDirs.push(dir);
|
|
@@ -4498,7 +4754,7 @@ function cleanModelName(p) {
|
|
|
4498
4754
|
return basename3(p).replace(/\.gguf$/i, "");
|
|
4499
4755
|
}
|
|
4500
4756
|
function readTail2(path, n) {
|
|
4501
|
-
if (!path || !
|
|
4757
|
+
if (!path || !existsSync11(path)) return [];
|
|
4502
4758
|
try {
|
|
4503
4759
|
const lines = readFileSync6(path, "utf8").replace(/[\r\n]+$/, "").split("\n").map((l) => l.replace(/\r$/, ""));
|
|
4504
4760
|
return lines.length > n ? lines.slice(-n) : lines;
|
|
@@ -4512,7 +4768,7 @@ function generateApiKey() {
|
|
|
4512
4768
|
let key = "";
|
|
4513
4769
|
for (let i = 0; i < 40; i++) key += charset[buf[i] % 62];
|
|
4514
4770
|
const full = `tllm-${key}`;
|
|
4515
|
-
const hash =
|
|
4771
|
+
const hash = createHash4("sha256").update(full).digest("hex");
|
|
4516
4772
|
return { full, hash, prefix: full.slice(0, 12) };
|
|
4517
4773
|
}
|
|
4518
4774
|
function realHome() {
|
|
@@ -5629,11 +5885,11 @@ async function recordOpenAiStreamUsage(d, body3) {
|
|
|
5629
5885
|
}
|
|
5630
5886
|
|
|
5631
5887
|
// src/auth.ts
|
|
5632
|
-
import { createHash as
|
|
5888
|
+
import { createHash as createHash5 } from "crypto";
|
|
5633
5889
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
5634
5890
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
|
|
5635
5891
|
function hashKey(raw) {
|
|
5636
|
-
return
|
|
5892
|
+
return createHash5("sha256").update(raw).digest("hex");
|
|
5637
5893
|
}
|
|
5638
5894
|
function presentedKey(c) {
|
|
5639
5895
|
const direct = c.req.header("X-TurboLLM-Auth") ?? c.req.header("x-api-key");
|
|
@@ -5691,7 +5947,7 @@ function lanAuth(d) {
|
|
|
5691
5947
|
|
|
5692
5948
|
// src/server.ts
|
|
5693
5949
|
setGlobalDispatcher(new Agent({ keepAliveMaxTimeout: 6e4, connections: 10 }));
|
|
5694
|
-
var WEB_ROOT =
|
|
5950
|
+
var WEB_ROOT = join13(dirname6(fileURLToPath(import.meta.url)), "webdist");
|
|
5695
5951
|
function createApp(d) {
|
|
5696
5952
|
const app2 = new Hono();
|
|
5697
5953
|
app2.use(
|
|
@@ -5716,11 +5972,11 @@ function createApp(d) {
|
|
|
5716
5972
|
if (path.startsWith("api/") || path.startsWith("v1/")) {
|
|
5717
5973
|
return c.json({ error: { code: "not_found", message: "Unknown endpoint." } }, 404);
|
|
5718
5974
|
}
|
|
5719
|
-
let file = normalize2(
|
|
5720
|
-
if (!file.startsWith(WEB_ROOT) || !
|
|
5721
|
-
file =
|
|
5975
|
+
let file = normalize2(join13(WEB_ROOT, path || "index.html"));
|
|
5976
|
+
if (!file.startsWith(WEB_ROOT) || !existsSync12(file) || statSync5(file).isDirectory()) {
|
|
5977
|
+
file = join13(WEB_ROOT, "index.html");
|
|
5722
5978
|
}
|
|
5723
|
-
if (!
|
|
5979
|
+
if (!existsSync12(file)) return c.text("web ui not built \u2014 run `npm run build:web`", 500);
|
|
5724
5980
|
return new Response(readFileSync7(file), { status: 200, headers: { "Content-Type": contentType(file) } });
|
|
5725
5981
|
});
|
|
5726
5982
|
return app2;
|
|
@@ -5748,7 +6004,7 @@ function contentType(file) {
|
|
|
5748
6004
|
// src/cli.ts
|
|
5749
6005
|
var version = "0.1.1";
|
|
5750
6006
|
try {
|
|
5751
|
-
const pkgPath =
|
|
6007
|
+
const pkgPath = join14(dirname7(fileURLToPath2(import.meta.url)), "..", "package.json");
|
|
5752
6008
|
version = JSON.parse(readFileSync8(pkgPath, "utf8")).version ?? version;
|
|
5753
6009
|
} catch {
|
|
5754
6010
|
}
|
|
@@ -5818,7 +6074,7 @@ var registry = new Registry(store);
|
|
|
5818
6074
|
var pruned = registry.pruneDeadManagedBuilds();
|
|
5819
6075
|
if (pruned > 0) console.log(`pruned ${pruned} dangling engine build(s)`);
|
|
5820
6076
|
var provision = new ProvisionState();
|
|
5821
|
-
var enginesDir =
|
|
6077
|
+
var enginesDir = join14(store.dir(), "engines");
|
|
5822
6078
|
void seedDefaultEngines(registry, enginesDir, provision).then(() => registry.ensureProbed());
|
|
5823
6079
|
var manager = new Manager(store);
|
|
5824
6080
|
var scanner = new Scanner(store);
|
|
@@ -5944,7 +6200,7 @@ var restarting = false;
|
|
|
5944
6200
|
function spawnReplacement() {
|
|
5945
6201
|
let out = "ignore";
|
|
5946
6202
|
try {
|
|
5947
|
-
out = openSync3(
|
|
6203
|
+
out = openSync3(join14(store.dir(), "restart.log"), "a");
|
|
5948
6204
|
} catch {
|
|
5949
6205
|
out = "ignore";
|
|
5950
6206
|
}
|
|
@@ -6029,7 +6285,10 @@ void (async () => {
|
|
|
6029
6285
|
extraArgs: cfg.devModel.extraArgs
|
|
6030
6286
|
};
|
|
6031
6287
|
}
|
|
6032
|
-
if (opts)
|
|
6288
|
+
if (opts) {
|
|
6289
|
+
await comfy.freeComfyUIBeforeLoad();
|
|
6290
|
+
manager.start(opts).catch((e) => console.warn(`auto-load failed: ${e}`));
|
|
6291
|
+
}
|
|
6033
6292
|
})();
|
|
6034
6293
|
var shuttingDown = false;
|
|
6035
6294
|
for (const sig of ["SIGINT", "SIGTERM"]) {
|