Files
LocalAI/pkg/huggingface-api/snapshot_test.go
LocalAI [bot] bcc41219f7 feat: materialize Hugging Face model artifacts (#10825)
* feat(config): add model artifact source contract

Assisted-by: Codex:GPT-5 [Codex]

* feat(downloader): add authenticated raw-byte progress

Assisted-by: Codex:GPT-5 [Codex]

* feat(huggingface): resolve immutable snapshot manifests

Assisted-by: Codex:GPT-5 [Codex]

* feat(models): add artifact storage primitives

Assisted-by: Codex:GPT-5 [Codex]

* feat(models): materialize pinned Hugging Face snapshots

Assisted-by: Codex:GPT-5 [Codex]

* feat(models): bind managed snapshots at runtime

Assisted-by: Codex:GPT-5 [Codex]

* feat(gallery): materialize model artifacts during install

Assisted-by: Codex:GPT-5 [Codex]

* feat(gallery): declare managed Hugging Face artifacts

Assisted-by: Codex:GPT-5 [Codex]

* feat(models): preload managed model artifacts

Assisted-by: Codex:GPT-5 [Codex]

* fix(gallery): retain shared artifact caches on delete

Assisted-by: Codex:GPT-5 [Codex]

* feat(models): report artifact acquisition progress

Assisted-by: Codex:GPT-5 [Codex]

* refactor(backends): load managed models from ModelFile

Assisted-by: Codex:GPT-5 [Codex]

* refactor(backends): load staged speech model snapshots

Assisted-by: Codex:GPT-5 [Codex]

* refactor(backends): use staged snapshots in engine backends

Assisted-by: Codex:GPT-5 [Codex]

* test(distributed): cover staged artifact snapshots

Assisted-by: Codex:GPT-5 [Codex]

* docs: explain managed model artifacts

Assisted-by: Codex:GPT-5 [Codex]

* docs: add product design context

Assisted-by: Codex:GPT-5 [Codex]

* feat(ui): show model artifact download progress

Assisted-by: Codex:GPT-5 [Codex]

* Eagerly materialize Hugging Face artifacts

Materialize HF-backed model references as managed GGUF artifacts during load, with lazy download retained only as fallback.

Assisted-by: Codex:GPT-5 [shell]

* Refactor HF
  downloads through a shared executor

Assisted-by: Codex:GPT-5 [shell]

* drop

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-15 01:09:33 +02:00

150 lines
6.0 KiB
Go

package hfapi_test
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/mudler/LocalAI/pkg/downloader"
hfapi "github.com/mudler/LocalAI/pkg/huggingface-api"
)
var _ = Describe("immutable snapshot resolution", func() {
It("pins the metadata sha and follows tree pagination", func() {
const commit = "0123456789abcdef0123456789abcdef01234567"
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expect(r.Header.Get("Authorization")).To(Equal("Bearer test-token"))
switch {
case strings.Contains(r.URL.Path, "/revision/main"):
_, _ = w.Write([]byte(`{"sha":"` + commit + `"}`))
case strings.Contains(r.URL.Path, "/tree/"+commit) && r.URL.Query().Get("cursor") == "page-2":
_, _ = w.Write([]byte(`[{"type":"file","path":"tokenizer/tokenizer.json","size":7,"oid":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]`))
case strings.Contains(r.URL.Path, "/tree/"+commit):
Expect(r.URL.Query().Get("recursive")).To(Equal("true"))
w.Header().Set("Link", fmt.Sprintf(`<%s%s?recursive=true&expand=true&cursor=page-2>; rel="next"`, server.URL, r.URL.Path))
_, _ = w.Write([]byte(`[{"type":"file","path":"model.safetensors","size":11,"oid":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","lfs":{"oid":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","size":11,"pointerSize":130}}]`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
DeferCleanup(server.Close)
client := hfapi.NewClient()
client.SetBaseURL(server.URL + "/api/models")
snapshot, err := client.ResolveSnapshot(context.Background(), hfapi.SnapshotRequest{
Repo: "owner/repo",
Revision: "main",
Token: "test-token",
MaxFiles: 10,
})
Expect(err).NotTo(HaveOccurred())
Expect(snapshot.Endpoint).To(Equal(server.URL))
Expect(snapshot.ResolvedRevision).To(Equal(commit))
Expect(snapshot.Files).To(HaveLen(2))
Expect(snapshot.Files[0].Path).To(Equal("model.safetensors"))
Expect(snapshot.Files[0].LFSOID).To(HaveLen(64))
Expect(snapshot.Files[1].Path).To(Equal("tokenizer/tokenizer.json"))
Expect(snapshot.Files[0].URL).To(ContainSubstring("/resolve/" + commit + "/model.safetensors"))
})
It("applies deterministic allow and ignore filters", func() {
files := []hfapi.SnapshotFile{
{Path: "config.json"},
{Path: "nested/tokenizer.json"},
{Path: "weights.bin"},
{Path: "weights.safetensors"},
}
filtered, err := hfapi.FilterSnapshotFiles(files, []string{"*.json", "*.safetensors"}, []string{"nested/*"})
Expect(err).NotTo(HaveOccurred())
Expect(filtered).To(ConsistOf(
hfapi.SnapshotFile{Path: "config.json"},
hfapi.SnapshotFile{Path: "weights.safetensors"},
))
})
It("rejects a manifest larger than MaxFiles", func() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/revision/") {
_, _ = w.Write([]byte(`{"sha":"0123456789abcdef0123456789abcdef01234567"}`))
return
}
_, _ = w.Write([]byte(`[{"type":"file","path":"a","size":1},{"type":"file","path":"b","size":1}]`))
}))
DeferCleanup(server.Close)
client := hfapi.NewClient()
client.SetBaseURL(server.URL + "/api/models")
_, err := client.ResolveSnapshot(context.Background(), hfapi.SnapshotRequest{Repo: "owner/repo", Revision: "main", MaxFiles: 1})
Expect(err).To(MatchError(ContainSubstring("maximum file count")))
})
It("honors a cancelled context before metadata access", func() {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := hfapi.NewClient().ResolveSnapshot(ctx, hfapi.SnapshotRequest{Repo: "owner/repo", Revision: "main"})
Expect(err).To(MatchError(context.Canceled))
})
It("rejects a cross-origin pagination link before sending credentials", func() {
const commit = "0123456789abcdef0123456789abcdef01234567"
requestedCrossOrigin := false
evil := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
requestedCrossOrigin = true
w.WriteHeader(http.StatusNoContent)
}))
DeferCleanup(evil.Close)
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/revision/") {
_, _ = w.Write([]byte(`{"sha":"` + commit + `"}`))
return
}
w.Header().Set("Link", `<`+evil.URL+`/steal>; rel="next"`)
_, _ = w.Write([]byte(`[]`))
}))
DeferCleanup(origin.Close)
client := hfapi.NewClient()
client.SetBaseURL(origin.URL + "/api/models")
_, err := client.ResolveSnapshot(context.Background(), hfapi.SnapshotRequest{
Repo: "owner/repo", Revision: "main", Token: "test-token",
})
Expect(err).To(MatchError(ContainSubstring("cross-origin pagination")))
Expect(requestedCrossOrigin).To(BeFalse())
})
It("downloads a pinned public Xet fixture through the ordinary resolve URL", Label("network"), func() {
if os.Getenv("LOCALAI_HF_XET_SMOKE") != "1" {
Skip("set LOCALAI_HF_XET_SMOKE=1 to run the public Hub compatibility gate")
}
client := hfapi.NewClient()
snapshot, err := client.ResolveSnapshot(context.Background(), hfapi.SnapshotRequest{
Repo: "hf-internal-testing/tiny-random-t5-v1.1",
Revision: "main",
AllowPatterns: []string{"model.safetensors"},
})
Expect(err).NotTo(HaveOccurred())
Expect(snapshot.ResolvedRevision).To(MatchRegexp(`^[0-9a-f]{40}$`))
Expect(snapshot.Files).To(HaveLen(1))
file := snapshot.Files[0]
Expect(file.Path).To(Equal("model.safetensors"))
Expect(file.XetHash).NotTo(BeEmpty())
target := filepath.Join(GinkgoT().TempDir(), file.Path)
var final downloader.TransferProgress
err = downloader.URI(file.URL).DownloadFileWithContext(
context.Background(), target, file.LFSOID, 0, 1, nil,
downloader.WithTransferProgress(func(event downloader.TransferProgress) { final = event }),
)
Expect(err).NotTo(HaveOccurred())
info, err := os.Stat(target)
Expect(err).NotTo(HaveOccurred())
Expect(info.Size()).To(Equal(file.Size))
Expect(final.Written).To(Equal(file.Size))
})
})