turbollm 0.3.0 → 0.6.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 +401 -56
- package/dist/webdist/assets/index-CFP1UlEC.js +127 -0
- package/dist/webdist/assets/index-DeRFSZA-.css +1 -0
- package/dist/webdist/index.html +2 -2
- package/package.json +1 -1
- package/dist/webdist/assets/index-BQf9sJ9F.js +0 -104
- package/dist/webdist/assets/index-_V4G_NqR.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -87,7 +87,8 @@ 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: "", url: "", reverseGate: false, cachePersist: false }
|
|
90
|
+
comfyui: { enabled: false, gatePath: "", url: "", reverseGate: false, cachePersist: false },
|
|
91
|
+
gateway: { autoSwap: true, keepN: 1 }
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
var ConfigStore = class _ConfigStore {
|
|
@@ -228,6 +229,11 @@ function normalize(c) {
|
|
|
228
229
|
// Reseated like the other known fields so it isn't dropped on every load.
|
|
229
230
|
cachePersist: !!cu.cachePersist
|
|
230
231
|
};
|
|
232
|
+
const gw = c.gateway ?? {};
|
|
233
|
+
c.gateway = {
|
|
234
|
+
autoSwap: gw.autoSwap !== false,
|
|
235
|
+
keepN: typeof gw.keepN === "number" && gw.keepN >= 1 ? Math.min(Math.floor(gw.keepN), 4) : 1
|
|
236
|
+
};
|
|
231
237
|
c.telemetry.level = normalizeTelemetryLevel(c.telemetry.level);
|
|
232
238
|
for (const e of c.engines) {
|
|
233
239
|
e.capabilities ??= { kvTypes: [], flags: [] };
|
|
@@ -1966,6 +1972,20 @@ function quantFromName(filename) {
|
|
|
1966
1972
|
// src/models/scanner.ts
|
|
1967
1973
|
var CACHE_VERSION = 2;
|
|
1968
1974
|
var SPLIT_RE = /^(.*)-(\d{5})-of-(\d{5})\.gguf$/i;
|
|
1975
|
+
var EMBED_ARCHS = /* @__PURE__ */ new Set([
|
|
1976
|
+
"bert",
|
|
1977
|
+
"nomic-bert",
|
|
1978
|
+
"jina-bert-v3-base",
|
|
1979
|
+
"jina-bert",
|
|
1980
|
+
"distilbert",
|
|
1981
|
+
"roberta",
|
|
1982
|
+
"xlm-roberta",
|
|
1983
|
+
"electra"
|
|
1984
|
+
]);
|
|
1985
|
+
var EMBED_FILE_RE = /\b(bge[-_]|nomic[-_]embed|all[-_]minilm|e5[-_]|gte[-_]|stella[-_]embed|jina[-_]embed|mxbai[-_]embed)\b/i;
|
|
1986
|
+
function isEmbeddingModel(arch2, name) {
|
|
1987
|
+
return EMBED_ARCHS.has(arch2.toLowerCase()) || EMBED_FILE_RE.test(name);
|
|
1988
|
+
}
|
|
1969
1989
|
var ScannerError = class extends Error {
|
|
1970
1990
|
constructor(code, message) {
|
|
1971
1991
|
super(message);
|
|
@@ -2111,6 +2131,7 @@ var Scanner = class {
|
|
|
2111
2131
|
const quant = meta?.quant || quantFromName(fileName);
|
|
2112
2132
|
const name = meta?.name || cleanName(fileName);
|
|
2113
2133
|
const vision = mmprojPath !== null;
|
|
2134
|
+
const arch2 = meta?.arch ?? "unknown";
|
|
2114
2135
|
return {
|
|
2115
2136
|
key: `${name.toLowerCase()}|${quant}|${sizeBytes}`,
|
|
2116
2137
|
name,
|
|
@@ -2119,7 +2140,7 @@ var Scanner = class {
|
|
|
2119
2140
|
format: "gguf",
|
|
2120
2141
|
sizeBytes,
|
|
2121
2142
|
sizeLabel: meta?.sizeLabel ?? "",
|
|
2122
|
-
arch:
|
|
2143
|
+
arch: arch2,
|
|
2123
2144
|
quant,
|
|
2124
2145
|
nativeCtx: meta?.nativeCtx ?? 0,
|
|
2125
2146
|
blockCount: meta?.blockCount ?? 0,
|
|
@@ -2130,6 +2151,7 @@ var Scanner = class {
|
|
|
2130
2151
|
vision,
|
|
2131
2152
|
mmprojPath: vision ? mmprojPath : null,
|
|
2132
2153
|
hasChatTemplate: meta?.hasChatTemplate ?? false,
|
|
2154
|
+
embedding: isEmbeddingModel(arch2, fileName),
|
|
2133
2155
|
incomplete,
|
|
2134
2156
|
parseError,
|
|
2135
2157
|
loaded: false,
|
|
@@ -2240,6 +2262,7 @@ function mlxEntryFor(dir) {
|
|
|
2240
2262
|
vision: false,
|
|
2241
2263
|
mmprojPath: null,
|
|
2242
2264
|
hasChatTemplate,
|
|
2265
|
+
embedding: isEmbeddingModel(arch2, basename(dir)),
|
|
2243
2266
|
incomplete: false,
|
|
2244
2267
|
parseError,
|
|
2245
2268
|
loaded: false,
|
|
@@ -2342,7 +2365,7 @@ function kvBytesPerElem(t) {
|
|
|
2342
2365
|
}
|
|
2343
2366
|
}
|
|
2344
2367
|
function defaultSampling() {
|
|
2345
|
-
return { temp: 0.8, topP: 0.95, topK: 40, minP: 0.05, repeatPenalty: 1, presencePenalty: 0 };
|
|
2368
|
+
return { temp: 0.8, topP: 0.95, topK: 40, minP: 0.05, repeatPenalty: 1, presencePenalty: 0, frequencyPenalty: 0, stop: [] };
|
|
2346
2369
|
}
|
|
2347
2370
|
function gpuBudgetMb(sys, p) {
|
|
2348
2371
|
if (sys.gpus.length === 0) return 0;
|
|
@@ -2398,7 +2421,13 @@ function deriveDefault(m, sys) {
|
|
|
2398
2421
|
mtpHeadPath: "",
|
|
2399
2422
|
draftModelPath: "",
|
|
2400
2423
|
sampling: defaultSampling(),
|
|
2424
|
+
contextOverflow: "shift",
|
|
2425
|
+
nKeep: 0,
|
|
2426
|
+
ropeScalingType: "none",
|
|
2427
|
+
ropeFreqBase: 0,
|
|
2428
|
+
ropeFreqScale: 0,
|
|
2401
2429
|
gpu: defaultGpu(),
|
|
2430
|
+
grammar: "",
|
|
2402
2431
|
extraArgs: []
|
|
2403
2432
|
};
|
|
2404
2433
|
if (m.moe && hasGpu && m.blockCount > 0) {
|
|
@@ -2475,6 +2504,21 @@ function profileToArgs(p, m, caps, cores = 0) {
|
|
|
2475
2504
|
if (specType) a.push("--spec-type", "draft");
|
|
2476
2505
|
a.push("--model-draft", p.draftModelPath, "--draft-max", "16", "--draft-min", "1");
|
|
2477
2506
|
}
|
|
2507
|
+
if (p.sampling.temp !== 0.8 && has("--temp")) a.push("--temp", String(p.sampling.temp));
|
|
2508
|
+
if (p.sampling.topP !== 0.95 && has("--top-p")) a.push("--top-p", String(p.sampling.topP));
|
|
2509
|
+
if (p.sampling.topK !== 40 && has("--top-k")) a.push("--top-k", String(p.sampling.topK));
|
|
2510
|
+
if (p.sampling.minP !== 0.05 && has("--min-p")) a.push("--min-p", String(p.sampling.minP));
|
|
2511
|
+
if (p.sampling.repeatPenalty !== 1 && has("--repeat-penalty")) a.push("--repeat-penalty", String(p.sampling.repeatPenalty));
|
|
2512
|
+
if (p.sampling.presencePenalty !== 0 && has("--presence-penalty")) a.push("--presence-penalty", String(p.sampling.presencePenalty));
|
|
2513
|
+
if (p.sampling.frequencyPenalty !== 0 && has("--frequency-penalty")) a.push("--frequency-penalty", String(p.sampling.frequencyPenalty));
|
|
2514
|
+
if (p.contextOverflow === "keep" && p.nKeep > 0 && has("--n-keep")) a.push("--n-keep", String(p.nKeep));
|
|
2515
|
+
if (p.ropeScalingType !== "none" && has("--rope-scaling")) {
|
|
2516
|
+
a.push("--rope-scaling", p.ropeScalingType);
|
|
2517
|
+
if (p.ropeFreqBase > 0 && has("--rope-freq-base")) a.push("--rope-freq-base", String(p.ropeFreqBase));
|
|
2518
|
+
if (p.ropeFreqScale > 0 && has("--rope-freq-scale")) a.push("--rope-freq-scale", String(p.ropeFreqScale));
|
|
2519
|
+
}
|
|
2520
|
+
if (m.embedding && has("--embeddings")) a.push("--embeddings");
|
|
2521
|
+
if (p.grammar && has("--grammar")) a.push("--grammar", p.grammar);
|
|
2478
2522
|
a.push(...p.extraArgs);
|
|
2479
2523
|
return a;
|
|
2480
2524
|
}
|
|
@@ -2685,10 +2729,15 @@ var HfClient = class {
|
|
|
2685
2729
|
tokenFn;
|
|
2686
2730
|
version;
|
|
2687
2731
|
cache = /* @__PURE__ */ new Map();
|
|
2688
|
-
/** Search
|
|
2689
|
-
|
|
2732
|
+
/** Search repos (spec 10 §2). Returns up to 30 rows sorted by downloads.
|
|
2733
|
+
* Format filter adapts to the active engine kind:
|
|
2734
|
+
* - llama-server / TurboQuant → filter=gguf
|
|
2735
|
+
* - mlx → filter=mlx (HF library tag)
|
|
2736
|
+
* - vllm → no format filter (searches all HF repos) */
|
|
2737
|
+
async searchModels(query, engineKind) {
|
|
2690
2738
|
const q = query.trim();
|
|
2691
|
-
const
|
|
2739
|
+
const formatFilter = engineKind === "mlx" ? "&filter=mlx" : engineKind === "vllm" ? "" : "&filter=gguf";
|
|
2740
|
+
const url = `${BASE}/api/models?search=${encodeURIComponent(q)}${formatFilter}&sort=downloads&direction=-1&limit=30&full=false`;
|
|
2692
2741
|
const raw = await this.getJson(url);
|
|
2693
2742
|
return raw.map((m) => ({
|
|
2694
2743
|
repo: m.id ?? m.modelId ?? "",
|
|
@@ -2704,8 +2753,30 @@ var HfClient = class {
|
|
|
2704
2753
|
async getRepo(repo) {
|
|
2705
2754
|
const info = await this.getJson(`${BASE}/api/models/${repo}`);
|
|
2706
2755
|
const tree = await this.getJson(`${BASE}/api/models/${repo}/tree/main?recursive=true`);
|
|
2707
|
-
const
|
|
2708
|
-
const
|
|
2756
|
+
const ggufEntries = tree.filter((e) => e.type === "file" && /\.gguf$/i.test(e.path));
|
|
2757
|
+
const safetensorsEntries = tree.filter((e) => e.type === "file" && /\.safetensors$/i.test(e.path));
|
|
2758
|
+
const isSafetensors = ggufEntries.length === 0 && safetensorsEntries.length > 0;
|
|
2759
|
+
let files;
|
|
2760
|
+
let safetensors;
|
|
2761
|
+
if (isSafetensors) {
|
|
2762
|
+
safetensors = true;
|
|
2763
|
+
const components = tree.filter(
|
|
2764
|
+
(e) => e.type === "file" && (/\.safetensors$/i.test(e.path) || /\.json$/i.test(e.path)) && !e.path.includes("/")
|
|
2765
|
+
// root-level only — no nested model card assets
|
|
2766
|
+
);
|
|
2767
|
+
files = components.map((e) => ({
|
|
2768
|
+
name: e.path,
|
|
2769
|
+
quant: "mlx",
|
|
2770
|
+
sizeBytes: e.lfs?.size ?? e.size ?? 0,
|
|
2771
|
+
parts: 1,
|
|
2772
|
+
mmproj: false,
|
|
2773
|
+
safetensors: true,
|
|
2774
|
+
sha256: e.lfs?.oid,
|
|
2775
|
+
url: this.fileUrl(repo, e.path)
|
|
2776
|
+
}));
|
|
2777
|
+
} else {
|
|
2778
|
+
files = groupFiles(repo, ggufEntries);
|
|
2779
|
+
}
|
|
2709
2780
|
const gated = info.gated === true || info.gated === "auto" || info.gated === "manual";
|
|
2710
2781
|
const license = info.cardData?.license ?? (info.tags?.find((t) => t.startsWith("license:"))?.slice("license:".length) || "");
|
|
2711
2782
|
return {
|
|
@@ -2715,7 +2786,8 @@ var HfClient = class {
|
|
|
2715
2786
|
downloads: info.downloads ?? 0,
|
|
2716
2787
|
likes: info.likes ?? 0,
|
|
2717
2788
|
card: await this.getCard(repo),
|
|
2718
|
-
files
|
|
2789
|
+
files,
|
|
2790
|
+
...safetensors ? { safetensors } : {}
|
|
2719
2791
|
};
|
|
2720
2792
|
}
|
|
2721
2793
|
/** Fetch the repo README (the model card), strip its YAML frontmatter, and cap the
|
|
@@ -2910,27 +2982,30 @@ var DownloadManager = class {
|
|
|
2910
2982
|
const dir = this.primaryDir();
|
|
2911
2983
|
if (!dir) throw new DownloadError("no_model_dir", "Add a model folder in Settings before downloading.");
|
|
2912
2984
|
let repo = (input.repo ?? "").trim();
|
|
2985
|
+
const subdir = (input.subdir ?? "").trim();
|
|
2913
2986
|
let url;
|
|
2914
2987
|
let filename;
|
|
2915
2988
|
if (input.url) {
|
|
2916
2989
|
const u = input.url.trim();
|
|
2917
2990
|
if (!/^https?:\/\//i.test(u)) throw new DownloadError("invalid_url", "URL must start with http:// or https://.");
|
|
2918
2991
|
const path = safePathname(u);
|
|
2919
|
-
if (!/\.gguf$/i.test(path) && !HF_BLOB_RE.test(u)) {
|
|
2992
|
+
if (!subdir && !/\.gguf$/i.test(path) && !HF_BLOB_RE.test(u)) {
|
|
2920
2993
|
throw new DownloadError("invalid_url", "URL must point to a .gguf file.");
|
|
2921
2994
|
}
|
|
2922
2995
|
filename = basename2(path);
|
|
2923
|
-
if (!/\.gguf$/i.test(filename)) throw new DownloadError("invalid_url", "Could not derive a .gguf filename from that URL.");
|
|
2996
|
+
if (!subdir && !/\.gguf$/i.test(filename)) throw new DownloadError("invalid_url", "Could not derive a .gguf filename from that URL.");
|
|
2924
2997
|
url = u;
|
|
2925
2998
|
repo = "";
|
|
2926
2999
|
} else {
|
|
2927
3000
|
const rfilename = (input.rfilename ?? "").trim();
|
|
2928
3001
|
if (!repo || !rfilename) throw new DownloadError("invalid_request", "repo and rfilename are required.");
|
|
2929
|
-
if (!/\.gguf$/i.test(rfilename)) throw new DownloadError("invalid_url", "The file must be a .gguf.");
|
|
3002
|
+
if (!subdir && !/\.gguf$/i.test(rfilename)) throw new DownloadError("invalid_url", "The file must be a .gguf.");
|
|
2930
3003
|
filename = basename2(rfilename);
|
|
2931
3004
|
url = `https://huggingface.co/${repo}/resolve/main/${rfilename}`;
|
|
2932
3005
|
}
|
|
2933
3006
|
const total = input.size ?? 0;
|
|
3007
|
+
const destDir = subdir ? join10(dir, subdir) : dir;
|
|
3008
|
+
if (subdir) mkdirSync6(destDir, { recursive: true });
|
|
2934
3009
|
if (total > 0) this.assertDisk(dir, total);
|
|
2935
3010
|
const id = `dl-${Date.now().toString(36)}-${(this.nextSeq++).toString(36)}`;
|
|
2936
3011
|
const rec = {
|
|
@@ -2938,7 +3013,7 @@ var DownloadManager = class {
|
|
|
2938
3013
|
name: filename,
|
|
2939
3014
|
repo,
|
|
2940
3015
|
url,
|
|
2941
|
-
dest: join10(
|
|
3016
|
+
dest: join10(destDir, filename),
|
|
2942
3017
|
total,
|
|
2943
3018
|
received: 0,
|
|
2944
3019
|
status: "queued",
|
|
@@ -3508,6 +3583,219 @@ function sleep2(ms) {
|
|
|
3508
3583
|
return new Promise((r) => setTimeout(r, ms));
|
|
3509
3584
|
}
|
|
3510
3585
|
|
|
3586
|
+
// src/gateway/model-router.ts
|
|
3587
|
+
var ModelRouter = class {
|
|
3588
|
+
constructor(store2, registry2, manager2, scanner2, comfy2) {
|
|
3589
|
+
this.store = store2;
|
|
3590
|
+
this.registry = registry2;
|
|
3591
|
+
this.manager = manager2;
|
|
3592
|
+
this.scanner = scanner2;
|
|
3593
|
+
this.comfy = comfy2;
|
|
3594
|
+
}
|
|
3595
|
+
store;
|
|
3596
|
+
registry;
|
|
3597
|
+
manager;
|
|
3598
|
+
scanner;
|
|
3599
|
+
comfy;
|
|
3600
|
+
/** Extra pool slots beyond the primary manager. Only populated when keepN > 1. */
|
|
3601
|
+
extraSlots = /* @__PURE__ */ new Map();
|
|
3602
|
+
/** Last-used timestamp for the primary manager slot (for LRU eviction). */
|
|
3603
|
+
primaryLastUsed = 0;
|
|
3604
|
+
/** Promise chain that serialises swap operations so concurrent requests for
|
|
3605
|
+
* different models queue rather than race. */
|
|
3606
|
+
swapChain = Promise.resolve();
|
|
3607
|
+
/** Route a request to the correct model target URL.
|
|
3608
|
+
* - If autoSwap is off: returns whatever the primary manager has loaded.
|
|
3609
|
+
* - If the requested model is already loaded: returns its target immediately.
|
|
3610
|
+
* - Otherwise: loads the model (swapping / evicting LRU as needed) and waits. */
|
|
3611
|
+
async route(requestedModel) {
|
|
3612
|
+
const cfg2 = this.store.snapshot();
|
|
3613
|
+
if (!cfg2.gateway.autoSwap || !requestedModel.trim()) {
|
|
3614
|
+
const t = this.manager.target();
|
|
3615
|
+
return t ? { target: t } : { status: 503, message: "No model loaded. Load one in TurboLLM." };
|
|
3616
|
+
}
|
|
3617
|
+
const entry = this.resolveEntry(requestedModel);
|
|
3618
|
+
if (!entry) {
|
|
3619
|
+
const t = this.manager.target();
|
|
3620
|
+
return t ? { target: t } : { status: 503, message: `No model matching '${requestedModel}' found. Add one in TurboLLM.` };
|
|
3621
|
+
}
|
|
3622
|
+
{
|
|
3623
|
+
const ms = this.manager.status();
|
|
3624
|
+
if (ms.state === "running" && ms.model && this.keysMatch(ms.model.key, entry)) {
|
|
3625
|
+
this.primaryLastUsed = Date.now();
|
|
3626
|
+
this.manager.touch();
|
|
3627
|
+
return { target: this.manager.target() };
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
const slot = this.extraSlots.get(entry.key);
|
|
3631
|
+
if (slot) {
|
|
3632
|
+
const ss = slot.manager.status();
|
|
3633
|
+
if (ss.state === "running") {
|
|
3634
|
+
slot.lastUsedMs = Date.now();
|
|
3635
|
+
slot.manager.touch();
|
|
3636
|
+
return { target: slot.manager.target() };
|
|
3637
|
+
}
|
|
3638
|
+
this.extraSlots.delete(entry.key);
|
|
3639
|
+
}
|
|
3640
|
+
let unlock;
|
|
3641
|
+
const prev = this.swapChain;
|
|
3642
|
+
this.swapChain = new Promise((r) => {
|
|
3643
|
+
unlock = r;
|
|
3644
|
+
});
|
|
3645
|
+
try {
|
|
3646
|
+
await prev;
|
|
3647
|
+
return await this.doLoad(entry);
|
|
3648
|
+
} finally {
|
|
3649
|
+
unlock();
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
// ── internal ──────────────────────────────────────────────────────────────
|
|
3653
|
+
async doLoad(entry) {
|
|
3654
|
+
{
|
|
3655
|
+
const ms = this.manager.status();
|
|
3656
|
+
if (ms.state === "running" && ms.model && this.keysMatch(ms.model.key, entry)) {
|
|
3657
|
+
this.primaryLastUsed = Date.now();
|
|
3658
|
+
this.manager.touch();
|
|
3659
|
+
return { target: this.manager.target() };
|
|
3660
|
+
}
|
|
3661
|
+
const slot = this.extraSlots.get(entry.key);
|
|
3662
|
+
if (slot && slot.manager.status().state === "running") {
|
|
3663
|
+
slot.lastUsedMs = Date.now();
|
|
3664
|
+
slot.manager.touch();
|
|
3665
|
+
return { target: slot.manager.target() };
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
if (this.comfy?.isBlocked()) {
|
|
3669
|
+
return { status: 503, message: "ComfyUI is rendering \u2014 model swap paused until its queue finishes." };
|
|
3670
|
+
}
|
|
3671
|
+
const active = this.registry.active();
|
|
3672
|
+
if (!active) return { status: 503, message: "No active engine. Set one up in TurboLLM." };
|
|
3673
|
+
if (!engineAcceptsFormat(active.kind, entry.format)) {
|
|
3674
|
+
return { status: 503, message: `Active engine cannot load model format '${entry.format}'.` };
|
|
3675
|
+
}
|
|
3676
|
+
const opts = this.buildOpts(entry, active);
|
|
3677
|
+
if (!opts) return { status: 503, message: "Model is incomplete or unreadable." };
|
|
3678
|
+
const keepN = Math.max(1, this.store.snapshot().gateway.keepN);
|
|
3679
|
+
const needsNewSlot = entry.embedding || this.chatSlotCount() < keepN;
|
|
3680
|
+
const targetManager = needsNewSlot ? this.manager.status().state === "stopped" || this.manager.status().state === "error" ? this.manager : new Manager(this.store) : this.evictChatLru();
|
|
3681
|
+
await targetManager.stopAndWait();
|
|
3682
|
+
await this.comfy?.freeComfyUIBeforeLoad();
|
|
3683
|
+
try {
|
|
3684
|
+
await targetManager.start(opts);
|
|
3685
|
+
} catch (e) {
|
|
3686
|
+
return { status: 503, message: `Engine start failed: ${e.message}` };
|
|
3687
|
+
}
|
|
3688
|
+
const ready = await this.waitReady(targetManager, active.kind);
|
|
3689
|
+
if (!ready) {
|
|
3690
|
+
const s = targetManager.status();
|
|
3691
|
+
return { status: 503, message: s.err?.message ?? "Model failed to become ready." };
|
|
3692
|
+
}
|
|
3693
|
+
const target = targetManager.target();
|
|
3694
|
+
if (!target) return { status: 503, message: "Model loaded but target URL unavailable." };
|
|
3695
|
+
if (targetManager === this.manager) {
|
|
3696
|
+
this.primaryLastUsed = Date.now();
|
|
3697
|
+
} else {
|
|
3698
|
+
this.extraSlots.set(entry.key, { manager: targetManager, modelKey: entry.key, lastUsedMs: Date.now() });
|
|
3699
|
+
}
|
|
3700
|
+
this.store.update((x) => {
|
|
3701
|
+
x.lastLoaded = { modelKey: entry.key, engineId: active.id };
|
|
3702
|
+
});
|
|
3703
|
+
return { target };
|
|
3704
|
+
}
|
|
3705
|
+
/** Count of alive chat (non-embedding) slots. Embedding models don't consume
|
|
3706
|
+
* a keepN slot so chat models and embedding models can coexist independently. */
|
|
3707
|
+
chatSlotCount() {
|
|
3708
|
+
const isAlive = (s) => s === "running" || s === "starting";
|
|
3709
|
+
const ms = this.manager.status();
|
|
3710
|
+
const primaryAlive = isAlive(ms.state);
|
|
3711
|
+
const primaryEmbed = primaryAlive && !!ms.model && (this.scanner.get(ms.model.key)?.embedding ?? false);
|
|
3712
|
+
const extraChat = [...this.extraSlots.values()].filter(
|
|
3713
|
+
(s) => isAlive(s.manager.status().state) && !(this.scanner.get(s.modelKey)?.embedding ?? false)
|
|
3714
|
+
).length;
|
|
3715
|
+
return (primaryAlive && !primaryEmbed ? 1 : 0) + extraChat;
|
|
3716
|
+
}
|
|
3717
|
+
/** Evict the least-recently-used chat (non-embedding) slot. Embedding slots are
|
|
3718
|
+
* skipped; if every alive slot is an embedding model the true LRU is used as
|
|
3719
|
+
* a fallback so we never deadlock. */
|
|
3720
|
+
evictChatLru() {
|
|
3721
|
+
const isAlive = (s) => s === "running" || s === "starting";
|
|
3722
|
+
const ms = this.manager.status();
|
|
3723
|
+
const primaryAlive = isAlive(ms.state);
|
|
3724
|
+
const primaryEmbed = primaryAlive && !!ms.model && (this.scanner.get(ms.model.key)?.embedding ?? false);
|
|
3725
|
+
let lruManager = this.manager;
|
|
3726
|
+
let lruTime = primaryAlive && !primaryEmbed ? this.primaryLastUsed : Infinity;
|
|
3727
|
+
let lruKey = null;
|
|
3728
|
+
for (const slot of this.extraSlots.values()) {
|
|
3729
|
+
const slotEmbed = this.scanner.get(slot.modelKey)?.embedding ?? false;
|
|
3730
|
+
if (isAlive(slot.manager.status().state) && !slotEmbed && slot.lastUsedMs < lruTime) {
|
|
3731
|
+
lruTime = slot.lastUsedMs;
|
|
3732
|
+
lruManager = slot.manager;
|
|
3733
|
+
lruKey = slot.modelKey;
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
if (lruTime === Infinity) {
|
|
3737
|
+
lruTime = primaryAlive ? this.primaryLastUsed : Infinity;
|
|
3738
|
+
lruManager = this.manager;
|
|
3739
|
+
lruKey = null;
|
|
3740
|
+
for (const slot of this.extraSlots.values()) {
|
|
3741
|
+
if (isAlive(slot.manager.status().state) && slot.lastUsedMs < lruTime) {
|
|
3742
|
+
lruTime = slot.lastUsedMs;
|
|
3743
|
+
lruManager = slot.manager;
|
|
3744
|
+
lruKey = slot.modelKey;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
if (lruKey !== null) this.extraSlots.delete(lruKey);
|
|
3749
|
+
return lruManager;
|
|
3750
|
+
}
|
|
3751
|
+
resolveEntry(requested) {
|
|
3752
|
+
const models = this.scanner.list().models;
|
|
3753
|
+
return models.find((e) => e.key === requested) ?? models.find((e) => e.name === requested) ?? models.find((e) => e.name.toLowerCase() === requested.toLowerCase()) ?? models.find((e) => e.name.toLowerCase().includes(requested.toLowerCase()));
|
|
3754
|
+
}
|
|
3755
|
+
keysMatch(loadedKey, entry) {
|
|
3756
|
+
return loadedKey === entry.key || loadedKey === entry.path;
|
|
3757
|
+
}
|
|
3758
|
+
buildOpts(entry, engine) {
|
|
3759
|
+
if (entry.incomplete || entry.parseError) return null;
|
|
3760
|
+
const cfg2 = this.store.snapshot();
|
|
3761
|
+
const sys = getSysInfo();
|
|
3762
|
+
if (entry.format !== "gguf") {
|
|
3763
|
+
const savedGpu = cfg2.modelProfiles[entry.key]?.gpu;
|
|
3764
|
+
return {
|
|
3765
|
+
engine,
|
|
3766
|
+
model: { key: entry.key, name: entry.name, quant: entry.quant, ctx: entry.nativeCtx, vision: false },
|
|
3767
|
+
modelPath: entry.path,
|
|
3768
|
+
extraArgs: [],
|
|
3769
|
+
tensorParallelSize: savedGpu?.tensorParallelSize
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
const saved = cfg2.modelProfiles[entry.key];
|
|
3773
|
+
const profile = resolveProfile(entry, sys, saved, void 0, cfg2.modelDefaults);
|
|
3774
|
+
return {
|
|
3775
|
+
engine,
|
|
3776
|
+
model: { key: entry.key, name: entry.name, quant: entry.quant, ctx: profile.ctx, vision: entry.vision },
|
|
3777
|
+
modelPath: entry.path,
|
|
3778
|
+
extraArgs: profileToArgs(profile, entry, engine.capabilities, sys.cores)
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
/** Poll until the manager's engine process becomes ready or fails.
|
|
3782
|
+
* Mirrors the Manager's internal readiness timeout by engine kind. */
|
|
3783
|
+
async waitReady(manager2, engineKind) {
|
|
3784
|
+
const timeoutMs = engineKind === "vllm" ? 6e5 : 12e4;
|
|
3785
|
+
const deadline = Date.now() + timeoutMs;
|
|
3786
|
+
while (Date.now() < deadline) {
|
|
3787
|
+
const s = manager2.status();
|
|
3788
|
+
if (s.state === "running") return true;
|
|
3789
|
+
if (s.state === "error" || s.state === "stopped") return false;
|
|
3790
|
+
await sleep3(250);
|
|
3791
|
+
}
|
|
3792
|
+
return false;
|
|
3793
|
+
}
|
|
3794
|
+
};
|
|
3795
|
+
function sleep3(ms) {
|
|
3796
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3511
3799
|
// src/cli-launch.ts
|
|
3512
3800
|
import { spawn as spawn2 } from "child_process";
|
|
3513
3801
|
var SUPPORTED = {
|
|
@@ -3591,7 +3879,7 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as re
|
|
|
3591
3879
|
import { basename as basename3, dirname as dirname5, join as join12, resolve, sep } from "path";
|
|
3592
3880
|
|
|
3593
3881
|
// src/comfyui/gate-template.ts
|
|
3594
|
-
var GATE_VERSION =
|
|
3882
|
+
var GATE_VERSION = 2;
|
|
3595
3883
|
function gateNodeSource(baseUrl) {
|
|
3596
3884
|
const base2 = baseUrl.replace(/\/+$/, "");
|
|
3597
3885
|
const baseLit = JSON.stringify(base2);
|
|
@@ -3648,6 +3936,19 @@ def _acquire_blocking() -> None:
|
|
|
3648
3936
|
_post("/api/v1/comfyui/acquire", ACQUIRE_TIMEOUT_SEC)
|
|
3649
3937
|
|
|
3650
3938
|
|
|
3939
|
+
def _free_self() -> None:
|
|
3940
|
+
"""Unload ComfyUI's own models and clear the CUDA cache so VRAM is available
|
|
3941
|
+
for TurboLLM to reload its model. Called on the release edge (queue drained),
|
|
3942
|
+
before notifying TurboLLM. In-process \u2014 no HTTP roundtrip."""
|
|
3943
|
+
try:
|
|
3944
|
+
import comfy.model_management as mm # type: ignore[import]
|
|
3945
|
+
mm.unload_all_models()
|
|
3946
|
+
mm.soft_empty_cache()
|
|
3947
|
+
_log("freed ComfyUI model cache.")
|
|
3948
|
+
except Exception as e: # noqa: BLE001
|
|
3949
|
+
_log(f"could not free ComfyUI models ({e}); continuing.")
|
|
3950
|
+
|
|
3951
|
+
|
|
3651
3952
|
def _release() -> None:
|
|
3652
3953
|
_post("/api/v1/comfyui/release", RELEASE_TIMEOUT_SEC)
|
|
3653
3954
|
|
|
@@ -3697,6 +3998,7 @@ def _install_hooks() -> None:
|
|
|
3697
3998
|
_busy = False
|
|
3698
3999
|
do_release = True
|
|
3699
4000
|
if do_release:
|
|
4001
|
+
_free_self()
|
|
3700
4002
|
_release()
|
|
3701
4003
|
return result
|
|
3702
4004
|
|
|
@@ -3802,6 +4104,7 @@ function registerApi(app2, d) {
|
|
|
3802
4104
|
const engine = {
|
|
3803
4105
|
id: active?.id ?? "",
|
|
3804
4106
|
name: active?.name ?? "",
|
|
4107
|
+
kind: active?.kind ?? "",
|
|
3805
4108
|
state: ms.state,
|
|
3806
4109
|
port: ms.port,
|
|
3807
4110
|
pid: ms.pid
|
|
@@ -3822,7 +4125,22 @@ function registerApi(app2, d) {
|
|
|
3822
4125
|
downloads: { active: d.downloads.activeCount() },
|
|
3823
4126
|
engineProvision: d.provision.get(),
|
|
3824
4127
|
// ComfyUI GPU coordination: lets the UI explain a paused/unloaded engine.
|
|
3825
|
-
|
|
4128
|
+
// Also expose the installed gate node version so the UI can prompt an upgrade.
|
|
4129
|
+
comfyui: (() => {
|
|
4130
|
+
const snap = d.comfy?.snapshot() ?? null;
|
|
4131
|
+
if (!snap) return null;
|
|
4132
|
+
const gatePath = d.store.snapshot().comfyui.gatePath;
|
|
4133
|
+
let installedVersion = null;
|
|
4134
|
+
if (gatePath) {
|
|
4135
|
+
try {
|
|
4136
|
+
const src = readFileSync6(join12(gatePath, "__init__.py"), "utf-8");
|
|
4137
|
+
const m = src.match(/^GATE_VERSION\s*=\s*(\d+)/m);
|
|
4138
|
+
if (m) installedVersion = Number(m[1]);
|
|
4139
|
+
} catch {
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
return { ...snap, installedVersion, currentVersion: GATE_VERSION };
|
|
4143
|
+
})(),
|
|
3826
4144
|
telemetryLevel: d.store.snapshot().telemetry.level,
|
|
3827
4145
|
uptimeSec: Math.floor((Date.now() - d.startedAt) / 1e3)
|
|
3828
4146
|
});
|
|
@@ -4382,11 +4700,19 @@ function registerApi(app2, d) {
|
|
|
4382
4700
|
}
|
|
4383
4701
|
cuUpdates.url = u;
|
|
4384
4702
|
}
|
|
4703
|
+
const gwUpdates = {};
|
|
4704
|
+
if (b.gateway?.autoSwap !== void 0) gwUpdates.autoSwap = !!b.gateway.autoSwap;
|
|
4705
|
+
if (b.gateway?.keepN !== void 0) {
|
|
4706
|
+
const v = Number(b.gateway.keepN);
|
|
4707
|
+
if (!Number.isInteger(v) || v < 1 || v > 4) return err(c, 400, "invalid_config_value", "gateway.keepN must be 1\u20134.");
|
|
4708
|
+
gwUpdates.keepN = v;
|
|
4709
|
+
}
|
|
4385
4710
|
const before = d.store.snapshot().daemon;
|
|
4386
4711
|
d.store.update((cfg2) => {
|
|
4387
4712
|
Object.assign(cfg2.daemon, updates);
|
|
4388
4713
|
Object.assign(cfg2.modelDefaults, mdUpdates);
|
|
4389
4714
|
Object.assign(cfg2.comfyui, cuUpdates);
|
|
4715
|
+
Object.assign(cfg2.gateway, gwUpdates);
|
|
4390
4716
|
if (b.autoLoadOnStart !== void 0) cfg2.autoLoadOnStart = !!b.autoLoadOnStart;
|
|
4391
4717
|
if (telemetryLevel !== void 0) cfg2.telemetry.level = telemetryLevel;
|
|
4392
4718
|
if (b.hfToken !== void 0) cfg2.hf.token = String(b.hfToken).trim();
|
|
@@ -4455,7 +4781,7 @@ function registerApi(app2, d) {
|
|
|
4455
4781
|
const q = (c.req.query("q") ?? "").trim();
|
|
4456
4782
|
if (!q) return c.json({ results: [] });
|
|
4457
4783
|
try {
|
|
4458
|
-
const results = await d.hf.searchModels(q);
|
|
4784
|
+
const results = await d.hf.searchModels(q, d.registry.active()?.kind);
|
|
4459
4785
|
const withLocal = results.map((r) => ({ ...r, localCount: localCountFor(d, r.repo) }));
|
|
4460
4786
|
return c.json({ results: withLocal });
|
|
4461
4787
|
} catch (e) {
|
|
@@ -4651,6 +4977,7 @@ function settingsPayload(d) {
|
|
|
4651
4977
|
telemetryLevel,
|
|
4652
4978
|
modelDefaults: cfg2.modelDefaults,
|
|
4653
4979
|
comfyui: cfg2.comfyui,
|
|
4980
|
+
gateway: cfg2.gateway,
|
|
4654
4981
|
// The HF token is write-only over the wire (spec 10 §4): we never echo it back,
|
|
4655
4982
|
// only whether one is set, so the UI can show "configured" without leaking it.
|
|
4656
4983
|
hfTokenSet: cfg2.hf.token.length > 0
|
|
@@ -5075,17 +5402,33 @@ ${content}` : b.docContext : content;
|
|
|
5075
5402
|
async function runGeneration(d, stream, ctx) {
|
|
5076
5403
|
const { db: db2 } = d;
|
|
5077
5404
|
const { convId, conv, engineMessages, assistantMsg, ms, target, ac, disableThinking } = ctx;
|
|
5078
|
-
const
|
|
5079
|
-
const
|
|
5080
|
-
|
|
5405
|
+
const convS = conv.sampling ?? {};
|
|
5406
|
+
const SAMPLING_KEYS = {
|
|
5407
|
+
temp: "temperature",
|
|
5408
|
+
topP: "top_p",
|
|
5409
|
+
topK: "top_k",
|
|
5410
|
+
minP: "min_p",
|
|
5411
|
+
repeatPenalty: "repeat_penalty",
|
|
5412
|
+
presencePenalty: "presence_penalty",
|
|
5413
|
+
frequencyPenalty: "frequency_penalty"
|
|
5414
|
+
};
|
|
5415
|
+
const samplingOverride = {};
|
|
5416
|
+
for (const [camel, snake] of Object.entries(SAMPLING_KEYS)) {
|
|
5417
|
+
if (camel in convS) samplingOverride[snake] = convS[camel];
|
|
5418
|
+
}
|
|
5419
|
+
for (const [k, v] of Object.entries(convS)) {
|
|
5420
|
+
if (!(k in SAMPLING_KEYS) && k !== "stop") samplingOverride[k] = v;
|
|
5421
|
+
}
|
|
5081
5422
|
const reqBody = {
|
|
5082
5423
|
model: ms.model.key,
|
|
5083
5424
|
messages: engineMessages,
|
|
5084
5425
|
stream: true,
|
|
5085
5426
|
stream_options: { include_usage: true },
|
|
5086
5427
|
return_progress: true,
|
|
5087
|
-
...
|
|
5428
|
+
...samplingOverride
|
|
5088
5429
|
};
|
|
5430
|
+
const stopStrings = convS.stop;
|
|
5431
|
+
if (stopStrings?.length) reqBody.stop = stopStrings;
|
|
5089
5432
|
const maxLimit = d.store.snapshot().modelDefaults.maxTokens ?? 0;
|
|
5090
5433
|
const cappedMax = clampMaxTokens(reqBody.max_tokens, maxLimit);
|
|
5091
5434
|
if (cappedMax != null) reqBody.max_tokens = cappedMax;
|
|
@@ -5123,6 +5466,12 @@ async function runGeneration(d, stream, ctx) {
|
|
|
5123
5466
|
const reader = res.body.getReader();
|
|
5124
5467
|
const decoder = new TextDecoder();
|
|
5125
5468
|
let buf = "";
|
|
5469
|
+
const cancelReader = () => void reader.cancel();
|
|
5470
|
+
if (ac.signal.aborted) {
|
|
5471
|
+
cancelReader();
|
|
5472
|
+
} else {
|
|
5473
|
+
ac.signal.addEventListener("abort", cancelReader, { once: true });
|
|
5474
|
+
}
|
|
5126
5475
|
outer: while (true) {
|
|
5127
5476
|
const { done, value } = await reader.read();
|
|
5128
5477
|
if (done) break;
|
|
@@ -5234,6 +5583,7 @@ async function runGeneration(d, stream, ctx) {
|
|
|
5234
5583
|
}
|
|
5235
5584
|
}
|
|
5236
5585
|
}
|
|
5586
|
+
ac.signal.removeEventListener("abort", cancelReader);
|
|
5237
5587
|
if (pendingThinkBuf && inThink) {
|
|
5238
5588
|
fullReasoning += pendingThinkBuf;
|
|
5239
5589
|
await stream.writeSSE({ event: "reasoning", data: JSON.stringify({ delta: pendingThinkBuf }) });
|
|
@@ -5643,13 +5993,6 @@ function cbStop(index) {
|
|
|
5643
5993
|
// src/gateway/gateway.ts
|
|
5644
5994
|
function registerGateway(app2, d) {
|
|
5645
5995
|
app2.post("/v1/messages", async (c) => {
|
|
5646
|
-
const target = d.manager.target();
|
|
5647
|
-
if (!target) {
|
|
5648
|
-
return c.json(
|
|
5649
|
-
{ type: "error", error: { type: "api_error", message: "No model loaded. Load one in TurboLLM." } },
|
|
5650
|
-
503
|
|
5651
|
-
);
|
|
5652
|
-
}
|
|
5653
5996
|
let req;
|
|
5654
5997
|
try {
|
|
5655
5998
|
req = await c.req.json();
|
|
@@ -5667,7 +6010,14 @@ function registerGateway(app2, d) {
|
|
|
5667
6010
|
}
|
|
5668
6011
|
const maxLimit = d.store.snapshot().modelDefaults.maxTokens ?? 0;
|
|
5669
6012
|
req.max_tokens = clampMaxTokens(req.max_tokens, maxLimit) ?? req.max_tokens;
|
|
5670
|
-
d.
|
|
6013
|
+
const routeResult = await d.modelRouter.route(req.model ?? "");
|
|
6014
|
+
if ("status" in routeResult) {
|
|
6015
|
+
return c.json(
|
|
6016
|
+
{ type: "error", error: { type: "api_error", message: routeResult.message } },
|
|
6017
|
+
routeResult.status
|
|
6018
|
+
);
|
|
6019
|
+
}
|
|
6020
|
+
const target = routeResult.target;
|
|
5671
6021
|
const status = d.manager.status();
|
|
5672
6022
|
const modelName = status.state === "running" ? status.model?.name ?? req.model ?? "local" : req.model ?? "local";
|
|
5673
6023
|
const oaiBody = mapToOpenAI(req);
|
|
@@ -5762,46 +6112,40 @@ function registerGateway(app2, d) {
|
|
|
5762
6112
|
return c.json({ input_tokens: estimate });
|
|
5763
6113
|
});
|
|
5764
6114
|
app2.all("/v1/*", async (c) => {
|
|
5765
|
-
const
|
|
5766
|
-
|
|
5767
|
-
|
|
6115
|
+
const url = new URL(c.req.url);
|
|
6116
|
+
const isChat = c.req.method === "POST" && url.pathname === "/v1/chat/completions";
|
|
6117
|
+
let parsedBody = null;
|
|
6118
|
+
if (isChat) {
|
|
6119
|
+
try {
|
|
6120
|
+
parsedBody = await c.req.json();
|
|
6121
|
+
} catch {
|
|
6122
|
+
parsedBody = null;
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
const requestedModel = isChat ? parsedBody?.model ?? "" : "";
|
|
6126
|
+
const routeResult = await d.modelRouter.route(requestedModel);
|
|
6127
|
+
if ("status" in routeResult) {
|
|
6128
|
+
if (c.req.method === "GET" && url.pathname === "/v1/models") {
|
|
5768
6129
|
return c.json({ object: "list", data: [] });
|
|
5769
6130
|
}
|
|
5770
6131
|
return c.json(
|
|
5771
|
-
{
|
|
5772
|
-
error: {
|
|
5773
|
-
message: "No model loaded. Load one in TurboLLM.",
|
|
5774
|
-
type: "model_not_loaded",
|
|
5775
|
-
code: "model_not_loaded"
|
|
5776
|
-
}
|
|
5777
|
-
},
|
|
6132
|
+
{ error: { message: routeResult.message, type: "model_not_loaded", code: "model_not_loaded" } },
|
|
5778
6133
|
503
|
|
5779
6134
|
);
|
|
5780
6135
|
}
|
|
5781
|
-
|
|
5782
|
-
const url = new URL(c.req.url);
|
|
6136
|
+
const target = routeResult.target;
|
|
5783
6137
|
const upstream = target + url.pathname + url.search;
|
|
5784
6138
|
const headers = new Headers(c.req.raw.headers);
|
|
5785
6139
|
headers.delete("host");
|
|
5786
|
-
const isChat = c.req.method === "POST" && url.pathname === "/v1/chat/completions";
|
|
5787
6140
|
const maxLimit = d.store.snapshot().modelDefaults.maxTokens ?? 0;
|
|
5788
6141
|
const init = { method: c.req.method, headers };
|
|
5789
6142
|
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
5790
|
-
if (isChat
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
parsed = await c.req.json();
|
|
5794
|
-
} catch {
|
|
5795
|
-
parsed = null;
|
|
5796
|
-
}
|
|
5797
|
-
if (parsed) {
|
|
5798
|
-
parsed.max_tokens = clampMaxTokens(parsed.max_tokens, maxLimit);
|
|
5799
|
-
headers.delete("content-length");
|
|
5800
|
-
init.body = JSON.stringify(parsed);
|
|
5801
|
-
} else {
|
|
5802
|
-
init.body = c.req.raw.body;
|
|
5803
|
-
init.duplex = "half";
|
|
6143
|
+
if (isChat) {
|
|
6144
|
+
if (parsedBody && maxLimit > 0) {
|
|
6145
|
+
parsedBody.max_tokens = clampMaxTokens(parsedBody.max_tokens, maxLimit);
|
|
5804
6146
|
}
|
|
6147
|
+
headers.delete("content-length");
|
|
6148
|
+
init.body = parsedBody ? JSON.stringify(parsedBody) : "";
|
|
5805
6149
|
} else {
|
|
5806
6150
|
init.body = c.req.raw.body;
|
|
5807
6151
|
init.duplex = "half";
|
|
@@ -6085,8 +6429,9 @@ var hf = new HfClient(() => store.snapshot().hf.token, version);
|
|
|
6085
6429
|
var downloads = new DownloadManager(store, () => void scanner.rescan(), () => hf.authHeaders());
|
|
6086
6430
|
var bench = new BenchRunner(manager, store, scanner, registry, version);
|
|
6087
6431
|
var comfy = new ComfyGuard(store, manager);
|
|
6432
|
+
var modelRouter = new ModelRouter(store, registry, manager, scanner, comfy);
|
|
6088
6433
|
var startedAt = Date.now();
|
|
6089
|
-
var deps = { store, registry, manager, scanner, hashes, db, provision, hf, downloads, bench, comfy, version, startedAt };
|
|
6434
|
+
var deps = { store, registry, manager, scanner, hashes, db, provision, hf, downloads, bench, modelRouter, comfy, version, startedAt };
|
|
6090
6435
|
var app = createApp(deps);
|
|
6091
6436
|
var cfg = store.snapshot();
|
|
6092
6437
|
var defaultHost = cfg.daemon.lanBind ? "0.0.0.0" : cfg.daemon.host || "127.0.0.1";
|