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.
Files changed (27) hide show
  1. package/README.md +178 -211
  2. package/dist/cli.js +664 -232
  3. package/dist/webdist/assets/{ChatScreen-DY2flMe9.js → ChatScreen-DwW1RFdb.js} +10 -10
  4. package/dist/webdist/assets/{CustomizeScreen-Cl6H68Sy.js → CustomizeScreen-DSE61GGl.js} +1 -1
  5. package/dist/webdist/assets/{DeveloperScreen-zC1nePXh.js → DeveloperScreen-BYGLUEKp.js} +1 -1
  6. package/dist/webdist/assets/{EnginesScreen-CrsziC3B.js → EnginesScreen-D1fV7OUs.js} +1 -1
  7. package/dist/webdist/assets/{ModelDetailDialog-BOvumdHI.js → ModelDetailDialog-DOWC_r7x.js} +1 -1
  8. package/dist/webdist/assets/{ModelDirs-Bj-XrX0c.js → ModelDirs-BnEgYeD7.js} +1 -1
  9. package/dist/webdist/assets/{ModelsScreen-BTir-kDY.js → ModelsScreen-CsIWlTix.js} +1 -1
  10. package/dist/webdist/assets/{SettingsScreen-B696egCa.js → SettingsScreen-BsoeEYxo.js} +1 -1
  11. package/dist/webdist/assets/check-BEcwngnz.js +1 -0
  12. package/dist/webdist/assets/{circle-x-CsuAddI_.js → circle-x-BMU1-bqZ.js} +1 -1
  13. package/dist/webdist/assets/{common-DIXPIrUd.js → common-CsaPxOo6.js} +1 -1
  14. package/dist/webdist/assets/{copy-button-BtQZA6P8.js → copy-button-VN55eluX.js} +1 -1
  15. package/dist/webdist/assets/{dialog-Dpyml8ZT.js → dialog-oym6ZR4Z.js} +1 -1
  16. package/dist/webdist/assets/{index-sjgdofsd.js → index-DdsLEQg8.js} +2 -2
  17. package/dist/webdist/assets/{pencil-BuQwoNyj.js → pencil-BUeTGZ2m.js} +1 -1
  18. package/dist/webdist/assets/{personas-CQapYHeK.js → personas-B44JP8XF.js} +1 -1
  19. package/dist/webdist/assets/{plus-B1w15FoZ.js → plus-B4N6E4A9.js} +1 -1
  20. package/dist/webdist/assets/{refresh-cw-BoAcjLtt.js → refresh-cw-35eljdhP.js} +1 -1
  21. package/dist/webdist/assets/{save-DE1APWTG.js → save-AXon2pXP.js} +1 -1
  22. package/dist/webdist/assets/{skeleton-Dvq8tYzC.js → skeleton-BqBFt4sB.js} +1 -1
  23. package/dist/webdist/assets/{sparkles-C54kYWeQ.js → sparkles-C1EIsU4T.js} +1 -1
  24. package/dist/webdist/assets/{trash-2-DIMVgidH.js → trash-2-BOZ6n1d2.js} +1 -1
  25. package/dist/webdist/index.html +1 -1
  26. package/package.json +1 -1
  27. 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 readFileSync10 } from "fs";
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 join21 } from "path";
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 totalVramMb = gpuBudgetMb(sys, p);
3529
- if (totalVramMb === 0) return { estMb: 0, totalVramMb: 0, pct: 0, verdict: "cpu" };
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 / totalVramMb;
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 baseProfile = resolveProfile(entry, sys, saved, base2, defaults);
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
- if (!entry.moe) {
4720
- best = await this.denseSearch(entry, sys, baseProfile, caps, results);
4721
- } else {
4722
- best = await this.moeSearch(entry, sys, baseProfile, caps, results);
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
- /** Binary search over ngl to find the highest number of GPU layers that does not OOM.
4768
- * For dense models, more GPU layers = faster (monotonically), so the optimal is simply
4769
- * the maximum ngl that fits in VRAM. O(log blockCount) trials — far more precise than
4770
- * the old fixed-set sweep. CPU-only machines skip straight to ngl=0. */
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
- const hasGpu = sys.gpus.length > 0;
4773
- if (!hasGpu) {
4774
- const label = "ngl=0 (CPU-only)";
4775
- const cand = await this.measure(entry, sys, { ...base2, ngl: 0 }, caps, label, label);
4776
- results.push(cand);
4777
- this.state = { ...this.state, candidates: results };
4778
- if (cand.outcome === "ok") return { cand, profile: { ...base2, ngl: 0 } };
4779
- return null;
4780
- }
4781
- const hi0 = entry.blockCount > 0 ? entry.blockCount : 99;
4782
- let lo = 0, hi = hi0;
4783
- let best = null;
4784
- while (lo <= hi && !this.cancelled && Date.now() <= this.deadline) {
4785
- const mid = Math.floor((lo + hi) / 2);
4786
- const label = `ngl=${mid}`;
4787
- const stepPrefix = `Trial ${results.length + 1}: ${label} (range ${lo}\u2013${hi})`;
4788
- this.state = { ...this.state, step: `${stepPrefix}\u2026`, candidates: results };
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
- return best ? await this.confirmPeak(entry, sys, base2, caps, results, best, "ngl", 0) : null;
4823
+ if (bestNgl === null) return null;
4824
+ return this.benchAt(entry, sys, { ...base2, ngl: bestNgl }, caps, results, `ngl=${bestNgl}`);
4805
4825
  }
4806
- /** Binary search over nCpuMoe to find the minimum number of MoE experts kept on CPU
4807
- * that still fits in VRAM. Fewer CPU experts = more on GPU = faster; we want the
4808
- * minimum that doesn't OOM. O(log blockCount) trials. */
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 best = null;
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
- const label = `nCpuMoe=${mid}`;
4817
- const stepPrefix = `Trial ${results.length + 1}: ${label} (range ${lo}\u2013${hi})`;
4818
- this.state = { ...this.state, step: `${stepPrefix}\u2026`, candidates: results };
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 (cand.outcome === "oom" || overVram(cand.vramMb, sys)) {
4841
+ if (probe2.outcome === "oom" || overHeadroom(probe2.vramAbsMb, sys)) {
4825
4842
  lo = mid + 1;
4826
- } else if (cand.outcome === "ok" && cand.tps !== null) {
4827
- if (!best || cand.tps > (best.cand.tps ?? 0)) best = { cand, profile };
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
- return best ? await this.confirmPeak(entry, sys, base2, caps, results, best, "nCpuMoe", maxN) : null;
4835
- }
4836
- /** Spill correction (unimodal throughput). The binary search picks the config that "fits", but a
4837
- * config that overflows VRAM into shared memory passes the fit/prefill check yet is PCIe-bottlenecked
4838
- * — so t/s actually PEAKS at the no-spill edge and drops once spilling. After the search, step ONE
4839
- * toward LESS GPU (moe: +1 expert on CPU; dense: -1 GPU layer) and keep it only while it's faster:
4840
- * if the pick was spilling, this follows the curve up to the real peak; if not, it confirms the pick
4841
- * in one extra trial. */
4842
- async confirmPeak(entry, sys, base2, caps, results, best, knob, maxNCpuMoe) {
4843
- for (let guard = 0; guard < 8 && !this.cancelled && Date.now() <= this.deadline; guard++) {
4844
- const cur = knob === "nCpuMoe" ? best.cand.params.nCpuMoe : best.cand.params.ngl;
4845
- const next = knob === "nCpuMoe" ? cur + 1 : cur - 1;
4846
- if (knob === "nCpuMoe" ? next > maxNCpuMoe : next < 0) break;
4847
- const label = `${knob}=${next}`;
4848
- this.state = { ...this.state, step: `Confirming ${label} (spill check)\u2026`, candidates: results };
4849
- const profile = knob === "nCpuMoe" ? { ...base2, nCpuMoe: next } : { ...base2, ngl: next };
4850
- const cand = await this.measure(entry, sys, profile, caps, label, `Confirming ${label}`);
4851
- results.push(cand);
4852
- this.state = { ...this.state, candidates: results };
4853
- await this.settleGpu();
4854
- if (cand.outcome === "ok" && cand.tps !== null && cand.tps > (best.cand.tps ?? 0)) {
4855
- best = { cand, profile };
4856
- this.state = { ...this.state, bestTps: best.cand.tps ?? void 0 };
4857
- } else {
4858
- break;
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
- return best;
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 { tps: t.predicted_per_second, ttftMs: typeof t.prompt_ms === "number" ? t.prompt_ms : 0 };
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(5e4, Math.floor(ctx * 0.75)));
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 overVram(vramMb, sys) {
5288
- const total = sys.gpus[0]?.vramMb ?? 0;
5289
- if (!vramMb || total <= 0) return false;
5290
- return vramMb > total * 0.95;
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 launchCli(target, port2, passthrough, _spawn = spawn3) {
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 fetch(`${base2}/api/v1/status`, { signal: AbortSignal.timeout(3e3) });
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
- if (status.engine?.state !== "running" || !status.model?.name) {
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 existsSync18, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
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 join20, normalize as normalize2 } from "path";
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 existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync8, readdirSync as readdirSync7, realpathSync, rmSync as rmSync10, statSync as statSync5, writeFileSync as writeFileSync8 } from "fs";
6424
- import { basename as basename3, dirname as dirname7, join as join19, resolve, sep } from "path";
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 payload;
7412
+ let raw;
7060
7413
  try {
7061
- payload = await c.req.json();
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 (!payload.format || payload.format !== "debug" && payload.format !== "export") {
7066
- return err(c, 400, "invalid_file", 'Missing or invalid "format" field. Expected a .turbollm-chat.json file.');
7067
- }
7068
- if (!payload.messages || !Array.isArray(payload.messages)) {
7069
- return err(c, 400, "invalid_file", 'Missing "messages" array.');
7070
- }
7071
- if (typeof payload.chat_id !== "string") {
7072
- return err(c, 400, "invalid_file", 'Missing "chat_id" field.');
7073
- }
7074
- if (typeof payload.title !== "string") {
7075
- return err(c, 400, "invalid_file", 'Missing "title" field.');
7076
- }
7077
- const title = payload.title || "Imported chat";
7078
- const modelKey = typeof payload.model === "string" ? payload.model : "";
7079
- const personaId = typeof payload.persona === "string" ? payload.persona : "default";
7080
- const toolPolicy = personaId === "research" ? "force_web_search" : void 0;
7081
- const newConv = db2.createConversation({ title, modelKey, toolPolicy });
7082
- const messages = payload.messages;
7083
- for (const m of messages) {
7084
- const role = m.role;
7085
- if (role !== "user" && role !== "assistant") continue;
7086
- const content = typeof m.content === "string" ? m.content : "";
7087
- const toolCalls = Array.isArray(m.tool_calls) ? m.tool_calls : void 0;
7088
- db2.addMessage(newConv.id, role, content, {
7089
- toolCalls: toolCalls && toolCalls.length > 0 ? toolCalls : void 0
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({ id: newConv.id }, 201);
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 existsSync14, statSync as statSync4 } from "fs";
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 (!existsSync14(path)) return null;
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 join16 } from "path";
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 = join16(programFilesX86, "Microsoft Visual Studio", "Installer", "vswhere.exe");
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 existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync7, readdirSync as readdirSync5, rmSync as rmSync8, writeFileSync as writeFileSync7 } from "fs";
8075
- import { delimiter as delimiter2, dirname as dirname6, join as join17 } from "path";
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 = join17(enginesRoot, "build", buildDirName(repoUrl, branch));
8092
- return resolveServerBinary(join17(root, "build")) ?? resolveServerBinary(root);
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 join17(enginesRoot, "build", m[1]);
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 existsSync15(join17(dir, exe));
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 existsSync15(join17(dir, "nvcc.exe"));
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, join17(nvccDir, "x64")].filter((d) => {
8538
+ return [nvccDir, join18(nvccDir, "x64")].filter((d) => {
8137
8539
  try {
8138
- return existsSync15(d);
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(join17(dir, name), join17(destDir, name));
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 = join17(programFilesX86, "Microsoft Visual Studio", "Installer", "vswhere.exe");
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 && existsSync15(p) ? p : null;
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(join17(d, e.name));
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 = [join17(srcDir, "CMakeLists.txt"), join17(srcDir, "ggml", "CMakeLists.txt")];
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 (!existsSync15(file)) continue;
8672
+ if (!existsSync16(file)) continue;
8271
8673
  let text;
8272
8674
  try {
8273
- text = readFileSync7(file, "utf8");
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
- writeFileSync7(file, next);
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 = join17(req.enginesRoot, "build", buildDirName(req.repoUrl, req.branch));
8311
- const srcDir = join17(buildRoot, "src");
8312
- const buildSubdir = join17(buildRoot, "build");
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 = join17(buildRoot, "_tllm_configure.bat");
8329
- writeFileSync7(configureBat, vcvarsBatch(vcvars, ["-G", generator, "-B", buildSubdir, "-S", srcDir, ...CMAKE_CONFIGURE_ARGS]));
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 = join17(buildRoot, "_tllm_build.bat");
8333
- writeFileSync7(compileBat, vcvarsBatch(vcvars, ["--build", buildSubdir, "-j", "--target", "llama-server"]));
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 existsSync16, mkdirSync as mkdirSync11, readdirSync as readdirSync6, rmSync as rmSync9 } from "fs";
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 join18 } from "path";
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 = join18(dataDir, "cuda", version2);
8404
- const binDir = join18(root, "bin");
8405
- if (existsSync16(join18(binDir, "nvcc.exe"))) {
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 = join18(tmpdir(), `tllm-cuda-${version2}`);
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 = join18(work, `${id}.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 = join18(work, id);
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) => join18(exDir, n)).find((p) => existsSync16(join18(p, "include")) || existsSync16(join18(p, "bin")) || existsSync16(join18(p, "lib")));
8437
- const srcRoot = inner ?? join18(exDir, readdirSync6(exDir)[0] ?? "");
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 (!existsSync16(join18(stageRoot, "bin", "nvcc.exe"))) {
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(join18(dataDir, "cuda"), { recursive: true });
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 = readFileSync8(join19(gatePath, "__init__.py"), "utf-8");
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 = join19(d.store.dir(), "engines");
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 = join19(root, "mlx", "venv", process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
8617
- const mlxInstalledOnDisk = existsSync17(mlxVenvPy);
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 = join19(d.store.dir(), "engines");
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 = join19(d.store.dir(), "engines");
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 = join19(d.store.dir(), "engines");
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 = join19(d.store.dir(), "engines");
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 = join19(d.store.dir(), "engines");
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 = join19(d.store.dir(), "engines");
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 = join19(
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 = existsSync17(pyPath);
9202
+ installed = existsSync18(pyPath);
8798
9203
  enabled = regEngines.some((x) => x.kind === e.kind);
8799
9204
  } else if (e.id === "turboquant") {
8800
- const tqDir = join19(enginesRoot, "turboquant");
8801
- installed = existsSync17(tqDir);
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 = existsSync17(koboldcppBinPath(enginesRoot));
9209
+ installed = existsSync18(koboldcppBinPath(enginesRoot));
8805
9210
  enabled = regEngines.some((x) => x.kind === "koboldcpp");
8806
9211
  } else if (e.id === "llamafile") {
8807
- installed = existsSync17(llamafileBinPath(enginesRoot));
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 = join19(d.store.dir(), "engines");
9262
+ const enginesRoot = join20(d.store.dir(), "engines");
8858
9263
  const toolchainDirs = d.store.snapshot().build.toolchainDirs;
8859
- const buildRoot = join19(enginesRoot, "build", buildDirName(repoUrl, branch));
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 = join19(d.store.dir(), "engines");
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 = join19(d.store.dir(), "engines");
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 = join19(root, "turboquant");
8994
- if (existsSync17(tqDir)) rmSync10(tqDir, { recursive: true, force: true });
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 = join19(d.store.dir(), "engines");
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 (existsSync17(dir)) rmSync10(dir, { recursive: true, force: true });
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 = join19(d.store.dir(), "engines");
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 (existsSync17(dir)) rmSync10(dir, { recursive: true, force: true });
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 = join19(d.store.dir(), "engines");
9545
+ const enginesRoot = join20(d.store.dir(), "engines");
9141
9546
  const purgeDir = engineInstallDir(eng, enginesRoot);
9142
- if (purgeDir && existsSync17(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(join19(real, d2.name)).isDirectory();
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: join19(real, d2.name), isDir };
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 (!existsSync17(root) || !statSync5(root).isDirectory()) {
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 (existsSync17(join19(root, "custom_nodes"))) customNodes = join19(root, "custom_nodes");
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 = join19(customNodes, "turbollm_gate");
9749
+ const gateDir = join20(customNodes, "turbollm_gate");
9345
9750
  try {
9346
9751
  mkdirSync12(gateDir, { recursive: true });
9347
- writeFileSync8(join19(gateDir, "__init__.py"), gateNodeSource(base2));
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 && existsSync17(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 && existsSync17(path)) {
9394
- const lines = readFileSync8(path, "utf8").split("\n");
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 (!existsSync17(dir)) return err2(c, 400, "invalid_config_value", "That folder does not exist.");
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 loaded = loadedKey === e.path || loadedKey === e.key;
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 = join19(enginesRoot, "mlx", "venv");
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 = join19(enginesRoot, "vllm", "venv");
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 = join19(enginesRoot, "turboquant");
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 = join19(enginesRoot, llamaMatch[1]);
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 || !existsSync17(path)) return [];
10466
+ if (!path || !existsSync18(path)) return [];
10060
10467
  try {
10061
- const lines = readFileSync8(path, "utf8").replace(/[\r\n]+$/, "").split("\n").map((l) => l.replace(/\r$/, ""));
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
- if (timings?.prompt_n_reuse != null) cacheReadTokens = timings.prompt_n_reuse;
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 = join20(dirname8(fileURLToPath(import.meta.url)), "webdist");
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(join20(WEB_ROOT, path || "index.html"));
10778
- if (!file.startsWith(WEB_ROOT) || !existsSync18(file) || statSync6(file).isDirectory()) {
10779
- file = join20(WEB_ROOT, "index.html");
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 (!existsSync18(file)) return c.text("web ui not built \u2014 run `npm run build:web`", 500);
10782
- return new Response(readFileSync9(file), { status: 200, headers: { "Content-Type": contentType(file) } });
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 = join21(dirname9(fileURLToPath2(import.meta.url)), "..", "package.json");
10810
- version = JSON.parse(readFileSync10(pkgPath, "utf8")).version ?? version;
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 passthrough = argv.slice(2).filter((a, i, arr) => a !== "--port" && arr[i - 1] !== "--port");
10839
- const code = await launchCli(target, port2, passthrough);
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
- (daemon must be running with a model loaded)
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 = join21(store.dir(), "engines");
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(join21(store.dir(), "restart.log"), "a");
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
  });