dsh-local-models 0.3.2 → 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 +1 -1
- package/README-hi.md +1 -1
- package/README-pt.md +1 -1
- package/README-zh.md +1 -1
- package/README.md +1 -1
- package/lib/client.js +14 -2
- package/lib/index.js +48 -23
- package/package.json +1 -1
- package/skills/dsh-local-models-ops/SKILL.md +4 -3
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
@@ -1566,19 +1566,36 @@ function sendJson(res, code, body) {
|
|
|
1566
1566
|
res.end(payload);
|
|
1567
1567
|
}
|
|
1568
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
|
+
|
|
1569
1581
|
/** Roots the file endpoints (browse/gguf-meta/run, and profile saves) may
|
|
1570
|
-
* touch
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
1575
|
-
*
|
|
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.
|
|
1576
1593
|
*
|
|
1577
|
-
*
|
|
1578
|
-
*
|
|
1579
|
-
*
|
|
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. */
|
|
1580
1597
|
function allowedRoots() {
|
|
1581
|
-
const roots = [homedir(), ...shortcutDirs().map((s) => s.path)];
|
|
1598
|
+
const roots = [FILESYSTEM_ROOT, homedir(), ...shortcutDirs().map((s) => s.path)];
|
|
1582
1599
|
try {
|
|
1583
1600
|
for (const profile of cachedProfiles()) {
|
|
1584
1601
|
for (const declared of [profile?.modelPath, profile?.mmprojPath]) {
|
|
@@ -1616,9 +1633,10 @@ function allowedRoots() {
|
|
|
1616
1633
|
return roots;
|
|
1617
1634
|
}
|
|
1618
1635
|
|
|
1619
|
-
/** A path we are willing to
|
|
1620
|
-
* segment, and
|
|
1621
|
-
*
|
|
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. */
|
|
1622
1640
|
export function isSafeRootPath(p) {
|
|
1623
1641
|
if (typeof p !== "string" || !p.trim()) return false;
|
|
1624
1642
|
const raw = p.trim();
|
|
@@ -1644,7 +1662,9 @@ function isPathAllowed(p) {
|
|
|
1644
1662
|
} catch {
|
|
1645
1663
|
return false;
|
|
1646
1664
|
}
|
|
1647
|
-
|
|
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);
|
|
1648
1668
|
});
|
|
1649
1669
|
}
|
|
1650
1670
|
|
|
@@ -1868,12 +1888,12 @@ function cachedProfiles() {
|
|
|
1868
1888
|
export function normalizeProfileConfig(raw) {
|
|
1869
1889
|
const modelPath = typeof raw?.modelPath === "string" && raw.modelPath ? raw.modelPath : "";
|
|
1870
1890
|
if (!modelPath || !existsSync(modelPath)) throw new Error("profile needs a modelPath that exists on disk");
|
|
1871
|
-
if (!isPathAllowed(modelPath)) throw new Error("modelPath
|
|
1891
|
+
if (!isPathAllowed(modelPath)) throw new Error("modelPath " + NOT_READABLE + ": " + modelPath);
|
|
1872
1892
|
const ctx = Number.isInteger(raw?.ctx) && raw.ctx > 0 ? raw.ctx : 8192;
|
|
1873
1893
|
const mtpHeads = Number.isInteger(raw?.mtpHeads) && raw.mtpHeads >= 0 ? Math.min(raw.mtpHeads, MTP_MAX) : 0;
|
|
1874
1894
|
const mmprojPath = typeof raw?.mmprojPath === "string" && raw.mmprojPath ? raw.mmprojPath : null;
|
|
1875
1895
|
if (mmprojPath && !existsSync(mmprojPath)) throw new Error("mmprojPath does not exist: " + mmprojPath);
|
|
1876
|
-
if (mmprojPath && !isPathAllowed(mmprojPath)) throw new Error("mmprojPath
|
|
1896
|
+
if (mmprojPath && !isPathAllowed(mmprojPath)) throw new Error("mmprojPath " + NOT_READABLE + ": " + mmprojPath);
|
|
1877
1897
|
const effort = normalizeEffort(raw?.effort);
|
|
1878
1898
|
const preserveThinking = normalizePreserveThinking(raw);
|
|
1879
1899
|
// Multi-GPU placement. Legacy profiles have neither field and keep
|
|
@@ -2088,11 +2108,16 @@ export function apply(ctx) {
|
|
|
2088
2108
|
const url = new URL(req.url ?? "/", "http://x");
|
|
2089
2109
|
const dir = url.searchParams.get("dir");
|
|
2090
2110
|
const allFiles = url.searchParams.get("all") === "1";
|
|
2091
|
-
const
|
|
2111
|
+
const raw = dir && dir.indexOf("/") === 0
|
|
2092
2112
|
? dir
|
|
2093
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);
|
|
2094
2119
|
if (!isPathAllowed(target)) {
|
|
2095
|
-
sendJson(res, 400, { error:
|
|
2120
|
+
sendJson(res, 400, { error: NOT_READABLE + ": " + target });
|
|
2096
2121
|
return;
|
|
2097
2122
|
}
|
|
2098
2123
|
const listing = listDir(target, allFiles);
|
|
@@ -2120,7 +2145,7 @@ export function apply(ctx) {
|
|
|
2120
2145
|
return;
|
|
2121
2146
|
}
|
|
2122
2147
|
if (!isPathAllowed(path)) {
|
|
2123
|
-
sendJson(res, 400, { error:
|
|
2148
|
+
sendJson(res, 400, { error: NOT_READABLE + ": " + path });
|
|
2124
2149
|
return;
|
|
2125
2150
|
}
|
|
2126
2151
|
try {
|
|
@@ -2213,11 +2238,11 @@ export function apply(ctx) {
|
|
|
2213
2238
|
return;
|
|
2214
2239
|
}
|
|
2215
2240
|
if (!isPathAllowed(path)) {
|
|
2216
|
-
sendJson(res, 400, { error: "model
|
|
2241
|
+
sendJson(res, 400, { error: "model " + NOT_READABLE + ": " + path });
|
|
2217
2242
|
return;
|
|
2218
2243
|
}
|
|
2219
2244
|
if (mmproj && !isPathAllowed(mmproj)) {
|
|
2220
|
-
sendJson(res, 400, { error: "mmproj
|
|
2245
|
+
sendJson(res, 400, { error: "mmproj " + NOT_READABLE + ": " + mmproj });
|
|
2221
2246
|
return;
|
|
2222
2247
|
}
|
|
2223
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));
|
|
@@ -2362,8 +2387,8 @@ export function apply(ctx) {
|
|
|
2362
2387
|
}
|
|
2363
2388
|
patch.binPath = wanted;
|
|
2364
2389
|
}
|
|
2365
|
-
// File-browser shortcuts
|
|
2366
|
-
//
|
|
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,
|
|
2367
2392
|
// no "..", never the filesystem root, at most 12 entries.
|
|
2368
2393
|
// Missing dirs are still kept (an unmounted disk must not
|
|
2369
2394
|
// silently lose its shortcut); the tab marks them as missing.
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|