Files
LocalAI/pkg/downloader/auth_progress_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

174 lines
5.2 KiB
Go

package downloader_test
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/mudler/LocalAI/pkg/downloader"
)
var _ = Describe("authenticated HTTP downloads", func() {
It("sends bearer auth to the origin and strips it at a cross-host redirect", func() {
var originAuth, cdnAuth string
cdn := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cdnAuth = r.Header.Get("Authorization")
w.Header().Set("Content-Length", "5")
_, _ = w.Write([]byte("model"))
}))
DeferCleanup(cdn.Close)
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
originAuth = r.Header.Get("Authorization")
http.Redirect(w, r, cdn.URL+"/weights", http.StatusTemporaryRedirect)
}))
DeferCleanup(origin.Close)
target := filepath.Join(GinkgoT().TempDir(), "weights.bin")
err := downloader.URI(origin.URL+"/resolve").DownloadFileWithContext(
context.Background(),
target,
"",
0,
1,
nil,
downloader.WithBearerToken("secret-token"),
)
Expect(err).NotTo(HaveOccurred())
Expect(originAuth).To(Equal("Bearer secret-token"))
Expect(cdnAuth).To(BeEmpty())
})
It("authenticates range probes and reports resumed bytes", func() {
var mu sync.Mutex
seen := make(map[string]string)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
seen[r.Method] = r.Header.Get("Authorization")
mu.Unlock()
if r.Method == http.MethodHead {
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Length", "6")
return
}
Expect(r.Header.Get("Range")).To(Equal("bytes=3-"))
w.Header().Set("Content-Length", "3")
w.Header().Set("Content-Range", "bytes 3-5/6")
w.WriteHeader(http.StatusPartialContent)
_, _ = w.Write([]byte("def"))
}))
DeferCleanup(server.Close)
target := filepath.Join(GinkgoT().TempDir(), "resume.bin")
Expect(os.WriteFile(target+".partial", []byte("abc"), 0o600)).To(Succeed())
events := []downloader.TransferProgress{}
err := downloader.URI(server.URL+"/weights").DownloadFileWithContext(
context.Background(),
target,
"",
0,
1,
nil,
downloader.WithBearerToken("resume-token"),
downloader.WithTransferProgress(func(event downloader.TransferProgress) {
events = append(events, event)
}),
)
Expect(err).NotTo(HaveOccurred())
Expect(seen).To(HaveKeyWithValue(http.MethodHead, "Bearer resume-token"))
Expect(seen).To(HaveKeyWithValue(http.MethodGet, "Bearer resume-token"))
Expect(events).NotTo(BeEmpty())
Expect(events[len(events)-1].Written).To(Equal(int64(6)))
Expect(events[len(events)-1].Total).To(Equal(int64(6)))
Expect(os.ReadFile(target)).To(Equal([]byte("abcdef")))
})
It("rejects an origin that ignores a resume range", func() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodHead {
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Length", "6")
return
}
Expect(r.Header.Get("Range")).To(Equal("bytes=3-"))
w.Header().Set("Content-Length", "6")
_, _ = w.Write([]byte("abcdef"))
}))
DeferCleanup(server.Close)
target := filepath.Join(GinkgoT().TempDir(), "ignored-range.bin")
Expect(os.WriteFile(target+".partial", []byte("abc"), 0o600)).To(Succeed())
err := downloader.URI(server.URL).DownloadFileWithContext(
context.Background(),
target,
"",
0,
1,
nil,
)
Expect(err).To(MatchError(ContainSubstring("status 200 instead of 206")))
Expect(target).NotTo(BeAnExistingFile())
Expect(target + ".partial").NotTo(BeAnExistingFile())
})
It("creates resumable partial files with owner-only permissions", func() {
ctx, cancel := context.WithCancel(context.Background())
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Length", "5")
_, _ = w.Write([]byte("model"))
}))
DeferCleanup(server.Close)
target := filepath.Join(GinkgoT().TempDir(), "private.bin")
err := downloader.URI(server.URL).DownloadFileWithContext(
ctx,
target,
"",
0,
1,
nil,
downloader.WithTransferProgress(func(downloader.TransferProgress) {
cancel()
}),
)
Expect(errors.Is(err, context.Canceled)).To(BeTrue())
info, statErr := os.Stat(target + ".partial")
Expect(statErr).NotTo(HaveOccurred())
Expect(info.Mode().Perm()).To(Equal(os.FileMode(0o600)))
})
It("keeps the legacy total empty when the response length is unknown", func() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
_, _ = w.Write([]byte("model"))
}))
DeferCleanup(server.Close)
target := filepath.Join(GinkgoT().TempDir(), "unknown-size.bin")
totals := []string{}
err := downloader.URI(server.URL).DownloadFileWithContext(
context.Background(),
target,
"",
0,
1,
func(_, _, total string, _ float64) {
totals = append(totals, total)
},
)
Expect(err).NotTo(HaveOccurred())
Expect(totals).NotTo(BeEmpty())
Expect(totals[len(totals)-1]).To(BeEmpty())
})
})