turbollm 1.3.2 → 1.4.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/README.md +178 -211
- package/dist/cli.js +664 -232
- package/dist/webdist/assets/{ChatScreen-DY2flMe9.js → ChatScreen-DwW1RFdb.js} +10 -10
- package/dist/webdist/assets/{CustomizeScreen-Cl6H68Sy.js → CustomizeScreen-DSE61GGl.js} +1 -1
- package/dist/webdist/assets/{DeveloperScreen-zC1nePXh.js → DeveloperScreen-BYGLUEKp.js} +1 -1
- package/dist/webdist/assets/{EnginesScreen-CrsziC3B.js → EnginesScreen-D1fV7OUs.js} +1 -1
- package/dist/webdist/assets/{ModelDetailDialog-BOvumdHI.js → ModelDetailDialog-DOWC_r7x.js} +1 -1
- package/dist/webdist/assets/{ModelDirs-Bj-XrX0c.js → ModelDirs-BnEgYeD7.js} +1 -1
- package/dist/webdist/assets/{ModelsScreen-BTir-kDY.js → ModelsScreen-CsIWlTix.js} +1 -1
- package/dist/webdist/assets/{SettingsScreen-B696egCa.js → SettingsScreen-BsoeEYxo.js} +1 -1
- package/dist/webdist/assets/check-BEcwngnz.js +1 -0
- package/dist/webdist/assets/{circle-x-CsuAddI_.js → circle-x-BMU1-bqZ.js} +1 -1
- package/dist/webdist/assets/{common-DIXPIrUd.js → common-CsaPxOo6.js} +1 -1
- package/dist/webdist/assets/{copy-button-BtQZA6P8.js → copy-button-VN55eluX.js} +1 -1
- package/dist/webdist/assets/{dialog-Dpyml8ZT.js → dialog-oym6ZR4Z.js} +1 -1
- package/dist/webdist/assets/{index-sjgdofsd.js → index-DdsLEQg8.js} +2 -2
- package/dist/webdist/assets/{pencil-BuQwoNyj.js → pencil-BUeTGZ2m.js} +1 -1
- package/dist/webdist/assets/{personas-CQapYHeK.js → personas-B44JP8XF.js} +1 -1
- package/dist/webdist/assets/{plus-B1w15FoZ.js → plus-B4N6E4A9.js} +1 -1
- package/dist/webdist/assets/{refresh-cw-BoAcjLtt.js → refresh-cw-35eljdhP.js} +1 -1
- package/dist/webdist/assets/{save-DE1APWTG.js → save-AXon2pXP.js} +1 -1
- package/dist/webdist/assets/{skeleton-Dvq8tYzC.js → skeleton-BqBFt4sB.js} +1 -1
- package/dist/webdist/assets/{sparkles-C54kYWeQ.js → sparkles-C1EIsU4T.js} +1 -1
- package/dist/webdist/assets/{trash-2-DIMVgidH.js → trash-2-BOZ6n1d2.js} +1 -1
- package/dist/webdist/index.html +1 -1
- package/package.json +1 -1
- package/dist/webdist/assets/check-DFwkcl_5.js +0 -1
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { spawn as spawn5 } from "child_process";
|
|
5
|
-
import { openSync as openSync3, readFileSync as
|
|
5
|
+
import { openSync as openSync3, readFileSync as readFileSync11 } from "fs";
|
|
6
6
|
import { serve } from "@hono/node-server";
|
|
7
|
-
import { dirname as dirname9, join as
|
|
7
|
+
import { dirname as dirname9, join as join22 } from "path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
9
|
|
|
10
10
|
// src/config/config.ts
|
|
@@ -3525,8 +3525,8 @@ function gpuBudgetMb(sys, p) {
|
|
|
3525
3525
|
return sys.gpus.reduce((sum, g) => sum + (g.vramMb || 0), 0);
|
|
3526
3526
|
}
|
|
3527
3527
|
function estimateVram(p, m, sys) {
|
|
3528
|
-
const
|
|
3529
|
-
if (
|
|
3528
|
+
const totalVramMb2 = gpuBudgetMb(sys, p);
|
|
3529
|
+
if (totalVramMb2 === 0) return { estMb: 0, totalVramMb: 0, pct: 0, verdict: "cpu" };
|
|
3530
3530
|
const sizeMb = m.sizeBytes / 1e6;
|
|
3531
3531
|
const blocks = m.blockCount || 1;
|
|
3532
3532
|
const gpuFrac = m.moe ? 1 - 0.85 * (p.nCpuMoe / blocks) : Math.min(p.ngl, blocks) / blocks;
|
|
@@ -3536,9 +3536,9 @@ function estimateVram(p, m, sys) {
|
|
|
3536
3536
|
const kvMb = kvElems * kvBytesPerElem(p.kvTypeK) / 1e6 * (p.kvUnified ? 1 : Math.max(1, p.parallel));
|
|
3537
3537
|
const mmprojMb = p.useMmproj && p.mmprojGpu && m.mmprojPath ? 600 : 0;
|
|
3538
3538
|
const estMb = Math.round(weightsMb + kvMb + 800 + mmprojMb);
|
|
3539
|
-
const pct = estMb /
|
|
3539
|
+
const pct = estMb / totalVramMb2;
|
|
3540
3540
|
const verdict = pct <= 0.8 ? "fits" : pct <= 0.95 ? "tight" : "overflow";
|
|
3541
|
-
return { estMb, totalVramMb, pct, verdict };
|
|
3541
|
+
return { estMb, totalVramMb: totalVramMb2, pct, verdict };
|
|
3542
3542
|
}
|
|
3543
3543
|
function deriveDefault(m, sys) {
|
|
3544
3544
|
const hasGpu = sys.gpus.length > 0;
|
|
@@ -4609,6 +4609,25 @@ var PREFILL_GRACE_MS = 8e3;
|
|
|
4609
4609
|
var TOTAL_BUDGET_MS = 20 * 6e4;
|
|
4610
4610
|
var OOM_RE = /out of memory|cudaMalloc|failed to allocate|unable to allocate|device not ready|CUDA error/i;
|
|
4611
4611
|
var CHARS_PER_TOKEN = 4;
|
|
4612
|
+
var QUALITY_KV = ["f16", "q8_0", "turbo4"];
|
|
4613
|
+
var VRAM_HEADROOM_MB = 1024;
|
|
4614
|
+
var OUTPUT_TIE = 0.05;
|
|
4615
|
+
var KV_SPREAD_MIN = 1024;
|
|
4616
|
+
var KV_BYTES = {
|
|
4617
|
+
f32: 4,
|
|
4618
|
+
f16: 2,
|
|
4619
|
+
bf16: 2,
|
|
4620
|
+
q8_0: 1,
|
|
4621
|
+
q8_1: 1,
|
|
4622
|
+
q5_0: 0.625,
|
|
4623
|
+
q5_1: 0.625,
|
|
4624
|
+
q4_0: 0.5,
|
|
4625
|
+
q4_1: 0.5,
|
|
4626
|
+
iq4_nl: 0.5,
|
|
4627
|
+
turbo4: 0.5,
|
|
4628
|
+
turbo3: 0.375,
|
|
4629
|
+
turbo2: 0.25
|
|
4630
|
+
};
|
|
4612
4631
|
var BenchError = class extends Error {
|
|
4613
4632
|
constructor(code, message) {
|
|
4614
4633
|
super(message);
|
|
@@ -4713,13 +4732,24 @@ var BenchRunner = class {
|
|
|
4713
4732
|
const caps = active?.capabilities ?? { flags: [], kvTypes: [] };
|
|
4714
4733
|
const saved = this.store.snapshot().modelProfiles[modelKey];
|
|
4715
4734
|
const defaults = this.store.snapshot().modelDefaults;
|
|
4716
|
-
const
|
|
4735
|
+
const resolved = resolveProfile(entry, sys, saved, base2, defaults);
|
|
4736
|
+
const baseProfile = { ...resolved, speculative: "off", mtpHeadPath: "", draftModelPath: "", useMmproj: false };
|
|
4717
4737
|
const results = [];
|
|
4718
4738
|
let best = null;
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4739
|
+
const kvCandidates = pickKvQuants(baseProfile.kvTypeK, caps.kvTypes);
|
|
4740
|
+
let kvToTune = kvCandidates.slice(0, 1);
|
|
4741
|
+
if (sys.gpus.length > 0 && kvCandidates.length > 1) {
|
|
4742
|
+
const spread = await this.calibrateKvSpread(entry, sys, baseProfile, caps, kvCandidates, results);
|
|
4743
|
+
kvToTune = decideKvToBench(spread, kvCandidates);
|
|
4744
|
+
const swing = spread < 0 ? "unknown" : spread >= Number.MAX_SAFE_INTEGER ? "huge" : `${Math.round(spread)} MB`;
|
|
4745
|
+
this.state = { ...this.state, step: `KV cache swing ${swing} \u2192 tuning ${kvToTune.join(" + ")}`, candidates: results };
|
|
4746
|
+
}
|
|
4747
|
+
for (let i = 0; i < kvToTune.length && !this.cancelled && Date.now() <= this.deadline; i++) {
|
|
4748
|
+
const kv = kvToTune[i];
|
|
4749
|
+
const kvBase = { ...baseProfile, kvTypeK: kv, kvTypeV: kv };
|
|
4750
|
+
const found = entry.moe ? await this.moeSearch(entry, sys, kvBase, caps, results) : await this.denseSearch(entry, sys, kvBase, caps, results);
|
|
4751
|
+
if (found && (!best || betterBySpeed(found.cand, best.cand))) best = found;
|
|
4752
|
+
if (best) this.state = { ...this.state, bestTps: best.cand.tps ?? void 0 };
|
|
4723
4753
|
}
|
|
4724
4754
|
await this.manager.stopAndWait().catch(() => {
|
|
4725
4755
|
});
|
|
@@ -4744,6 +4774,7 @@ var BenchRunner = class {
|
|
|
4744
4774
|
result: {
|
|
4745
4775
|
params: best.cand.params,
|
|
4746
4776
|
tps: best.cand.tps ?? 0,
|
|
4777
|
+
prefillTps: best.cand.prefillTps,
|
|
4747
4778
|
ttftMs: best.cand.ttftMs ?? 0,
|
|
4748
4779
|
vramMb: best.cand.vramMb,
|
|
4749
4780
|
// The full sampling that Save will persist (winning profile, card values merged in) so
|
|
@@ -4764,101 +4795,151 @@ var BenchRunner = class {
|
|
|
4764
4795
|
this.state = { running: false, modelKey, done: true, error: err3, candidates: results };
|
|
4765
4796
|
}
|
|
4766
4797
|
}
|
|
4767
|
-
/**
|
|
4768
|
-
*
|
|
4769
|
-
*
|
|
4770
|
-
*
|
|
4798
|
+
/** Dense models: pin `ngl` by VRAM probing (Phase 1), then run the full bench once (Phase 2).
|
|
4799
|
+
* More GPU layers = faster, monotonically, up to the no-spill edge — so the best ngl is the
|
|
4800
|
+
* HIGHEST whose absolute VRAM still leaves the ≤1 GB headroom. Whether a config fits/spills is a
|
|
4801
|
+
* LOAD-time property, so we find that ngl with cheap load-and-read-VRAM probes (no generation),
|
|
4802
|
+
* and only measure t/s at the winner. CPU-only machines skip straight to ngl=0. */
|
|
4771
4803
|
async denseSearch(entry, sys, base2, caps, results) {
|
|
4772
|
-
|
|
4773
|
-
if (
|
|
4774
|
-
const
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
this.
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
const profile = { ...base2, ngl: mid };
|
|
4790
|
-
const cand = await this.measure(entry, sys, profile, caps, label, stepPrefix);
|
|
4791
|
-
results.push(cand);
|
|
4792
|
-
this.state = { ...this.state, candidates: results };
|
|
4793
|
-
await this.settleGpu();
|
|
4794
|
-
if (cand.outcome === "ok" && cand.tps !== null) {
|
|
4795
|
-
if (!best || cand.tps > (best.cand.tps ?? 0)) best = { cand, profile };
|
|
4796
|
-
this.state = { ...this.state, bestTps: best.cand.tps ?? void 0 };
|
|
4797
|
-
lo = mid + 1;
|
|
4798
|
-
} else if (cand.outcome === "oom") {
|
|
4799
|
-
hi = mid - 1;
|
|
4800
|
-
} else {
|
|
4801
|
-
hi = mid - 1;
|
|
4804
|
+
let bestNgl = 0;
|
|
4805
|
+
if (sys.gpus.length > 0) {
|
|
4806
|
+
const hi0 = entry.blockCount > 0 ? entry.blockCount : 99;
|
|
4807
|
+
let lo = 0, hi = hi0;
|
|
4808
|
+
bestNgl = null;
|
|
4809
|
+
while (lo <= hi && !this.cancelled && Date.now() <= this.deadline) {
|
|
4810
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
4811
|
+
this.state = { ...this.state, step: `KV ${base2.kvTypeK}: probing ngl=${mid} (range ${lo}\u2013${hi})\u2026`, candidates: results };
|
|
4812
|
+
const probe2 = await this.probeVram(entry, sys, { ...base2, ngl: mid }, caps);
|
|
4813
|
+
this.pushProbe(results, base2, "ngl", mid, probe2);
|
|
4814
|
+
await this.settleGpu();
|
|
4815
|
+
if (probe2.outcome === "ok" && !overHeadroom(probe2.vramAbsMb, sys)) {
|
|
4816
|
+
bestNgl = mid;
|
|
4817
|
+
lo = mid + 1;
|
|
4818
|
+
} else {
|
|
4819
|
+
hi = mid - 1;
|
|
4820
|
+
}
|
|
4802
4821
|
}
|
|
4803
4822
|
}
|
|
4804
|
-
|
|
4823
|
+
if (bestNgl === null) return null;
|
|
4824
|
+
return this.benchAt(entry, sys, { ...base2, ngl: bestNgl }, caps, results, `ngl=${bestNgl}`);
|
|
4805
4825
|
}
|
|
4806
|
-
/**
|
|
4807
|
-
*
|
|
4808
|
-
*
|
|
4826
|
+
/** MoE models: pin `nCpuMoe` by VRAM probing (Phase 1), then run the full bench once (Phase 2).
|
|
4827
|
+
* Fewer CPU experts = more on GPU = faster, so the best is the LOWEST nCpuMoe whose absolute VRAM
|
|
4828
|
+
* still leaves the ≤1 GB headroom. Found with cheap load-and-read-VRAM probes (no generation);
|
|
4829
|
+
* t/s is measured only at the winner. */
|
|
4809
4830
|
async moeSearch(entry, sys, base2, caps, results) {
|
|
4810
4831
|
const derived = deriveDefault(entry, sys);
|
|
4811
4832
|
const maxN = entry.blockCount > 0 ? entry.blockCount : derived.nCpuMoe || 0;
|
|
4812
4833
|
let lo = 0, hi = maxN;
|
|
4813
|
-
let
|
|
4834
|
+
let bestN = null;
|
|
4814
4835
|
while (lo <= hi && !this.cancelled && Date.now() <= this.deadline) {
|
|
4815
4836
|
const mid = Math.floor((lo + hi) / 2);
|
|
4816
|
-
|
|
4817
|
-
const
|
|
4818
|
-
this.
|
|
4819
|
-
const profile = { ...base2, nCpuMoe: mid };
|
|
4820
|
-
const cand = await this.measure(entry, sys, profile, caps, label, stepPrefix);
|
|
4821
|
-
results.push(cand);
|
|
4822
|
-
this.state = { ...this.state, candidates: results };
|
|
4837
|
+
this.state = { ...this.state, step: `KV ${base2.kvTypeK}: probing nCpuMoe=${mid} (range ${lo}\u2013${hi})\u2026`, candidates: results };
|
|
4838
|
+
const probe2 = await this.probeVram(entry, sys, { ...base2, nCpuMoe: mid }, caps);
|
|
4839
|
+
this.pushProbe(results, base2, "nCpuMoe", mid, probe2);
|
|
4823
4840
|
await this.settleGpu();
|
|
4824
|
-
if (
|
|
4841
|
+
if (probe2.outcome === "oom" || overHeadroom(probe2.vramAbsMb, sys)) {
|
|
4825
4842
|
lo = mid + 1;
|
|
4826
|
-
} else if (
|
|
4827
|
-
|
|
4828
|
-
this.state = { ...this.state, bestTps: best.cand.tps ?? void 0 };
|
|
4843
|
+
} else if (probe2.outcome === "ok") {
|
|
4844
|
+
bestN = mid;
|
|
4829
4845
|
hi = mid - 1;
|
|
4830
4846
|
} else {
|
|
4831
4847
|
lo = mid + 1;
|
|
4832
4848
|
}
|
|
4833
4849
|
}
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
*
|
|
4839
|
-
*
|
|
4840
|
-
*
|
|
4841
|
-
*
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4850
|
+
if (bestN === null) return null;
|
|
4851
|
+
return this.benchAt(entry, sys, { ...base2, nCpuMoe: bestN }, caps, results, `nCpuMoe=${bestN}`);
|
|
4852
|
+
}
|
|
4853
|
+
/** Measure how much the KV-cache QUANT swings VRAM at this context — so we tune only the quant(s)
|
|
4854
|
+
* that matter instead of sweeping all. Two cheap probes (largest vs smallest candidate) at ONE
|
|
4855
|
+
* shared offload: their VRAM difference is exactly the KV-size difference (weights + overhead are
|
|
4856
|
+
* identical at the same offload), valid for any architecture. Returns that swing in MB scaled to
|
|
4857
|
+
* the full model, `Number.MAX_SAFE_INTEGER` if even the largest quant won't load (cache enormous
|
|
4858
|
+
* → definitely the big-KV regime), or -1 if it couldn't be sized at all. */
|
|
4859
|
+
async calibrateKvSpread(entry, sys, base2, caps, candidates, results) {
|
|
4860
|
+
const big = kvLargest(candidates);
|
|
4861
|
+
const small = kvSmallest(candidates);
|
|
4862
|
+
if (big === small) return 0;
|
|
4863
|
+
const blocks = entry.blockCount > 0 ? entry.blockCount : 32;
|
|
4864
|
+
const moeMax = entry.blockCount > 0 ? entry.blockCount : deriveDefault(entry, sys).nCpuMoe || 0;
|
|
4865
|
+
const refNgl = Math.max(1, Math.floor(blocks / 4));
|
|
4866
|
+
const refOffload = entry.moe ? { nCpuMoe: moeMax, ngl: 99 } : { ngl: refNgl };
|
|
4867
|
+
const knob = entry.moe ? "nCpuMoe" : "ngl";
|
|
4868
|
+
const knobVal = entry.moe ? moeMax : refNgl;
|
|
4869
|
+
this.state = { ...this.state, step: `Sizing the KV cache (${big} vs ${small})\u2026`, candidates: results };
|
|
4870
|
+
const pBig = await this.probeVram(entry, sys, { ...base2, kvTypeK: big, kvTypeV: big, ...refOffload }, caps);
|
|
4871
|
+
this.pushProbe(results, { ...base2, kvTypeK: big, ...refOffload }, knob, knobVal, pBig);
|
|
4872
|
+
await this.settleGpu();
|
|
4873
|
+
const pSmall = await this.probeVram(entry, sys, { ...base2, kvTypeK: small, kvTypeV: small, ...refOffload }, caps);
|
|
4874
|
+
this.pushProbe(results, { ...base2, kvTypeK: small, ...refOffload }, knob, knobVal, pSmall);
|
|
4875
|
+
await this.settleGpu();
|
|
4876
|
+
if (pBig.outcome !== "ok" || pBig.vramAbsMb === null) return Number.MAX_SAFE_INTEGER;
|
|
4877
|
+
if (pSmall.outcome !== "ok" || pSmall.vramAbsMb === null) return -1;
|
|
4878
|
+
let spread = pBig.vramAbsMb - pSmall.vramAbsMb;
|
|
4879
|
+
if (!entry.moe) spread = spread * (blocks / refNgl);
|
|
4880
|
+
return Math.max(0, spread);
|
|
4881
|
+
}
|
|
4882
|
+
/** A cheap VRAM probe (Phase 1 of a search): load the candidate, wait for readiness — by which
|
|
4883
|
+
* point the weights, the full KV cache, AND the compute buffers are all allocated — read the
|
|
4884
|
+
* absolute GPU VRAM in use, then stop. NO prefill, NO generation. The offload param is decided
|
|
4885
|
+
* from this alone: whether a config fits-with-headroom or spills is a load-time property, so
|
|
4886
|
+
* measuring t/s at every search step would be wasted — we bench ONCE, at the chosen config. */
|
|
4887
|
+
async probeVram(entry, sys, profile, caps) {
|
|
4888
|
+
const active = this.registry.active();
|
|
4889
|
+
if (!active) return { outcome: "crash", vramAbsMb: null };
|
|
4890
|
+
const testDeadline = Math.min(Date.now() + READY_TIMEOUT_MS + 5e3, this.deadline);
|
|
4891
|
+
const opts = {
|
|
4892
|
+
engine: active,
|
|
4893
|
+
model: { key: entry.key, name: entry.name, quant: entry.quant, ctx: profile.ctx, vision: entry.vision },
|
|
4894
|
+
modelPath: entry.path,
|
|
4895
|
+
extraArgs: profileToArgs(profile, entry, caps, sys.cores)
|
|
4896
|
+
};
|
|
4897
|
+
try {
|
|
4898
|
+
await this.manager.start(opts);
|
|
4899
|
+
} catch {
|
|
4900
|
+
return { outcome: "crash", vramAbsMb: null };
|
|
4901
|
+
}
|
|
4902
|
+
const outcome = await this.awaitReady(testDeadline);
|
|
4903
|
+
let vramAbsMb = null;
|
|
4904
|
+
if (outcome === "ok") {
|
|
4905
|
+
await sleep2(800);
|
|
4906
|
+
vramAbsMb = await readNvidiaVramMb();
|
|
4860
4907
|
}
|
|
4861
|
-
|
|
4908
|
+
await this.manager.stopAndWait().catch(() => {
|
|
4909
|
+
});
|
|
4910
|
+
return { outcome, vramAbsMb };
|
|
4911
|
+
}
|
|
4912
|
+
/** Record a VRAM-probe trial in the candidate list (tps/prefill are null — nothing was generated;
|
|
4913
|
+
* only the load outcome and absolute VRAM are known). */
|
|
4914
|
+
pushProbe(results, base2, knob, value, probe2) {
|
|
4915
|
+
results.push({
|
|
4916
|
+
label: `probe ${knob}=${value}`,
|
|
4917
|
+
params: {
|
|
4918
|
+
ctx: base2.ctx,
|
|
4919
|
+
ngl: knob === "ngl" ? value : base2.ngl,
|
|
4920
|
+
nCpuMoe: knob === "nCpuMoe" ? value : base2.nCpuMoe,
|
|
4921
|
+
parallel: base2.parallel,
|
|
4922
|
+
kvTypeK: base2.kvTypeK,
|
|
4923
|
+
flashAttn: base2.flashAttn
|
|
4924
|
+
},
|
|
4925
|
+
outcome: probe2.outcome,
|
|
4926
|
+
tps: null,
|
|
4927
|
+
prefillTps: null,
|
|
4928
|
+
ttftMs: null,
|
|
4929
|
+
vramMb: null,
|
|
4930
|
+
vramAbsMb: probe2.vramAbsMb
|
|
4931
|
+
});
|
|
4932
|
+
this.state = { ...this.state, candidates: results };
|
|
4933
|
+
}
|
|
4934
|
+
/** Phase 2: the single full prefill + t/s benchmark, at the offload the VRAM probe chose. Pushes
|
|
4935
|
+
* the candidate and returns it as this KV quant's winner (null if the final measurement faulted). */
|
|
4936
|
+
async benchAt(entry, sys, profile, caps, results, label) {
|
|
4937
|
+
this.state = { ...this.state, step: `KV ${profile.kvTypeK}: measuring best (${label})\u2026`, candidates: results };
|
|
4938
|
+
const cand = await this.measure(entry, sys, profile, caps, label, `Measuring ${label}`);
|
|
4939
|
+
results.push(cand);
|
|
4940
|
+
this.state = { ...this.state, candidates: results, bestTps: cand.tps ?? this.state.bestTps };
|
|
4941
|
+
await this.settleGpu();
|
|
4942
|
+
return cand.outcome === "ok" && cand.tps !== null ? { cand, profile } : null;
|
|
4862
4943
|
}
|
|
4863
4944
|
/** The measurement primitive (spec 09 §1): launch the candidate, detect
|
|
4864
4945
|
* ready/timeout/crash/oom, then warm up + one measured request. Never throws —
|
|
@@ -4872,7 +4953,7 @@ var BenchRunner = class {
|
|
|
4872
4953
|
kvTypeK: profile.kvTypeK,
|
|
4873
4954
|
flashAttn: profile.flashAttn
|
|
4874
4955
|
};
|
|
4875
|
-
const fail = (outcome2) => ({ label, params, outcome: outcome2, tps: null, ttftMs: null, vramMb: null });
|
|
4956
|
+
const fail = (outcome2) => ({ label, params, outcome: outcome2, tps: null, prefillTps: null, ttftMs: null, vramMb: null, vramAbsMb: null });
|
|
4876
4957
|
const phase = (p) => {
|
|
4877
4958
|
this.state = { ...this.state, step: `${stepPrefix} \u2014 ${p}` };
|
|
4878
4959
|
};
|
|
@@ -4921,7 +5002,7 @@ var BenchRunner = class {
|
|
|
4921
5002
|
});
|
|
4922
5003
|
if ("fault" in measured) return fail(measured.fault);
|
|
4923
5004
|
const vramMb = vramBefore !== null && vramAfter !== null ? Math.max(0, vramAfter - vramBefore) : vramAfter;
|
|
4924
|
-
return { label, params, outcome: "ok", tps: measured.tps, ttftMs: measured.ttftMs, vramMb };
|
|
5005
|
+
return { label, params, outcome: "ok", tps: measured.tps, prefillTps: measured.prefillTps, ttftMs: measured.ttftMs, vramMb, vramAbsMb: vramAfter };
|
|
4925
5006
|
}
|
|
4926
5007
|
/** Poll the manager state until the engine is running, the readiness window
|
|
4927
5008
|
* elapses (timeout), the process exits (crash), or an OOM line appears in the
|
|
@@ -5112,7 +5193,11 @@ var BenchRunner = class {
|
|
|
5112
5193
|
max_tokens: maxTokens,
|
|
5113
5194
|
temperature: 0,
|
|
5114
5195
|
seed: 42,
|
|
5115
|
-
stream: false
|
|
5196
|
+
stream: false,
|
|
5197
|
+
// Re-process the prompt instead of reusing the warmup's cached prefill — otherwise the
|
|
5198
|
+
// engine only evaluates the few new template tokens and `prompt_per_second` reflects ~4
|
|
5199
|
+
// tokens, not the real prefill throughput over the whole bench prompt.
|
|
5200
|
+
cache_prompt: false
|
|
5116
5201
|
}),
|
|
5117
5202
|
signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0]
|
|
5118
5203
|
});
|
|
@@ -5120,7 +5205,11 @@ var BenchRunner = class {
|
|
|
5120
5205
|
const data = await res.json();
|
|
5121
5206
|
const t = data.timings;
|
|
5122
5207
|
if (!t || typeof t.predicted_per_second !== "number") return null;
|
|
5123
|
-
return {
|
|
5208
|
+
return {
|
|
5209
|
+
tps: t.predicted_per_second,
|
|
5210
|
+
prefillTps: typeof t.prompt_per_second === "number" ? t.prompt_per_second : null,
|
|
5211
|
+
ttftMs: typeof t.prompt_ms === "number" ? t.prompt_ms : 0
|
|
5212
|
+
};
|
|
5124
5213
|
}
|
|
5125
5214
|
// ---- card-derived recommended sampling (ADR-099) ------------------------
|
|
5126
5215
|
/** Resolve the model's HF card and extract recommended sampling (temp/top_k/top_p/min_p).
|
|
@@ -5282,12 +5371,47 @@ function makeBenchContent(targetTokens) {
|
|
|
5282
5371
|
return BENCH_BASE.repeat(reps).slice(0, targetChars) + "\n\nSummarize the passage above in one sentence.";
|
|
5283
5372
|
}
|
|
5284
5373
|
function benchPromptTokens(ctx) {
|
|
5285
|
-
return Math.max(256, Math.min(
|
|
5374
|
+
return Math.max(256, Math.min(8192, Math.floor(ctx * 0.75)));
|
|
5375
|
+
}
|
|
5376
|
+
function totalVramMb(sys) {
|
|
5377
|
+
return sys.gpus.reduce((s, g) => s + (g.vramMb || 0), 0);
|
|
5286
5378
|
}
|
|
5287
|
-
function
|
|
5288
|
-
const total = sys
|
|
5289
|
-
if (!
|
|
5290
|
-
return
|
|
5379
|
+
function overHeadroom(vramAbsMb, sys) {
|
|
5380
|
+
const total = totalVramMb(sys);
|
|
5381
|
+
if (!vramAbsMb || total <= 0) return false;
|
|
5382
|
+
return vramAbsMb > total - VRAM_HEADROOM_MB;
|
|
5383
|
+
}
|
|
5384
|
+
function pickKvQuants(baseKv, kvTypes) {
|
|
5385
|
+
const supported = (t) => kvTypes.length === 0 ? t === baseKv : kvTypes.includes(t);
|
|
5386
|
+
const out = [];
|
|
5387
|
+
for (const t of [baseKv, ...QUALITY_KV]) if (supported(t) && !out.includes(t)) out.push(t);
|
|
5388
|
+
return out;
|
|
5389
|
+
}
|
|
5390
|
+
function betterBySpeed(a, b) {
|
|
5391
|
+
const at = a.tps ?? 0;
|
|
5392
|
+
const bt = b.tps ?? 0;
|
|
5393
|
+
if (bt <= 0) return at > 0;
|
|
5394
|
+
const rel = (at - bt) / bt;
|
|
5395
|
+
if (rel > OUTPUT_TIE) return true;
|
|
5396
|
+
if (rel < -OUTPUT_TIE) return false;
|
|
5397
|
+
return (a.prefillTps ?? 0) > (b.prefillTps ?? 0);
|
|
5398
|
+
}
|
|
5399
|
+
function kvBytes(t) {
|
|
5400
|
+
return KV_BYTES[t] ?? 2;
|
|
5401
|
+
}
|
|
5402
|
+
function kvLargest(c) {
|
|
5403
|
+
return c.reduce((a, b) => kvBytes(b) > kvBytes(a) ? b : a, c[0]);
|
|
5404
|
+
}
|
|
5405
|
+
function kvSmallest(c) {
|
|
5406
|
+
return c.reduce((a, b) => kvBytes(b) < kvBytes(a) ? b : a, c[0]);
|
|
5407
|
+
}
|
|
5408
|
+
function decideKvToBench(spreadMb, candidates) {
|
|
5409
|
+
const has = (t) => candidates.includes(t);
|
|
5410
|
+
const largest = kvLargest(candidates);
|
|
5411
|
+
if (spreadMb >= 0 && spreadMb <= KV_SPREAD_MIN) return [largest];
|
|
5412
|
+
const smallest = has("turbo4") ? "turbo4" : kvSmallest(candidates);
|
|
5413
|
+
const stock = has("q8_0") ? "q8_0" : largest;
|
|
5414
|
+
return [.../* @__PURE__ */ new Set([smallest, stock])];
|
|
5291
5415
|
}
|
|
5292
5416
|
function readNvidiaVramMb() {
|
|
5293
5417
|
return new Promise((resolve2) => {
|
|
@@ -5384,6 +5508,27 @@ var ModelRouter = class {
|
|
|
5384
5508
|
unlock();
|
|
5385
5509
|
}
|
|
5386
5510
|
}
|
|
5511
|
+
/** Every model key currently loaded (or loading) across the WHOLE pool — the primary
|
|
5512
|
+
* manager plus every alive extra slot (F-033). "Alive" = running OR starting, matching
|
|
5513
|
+
* the delete-guard's notion of "loaded" (routes.ts), so a model loaded via gateway
|
|
5514
|
+
* auto-swap into an extra slot is reported as loaded even though it isn't in the
|
|
5515
|
+
* primary manager. Used by overlayModel to mark gateway-loaded models loaded on the
|
|
5516
|
+
* Models page (they were previously invisible — only the primary manager was consulted). */
|
|
5517
|
+
loadedModelKeys() {
|
|
5518
|
+
const isAlive = (s) => s === "running" || s === "starting";
|
|
5519
|
+
const keys = /* @__PURE__ */ new Set();
|
|
5520
|
+
const add = (key) => {
|
|
5521
|
+
keys.add(key);
|
|
5522
|
+
const path = this.scanner.get(key)?.path;
|
|
5523
|
+
if (path) keys.add(path);
|
|
5524
|
+
};
|
|
5525
|
+
const ms = this.manager.status();
|
|
5526
|
+
if (isAlive(ms.state) && ms.model) add(ms.model.key);
|
|
5527
|
+
for (const slot of this.extraSlots.values()) {
|
|
5528
|
+
if (isAlive(slot.manager.status().state)) add(slot.modelKey);
|
|
5529
|
+
}
|
|
5530
|
+
return keys;
|
|
5531
|
+
}
|
|
5387
5532
|
// ── internal ──────────────────────────────────────────────────────────────
|
|
5388
5533
|
async doLoad(entry) {
|
|
5389
5534
|
{
|
|
@@ -6341,7 +6486,58 @@ import { spawn as spawn3 } from "child_process";
|
|
|
6341
6486
|
var SUPPORTED = {
|
|
6342
6487
|
claude: { bin: "claude", label: "Claude Code", install: "npm install -g @anthropic-ai/claude-code" }
|
|
6343
6488
|
};
|
|
6344
|
-
async function
|
|
6489
|
+
async function fetchStatus(base2, _fetch = fetch) {
|
|
6490
|
+
try {
|
|
6491
|
+
const res = await _fetch(`${base2}/api/v1/status`, { signal: AbortSignal.timeout(3e3) });
|
|
6492
|
+
if (!res.ok) return null;
|
|
6493
|
+
return await res.json();
|
|
6494
|
+
} catch {
|
|
6495
|
+
return null;
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
async function fetchModels(base2, _fetch = fetch) {
|
|
6499
|
+
try {
|
|
6500
|
+
const res = await _fetch(`${base2}/api/v1/models`, { signal: AbortSignal.timeout(5e3) });
|
|
6501
|
+
if (!res.ok) return [];
|
|
6502
|
+
const data = await res.json();
|
|
6503
|
+
return data.models ?? [];
|
|
6504
|
+
} catch {
|
|
6505
|
+
return [];
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
function resolveModelKey(models, input) {
|
|
6509
|
+
const byKey = models.find((m) => m.key === input);
|
|
6510
|
+
if (byKey) return byKey.key;
|
|
6511
|
+
const byName = models.find((m) => m.name === input);
|
|
6512
|
+
if (byName) return byName.key;
|
|
6513
|
+
const lower = input.toLowerCase();
|
|
6514
|
+
const partial = models.find((m) => m.name.toLowerCase().includes(lower));
|
|
6515
|
+
if (partial) return partial.key;
|
|
6516
|
+
return null;
|
|
6517
|
+
}
|
|
6518
|
+
async function loadAndWait(base2, modelKey, timeoutMs = 18e4, _fetch = fetch) {
|
|
6519
|
+
const loadRes = await _fetch(`${base2}/api/v1/engine/start`, {
|
|
6520
|
+
method: "POST",
|
|
6521
|
+
headers: { "Content-Type": "application/json" },
|
|
6522
|
+
body: JSON.stringify({ modelKey }),
|
|
6523
|
+
signal: AbortSignal.timeout(5e3)
|
|
6524
|
+
});
|
|
6525
|
+
if (!loadRes.ok) return false;
|
|
6526
|
+
const deadline = Date.now() + timeoutMs;
|
|
6527
|
+
while (Date.now() < deadline) {
|
|
6528
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
6529
|
+
try {
|
|
6530
|
+
const res = await _fetch(`${base2}/api/v1/status`, { signal: AbortSignal.timeout(3e3) });
|
|
6531
|
+
if (!res.ok) continue;
|
|
6532
|
+
const st = await res.json();
|
|
6533
|
+
if (st.engine?.state === "running" && st.model?.key === modelKey) return true;
|
|
6534
|
+
if (st.engine?.state === "error") return false;
|
|
6535
|
+
} catch {
|
|
6536
|
+
}
|
|
6537
|
+
}
|
|
6538
|
+
return false;
|
|
6539
|
+
}
|
|
6540
|
+
async function launchCli(target, port2, passthrough, _spawn = spawn3, modelKey, _fetch = fetch) {
|
|
6345
6541
|
const spec = SUPPORTED[target];
|
|
6346
6542
|
if (!target || !spec) {
|
|
6347
6543
|
const list = Object.keys(SUPPORTED).join(", ");
|
|
@@ -6352,7 +6548,7 @@ async function launchCli(target, port2, passthrough, _spawn = spawn3) {
|
|
|
6352
6548
|
const base2 = `http://127.0.0.1:${port2}`;
|
|
6353
6549
|
let status;
|
|
6354
6550
|
try {
|
|
6355
|
-
const res = await
|
|
6551
|
+
const res = await _fetch(`${base2}/api/v1/status`, { signal: AbortSignal.timeout(3e3) });
|
|
6356
6552
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
6357
6553
|
status = await res.json();
|
|
6358
6554
|
} catch {
|
|
@@ -6363,7 +6559,62 @@ Start the daemon first \u2014 run \`turbollm\` in another terminal \u2014 or pas
|
|
|
6363
6559
|
);
|
|
6364
6560
|
return 1;
|
|
6365
6561
|
}
|
|
6366
|
-
|
|
6562
|
+
const alreadyRunning = status?.engine?.state === "running" && !!status?.model?.name;
|
|
6563
|
+
if (modelKey) {
|
|
6564
|
+
const models = await fetchModels(base2, _fetch);
|
|
6565
|
+
const resolvedKey = resolveModelKey(models, modelKey);
|
|
6566
|
+
if (!resolvedKey) {
|
|
6567
|
+
const list = models.map((m) => ` ${m.key} (${m.name})`).join("\n");
|
|
6568
|
+
process.stderr.write(
|
|
6569
|
+
`Model not found: "${modelKey}"
|
|
6570
|
+
` + (list ? `Available models:
|
|
6571
|
+
${list}
|
|
6572
|
+
` : `No models in library \u2014 add one via the TurboLLM UI.
|
|
6573
|
+
`)
|
|
6574
|
+
);
|
|
6575
|
+
return 1;
|
|
6576
|
+
}
|
|
6577
|
+
if (alreadyRunning && status?.model?.key === resolvedKey) {
|
|
6578
|
+
} else {
|
|
6579
|
+
process.stdout.write(`\u25B8 Loading model "${resolvedKey}"\u2026
|
|
6580
|
+
`);
|
|
6581
|
+
const loaded = await loadAndWait(base2, resolvedKey, 18e4, _fetch);
|
|
6582
|
+
if (!loaded) {
|
|
6583
|
+
process.stderr.write(
|
|
6584
|
+
`Model did not finish loading within 180 s. Check the TurboLLM UI for errors, or try again.
|
|
6585
|
+
`
|
|
6586
|
+
);
|
|
6587
|
+
return 1;
|
|
6588
|
+
}
|
|
6589
|
+
const refreshed = await fetchStatus(base2, _fetch);
|
|
6590
|
+
if (refreshed) status = refreshed;
|
|
6591
|
+
}
|
|
6592
|
+
} else if (!alreadyRunning) {
|
|
6593
|
+
const models = await fetchModels(base2, _fetch);
|
|
6594
|
+
if (models.length === 0) {
|
|
6595
|
+
process.stderr.write(
|
|
6596
|
+
`TurboLLM is running, but no model is loaded and no models are in the library.
|
|
6597
|
+
Open ${base2} \u2192 Models \u2192 add a model, then run this again.
|
|
6598
|
+
`
|
|
6599
|
+
);
|
|
6600
|
+
return 1;
|
|
6601
|
+
}
|
|
6602
|
+
const lastKey = status?.lastLoaded?.modelKey;
|
|
6603
|
+
const autoKey = lastKey && models.some((m) => m.key === lastKey) ? lastKey : models[0].key;
|
|
6604
|
+
process.stdout.write(`\u25B8 Auto-loading model "${autoKey}"\u2026
|
|
6605
|
+
`);
|
|
6606
|
+
const loaded = await loadAndWait(base2, autoKey, 18e4, _fetch);
|
|
6607
|
+
if (!loaded) {
|
|
6608
|
+
process.stderr.write(
|
|
6609
|
+
`Model did not finish loading within 180 s. Check the TurboLLM UI for errors, then run this again.
|
|
6610
|
+
`
|
|
6611
|
+
);
|
|
6612
|
+
return 1;
|
|
6613
|
+
}
|
|
6614
|
+
const refreshed = await fetchStatus(base2, _fetch);
|
|
6615
|
+
if (refreshed) status = refreshed;
|
|
6616
|
+
}
|
|
6617
|
+
if (status?.engine?.state !== "running" || !status?.model?.name) {
|
|
6367
6618
|
process.stderr.write(
|
|
6368
6619
|
`TurboLLM is running, but no model is loaded.
|
|
6369
6620
|
Open ${base2} \u2192 Models \u2192 Load a model, then run this again.
|
|
@@ -6410,18 +6661,120 @@ Install it: ${spec.install}
|
|
|
6410
6661
|
});
|
|
6411
6662
|
}
|
|
6412
6663
|
|
|
6664
|
+
// src/daemon-pid.ts
|
|
6665
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync, existsSync as existsSync14 } from "fs";
|
|
6666
|
+
import { join as join16 } from "path";
|
|
6667
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
6668
|
+
function writePidfile(dir, pid, port2) {
|
|
6669
|
+
const path = join16(dir, "daemon.pid");
|
|
6670
|
+
writeFileSync7(path, JSON.stringify({ pid, port: port2 }));
|
|
6671
|
+
}
|
|
6672
|
+
function readPidfile(dir) {
|
|
6673
|
+
const path = join16(dir, "daemon.pid");
|
|
6674
|
+
if (!existsSync14(path)) return null;
|
|
6675
|
+
try {
|
|
6676
|
+
const raw = readFileSync7(path, "utf8");
|
|
6677
|
+
const parsed = JSON.parse(raw);
|
|
6678
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.pid === "number" && typeof parsed.port === "number") {
|
|
6679
|
+
return parsed;
|
|
6680
|
+
}
|
|
6681
|
+
return null;
|
|
6682
|
+
} catch {
|
|
6683
|
+
return null;
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
function removePidfile(dir) {
|
|
6687
|
+
try {
|
|
6688
|
+
unlinkSync(join16(dir, "daemon.pid"));
|
|
6689
|
+
} catch {
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
var defaultHooks = {
|
|
6693
|
+
processExists(pid) {
|
|
6694
|
+
try {
|
|
6695
|
+
process.kill(pid, 0);
|
|
6696
|
+
return true;
|
|
6697
|
+
} catch {
|
|
6698
|
+
return false;
|
|
6699
|
+
}
|
|
6700
|
+
},
|
|
6701
|
+
kill(pid, signal) {
|
|
6702
|
+
process.kill(pid, signal);
|
|
6703
|
+
},
|
|
6704
|
+
taskkill(pid) {
|
|
6705
|
+
execFileSync3("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
6706
|
+
},
|
|
6707
|
+
async confirmTurbollm(port2) {
|
|
6708
|
+
try {
|
|
6709
|
+
const res = await fetch(`http://127.0.0.1:${port2}/api/v1/status`, { signal: AbortSignal.timeout(2e3) });
|
|
6710
|
+
if (!res.ok) return false;
|
|
6711
|
+
const body3 = await res.json();
|
|
6712
|
+
return typeof body3 === "object" && body3 !== null && "version" in body3 && "engine" in body3;
|
|
6713
|
+
} catch {
|
|
6714
|
+
return false;
|
|
6715
|
+
}
|
|
6716
|
+
},
|
|
6717
|
+
platform: process.platform
|
|
6718
|
+
};
|
|
6719
|
+
async function waitForExit(pid, timeoutMs, hooks) {
|
|
6720
|
+
const deadline = Date.now() + timeoutMs;
|
|
6721
|
+
while (Date.now() < deadline) {
|
|
6722
|
+
if (!hooks.processExists(pid)) return true;
|
|
6723
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
6724
|
+
}
|
|
6725
|
+
return !hooks.processExists(pid);
|
|
6726
|
+
}
|
|
6727
|
+
async function stopDaemon(dir, hooks = defaultHooks) {
|
|
6728
|
+
const data = readPidfile(dir);
|
|
6729
|
+
if (!data) {
|
|
6730
|
+
return { stopped: false, message: "No running TurboLLM daemon found." };
|
|
6731
|
+
}
|
|
6732
|
+
const { pid, port: port2 } = data;
|
|
6733
|
+
if (!hooks.processExists(pid)) {
|
|
6734
|
+
removePidfile(dir);
|
|
6735
|
+
return { stopped: false, message: "No running TurboLLM daemon found." };
|
|
6736
|
+
}
|
|
6737
|
+
if (!await hooks.confirmTurbollm(port2)) {
|
|
6738
|
+
const manual = hooks.platform === "win32" ? `taskkill /PID ${pid} /T /F` : `kill -9 ${pid}`;
|
|
6739
|
+
return {
|
|
6740
|
+
stopped: false,
|
|
6741
|
+
message: `Found a pidfile (pid ${pid}, port ${port2}), but no TurboLLM daemon is responding there \u2014 it may be stale or the PID was reused by another process. Not killing it automatically.
|
|
6742
|
+
If you're sure it's TurboLLM, stop it manually: ${manual}`
|
|
6743
|
+
};
|
|
6744
|
+
}
|
|
6745
|
+
try {
|
|
6746
|
+
if (hooks.platform === "win32") {
|
|
6747
|
+
hooks.taskkill(pid);
|
|
6748
|
+
await waitForExit(pid, 2e3, hooks);
|
|
6749
|
+
} else {
|
|
6750
|
+
hooks.kill(pid, "SIGTERM");
|
|
6751
|
+
const exited = await waitForExit(pid, 1e4, hooks);
|
|
6752
|
+
if (!exited) {
|
|
6753
|
+
try {
|
|
6754
|
+
hooks.kill(pid, "SIGKILL");
|
|
6755
|
+
} catch {
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6759
|
+
} catch (e) {
|
|
6760
|
+
return { stopped: false, message: `Failed to stop daemon (pid ${pid}): ${e instanceof Error ? e.message : e}` };
|
|
6761
|
+
}
|
|
6762
|
+
removePidfile(dir);
|
|
6763
|
+
return { stopped: true, port: port2, message: `TurboLLM daemon stopped (port ${port2})` };
|
|
6764
|
+
}
|
|
6765
|
+
|
|
6413
6766
|
// src/server.ts
|
|
6414
6767
|
import { Hono } from "hono";
|
|
6415
6768
|
import { cors } from "hono/cors";
|
|
6416
|
-
import { existsSync as
|
|
6769
|
+
import { existsSync as existsSync19, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
|
|
6417
6770
|
import { fileURLToPath } from "url";
|
|
6418
|
-
import { dirname as dirname8, join as
|
|
6771
|
+
import { dirname as dirname8, join as join21, normalize as normalize2 } from "path";
|
|
6419
6772
|
import { Agent, setGlobalDispatcher } from "undici";
|
|
6420
6773
|
|
|
6421
6774
|
// src/api/routes.ts
|
|
6422
6775
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
6423
|
-
import { existsSync as
|
|
6424
|
-
import { basename as basename3, dirname as dirname7, join as
|
|
6776
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync9, readdirSync as readdirSync7, realpathSync, rmSync as rmSync10, statSync as statSync5, writeFileSync as writeFileSync9 } from "fs";
|
|
6777
|
+
import { basename as basename3, dirname as dirname7, join as join20, resolve, sep } from "path";
|
|
6425
6778
|
|
|
6426
6779
|
// src/comfyui/gate-template.ts
|
|
6427
6780
|
var GATE_VERSION = 2;
|
|
@@ -7056,42 +7409,89 @@ ${content}` : b.docContext : content;
|
|
|
7056
7409
|
return c.json({ url, onlyLocal });
|
|
7057
7410
|
});
|
|
7058
7411
|
app2.post("/api/v1/conversations/import", async (c) => {
|
|
7059
|
-
let
|
|
7412
|
+
let raw;
|
|
7060
7413
|
try {
|
|
7061
|
-
|
|
7414
|
+
raw = await c.req.json();
|
|
7062
7415
|
} catch {
|
|
7063
7416
|
return err(c, 400, "invalid_file", "Body must be valid JSON.");
|
|
7064
7417
|
}
|
|
7065
|
-
if (
|
|
7066
|
-
return
|
|
7067
|
-
}
|
|
7068
|
-
if (
|
|
7069
|
-
return err(c, 400, "invalid_file",
|
|
7070
|
-
}
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
const
|
|
7085
|
-
|
|
7086
|
-
const
|
|
7087
|
-
const
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7418
|
+
if (Array.isArray(raw)) {
|
|
7419
|
+
return importOpenAiMessages(c, db2, raw, void 0, void 0);
|
|
7420
|
+
}
|
|
7421
|
+
if (typeof raw !== "object" || raw === null) {
|
|
7422
|
+
return err(c, 400, "invalid_file", "Body must be a JSON array or object.");
|
|
7423
|
+
}
|
|
7424
|
+
const payload = raw;
|
|
7425
|
+
if (payload.format === "debug" || payload.format === "export") {
|
|
7426
|
+
if (!Array.isArray(payload.messages)) {
|
|
7427
|
+
return err(c, 400, "invalid_file", 'Missing "messages" array.');
|
|
7428
|
+
}
|
|
7429
|
+
if (typeof payload.chat_id !== "string") {
|
|
7430
|
+
return err(c, 400, "invalid_file", 'Missing "chat_id" field.');
|
|
7431
|
+
}
|
|
7432
|
+
if (typeof payload.title !== "string") {
|
|
7433
|
+
return err(c, 400, "invalid_file", 'Missing "title" field.');
|
|
7434
|
+
}
|
|
7435
|
+
const title = payload.title || "Imported chat";
|
|
7436
|
+
const modelKey = typeof payload.model === "string" ? payload.model : "";
|
|
7437
|
+
const personaId = typeof payload.persona === "string" ? payload.persona : "default";
|
|
7438
|
+
const toolPolicy = personaId === "research" ? "force_web_search" : void 0;
|
|
7439
|
+
const newConv = db2.createConversation({ title, modelKey, toolPolicy });
|
|
7440
|
+
const messages = payload.messages;
|
|
7441
|
+
for (const m of messages) {
|
|
7442
|
+
const role = m.role;
|
|
7443
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
7444
|
+
const content = typeof m.content === "string" ? m.content : "";
|
|
7445
|
+
const toolCalls = Array.isArray(m.tool_calls) ? m.tool_calls : void 0;
|
|
7446
|
+
db2.addMessage(newConv.id, role, content, {
|
|
7447
|
+
toolCalls: toolCalls && toolCalls.length > 0 ? toolCalls : void 0
|
|
7448
|
+
});
|
|
7449
|
+
}
|
|
7450
|
+
return c.json({ id: newConv.id }, 201);
|
|
7451
|
+
}
|
|
7452
|
+
if (Array.isArray(payload.messages)) {
|
|
7453
|
+
const titleField = typeof payload.title === "string" ? payload.title : void 0;
|
|
7454
|
+
const modelField = typeof payload.model === "string" ? payload.model : void 0;
|
|
7455
|
+
return importOpenAiMessages(c, db2, payload.messages, titleField, modelField);
|
|
7091
7456
|
}
|
|
7092
|
-
return c.json(
|
|
7457
|
+
return err(c, 400, "invalid_file", 'Unrecognised format. Expected a .turbollm-chat.json file or an OpenAI-format JSON (array or object with "messages" array).');
|
|
7093
7458
|
});
|
|
7094
7459
|
}
|
|
7460
|
+
function coerceContent(content) {
|
|
7461
|
+
if (typeof content === "string") return content;
|
|
7462
|
+
if (Array.isArray(content)) {
|
|
7463
|
+
return content.filter((p) => typeof p === "object" && p !== null).map((p) => typeof p.text === "string" ? p.text : "").filter(Boolean).join("\n");
|
|
7464
|
+
}
|
|
7465
|
+
return String(content ?? "");
|
|
7466
|
+
}
|
|
7467
|
+
function deriveTitle(titleField, messages) {
|
|
7468
|
+
if (titleField?.trim()) return titleField.trim().slice(0, 60);
|
|
7469
|
+
for (const m of messages) {
|
|
7470
|
+
const role = m.role;
|
|
7471
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
7472
|
+
const text = coerceContent(m.content).trim();
|
|
7473
|
+
if (text) {
|
|
7474
|
+
const truncated = text.replace(/\s+/g, " ").slice(0, 60);
|
|
7475
|
+
return truncated;
|
|
7476
|
+
}
|
|
7477
|
+
}
|
|
7478
|
+
return "Imported chat";
|
|
7479
|
+
}
|
|
7480
|
+
function importOpenAiMessages(c, db2, messages, titleField, modelField) {
|
|
7481
|
+
const usable = messages.filter((m) => m.role === "user" || m.role === "assistant");
|
|
7482
|
+
if (usable.length === 0) {
|
|
7483
|
+
return err(c, 400, "invalid_file", "No usable messages found (must have at least one user or assistant message).");
|
|
7484
|
+
}
|
|
7485
|
+
const title = deriveTitle(titleField, messages);
|
|
7486
|
+
const modelKey = modelField ?? "";
|
|
7487
|
+
const newConv = db2.createConversation({ title, modelKey });
|
|
7488
|
+
for (const m of usable) {
|
|
7489
|
+
const role = m.role;
|
|
7490
|
+
const content = coerceContent(m.content);
|
|
7491
|
+
db2.addMessage(newConv.id, role, content);
|
|
7492
|
+
}
|
|
7493
|
+
return c.json({ id: newConv.id }, 201);
|
|
7494
|
+
}
|
|
7095
7495
|
function getLanIpForShare() {
|
|
7096
7496
|
const nets = networkInterfaces();
|
|
7097
7497
|
for (const ifaces of Object.values(nets)) {
|
|
@@ -7478,6 +7878,8 @@ async function runGeneration(d, stream, ctx) {
|
|
|
7478
7878
|
ttftMs,
|
|
7479
7879
|
totalMs,
|
|
7480
7880
|
thinkMs,
|
|
7881
|
+
// Full context occupancy: cache-reused prompt tokens still sit in the KV cache / context
|
|
7882
|
+
// window (they're skipped only for recomputation), so they must stay counted here.
|
|
7481
7883
|
ctxUsed: (finalUsage.prompt_tokens ?? 0) + (finalUsage.completion_tokens ?? 0),
|
|
7482
7884
|
ctxMax,
|
|
7483
7885
|
model: ms.model?.name ?? "",
|
|
@@ -7583,13 +7985,13 @@ async function autoTitle(d, convId, prevMessages, assistantReply, target) {
|
|
|
7583
7985
|
|
|
7584
7986
|
// src/engines/scan.ts
|
|
7585
7987
|
import { dirname as dirname5 } from "path";
|
|
7586
|
-
import { existsSync as
|
|
7988
|
+
import { existsSync as existsSync15, statSync as statSync4 } from "fs";
|
|
7587
7989
|
var serverBinName = process.platform === "win32" ? "llama-server.exe" : "llama-server";
|
|
7588
7990
|
function skipScanDir(name) {
|
|
7589
7991
|
return name === "node_modules" || name === ".git" || name.startsWith(".");
|
|
7590
7992
|
}
|
|
7591
7993
|
function resolveServerBinary(path, binName = serverBinName) {
|
|
7592
|
-
if (!
|
|
7994
|
+
if (!existsSync15(path)) return null;
|
|
7593
7995
|
let isDir;
|
|
7594
7996
|
try {
|
|
7595
7997
|
isDir = statSync4(path).isDirectory();
|
|
@@ -7960,7 +8362,7 @@ function catalogEngine(id) {
|
|
|
7960
8362
|
|
|
7961
8363
|
// src/engines/build-prereqs.ts
|
|
7962
8364
|
import { execFile as execFile7 } from "child_process";
|
|
7963
|
-
import { delimiter, join as
|
|
8365
|
+
import { delimiter, join as join17 } from "path";
|
|
7964
8366
|
import { promisify as promisify4 } from "util";
|
|
7965
8367
|
var execFileP4 = promisify4(execFile7);
|
|
7966
8368
|
function buildEnv(toolchainDirs = []) {
|
|
@@ -8037,7 +8439,7 @@ async function checkMsvc(env) {
|
|
|
8037
8439
|
let version2;
|
|
8038
8440
|
try {
|
|
8039
8441
|
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
|
8040
|
-
const vswhere =
|
|
8442
|
+
const vswhere = join17(programFilesX86, "Microsoft Visual Studio", "Installer", "vswhere.exe");
|
|
8041
8443
|
const out = await runVersion(vswhere, [
|
|
8042
8444
|
"-latest",
|
|
8043
8445
|
"-products",
|
|
@@ -8071,8 +8473,8 @@ async function checkBuildPrereqs(toolchainDirs = []) {
|
|
|
8071
8473
|
|
|
8072
8474
|
// src/engines/build-runner.ts
|
|
8073
8475
|
import { execFile as execFile8, spawn as spawn4 } from "child_process";
|
|
8074
|
-
import { copyFileSync, existsSync as
|
|
8075
|
-
import { delimiter as delimiter2, dirname as dirname6, join as
|
|
8476
|
+
import { copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync8, readdirSync as readdirSync5, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
8477
|
+
import { delimiter as delimiter2, dirname as dirname6, join as join18 } from "path";
|
|
8076
8478
|
import { promisify as promisify5 } from "util";
|
|
8077
8479
|
var execFileP5 = promisify5(execFile8);
|
|
8078
8480
|
function buildDirName(repoUrl, branch) {
|
|
@@ -8088,13 +8490,13 @@ function sameRepo(a, b) {
|
|
|
8088
8490
|
return na !== "" && na === norm(b);
|
|
8089
8491
|
}
|
|
8090
8492
|
function sourceBuildBinary(enginesRoot, repoUrl, branch) {
|
|
8091
|
-
const root =
|
|
8092
|
-
return resolveServerBinary(
|
|
8493
|
+
const root = join18(enginesRoot, "build", buildDirName(repoUrl, branch));
|
|
8494
|
+
return resolveServerBinary(join18(root, "build")) ?? resolveServerBinary(root);
|
|
8093
8495
|
}
|
|
8094
8496
|
function sourceBuildDirOf(binPath, enginesRoot) {
|
|
8095
8497
|
const m = binPath.replace(/\\/g, "/").match(/\/engines\/build\/([^/]+)(?:\/|$)/i);
|
|
8096
8498
|
if (!m) return null;
|
|
8097
|
-
return
|
|
8499
|
+
return join18(enginesRoot, "build", m[1]);
|
|
8098
8500
|
}
|
|
8099
8501
|
function pickGenerator(hasNinja) {
|
|
8100
8502
|
return hasNinja ? "Ninja" : "NMake Makefiles";
|
|
@@ -8115,7 +8517,7 @@ function onPath(env, exe) {
|
|
|
8115
8517
|
const key = Object.keys(env).find((k) => k.toLowerCase() === "path") ?? "PATH";
|
|
8116
8518
|
return (env[key] ?? "").split(delimiter2).filter(Boolean).some((dir) => {
|
|
8117
8519
|
try {
|
|
8118
|
-
return
|
|
8520
|
+
return existsSync16(join18(dir, exe));
|
|
8119
8521
|
} catch {
|
|
8120
8522
|
return false;
|
|
8121
8523
|
}
|
|
@@ -8127,15 +8529,15 @@ function cudaDllSourceDirs(env) {
|
|
|
8127
8529
|
const pathDirs = (env[key] ?? "").split(delimiter2).filter(Boolean);
|
|
8128
8530
|
const nvccDir = pathDirs.find((dir) => {
|
|
8129
8531
|
try {
|
|
8130
|
-
return
|
|
8532
|
+
return existsSync16(join18(dir, "nvcc.exe"));
|
|
8131
8533
|
} catch {
|
|
8132
8534
|
return false;
|
|
8133
8535
|
}
|
|
8134
8536
|
});
|
|
8135
8537
|
if (!nvccDir) return [];
|
|
8136
|
-
return [nvccDir,
|
|
8538
|
+
return [nvccDir, join18(nvccDir, "x64")].filter((d) => {
|
|
8137
8539
|
try {
|
|
8138
|
-
return
|
|
8540
|
+
return existsSync16(d);
|
|
8139
8541
|
} catch {
|
|
8140
8542
|
return false;
|
|
8141
8543
|
}
|
|
@@ -8162,7 +8564,7 @@ function copyCudaRuntimeDlls(env, destDir, log) {
|
|
|
8162
8564
|
if (seen.has(lower)) continue;
|
|
8163
8565
|
if (!CUDA_RUNTIME_DLL_PREFIXES.some((p) => lower.startsWith(p))) continue;
|
|
8164
8566
|
try {
|
|
8165
|
-
copyFileSync(
|
|
8567
|
+
copyFileSync(join18(dir, name), join18(destDir, name));
|
|
8166
8568
|
seen.add(lower);
|
|
8167
8569
|
copied++;
|
|
8168
8570
|
} catch {
|
|
@@ -8175,14 +8577,14 @@ function copyCudaRuntimeDlls(env, destDir, log) {
|
|
|
8175
8577
|
async function findVcvarsall() {
|
|
8176
8578
|
try {
|
|
8177
8579
|
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
|
8178
|
-
const vswhere =
|
|
8580
|
+
const vswhere = join18(programFilesX86, "Microsoft Visual Studio", "Installer", "vswhere.exe");
|
|
8179
8581
|
const { stdout } = await execFileP5(
|
|
8180
8582
|
vswhere,
|
|
8181
8583
|
["-latest", "-products", "*", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-find", "VC\\Auxiliary\\Build\\vcvarsall.bat"],
|
|
8182
8584
|
{ timeout: 8e3, windowsHide: true, maxBuffer: 1024 * 1024 }
|
|
8183
8585
|
);
|
|
8184
8586
|
const p = stdout.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
|
|
8185
|
-
return p &&
|
|
8587
|
+
return p && existsSync16(p) ? p : null;
|
|
8186
8588
|
} catch {
|
|
8187
8589
|
return null;
|
|
8188
8590
|
}
|
|
@@ -8236,7 +8638,7 @@ function hasAssemblySources(dir) {
|
|
|
8236
8638
|
}
|
|
8237
8639
|
for (const e of entries) {
|
|
8238
8640
|
if (e.isDirectory()) {
|
|
8239
|
-
if (!SKIP.has(e.name.toLowerCase()) && !e.name.startsWith(".")) stack.push(
|
|
8641
|
+
if (!SKIP.has(e.name.toLowerCase()) && !e.name.startsWith(".")) stack.push(join18(d, e.name));
|
|
8240
8642
|
} else if (/\.(s|asm)$/i.test(e.name)) {
|
|
8241
8643
|
return true;
|
|
8242
8644
|
}
|
|
@@ -8264,20 +8666,20 @@ function stripGenericAsmLanguage(text) {
|
|
|
8264
8666
|
}
|
|
8265
8667
|
function neutralizeGratuitousAsm(srcDir, log) {
|
|
8266
8668
|
if (hasAssemblySources(srcDir)) return;
|
|
8267
|
-
const candidates = [
|
|
8669
|
+
const candidates = [join18(srcDir, "CMakeLists.txt"), join18(srcDir, "ggml", "CMakeLists.txt")];
|
|
8268
8670
|
let patched = 0;
|
|
8269
8671
|
for (const file of candidates) {
|
|
8270
|
-
if (!
|
|
8672
|
+
if (!existsSync16(file)) continue;
|
|
8271
8673
|
let text;
|
|
8272
8674
|
try {
|
|
8273
|
-
text =
|
|
8675
|
+
text = readFileSync8(file, "utf8");
|
|
8274
8676
|
} catch {
|
|
8275
8677
|
continue;
|
|
8276
8678
|
}
|
|
8277
8679
|
const { text: next, changed } = stripGenericAsmLanguage(text);
|
|
8278
8680
|
if (changed) {
|
|
8279
8681
|
try {
|
|
8280
|
-
|
|
8682
|
+
writeFileSync8(file, next);
|
|
8281
8683
|
patched++;
|
|
8282
8684
|
} catch {
|
|
8283
8685
|
}
|
|
@@ -8307,9 +8709,9 @@ async function runBuild(req, hooks, signal) {
|
|
|
8307
8709
|
'Could not locate the Visual Studio C++ build environment (vcvarsall.bat). Install the "Desktop development with C++" workload from the Visual Studio Build Tools.'
|
|
8308
8710
|
);
|
|
8309
8711
|
}
|
|
8310
|
-
const buildRoot =
|
|
8311
|
-
const srcDir =
|
|
8312
|
-
const buildSubdir =
|
|
8712
|
+
const buildRoot = join18(req.enginesRoot, "build", buildDirName(req.repoUrl, req.branch));
|
|
8713
|
+
const srcDir = join18(buildRoot, "src");
|
|
8714
|
+
const buildSubdir = join18(buildRoot, "build");
|
|
8313
8715
|
rmSync8(buildRoot, { recursive: true, force: true });
|
|
8314
8716
|
mkdirSync10(buildRoot, { recursive: true });
|
|
8315
8717
|
hooks.phase("cloning");
|
|
@@ -8325,12 +8727,12 @@ async function runBuild(req, hooks, signal) {
|
|
|
8325
8727
|
generator === "Ninja" ? "Using the Ninja generator (drives nvcc directly \u2014 no Visual Studio CUDA integration needed)." : "Ninja not found on PATH \u2014 using the NMake generator (works, but single-threaded and slower; add a folder containing ninja.exe under Build environment for much faster builds)."
|
|
8326
8728
|
);
|
|
8327
8729
|
hooks.phase("configuring");
|
|
8328
|
-
const configureBat =
|
|
8329
|
-
|
|
8730
|
+
const configureBat = join18(buildRoot, "_tllm_configure.bat");
|
|
8731
|
+
writeFileSync8(configureBat, vcvarsBatch(vcvars, ["-G", generator, "-B", buildSubdir, "-S", srcDir, ...CMAKE_CONFIGURE_ARGS]));
|
|
8330
8732
|
await runStep("cmd.exe", ["/c", configureBat], { cwd: buildRoot, env, signal, onLine: hooks.log });
|
|
8331
8733
|
hooks.phase("compiling");
|
|
8332
|
-
const compileBat =
|
|
8333
|
-
|
|
8734
|
+
const compileBat = join18(buildRoot, "_tllm_build.bat");
|
|
8735
|
+
writeFileSync8(compileBat, vcvarsBatch(vcvars, ["--build", buildSubdir, "-j", "--target", "llama-server"]));
|
|
8334
8736
|
await runStep("cmd.exe", ["/c", compileBat], { cwd: buildRoot, env, signal, onLine: hooks.log });
|
|
8335
8737
|
hooks.phase("registering");
|
|
8336
8738
|
const binPath = resolveServerBinary(buildSubdir) ?? resolveServerBinary(buildRoot);
|
|
@@ -8343,9 +8745,9 @@ async function runBuild(req, hooks, signal) {
|
|
|
8343
8745
|
|
|
8344
8746
|
// src/engines/cuda-provision.ts
|
|
8345
8747
|
import { execFile as execFile9 } from "child_process";
|
|
8346
|
-
import { cpSync as cpSync2, existsSync as
|
|
8748
|
+
import { cpSync as cpSync2, existsSync as existsSync17, mkdirSync as mkdirSync11, readdirSync as readdirSync6, rmSync as rmSync9 } from "fs";
|
|
8347
8749
|
import { tmpdir } from "os";
|
|
8348
|
-
import { join as
|
|
8750
|
+
import { join as join19 } from "path";
|
|
8349
8751
|
import { promisify as promisify6 } from "util";
|
|
8350
8752
|
var execFileP6 = promisify6(execFile9);
|
|
8351
8753
|
var REDIST_BASE = "https://developer.download.nvidia.com/compute/cuda/redist/";
|
|
@@ -8400,9 +8802,9 @@ async function provisionCuda(dataDir, onProgress, signal) {
|
|
|
8400
8802
|
phase: "resolving",
|
|
8401
8803
|
message: `Using CUDA ${version2}${driverMax ? ` (driver supports up to ${driverMax})` : ""}.`
|
|
8402
8804
|
});
|
|
8403
|
-
const root =
|
|
8404
|
-
const binDir =
|
|
8405
|
-
if (
|
|
8805
|
+
const root = join19(dataDir, "cuda", version2);
|
|
8806
|
+
const binDir = join19(root, "bin");
|
|
8807
|
+
if (existsSync17(join19(binDir, "nvcc.exe"))) {
|
|
8406
8808
|
onProgress({ phase: "assembling", message: `CUDA ${version2} already downloaded \u2014 reusing it.` });
|
|
8407
8809
|
return { root, binDir, version: version2 };
|
|
8408
8810
|
}
|
|
@@ -8417,7 +8819,7 @@ async function provisionCuda(dataDir, onProgress, signal) {
|
|
|
8417
8819
|
const rel = relOf(id);
|
|
8418
8820
|
if (rel) components.push({ id, url: REDIST_BASE + rel });
|
|
8419
8821
|
}
|
|
8420
|
-
const work =
|
|
8822
|
+
const work = join19(tmpdir(), `tllm-cuda-${version2}`);
|
|
8421
8823
|
rmSync9(work, { recursive: true, force: true });
|
|
8422
8824
|
mkdirSync11(work, { recursive: true });
|
|
8423
8825
|
const stageRoot = `${root}.partial`;
|
|
@@ -8426,23 +8828,23 @@ async function provisionCuda(dataDir, onProgress, signal) {
|
|
|
8426
8828
|
try {
|
|
8427
8829
|
for (let i = 0; i < components.length; i++) {
|
|
8428
8830
|
const { id, url } = components[i];
|
|
8429
|
-
const zip =
|
|
8831
|
+
const zip = join19(work, `${id}.zip`);
|
|
8430
8832
|
onProgress({ phase: "downloading", message: `Downloading ${id} (${i + 1}/${components.length})\u2026`, pct: 0 });
|
|
8431
8833
|
await downloadFile(url, zip, (p) => onProgress({ phase: "downloading", message: `Downloading ${id} (${i + 1}/${components.length})\u2026`, pct: p.pct }), signal);
|
|
8432
|
-
const exDir =
|
|
8834
|
+
const exDir = join19(work, id);
|
|
8433
8835
|
mkdirSync11(exDir, { recursive: true });
|
|
8434
8836
|
onProgress({ phase: "extracting", message: `Extracting ${id}\u2026` });
|
|
8435
8837
|
await extractArchive(zip, exDir);
|
|
8436
|
-
const inner = readdirSync6(exDir).map((n) =>
|
|
8437
|
-
const srcRoot = inner ??
|
|
8838
|
+
const inner = readdirSync6(exDir).map((n) => join19(exDir, n)).find((p) => existsSync17(join19(p, "include")) || existsSync17(join19(p, "bin")) || existsSync17(join19(p, "lib")));
|
|
8839
|
+
const srcRoot = inner ?? join19(exDir, readdirSync6(exDir)[0] ?? "");
|
|
8438
8840
|
cpSync2(srcRoot, stageRoot, { recursive: true, force: true });
|
|
8439
8841
|
rmSync9(zip, { force: true });
|
|
8440
8842
|
}
|
|
8441
|
-
if (!
|
|
8843
|
+
if (!existsSync17(join19(stageRoot, "bin", "nvcc.exe"))) {
|
|
8442
8844
|
throw new Error("Assembled CUDA toolkit is missing bin/nvcc.exe \u2014 the component set may have changed.");
|
|
8443
8845
|
}
|
|
8444
8846
|
rmSync9(root, { recursive: true, force: true });
|
|
8445
|
-
mkdirSync11(
|
|
8847
|
+
mkdirSync11(join19(dataDir, "cuda"), { recursive: true });
|
|
8446
8848
|
cpSync2(stageRoot, root, { recursive: true, force: true });
|
|
8447
8849
|
onProgress({ phase: "assembling", message: `CUDA ${version2} ready.` });
|
|
8448
8850
|
return { root, binDir, version: version2 };
|
|
@@ -8552,6 +8954,9 @@ function registerApi(app2, d) {
|
|
|
8552
8954
|
version: d.version,
|
|
8553
8955
|
engine,
|
|
8554
8956
|
model,
|
|
8957
|
+
// Last model the user loaded (config-tracked) — lets `turbollm launch` auto-load the
|
|
8958
|
+
// true last-used model instead of guessing the first library entry (F-034).
|
|
8959
|
+
lastLoaded: d.store.snapshot().lastLoaded,
|
|
8555
8960
|
engineStats,
|
|
8556
8961
|
liveGeneration,
|
|
8557
8962
|
// Auto-tune runner state (spec 09 §1): real progress while a sweep runs, then
|
|
@@ -8570,7 +8975,7 @@ function registerApi(app2, d) {
|
|
|
8570
8975
|
let installedVersion = null;
|
|
8571
8976
|
if (gatePath) {
|
|
8572
8977
|
try {
|
|
8573
|
-
const src =
|
|
8978
|
+
const src = readFileSync9(join20(gatePath, "__init__.py"), "utf-8");
|
|
8574
8979
|
const m = src.match(/^GATE_VERSION\s*=\s*(\d+)/m);
|
|
8575
8980
|
if (m) installedVersion = Number(m[1]);
|
|
8576
8981
|
} catch {
|
|
@@ -8592,7 +8997,7 @@ function registerApi(app2, d) {
|
|
|
8592
8997
|
const sys = getSysInfo();
|
|
8593
8998
|
const vendor = primaryVendor(sys);
|
|
8594
8999
|
const recommended = recommendBackendId(vendor, sys.gpus.length > 0);
|
|
8595
|
-
const root =
|
|
9000
|
+
const root = join20(d.store.dir(), "engines");
|
|
8596
9001
|
const active = d.registry.active();
|
|
8597
9002
|
const regEngines = d.registry.list().engines;
|
|
8598
9003
|
const backendOf = (p) => p.match(/llama\.cpp-[^/\\]+-(cuda|rocm|sycl|vulkan|metal|cpu)/i)?.[1]?.toLowerCase();
|
|
@@ -8613,8 +9018,8 @@ function registerApi(app2, d) {
|
|
|
8613
9018
|
engineId: eng?.id ?? ""
|
|
8614
9019
|
};
|
|
8615
9020
|
});
|
|
8616
|
-
const mlxVenvPy =
|
|
8617
|
-
const mlxInstalledOnDisk =
|
|
9021
|
+
const mlxVenvPy = join20(root, "mlx", "venv", process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
|
|
9022
|
+
const mlxInstalledOnDisk = existsSync18(mlxVenvPy);
|
|
8618
9023
|
const mlxEngine = regEngines.find((e) => e.kind === "mlx");
|
|
8619
9024
|
const mlx = {
|
|
8620
9025
|
supported: process.platform === "darwin",
|
|
@@ -8629,7 +9034,7 @@ function registerApi(app2, d) {
|
|
|
8629
9034
|
const b = await body2(c);
|
|
8630
9035
|
const def = availableBackends().find((x) => x.id === b.backend);
|
|
8631
9036
|
if (!def) return err2(c, 400, "invalid_config_value", "Unknown backend for this platform.");
|
|
8632
|
-
const root =
|
|
9037
|
+
const root = join20(d.store.dir(), "engines");
|
|
8633
9038
|
const existing = installedBackendBuild(root, def.id);
|
|
8634
9039
|
if (existing) {
|
|
8635
9040
|
return c.json({ accepted: false, alreadyInstalled: true, build: existing.tag });
|
|
@@ -8673,7 +9078,7 @@ function registerApi(app2, d) {
|
|
|
8673
9078
|
app2.post("/api/v1/engines/backends/:id/update", async (c) => {
|
|
8674
9079
|
const def = availableBackends().find((x) => x.id === c.req.param("id"));
|
|
8675
9080
|
if (!def) return err2(c, 400, "invalid_config_value", "Unknown backend for this platform.");
|
|
8676
|
-
const root =
|
|
9081
|
+
const root = join20(d.store.dir(), "engines");
|
|
8677
9082
|
const oldBin = installedBackendBuild(root, def.id)?.bin ?? null;
|
|
8678
9083
|
if (!oldBin) return err2(c, 409, "not_installed", "Backend is not installed \u2014 download it first.");
|
|
8679
9084
|
{
|
|
@@ -8732,7 +9137,7 @@ function registerApi(app2, d) {
|
|
|
8732
9137
|
app2.post("/api/v1/engines/backends/:id/enable", async (c) => {
|
|
8733
9138
|
const def = availableBackends().find((x) => x.id === c.req.param("id"));
|
|
8734
9139
|
if (!def) return err2(c, 400, "invalid_config_value", "Unknown backend for this platform.");
|
|
8735
|
-
const root =
|
|
9140
|
+
const root = join20(d.store.dir(), "engines");
|
|
8736
9141
|
const build2 = installedBackendBuild(root, def.id);
|
|
8737
9142
|
if (!build2) return err2(c, 409, "not_installed", "Backend is not installed on disk \u2014 download it first.");
|
|
8738
9143
|
let eng = d.registry.list().engines.find((e) => e.binPath === build2.bin);
|
|
@@ -8743,7 +9148,7 @@ function registerApi(app2, d) {
|
|
|
8743
9148
|
app2.delete("/api/v1/engines/backends/:id", async (c) => {
|
|
8744
9149
|
const def = availableBackends().find((x) => x.id === c.req.param("id"));
|
|
8745
9150
|
if (!def) return err2(c, 400, "invalid_config_value", "Unknown backend for this platform.");
|
|
8746
|
-
const root =
|
|
9151
|
+
const root = join20(d.store.dir(), "engines");
|
|
8747
9152
|
const backendDirRe = new RegExp(`[\\\\/]engines[\\\\/]llama\\.cpp-[^\\\\/]+-${def.id}[\\\\/]`);
|
|
8748
9153
|
const engs = d.registry.list().engines.filter((e) => backendDirRe.test(e.binPath));
|
|
8749
9154
|
const activeId = d.registry.active()?.id;
|
|
@@ -8765,7 +9170,7 @@ function registerApi(app2, d) {
|
|
|
8765
9170
|
const busy = engineWorkBusy(d);
|
|
8766
9171
|
if (busy) return err2(c, 409, "engine_already_running", busy);
|
|
8767
9172
|
}
|
|
8768
|
-
const root =
|
|
9173
|
+
const root = join20(d.store.dir(), "engines");
|
|
8769
9174
|
const upgrade = c.req.query("update") === "1";
|
|
8770
9175
|
void (async () => {
|
|
8771
9176
|
try {
|
|
@@ -8782,29 +9187,29 @@ function registerApi(app2, d) {
|
|
|
8782
9187
|
});
|
|
8783
9188
|
app2.get("/api/v1/engines/catalog", (c) => {
|
|
8784
9189
|
const regEngines = d.registry.list().engines;
|
|
8785
|
-
const enginesRoot =
|
|
9190
|
+
const enginesRoot = join20(d.store.dir(), "engines");
|
|
8786
9191
|
const items = catalogForPlatform().map((e) => {
|
|
8787
9192
|
let installed;
|
|
8788
9193
|
let enabled;
|
|
8789
9194
|
if (e.provision === "pip") {
|
|
8790
9195
|
const venvSubdir = e.id === "mlx" ? "mlx" : "vllm";
|
|
8791
|
-
const pyPath =
|
|
9196
|
+
const pyPath = join20(
|
|
8792
9197
|
enginesRoot,
|
|
8793
9198
|
venvSubdir,
|
|
8794
9199
|
"venv",
|
|
8795
9200
|
process.platform === "win32" ? "Scripts/python.exe" : "bin/python"
|
|
8796
9201
|
);
|
|
8797
|
-
installed =
|
|
9202
|
+
installed = existsSync18(pyPath);
|
|
8798
9203
|
enabled = regEngines.some((x) => x.kind === e.kind);
|
|
8799
9204
|
} else if (e.id === "turboquant") {
|
|
8800
|
-
const tqDir =
|
|
8801
|
-
installed =
|
|
9205
|
+
const tqDir = join20(enginesRoot, "turboquant");
|
|
9206
|
+
installed = existsSync18(tqDir);
|
|
8802
9207
|
enabled = regEngines.some((x) => /[\\/]engines[\\/]turboquant[\\/]/.test(x.binPath));
|
|
8803
9208
|
} else if (e.id === "koboldcpp") {
|
|
8804
|
-
installed =
|
|
9209
|
+
installed = existsSync18(koboldcppBinPath(enginesRoot));
|
|
8805
9210
|
enabled = regEngines.some((x) => x.kind === "koboldcpp");
|
|
8806
9211
|
} else if (e.id === "llamafile") {
|
|
8807
|
-
installed =
|
|
9212
|
+
installed = existsSync18(llamafileBinPath(enginesRoot));
|
|
8808
9213
|
enabled = regEngines.some((x) => x.kind === "llamafile");
|
|
8809
9214
|
}
|
|
8810
9215
|
const srcEng = regEngines.find((x) => sameRepo(x.sourceRepo, e.homepage));
|
|
@@ -8854,9 +9259,9 @@ function registerApi(app2, d) {
|
|
|
8854
9259
|
const busy = engineWorkBusy(d);
|
|
8855
9260
|
if (busy) return err2(c, 409, "engine_already_running", busy);
|
|
8856
9261
|
const name = (b.name ?? "").trim() || void 0;
|
|
8857
|
-
const enginesRoot =
|
|
9262
|
+
const enginesRoot = join20(d.store.dir(), "engines");
|
|
8858
9263
|
const toolchainDirs = d.store.snapshot().build.toolchainDirs;
|
|
8859
|
-
const buildRoot =
|
|
9264
|
+
const buildRoot = join20(enginesRoot, "build", buildDirName(repoUrl, branch));
|
|
8860
9265
|
const ac = new AbortController();
|
|
8861
9266
|
buildAbort = ac;
|
|
8862
9267
|
d.build.start(name ?? repoUrl);
|
|
@@ -8959,7 +9364,7 @@ function registerApi(app2, d) {
|
|
|
8959
9364
|
const busy = engineWorkBusy(d);
|
|
8960
9365
|
if (busy) return err2(c, 409, "engine_already_running", busy);
|
|
8961
9366
|
}
|
|
8962
|
-
const root =
|
|
9367
|
+
const root = join20(d.store.dir(), "engines");
|
|
8963
9368
|
const upgrade = c.req.query("update") === "1";
|
|
8964
9369
|
void (async () => {
|
|
8965
9370
|
try {
|
|
@@ -8984,14 +9389,14 @@ function registerApi(app2, d) {
|
|
|
8984
9389
|
const busy = engineWorkBusy(d);
|
|
8985
9390
|
if (busy) return err2(c, 409, "engine_already_running", busy);
|
|
8986
9391
|
}
|
|
8987
|
-
const root =
|
|
9392
|
+
const root = join20(d.store.dir(), "engines");
|
|
8988
9393
|
const upgrade = c.req.query("update") === "1";
|
|
8989
9394
|
void (async () => {
|
|
8990
9395
|
try {
|
|
8991
9396
|
d.provision.start("turboquant");
|
|
8992
9397
|
if (upgrade) {
|
|
8993
|
-
const tqDir =
|
|
8994
|
-
if (
|
|
9398
|
+
const tqDir = join20(root, "turboquant");
|
|
9399
|
+
if (existsSync18(tqDir)) rmSync10(tqDir, { recursive: true, force: true });
|
|
8995
9400
|
}
|
|
8996
9401
|
const bin = await provisionTurboquant(
|
|
8997
9402
|
root,
|
|
@@ -9019,7 +9424,7 @@ function registerApi(app2, d) {
|
|
|
9019
9424
|
const busy = engineWorkBusy(d);
|
|
9020
9425
|
if (busy) return err2(c, 409, "engine_already_running", busy);
|
|
9021
9426
|
}
|
|
9022
|
-
const root =
|
|
9427
|
+
const root = join20(d.store.dir(), "engines");
|
|
9023
9428
|
const upgrade = c.req.query("update") === "1";
|
|
9024
9429
|
const hasNvidia = primaryVendor(getSysInfo()) === "nvidia";
|
|
9025
9430
|
void (async () => {
|
|
@@ -9027,7 +9432,7 @@ function registerApi(app2, d) {
|
|
|
9027
9432
|
d.provision.start("koboldcpp");
|
|
9028
9433
|
if (upgrade) {
|
|
9029
9434
|
const dir = koboldcppDir(root);
|
|
9030
|
-
if (
|
|
9435
|
+
if (existsSync18(dir)) rmSync10(dir, { recursive: true, force: true });
|
|
9031
9436
|
}
|
|
9032
9437
|
const rt = await ensureKoboldcpp(root, hasNvidia, (p) => d.provision.progress(p.phase, p.pct, p.part, p.parts));
|
|
9033
9438
|
const eng = d.registry.addKoboldcpp(`KoboldCpp (${rt.version})`, rt.binPath, rt.version);
|
|
@@ -9050,14 +9455,14 @@ function registerApi(app2, d) {
|
|
|
9050
9455
|
const busy = engineWorkBusy(d);
|
|
9051
9456
|
if (busy) return err2(c, 409, "engine_already_running", busy);
|
|
9052
9457
|
}
|
|
9053
|
-
const root =
|
|
9458
|
+
const root = join20(d.store.dir(), "engines");
|
|
9054
9459
|
const upgrade = c.req.query("update") === "1";
|
|
9055
9460
|
void (async () => {
|
|
9056
9461
|
try {
|
|
9057
9462
|
d.provision.start("llamafile");
|
|
9058
9463
|
if (upgrade) {
|
|
9059
9464
|
const dir = llamafileDir(root);
|
|
9060
|
-
if (
|
|
9465
|
+
if (existsSync18(dir)) rmSync10(dir, { recursive: true, force: true });
|
|
9061
9466
|
}
|
|
9062
9467
|
const rt = await ensureLlamafile(root, (p) => d.provision.progress(p.phase, p.pct, p.part, p.parts));
|
|
9063
9468
|
const eng = d.registry.addLlamafile(`llamafile (${rt.version})`, rt.binPath, rt.version);
|
|
@@ -9137,9 +9542,9 @@ function registerApi(app2, d) {
|
|
|
9137
9542
|
const eng = d.registry.get(id);
|
|
9138
9543
|
d.registry.remove(id);
|
|
9139
9544
|
if (purge && eng) {
|
|
9140
|
-
const enginesRoot =
|
|
9545
|
+
const enginesRoot = join20(d.store.dir(), "engines");
|
|
9141
9546
|
const purgeDir = engineInstallDir(eng, enginesRoot);
|
|
9142
|
-
if (purgeDir &&
|
|
9547
|
+
if (purgeDir && existsSync18(purgeDir)) {
|
|
9143
9548
|
rmSync10(purgeDir, { recursive: true, force: true });
|
|
9144
9549
|
}
|
|
9145
9550
|
}
|
|
@@ -9224,12 +9629,12 @@ function registerApi(app2, d) {
|
|
|
9224
9629
|
let isDir = d2.isDirectory();
|
|
9225
9630
|
if (d2.isSymbolicLink()) {
|
|
9226
9631
|
try {
|
|
9227
|
-
isDir = statSync5(
|
|
9632
|
+
isDir = statSync5(join20(real, d2.name)).isDirectory();
|
|
9228
9633
|
} catch {
|
|
9229
9634
|
isDir = false;
|
|
9230
9635
|
}
|
|
9231
9636
|
}
|
|
9232
|
-
return { name: d2.name, path:
|
|
9637
|
+
return { name: d2.name, path: join20(real, d2.name), isDir };
|
|
9233
9638
|
}).sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1);
|
|
9234
9639
|
} catch {
|
|
9235
9640
|
return err2(c, 400, "fs_read_failed", "Could not read that folder (permission denied or not a directory).");
|
|
@@ -9329,22 +9734,22 @@ function registerApi(app2, d) {
|
|
|
9329
9734
|
const raw = (b.path ?? "").trim();
|
|
9330
9735
|
if (!raw) return err2(c, 400, "invalid_config_value", "Enter the path to your ComfyUI folder.");
|
|
9331
9736
|
const root = resolve(raw);
|
|
9332
|
-
if (!
|
|
9737
|
+
if (!existsSync18(root) || !statSync5(root).isDirectory()) {
|
|
9333
9738
|
return err2(c, 400, "invalid_config_value", "That folder does not exist.");
|
|
9334
9739
|
}
|
|
9335
9740
|
let customNodes;
|
|
9336
9741
|
if (basename3(root).toLowerCase() === "custom_nodes") customNodes = root;
|
|
9337
|
-
else if (
|
|
9742
|
+
else if (existsSync18(join20(root, "custom_nodes"))) customNodes = join20(root, "custom_nodes");
|
|
9338
9743
|
else {
|
|
9339
9744
|
return err2(c, 400, "invalid_config_value", "No 'custom_nodes' folder here. Point me at your ComfyUI folder or its custom_nodes folder.");
|
|
9340
9745
|
}
|
|
9341
9746
|
const reqUrl = new URL(c.req.url);
|
|
9342
9747
|
const port2 = reqUrl.port || (reqUrl.protocol === "https:" ? "443" : "80");
|
|
9343
9748
|
const base2 = `http://127.0.0.1:${port2}`;
|
|
9344
|
-
const gateDir =
|
|
9749
|
+
const gateDir = join20(customNodes, "turbollm_gate");
|
|
9345
9750
|
try {
|
|
9346
9751
|
mkdirSync12(gateDir, { recursive: true });
|
|
9347
|
-
|
|
9752
|
+
writeFileSync9(join20(gateDir, "__init__.py"), gateNodeSource(base2));
|
|
9348
9753
|
} catch (e) {
|
|
9349
9754
|
return err2(c, 500, "fs_write_failed", `Could not write the gate node: ${e instanceof Error ? e.message : e}`);
|
|
9350
9755
|
}
|
|
@@ -9355,7 +9760,7 @@ function registerApi(app2, d) {
|
|
|
9355
9760
|
});
|
|
9356
9761
|
app2.post("/api/v1/comfyui/uninstall", (c) => {
|
|
9357
9762
|
const dir = d.store.snapshot().comfyui.gatePath;
|
|
9358
|
-
if (dir &&
|
|
9763
|
+
if (dir && existsSync18(dir)) {
|
|
9359
9764
|
try {
|
|
9360
9765
|
rmSync10(dir, { recursive: true, force: true });
|
|
9361
9766
|
} catch (e) {
|
|
@@ -9390,8 +9795,8 @@ function registerApi(app2, d) {
|
|
|
9390
9795
|
});
|
|
9391
9796
|
while (!aborted) {
|
|
9392
9797
|
const path = d.manager.logPath();
|
|
9393
|
-
if (path &&
|
|
9394
|
-
const lines =
|
|
9798
|
+
if (path && existsSync18(path)) {
|
|
9799
|
+
const lines = readFileSync9(path, "utf8").split("\n");
|
|
9395
9800
|
for (; sent < lines.length - 1; sent++) {
|
|
9396
9801
|
await stream.writeSSE({ event: "line", data: JSON.stringify({ line: lines[sent].replace(/\r$/, "") }) });
|
|
9397
9802
|
}
|
|
@@ -9687,7 +10092,7 @@ function registerApi(app2, d) {
|
|
|
9687
10092
|
const b = await body2(c);
|
|
9688
10093
|
const dir = (b.dir ?? "").trim();
|
|
9689
10094
|
if (!dir || !/^([a-zA-Z]:[\\/]|[\\/])/.test(dir)) return err2(c, 400, "invalid_config_value", "Path must be absolute.");
|
|
9690
|
-
if (!
|
|
10095
|
+
if (!existsSync18(dir)) return err2(c, 400, "invalid_config_value", "That folder does not exist.");
|
|
9691
10096
|
try {
|
|
9692
10097
|
d.store.update((cfg2) => {
|
|
9693
10098
|
if (!cfg2.modelDirs.includes(dir)) cfg2.modelDirs.push(dir);
|
|
@@ -9865,7 +10270,9 @@ function overlayModel(e, d, lastTpsMap) {
|
|
|
9865
10270
|
const loadedKey = ms.state === "running" ? ms.model?.key : void 0;
|
|
9866
10271
|
const snap = d.store.snapshot();
|
|
9867
10272
|
const profiles = snap.modelProfiles;
|
|
9868
|
-
const
|
|
10273
|
+
const loadedInPrimary = loadedKey === e.path || loadedKey === e.key;
|
|
10274
|
+
const poolKeys = d.modelRouter.loadedModelKeys();
|
|
10275
|
+
const loaded = loadedInPrimary || poolKeys.has(e.key) || poolKeys.has(e.path);
|
|
9869
10276
|
const lastTps = (lastTpsMap ?? d.db.lastGenTpsByModel()).get(e.key) ?? null;
|
|
9870
10277
|
const liveTps = loaded && lastTps !== null ? lastTps : null;
|
|
9871
10278
|
const benchTps = snap.benchResults[e.key]?.tps ?? null;
|
|
@@ -10017,15 +10424,15 @@ function engineInstallDir(eng, enginesRoot) {
|
|
|
10017
10424
|
return n.startsWith(norm + "/") || n.startsWith(norm + "\\");
|
|
10018
10425
|
};
|
|
10019
10426
|
if (eng.kind === "mlx") {
|
|
10020
|
-
const d =
|
|
10427
|
+
const d = join20(enginesRoot, "mlx", "venv");
|
|
10021
10428
|
return inside(d) ? d : null;
|
|
10022
10429
|
}
|
|
10023
10430
|
if (eng.kind === "vllm") {
|
|
10024
|
-
const d =
|
|
10431
|
+
const d = join20(enginesRoot, "vllm", "venv");
|
|
10025
10432
|
return inside(d) ? d : null;
|
|
10026
10433
|
}
|
|
10027
10434
|
if (/[\\/]engines[\\/]turboquant[\\/]/.test(eng.binPath)) {
|
|
10028
|
-
const d =
|
|
10435
|
+
const d = join20(enginesRoot, "turboquant");
|
|
10029
10436
|
return inside(d) ? d : null;
|
|
10030
10437
|
}
|
|
10031
10438
|
if (eng.kind === "koboldcpp") {
|
|
@@ -10040,7 +10447,7 @@ function engineInstallDir(eng, enginesRoot) {
|
|
|
10040
10447
|
if (srcDir) return inside(srcDir) ? srcDir : null;
|
|
10041
10448
|
const llamaMatch = eng.binPath.replace(/\\/g, "/").match(/\/(llama\.cpp-[^/]+)\//i);
|
|
10042
10449
|
if (llamaMatch) {
|
|
10043
|
-
const d =
|
|
10450
|
+
const d = join20(enginesRoot, llamaMatch[1]);
|
|
10044
10451
|
return inside(d) ? d : null;
|
|
10045
10452
|
}
|
|
10046
10453
|
return null;
|
|
@@ -10056,9 +10463,9 @@ function cleanModelName(p) {
|
|
|
10056
10463
|
return basename3(p).replace(/\.gguf$/i, "");
|
|
10057
10464
|
}
|
|
10058
10465
|
function readTail2(path, n) {
|
|
10059
|
-
if (!path || !
|
|
10466
|
+
if (!path || !existsSync18(path)) return [];
|
|
10060
10467
|
try {
|
|
10061
|
-
const lines =
|
|
10468
|
+
const lines = readFileSync9(path, "utf8").replace(/[\r\n]+$/, "").split("\n").map((l) => l.replace(/\r$/, ""));
|
|
10062
10469
|
return lines.length > n ? lines.slice(-n) : lines;
|
|
10063
10470
|
} catch {
|
|
10064
10471
|
return [];
|
|
@@ -10375,7 +10782,8 @@ async function* streamToAnthropic(oaiStream, modelName, msgId, onUsage, onLive)
|
|
|
10375
10782
|
if (usage?.completion_tokens) outputTokens = usage.completion_tokens;
|
|
10376
10783
|
if (usage?.prompt_tokens) inputTokens = usage.prompt_tokens;
|
|
10377
10784
|
const timings = chunk.timings;
|
|
10378
|
-
|
|
10785
|
+
const cacheN = timings?.cache_n ?? timings?.prompt_n_reuse;
|
|
10786
|
+
if (cacheN != null) cacheReadTokens = cacheN;
|
|
10379
10787
|
const choices = chunk.choices;
|
|
10380
10788
|
if (!choices?.length) continue;
|
|
10381
10789
|
const choice = choices[0];
|
|
@@ -10749,7 +11157,7 @@ async function recordOpenAiStreamUsage(d, body3) {
|
|
|
10749
11157
|
|
|
10750
11158
|
// src/server.ts
|
|
10751
11159
|
setGlobalDispatcher(new Agent({ keepAliveMaxTimeout: 6e4, connections: 10 }));
|
|
10752
|
-
var WEB_ROOT =
|
|
11160
|
+
var WEB_ROOT = join21(dirname8(fileURLToPath(import.meta.url)), "webdist");
|
|
10753
11161
|
function createApp(d) {
|
|
10754
11162
|
const app2 = new Hono();
|
|
10755
11163
|
app2.use(
|
|
@@ -10774,12 +11182,12 @@ function createApp(d) {
|
|
|
10774
11182
|
if (path.startsWith("api/") || path.startsWith("v1/")) {
|
|
10775
11183
|
return c.json({ error: { code: "not_found", message: "Unknown endpoint." } }, 404);
|
|
10776
11184
|
}
|
|
10777
|
-
let file = normalize2(
|
|
10778
|
-
if (!file.startsWith(WEB_ROOT) || !
|
|
10779
|
-
file =
|
|
11185
|
+
let file = normalize2(join21(WEB_ROOT, path || "index.html"));
|
|
11186
|
+
if (!file.startsWith(WEB_ROOT) || !existsSync19(file) || statSync6(file).isDirectory()) {
|
|
11187
|
+
file = join21(WEB_ROOT, "index.html");
|
|
10780
11188
|
}
|
|
10781
|
-
if (!
|
|
10782
|
-
return new Response(
|
|
11189
|
+
if (!existsSync19(file)) return c.text("web ui not built \u2014 run `npm run build:web`", 500);
|
|
11190
|
+
return new Response(readFileSync10(file), { status: 200, headers: { "Content-Type": contentType(file) } });
|
|
10783
11191
|
});
|
|
10784
11192
|
return app2;
|
|
10785
11193
|
}
|
|
@@ -10806,8 +11214,8 @@ function contentType(file) {
|
|
|
10806
11214
|
// src/cli.ts
|
|
10807
11215
|
var version = "0.1.1";
|
|
10808
11216
|
try {
|
|
10809
|
-
const pkgPath =
|
|
10810
|
-
version = JSON.parse(
|
|
11217
|
+
const pkgPath = join22(dirname9(fileURLToPath2(import.meta.url)), "..", "package.json");
|
|
11218
|
+
version = JSON.parse(readFileSync11(pkgPath, "utf8")).version ?? version;
|
|
10811
11219
|
} catch {
|
|
10812
11220
|
}
|
|
10813
11221
|
var nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
@@ -10835,8 +11243,11 @@ function argValue(name, fallback) {
|
|
|
10835
11243
|
if (argv[0] === "launch") {
|
|
10836
11244
|
const target = argv[1] ?? "";
|
|
10837
11245
|
const port2 = Number(argValue("--port", "")) || 6996;
|
|
10838
|
-
const
|
|
10839
|
-
const
|
|
11246
|
+
const modelKey = argValue("--model", "") || void 0;
|
|
11247
|
+
const passthrough = argv.slice(2).filter(
|
|
11248
|
+
(a, i, arr) => a !== "--port" && arr[i - 1] !== "--port" && a !== "--model" && arr[i - 1] !== "--model"
|
|
11249
|
+
);
|
|
11250
|
+
const code = await launchCli(target, port2, passthrough, void 0, modelKey);
|
|
10840
11251
|
process.exit(code);
|
|
10841
11252
|
}
|
|
10842
11253
|
if (hasFlag("--help", "-h")) {
|
|
@@ -10851,13 +11262,15 @@ Usage:
|
|
|
10851
11262
|
|
|
10852
11263
|
Commands:
|
|
10853
11264
|
launch claude Launch Claude Code wired to TurboLLM
|
|
10854
|
-
|
|
11265
|
+
Auto-loads the last-used model if none is loaded.
|
|
11266
|
+
launch claude --model <key> Load a specific model by key or name, then launch
|
|
10855
11267
|
|
|
10856
11268
|
Options:
|
|
10857
11269
|
--port <n> Port to listen on / connect to (default: 6996)
|
|
10858
11270
|
--addr <h:p> Full host:port override (e.g. 0.0.0.0:6996)
|
|
10859
11271
|
--no-open Do not open a browser window on startup
|
|
10860
11272
|
--config <f> Path to a custom config file
|
|
11273
|
+
--stop Stop a running TurboLLM daemon and exit
|
|
10861
11274
|
--help, -h Show this help message
|
|
10862
11275
|
|
|
10863
11276
|
Examples:
|
|
@@ -10865,7 +11278,9 @@ Examples:
|
|
|
10865
11278
|
turbollm --port 9000 # listen on port 9000
|
|
10866
11279
|
turbollm --no-open # start without opening a browser
|
|
10867
11280
|
turbollm --addr 0.0.0.0:6996 # bind to all interfaces (LAN sharing)
|
|
11281
|
+
turbollm --stop # stop the running daemon
|
|
10868
11282
|
turbollm launch claude # open Claude Code on your loaded model
|
|
11283
|
+
turbollm launch claude --model qwen3-8b # load qwen3-8b, then launch
|
|
10869
11284
|
|
|
10870
11285
|
`
|
|
10871
11286
|
);
|
|
@@ -10876,6 +11291,11 @@ var store = ConfigStore.load(argValue("--config", defaultConfigPath()));
|
|
|
10876
11291
|
if (store.brokenBackup()) {
|
|
10877
11292
|
console.warn(`config was reset; previous file backed up at ${store.brokenBackup()}`);
|
|
10878
11293
|
}
|
|
11294
|
+
if (hasFlag("--stop")) {
|
|
11295
|
+
const result = await stopDaemon(store.dir());
|
|
11296
|
+
process.stdout.write(result.message + "\n");
|
|
11297
|
+
process.exit(0);
|
|
11298
|
+
}
|
|
10879
11299
|
var reaped = await reapStaleEngines(store.dir()).catch(() => 0);
|
|
10880
11300
|
if (reaped > 0) console.log(`reaped ${reaped} orphaned engine process(es) from a previous run`);
|
|
10881
11301
|
var registry = new Registry(store);
|
|
@@ -10884,7 +11304,7 @@ if (pruned > 0) console.log(`pruned ${pruned} dangling engine build(s)`);
|
|
|
10884
11304
|
var provision = new ProvisionState();
|
|
10885
11305
|
var build = new BuildState();
|
|
10886
11306
|
var updates = new UpdateChecker();
|
|
10887
|
-
var enginesDir =
|
|
11307
|
+
var enginesDir = join22(store.dir(), "engines");
|
|
10888
11308
|
void seedDefaultEngines(registry, enginesDir, provision).then(() => registry.ensureProbed());
|
|
10889
11309
|
var manager = new Manager(store);
|
|
10890
11310
|
var scanner = new Scanner(store);
|
|
@@ -10980,6 +11400,10 @@ function listen(attempt = 0) {
|
|
|
10980
11400
|
}
|
|
10981
11401
|
process.stdout.write(`TurboLLM ${version} listening on http://${displayHost}:${info.port}
|
|
10982
11402
|
`);
|
|
11403
|
+
try {
|
|
11404
|
+
writePidfile(store.dir(), process.pid, info.port);
|
|
11405
|
+
} catch {
|
|
11406
|
+
}
|
|
10983
11407
|
});
|
|
10984
11408
|
s.on?.(
|
|
10985
11409
|
"error",
|
|
@@ -11031,7 +11455,7 @@ var restarting = false;
|
|
|
11031
11455
|
function spawnReplacement() {
|
|
11032
11456
|
let out = "ignore";
|
|
11033
11457
|
try {
|
|
11034
|
-
out = openSync3(
|
|
11458
|
+
out = openSync3(join22(store.dir(), "restart.log"), "a");
|
|
11035
11459
|
} catch {
|
|
11036
11460
|
out = "ignore";
|
|
11037
11461
|
}
|
|
@@ -11127,6 +11551,10 @@ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
|
11127
11551
|
if (shuttingDown) return;
|
|
11128
11552
|
shuttingDown = true;
|
|
11129
11553
|
console.log("shutting down");
|
|
11554
|
+
try {
|
|
11555
|
+
removePidfile(store.dir());
|
|
11556
|
+
} catch {
|
|
11557
|
+
}
|
|
11130
11558
|
updateScheduler.stop();
|
|
11131
11559
|
comfy.stop();
|
|
11132
11560
|
toolRegistry.disconnectAll();
|
|
@@ -11142,4 +11570,8 @@ process.on("exit", () => {
|
|
|
11142
11570
|
killTrackedEnginesSync(store.dir());
|
|
11143
11571
|
} catch {
|
|
11144
11572
|
}
|
|
11573
|
+
try {
|
|
11574
|
+
removePidfile(store.dir());
|
|
11575
|
+
} catch {
|
|
11576
|
+
}
|
|
11145
11577
|
});
|