Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,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.
Expand Down
5 changes: 4 additions & 1 deletion internal/models/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
52 changes: 52 additions & 0 deletions internal/models/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
Loading