dsh-local-models 0.3.1 → 0.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-es.md CHANGED
@@ -67,7 +67,7 @@ dsh plugin --profile web remove dsh-local-models
67
67
 
68
68
  ## Usage
69
69
 
70
- 1. **Choose GGUF…** — elige un archivo de modelo (atajos Home / Models, navegación con Up).
70
+ 1. **Choose GGUF…** — elige un archivo de modelo (botones Home / Root, atajos Models, navegación con Up). Up sube hasta `/`, así que un modelo en un disco montado (`/mnt/…`, `/media/…`) se abre sin registrar antes su carpeta.
71
71
  2. Ajusta **context**, **KV cache K / V**, **Max MTP head** (borrador fijo, 0-7; 3 es el punto óptimo ajustado — más profundidad colapsa con ctx grandes), **thinking level** + casilla **preserve thinking**, y los ajustes opcionales de **mmproj** y **MoE**.
72
72
  3. **Load model**, observa la tarjeta de estado, inspecciona la salida con **Open terminal**.
73
73
  4. **Register in dsh** — la ruta (por defecto `local-<alias>`) aparece en el selector de modelos.
package/README-hi.md CHANGED
@@ -53,7 +53,7 @@ dsh plugin --profile web remove dsh-local-models
53
53
 
54
54
  ## Usage
55
55
 
56
- 1. **Choose GGUF…** — कोई model फ़ाइल चुनें (Home / Models shortcuts, Up navigation)।
56
+ 1. **Choose GGUF…** — कोई model फ़ाइल चुनें (Home / Root buttons, Models shortcuts, Up navigation)। Up सीधे `/` तक जाता है, इसलिए mounted disk पर रखा model (`/mnt/…`, `/media/…`) बिना folder दर्ज किए खुल जाता है।
57
57
  2. **context**, **KV cache K / V**, **Max MTP head** (fixed draft, 0-7; 3 ही ट्यून किया गया sweet spot है — इससे गहरा draft बड़े ctx पर collapse हो जाता है), **thinking level** + **preserve thinking** checkbox, वैकल्पिक **mmproj** और **MoE** settings ट्यून करें।
58
58
  3. **Load model**, status card देखें, **Open terminal** से output जाँचें।
59
59
  4. **Register in dsh** — route (default `local-<alias>`) Models picker में दिखने लगता है।
package/README-pt.md CHANGED
@@ -67,7 +67,7 @@ dsh plugin --profile web remove dsh-local-models
67
67
 
68
68
  ## Usage
69
69
 
70
- 1. **Choose GGUF…** — escolha um arquivo de modelo (atalhos Home / Models, navegação Up).
70
+ 1. **Choose GGUF…** — escolha um arquivo de modelo (botões Home / Root, atalhos Models, navegação Up). O Up sobe até `/`, então um modelo em um disco montado (`/mnt/…`, `/media/…`) abre sem cadastrar a pasta antes.
71
71
  2. Ajuste **context**, **KV cache K / V**, **Max MTP head** (draft fixo, 0-7; 3 é o ponto ideal ajustado — profundidades maiores colapsam em ctx grande), **thinking level** + checkbox **preserve thinking**, e as configurações opcionais de **mmproj** e **MoE**.
72
72
  3. **Load model**, acompanhe o card de status e inspecione a saída via **Open terminal**.
73
73
  4. **Register in dsh** — a rota (padrão `local-<alias>`) aparece no seletor de modelos.
package/README-zh.md CHANGED
@@ -67,7 +67,7 @@ dsh plugin --profile web remove dsh-local-models
67
67
 
68
68
  ## Usage
69
69
 
70
- 1. **Choose GGUF…** —— 选择模型文件(Home / Models 快捷入口,Up 向上导航)。
70
+ 1. **Choose GGUF…** —— 选择模型文件(Home / Root 按钮,Models 快捷入口,Up 向上导航)。Up 可以一直走到 `/`,因此挂载磁盘上的模型(`/mnt/…`、`/media/…`)无需先登记文件夹即可打开。
71
71
  2. 调整 **context**、**KV cache K / V**、**Max MTP head**(固定草稿,0-7;3 是调校好的甜点值 —— 更深的草稿在大 ctx 下会崩溃)、**thinking level** + **preserve thinking** 复选框,以及可选的 **mmproj** 和 **MoE** 设置。
72
72
  3. **Load model**,观察状态卡片,通过 **Open terminal** 查看输出。
73
73
  4. **Register in dsh** —— 该路由(默认 `local-<alias>`)会出现在 Models 选择器中。
package/README.md CHANGED
@@ -69,7 +69,7 @@ dsh plugin --profile web remove dsh-local-models
69
69
 
70
70
  ## Usage
71
71
 
72
- 1. **Choose GGUF…** — pick a model file (Home / Models shortcuts, Up navigation).
72
+ 1. **Choose GGUF…** — pick a model file (Home / Root buttons, Models shortcuts, Up navigation). Up goes all the way to `/`, so a model on a mounted disk (`/mnt/…`, `/media/…`, a network export) opens without adding its folder to Model folders first.
73
73
  2. Tune **context**, **KV cache K / V**, **Max MTP head** (fixed draft, 0-7; 3 is the tuned sweet spot — deeper collapses at large ctx), **thinking level** + **preserve thinking** checkbox, optional **mmproj** and **MoE** settings.
74
74
  3. **Load model**, watch the status card, inspect output via **Open terminal**.
75
75
  4. **Register in dsh** — the route (default `local-<alias>`) appears in the Models picker.
package/lib/client.js CHANGED
@@ -326,6 +326,11 @@ window.__ModuleLoader__.load({
326
326
  var err = errPair[0], setErr = errPair[1];
327
327
  var selPair = useStatePair(null);
328
328
  var selected = selPair[0], setSelected = selPair[1];
329
+ // The server stops a huge folder at 2000 entries. Root browsing
330
+ // makes /proc and /usr/lib reachable, so say so rather than let the
331
+ // list look complete when it is not.
332
+ var truncPair = useStatePair(false);
333
+ var truncated = truncPair[0], setTrunc = truncPair[1];
329
334
  // Remembered folders (persisted): jump buttons + toggle for the current dir.
330
335
  var LS_FOLDERS = "dsh-local-models:remembered-folders";
331
336
  var readFolders = function () {
@@ -355,6 +360,10 @@ window.__ModuleLoader__.load({
355
360
  // empty-looking folder reads as a broken picker.
356
361
  var dirMode = props.dirMode === true;
357
362
  var allFiles = props.allFiles === true || dirMode;
363
+ // The listing starts at the filesystem root, so the naive
364
+ // base + "/" + name would ask for "//etc" and paint a path bar
365
+ // reading "//etc", with every deeper click stacking a slash.
366
+ var into = function (base, name) { return (base === "/" ? "" : base) + "/" + name; };
358
367
  var load = useCallback(function (d) {
359
368
  setLoading(true);
360
369
  setErr(null);
@@ -364,6 +373,7 @@ window.__ModuleLoader__.load({
364
373
  setDir(body.path || d);
365
374
  setEntries(body.entries);
366
375
  setParent(body.parent != null ? body.parent : null);
376
+ setTrunc(Boolean(body.truncated));
367
377
  setSelected(null);
368
378
  } else {
369
379
  setErr((body && body.error) || "could not list directory");
@@ -387,6 +397,7 @@ window.__ModuleLoader__.load({
387
397
  "div",
388
398
  { style: style.row },
389
399
  h("button", { style: style.secondaryButton, onClick: function () { load(""); } }, "Home"),
400
+ h("button", { style: style.secondaryButton, title: "the filesystem root — mounted disks hang off it (/mnt, /media, /run/media)", onClick: function () { load("/"); } }, "Root"),
390
401
  (props.shortcuts || []).map(function (sc, i) {
391
402
  return h("button", { key: i, style: style.secondaryButton, onClick: function () { load(sc.path); }, title: sc.path }, sc.label);
392
403
  }),
@@ -403,7 +414,7 @@ window.__ModuleLoader__.load({
403
414
  { style: style.list },
404
415
  (entries || []).map(function (en, i) {
405
416
  if (en.type === "dir") {
406
- return h("li", { key: i, style: Object.assign({}, style.item, style.itemDir), onClick: function () { load(dir + "/" + en.name); } }, en.name + "/");
417
+ return h("li", { key: i, style: Object.assign({}, style.item, style.itemDir), onClick: function () { load(into(dir, en.name)); } }, en.name + "/");
407
418
  }
408
419
  var sel = selected === en.name;
409
420
  return h("li", {
@@ -414,13 +425,14 @@ window.__ModuleLoader__.load({
414
425
  }, en.name + (en.size != null ? " (" + fmtGiB(en.size) + ")" : ""));
415
426
  }),
416
427
  ),
428
+ truncated ? h("p", { style: style.warn }, "showing the first 2000 entries — open a subfolder to narrow it down") : null,
417
429
  h(
418
430
  "div",
419
431
  { style: style.row },
420
432
  h("button", {
421
433
  style: (dirMode ? dir : selected) ? style.button : Object.assign({}, style.button, style.disabled),
422
434
  disabled: dirMode ? !dir : !selected,
423
- onClick: function () { if (dirMode) { if (dir) props.onPick(dir); } else if (selected) props.onPick(dir + "/" + selected); },
435
+ onClick: function () { if (dirMode) { if (dir) props.onPick(dir); } else if (selected) props.onPick(into(dir, selected)); },
424
436
  }, dirMode ? "Use this folder" : "Use this file"),
425
437
  h("button", { style: style.secondaryButton, onClick: props.onClose }, "Cancel"),
426
438
  ),
package/lib/index.js CHANGED
@@ -100,11 +100,14 @@ const MMPROJ_CPU = process.env.LOCAL_MODELS_MMPROJ_CPU !== "0";
100
100
  * above the raw threshold) — and with xhigh + preserveThinking every turn
101
101
  * adds 10-20K thinking tokens, so compaction fires again on the next step:
102
102
  * the "compaction loop". The honest fix for mid-size windows is a per-model
103
- * headroom override in the dsh profile (headroom ~6-16K moves the 131K
104
- * threshold to ~82-92K), not a smaller O — shrinking O truncates the xhigh
105
- * thinking blocks this cap exists to protect. Heavy xhigh thinking that
106
- * needs more must raise per-request maxTokens explicitly (which honestly
107
- * moves the compaction threshold earlier instead of silently disabling it).
103
+ * headroom override on the agent-preset row that mounts the running engine
104
+ * (`preset-standard` → `plugins` → `compaction` group → `compaction-basic`;
105
+ * headroom ~6-16K moves the 131K threshold to ~82-92K) — a patch on the
106
+ * top-level `compaction-basic` row is dead config (that row is disabled),
107
+ * and a smaller O would truncate the xhigh thinking blocks this cap
108
+ * exists to protect. Heavy xhigh thinking that needs more must raise
109
+ * per-request maxTokens explicitly (which honestly moves the compaction
110
+ * threshold earlier instead of silently disabling it).
108
111
  */
109
112
  const MAX_OUTPUT_TOKENS = 32768;
110
113
  /** Default output share of the window: never offer more than half as max
@@ -1563,19 +1566,36 @@ function sendJson(res, code, body) {
1563
1566
  res.end(payload);
1564
1567
  }
1565
1568
 
1569
+ /** The filesystem root, in the form resolve() produces (posix "/"). Every
1570
+ * absolute path is under it, so isPathAllowed() treats it as containing the
1571
+ * whole tree — the one root that makes "Up" from any folder land somewhere
1572
+ * real. */
1573
+ const FILESYSTEM_ROOT = resolve(sep);
1574
+
1575
+ /** Single wording for a path the file endpoints refuse. With the root in
1576
+ * allowedRoots() this is unreachable for any absolute path; it stays as the
1577
+ * one place the wording lives, so narrowing the roots again has something to
1578
+ * narrow. */
1579
+ const NOT_READABLE = "this tab may not read that path";
1580
+
1566
1581
  /** Roots the file endpoints (browse/gguf-meta/run, and profile saves) may
1567
- * touch: the user's home, the browser shortcut dirs (Runtime card, else
1568
- * LOCAL_MODELS_SHORTCUTS), the directories of the models already declared in
1569
- * saved profiles — a saved profile must always stay loadable, whatever the
1570
- * shortcut list says — and the llama.cpp dir when it really holds a
1571
- * llama-server. Resolves lexically and requires containment so inputs can't
1572
- * wander the filesystem.
1582
+ * touch. The list LEADS WITH THE FILESYSTEM ROOT: the picker walks up to "/"
1583
+ * on purpose, so a model can live on a mounted disk the user never declared
1584
+ * here (/mnt/data, /media/<user>/<label>, a network export) and reaching it
1585
+ * must not depend on having typed its path into Model folders first.
1586
+ *
1587
+ * Everything below the root is therefore readable, which makes the remaining
1588
+ * entries — home, the shortcut dirs (Runtime card, else
1589
+ * LOCAL_MODELS_SHORTCUTS), the model dirs of saved profiles, the llama.cpp
1590
+ * dir — jump buttons and back-compat rather than a confinement. They stay
1591
+ * because the picker offers them, and because a saved profile must stay
1592
+ * loadable whatever else happens.
1573
1593
  *
1574
- * Nothing here is taken on faith: a path only widens the roots once it is a
1575
- * directory the user configured, a profile that already exists, or a binary
1576
- * that passes isLlamaServerPath(). */
1594
+ * The guard that is still load-bearing is isLlamaServerPath(): it, not this
1595
+ * list, decides what may be spawned (POST /settings, /runtime/check and
1596
+ * requireBin()), so nothing a browse opens can turn into the executable. */
1577
1597
  function allowedRoots() {
1578
- const roots = [homedir(), ...shortcutDirs().map((s) => s.path)];
1598
+ const roots = [FILESYSTEM_ROOT, homedir(), ...shortcutDirs().map((s) => s.path)];
1579
1599
  try {
1580
1600
  for (const profile of cachedProfiles()) {
1581
1601
  for (const declared of [profile?.modelPath, profile?.mmprojPath]) {
@@ -1613,9 +1633,10 @@ function allowedRoots() {
1613
1633
  return roots;
1614
1634
  }
1615
1635
 
1616
- /** A path we are willing to register as a browsable root: absolute, no ".."
1617
- * segment, and not a filesystem root (which would list everything). Exported
1618
- * for tests. */
1636
+ /** A path we are willing to show as a picker button: absolute, no ".."
1637
+ * segment, and naming one folder rather than the whole tree. The root is
1638
+ * browsable on its own (there is a Root button and an Up that reaches it), so
1639
+ * this is button hygiene, not confinement. Exported for tests. */
1619
1640
  export function isSafeRootPath(p) {
1620
1641
  if (typeof p !== "string" || !p.trim()) return false;
1621
1642
  const raw = p.trim();
@@ -1641,7 +1662,9 @@ function isPathAllowed(p) {
1641
1662
  } catch {
1642
1663
  return false;
1643
1664
  }
1644
- return abs === r || abs.startsWith(r + sep);
1665
+ // The root needs its own clause: the prefix test below would only ever
1666
+ // match "/" itself (every other path starts "/…", not "//").
1667
+ return abs === r || r === FILESYSTEM_ROOT || abs.startsWith(r + sep);
1645
1668
  });
1646
1669
  }
1647
1670
 
@@ -1865,12 +1888,12 @@ function cachedProfiles() {
1865
1888
  export function normalizeProfileConfig(raw) {
1866
1889
  const modelPath = typeof raw?.modelPath === "string" && raw.modelPath ? raw.modelPath : "";
1867
1890
  if (!modelPath || !existsSync(modelPath)) throw new Error("profile needs a modelPath that exists on disk");
1868
- if (!isPathAllowed(modelPath)) throw new Error("modelPath is outside the folders this tab may read: " + modelPath + " — add its folder under Model folders in the Runtime card first");
1891
+ if (!isPathAllowed(modelPath)) throw new Error("modelPath " + NOT_READABLE + ": " + modelPath);
1869
1892
  const ctx = Number.isInteger(raw?.ctx) && raw.ctx > 0 ? raw.ctx : 8192;
1870
1893
  const mtpHeads = Number.isInteger(raw?.mtpHeads) && raw.mtpHeads >= 0 ? Math.min(raw.mtpHeads, MTP_MAX) : 0;
1871
1894
  const mmprojPath = typeof raw?.mmprojPath === "string" && raw.mmprojPath ? raw.mmprojPath : null;
1872
1895
  if (mmprojPath && !existsSync(mmprojPath)) throw new Error("mmprojPath does not exist: " + mmprojPath);
1873
- if (mmprojPath && !isPathAllowed(mmprojPath)) throw new Error("mmprojPath is outside the folders this tab may read: " + mmprojPath + " — add its folder under Model folders in the Runtime card first");
1896
+ if (mmprojPath && !isPathAllowed(mmprojPath)) throw new Error("mmprojPath " + NOT_READABLE + ": " + mmprojPath);
1874
1897
  const effort = normalizeEffort(raw?.effort);
1875
1898
  const preserveThinking = normalizePreserveThinking(raw);
1876
1899
  // Multi-GPU placement. Legacy profiles have neither field and keep
@@ -2085,11 +2108,16 @@ export function apply(ctx) {
2085
2108
  const url = new URL(req.url ?? "/", "http://x");
2086
2109
  const dir = url.searchParams.get("dir");
2087
2110
  const allFiles = url.searchParams.get("all") === "1";
2088
- const target = dir && dir.indexOf("/") === 0
2111
+ const raw = dir && dir.indexOf("/") === 0
2089
2112
  ? dir
2090
2113
  : dir ? join(homedir(), dir) : homedir();
2114
+ // Canonical before anything else. The picker joins with "/" and
2115
+ // starts at the root, so a click on /etc arrives as "//etc" — and
2116
+ // the path bar would then read "//etc", with every deeper click
2117
+ // stacking another slash. resolve() also collapses "..".
2118
+ const target = resolve(raw);
2091
2119
  if (!isPathAllowed(target)) {
2092
- sendJson(res, 400, { error: "path is outside the folders this tab may read (home + Model folders + the llama.cpp dir): " + target });
2120
+ sendJson(res, 400, { error: NOT_READABLE + ": " + target });
2093
2121
  return;
2094
2122
  }
2095
2123
  const listing = listDir(target, allFiles);
@@ -2117,7 +2145,7 @@ export function apply(ctx) {
2117
2145
  return;
2118
2146
  }
2119
2147
  if (!isPathAllowed(path)) {
2120
- sendJson(res, 400, { error: "path is outside the folders this tab may read (home + Model folders + the llama.cpp dir)" });
2148
+ sendJson(res, 400, { error: NOT_READABLE + ": " + path });
2121
2149
  return;
2122
2150
  }
2123
2151
  try {
@@ -2210,11 +2238,11 @@ export function apply(ctx) {
2210
2238
  return;
2211
2239
  }
2212
2240
  if (!isPathAllowed(path)) {
2213
- sendJson(res, 400, { error: "model path is outside the folders this tab may read: " + path + " — add its folder under Model folders in the Runtime card" });
2241
+ sendJson(res, 400, { error: "model " + NOT_READABLE + ": " + path });
2214
2242
  return;
2215
2243
  }
2216
2244
  if (mmproj && !isPathAllowed(mmproj)) {
2217
- sendJson(res, 400, { error: "mmproj path is outside the folders this tab may read: " + mmproj + " — add its folder under Model folders in the Runtime card" });
2245
+ sendJson(res, 400, { error: "mmproj " + NOT_READABLE + ": " + mmproj });
2218
2246
  return;
2219
2247
  }
2220
2248
  const result = await manager.run(path, Number.isFinite(ctx) ? ctx : 8192, Number.isFinite(mtp) ? mtp : 0, mmproj, effort, typeof body.mmprojCpu === "boolean" ? body.mmprojCpu : MMPROJ_CPU, moe, normalizePreserveThinking(body), kvTypeK, kvTypeV, normalizeSplitMode(body.splitMode), normalizeTensorSplit(body.tensorSplit));
@@ -2359,8 +2387,8 @@ export function apply(ctx) {
2359
2387
  }
2360
2388
  patch.binPath = wanted;
2361
2389
  }
2362
- // File-browser shortcuts are browsable roots, so they are the one
2363
- // place where a bad value widens the file endpoints: absolute,
2390
+ // File-browser shortcuts become one button each in the picker,
2391
+ // so a value here has to name a folder a person would: absolute,
2364
2392
  // no "..", never the filesystem root, at most 12 entries.
2365
2393
  // Missing dirs are still kept (an unmounted disk must not
2366
2394
  // silently lose its shortcut); the tab marks them as missing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-local-models",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "dsh addon: a Local Models settings tab that starts and stops a llama-server child process living with the dsh host process.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -37,9 +37,10 @@ cpuMoe, nCpuMoe, expertUsed, arch, updatedAt }`
37
37
 
38
38
  - **Paths must exist and live on the fast mount**. Referencing the failing/
39
39
  legacy `/mnt/disco1` is a red flag - targets are under `/mnt/raid0/GGUF/`.
40
- A path outside the tab's roots (home + the Runtime card's Model folders + the
41
- folders saved profiles already declare) is refused on save and on launch, so
42
- a profile pointing somewhere else needs its folder added in the tab first.
40
+ The tab can reach any absolute path (the picker's Up goes all the way to `/`,
41
+ so mounted disks are browsable without declaring them), so a path pointing
42
+ somewhere else is an operational mistake the guard will not catch - check it
43
+ by eye.
43
44
  - **MTP depth must be <= 7** (the tab's ceiling; upstream clamps the
44
45
  effective depth to the model's nextn depth) and **3 is the tuned value** —
45
46
  deeper collapses at large ctx. Old profiles may still carry 4-6 from the
@@ -111,21 +112,21 @@ so a 131072 window with O = 32768 thresholds at **32768**, firing at
111
112
  ~32-38K pressure tokens and looping with xhigh + preserveThinking (every
112
113
  turn re-adds 10-20K thinking tokens). A 98304 window is worse: pressure
113
114
  exactly 0, proactive compaction disabled entirely. The fix is a per-model
114
- headroom override in the dsh profile (needs a `dsh web` restart — loader
115
- normalization happens at boot), NOT a smaller O (that truncates xhigh
116
- thinking). Compute it with `compactionBudgetFor(ctx).recommendedHeadroom`
117
- (~6.5K for 131K → ~92K threshold; ~4K floor for 96K → ~61K). Example for
118
- the Swift 131K route (also add siblings as needed):
119
-
120
- ```yaml
121
- - id: compaction-basic
122
- name: '@deepseek-ai/dsh-compaction-basic'
123
- config:
124
- modelPolicies:
125
- - provider: local-router
126
- model: swift-1-5-qwen3-8-27b
127
- headroomTokens: 6554
128
- ```
115
+ headroom override — but placement matters: the engines that run live
116
+ inside the agent-preset scopes (`preset-standard` → `plugins` →
117
+ `compaction` group → `compaction-basic`), NOT the top-level
118
+ `compaction-basic` row (disabled there; a patch on it is dead config that
119
+ dump-config will happily show). So the override must ride a wholesale
120
+ `config` on the preset row (row config is replaced whole, not merged):
121
+ copy the installed preset file
122
+ (`dsh-web-app/presets/standard.patch.yml` → `insert[0].config`), add
123
+ `modelPolicies` to the nested engine, paste as `- id: preset-standard`.
124
+ Compute values with `compactionBudgetFor(ctx).recommendedHeadroom`
125
+ (~6.5K for 131K → ~92K threshold; ~4K floor for 96K → ~61K). Because it
126
+ is wholesale, the row freezes: re-generate after any dsh upgrade that
127
+ touches the preset file, or upstream changes to that preset are silently
128
+ dropped. Only patch presets actually used (`standard` here); `ptc` /
129
+ `cordis` / `minimal` need the same treatment if adopted.
129
130
 
130
131
  Shortcut with zero config edits: run the bigger window instead — the
131
132
  200K/250K profiles threshold at ~106K/158K under defaults.