From ac25d0876085d9f3589145f14b1d8a470b78ce98 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Tue, 4 Aug 2026 04:17:10 +0000 Subject: [PATCH] fix(ds4): resolve repository imports to GGUF Select a concrete DeepSeek V4 Flash GGUF from Hugging Face metadata while honoring quantization preference order. Reject unresolved repository imports instead of emitting an invalid bare URI. Assisted-by: Codex:gpt-5 --- core/gallery/importers/ds4.go | 45 +++++++++++++++++++- core/gallery/importers/ds4_test.go | 67 ++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/core/gallery/importers/ds4.go b/core/gallery/importers/ds4.go index f34f7106f..7ad6c83f0 100644 --- a/core/gallery/importers/ds4.go +++ b/core/gallery/importers/ds4.go @@ -2,6 +2,7 @@ package importers import ( "encoding/json" + "fmt" "path/filepath" "strings" @@ -10,6 +11,7 @@ import ( "github.com/mudler/LocalAI/core/schema" "github.com/mudler/LocalAI/pkg/downloader" "github.com/mudler/LocalAI/pkg/functions" + hfapi "github.com/mudler/LocalAI/pkg/huggingface-api" "go.yaml.in/yaml/v2" ) @@ -115,7 +117,48 @@ func (i *DS4Importer) Import(details Details) (gallery.ModelConfig, error) { // filename to "ds4flash.gguf" to match ds4's own convention (its CLI // defaults to that path), so users can run the model without extra // config. - uri := downloader.URI(details.URI) + selectedURI := details.URI + uriFilename, _ := downloader.URI(details.URI).FilenameFromUrl() + if !strings.HasSuffix(strings.ToLower(uriFilename), ".gguf") { + if details.HuggingFace == nil { + return gallery.ModelConfig{}, fmt.Errorf("cannot select a compatible GGUF from repository %q without HuggingFace file metadata", details.URI) + } + + quantizations, _ := preferencesMap["quantizations"].(string) + var compatible []hfapi.ModelFile + for _, file := range details.HuggingFace.Files { + base := strings.ToLower(filepath.Base(file.Path)) + if strings.HasPrefix(base, "deepseek-v4-flash-") && strings.HasSuffix(base, ".gguf") { + compatible = append(compatible, file) + } + } + if len(compatible) == 0 { + return gallery.ModelConfig{}, fmt.Errorf("no compatible GGUF found in repository %q", details.URI) + } + + selected := compatible[0].URL + if quantizations != "" { + selected = compatible[len(compatible)-1].URL + matched := false + for _, quantization := range strings.Split(quantizations, ",") { + quantization = strings.ToLower(strings.TrimSpace(quantization)) + for _, file := range compatible { + base := strings.ToLower(filepath.Base(file.Path)) + if quantTokenMatches(base, quantization) { + selected = file.URL + matched = true + break + } + } + if matched { + break + } + } + } + selectedURI = selected + } + + uri := downloader.URI(selectedURI) cfg.Files = append(cfg.Files, gallery.File{ Filename: "ds4flash.gguf", URI: string(uri), diff --git a/core/gallery/importers/ds4_test.go b/core/gallery/importers/ds4_test.go index fcdd6ff72..9db9fd018 100644 --- a/core/gallery/importers/ds4_test.go +++ b/core/gallery/importers/ds4_test.go @@ -5,6 +5,7 @@ import ( "strings" . "github.com/mudler/LocalAI/core/gallery/importers" + hfapi "github.com/mudler/LocalAI/pkg/huggingface-api" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -65,5 +66,71 @@ var _ = Describe("DS4Importer", func() { "ConfigFile must specify backend: ds4, got: %s", cfg.ConfigFile) Expect(strings.Contains(cfg.ConfigFile, "use_tokenizer_template: true")).To(BeTrue()) }) + + It("preserves an explicit GGUF URI with a query string", func() { + uri := "https://huggingface.co/antirez/deepseek-v4-gguf/resolve/main/DeepSeek-V4-Flash-UD-IQ3_XXS.gguf?download=true" + + cfg, err := importer.Import(Details{URI: uri}) + + Expect(err).NotTo(HaveOccurred()) + Expect(cfg.Files).To(HaveLen(1)) + Expect(cfg.Files[0].URI).To(Equal(uri)) + }) + + It("selects the highest-priority requested quantization from a repository", func() { + details := Details{ + URI: "huggingface://antirez/deepseek-v4-gguf", + Preferences: json.RawMessage(`{"quantizations":"UD-IQ3_XXS,IQ2_XXS"}`), + HuggingFace: &hfapi.ModelDetails{Files: []hfapi.ModelFile{ + {Path: "DeepSeek-V4-Flash-IQ2_XXS.gguf", URL: "https://huggingface.co/antirez/deepseek-v4-gguf/resolve/main/DeepSeek-V4-Flash-IQ2_XXS.gguf"}, + {Path: "DeepSeek-V4-Flash-UD-IQ3_XXS.gguf", URL: "https://huggingface.co/antirez/deepseek-v4-gguf/resolve/main/DeepSeek-V4-Flash-UD-IQ3_XXS.gguf"}, + }}, + } + + cfg, err := importer.Import(details) + + Expect(err).NotTo(HaveOccurred()) + Expect(cfg.Files).To(HaveLen(1)) + Expect(cfg.Files[0].URI).To(Equal("https://huggingface.co/antirez/deepseek-v4-gguf/resolve/main/DeepSeek-V4-Flash-UD-IQ3_XXS.gguf")) + }) + + It("falls back to the last compatible GGUF when requested quantizations do not match", func() { + details := Details{ + URI: "huggingface://antirez/deepseek-v4-gguf", + Preferences: json.RawMessage(`{"quantizations":"F16"}`), + HuggingFace: &hfapi.ModelDetails{Files: []hfapi.ModelFile{ + {Path: "DeepSeek-V4-Flash-BF16.gguf", URL: "https://huggingface.co/antirez/deepseek-v4-gguf/resolve/main/DeepSeek-V4-Flash-BF16.gguf"}, + {Path: "README.md", URL: "https://huggingface.co/antirez/deepseek-v4-gguf/resolve/main/README.md"}, + {Path: "DeepSeek-V4-Flash-Q8_0.gguf", URL: "https://huggingface.co/antirez/deepseek-v4-gguf/resolve/main/DeepSeek-V4-Flash-Q8_0.gguf"}, + }}, + } + + cfg, err := importer.Import(details) + + Expect(err).NotTo(HaveOccurred()) + Expect(cfg.Files).To(HaveLen(1)) + Expect(cfg.Files[0].URI).To(Equal("https://huggingface.co/antirez/deepseek-v4-gguf/resolve/main/DeepSeek-V4-Flash-Q8_0.gguf")) + }) + + It("rejects a repository URI when no HuggingFace file metadata is available", func() { + _, err := importer.Import(Details{URI: "huggingface://antirez/deepseek-v4-gguf"}) + + Expect(err).To(MatchError(ContainSubstring("compatible GGUF"))) + }) + + It("rejects repository metadata without a compatible GGUF", func() { + details := Details{ + URI: "huggingface://antirez/deepseek-v4-gguf", + HuggingFace: &hfapi.ModelDetails{Files: []hfapi.ModelFile{ + {Path: "README.md", URL: "https://huggingface.co/antirez/deepseek-v4-gguf/resolve/main/README.md"}, + {Path: "unrelated.gguf", URL: "https://huggingface.co/antirez/deepseek-v4-gguf/resolve/main/unrelated.gguf"}, + }}, + } + + cfg, err := importer.Import(details) + + Expect(err).To(MatchError(ContainSubstring("compatible GGUF"))) + Expect(cfg.Files).To(BeEmpty()) + }) }) })