From 867e1b1318d918b78bc9b0673194befd97724f10 Mon Sep 17 00:00:00 2001 From: T-Crypt <132922589+T-Crypt@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:47:27 -0600 Subject: [PATCH] fix(models): an empty models--* husk no longer hides a plain model store A root was read as a HuggingFace cache as soon as one directory was named models--*, so an abandoned, empty download in an LM Studio store made all 43 models beside it disappear. A root is an HF cache only when a models--* directory holds snapshots/, which is what the HF scan reads. --- CHANGELOG.md | 3 +++ internal/models/scan.go | 5 +++- internal/models/scan_test.go | 52 ++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f52387a..5361775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,9 @@ has a matching GitHub release whose body comes from its section below. ### Fixed +- One empty `models--*` husk no longer makes a whole store read as an HF cache with no models, + reported by @azrael-229 (#138). A root is an HF cache only when a `models--*` directory holds + `snapshots`, so an abandoned download beside plain models keeps them listed. - The gateway's `/gui/` board launches an engine that has a `start` command but no `gui` URL (Strata, a llama-server). Its Launch button answered 404 and stayed on "Launching..."; it now starts the engine under a lease and shows Running once it answers. A refused start shows its reason instead of hanging. diff --git a/internal/models/scan.go b/internal/models/scan.go index 135d0c1..8d756a0 100644 --- a/internal/models/scan.go +++ b/internal/models/scan.go @@ -340,7 +340,10 @@ func sourceOf(p string) string { } if ents, err := os.ReadDir(p); err == nil { for _, e := range ents { - if e.IsDir() && strings.HasPrefix(e.Name(), "models--") { + // The HF scan reads snapshots/, so that is what makes a root an + // HF cache. A models--* husk with none, from an abandoned + // download, must not hide the plain models around it. + if e.IsDir() && strings.HasPrefix(e.Name(), "models--") && isDir(filepath.Join(p, e.Name(), "snapshots")) { return SourceHFCache } } diff --git a/internal/models/scan_test.go b/internal/models/scan_test.go index 9006d6e..c46879c 100644 --- a/internal/models/scan_test.go +++ b/internal/models/scan_test.go @@ -195,6 +195,58 @@ func TestScanHFCache(t *testing.T) { } } +// A models--* directory with no snapshots is the husk of an abandoned +// download, not one repo of an HF cache. It must not hide the plain models +// around it, and a real cache with a husk beside it is still a cache. +func TestScanHFHusk(t *testing.T) { + cases := []struct { + name string + setup func(root string) + want string // "name/source" for every model, in scan order + }{ + { + name: "husk beside a plain store", + setup: func(root string) { + sampleFile(t, filepath.Join(root, "plain.gguf"), "Plain Model") + if err := os.Mkdir(filepath.Join(root, "models--x--y"), 0o755); err != nil { + t.Fatal(err) + } + }, + want: "plain/dir", + }, + { + name: "a real cache is an HF cache", + setup: func(root string) { + sampleFile(t, filepath.Join(root, "models--org--repo", "snapshots", "model.gguf"), "Whatever The Blob Says") + }, + want: "org/repo/hf-cache", + }, + { + name: "husk beside a real cache", + setup: func(root string) { + sampleFile(t, filepath.Join(root, "models--org--repo", "snapshots", "model.gguf"), "Whatever The Blob Says") + if err := os.Mkdir(filepath.Join(root, "models--x--y"), 0o755); err != nil { + t.Fatal(err) + } + }, + want: "org/repo/hf-cache", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + root := t.TempDir() + tc.setup(root) + var parts []string + for _, m := range Scan([]string{root}) { + parts = append(parts, m.Name+"/"+m.Source) + } + if got := strings.Join(parts, ", "); got != tc.want { + t.Errorf("scan = %s, want %s", got, tc.want) + } + }) + } +} + // testManifest is the Ollama tag file: a layer list, of which one layer is the weights. type testManifest struct { MediaType string `json:"mediaType"`