mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 09:57:57 -04:00
fix(model-artifacts): gate inferred artifact materialization by backend (#10910)
The managed-artifact materializer stages a HuggingFace snapshot into a directory (.artifacts/huggingface/<key>/snapshot/). That is the right load target for directory-consuming backends (transformers, vLLM, diffusers, ...), but PrimaryArtifactSpec inferred a managed artifact from ANY HuggingFace-shaped model reference regardless of backend. A single-file backend such as llama.cpp or whisper was therefore handed the snapshot directory instead of the weight file and failed to load it. The /import-model importer already guards this with a backend allow-list (managedArtifactBackends), but the loader-side inference did not. Move the allow-list into core/config as IsManagedArtifactBackend and apply it in PrimaryArtifactSpec: only directory-consuming backends may have an artifact inferred from a bare reference; every other backend stays on the legacy download-to-file path. An explicit artifacts: block still bypasses the gate, where single-file snapshot resolution handles the load path. The importer now shares the same predicate, so both paths agree on which backends auto-materialize. Assisted-by: Claude:opus-4.8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
c0a20d6ab1
commit
2dade4a9f9
@@ -9,6 +9,29 @@ import (
|
||||
"github.com/mudler/LocalAI/pkg/modelartifacts"
|
||||
)
|
||||
|
||||
// managedArtifactBackends is the set of backends that load a model from a
|
||||
// snapshot *directory* (a HuggingFace repo consumed as a folder of weights,
|
||||
// config, and tokenizer files). Only these backends may have a managed
|
||||
// artifact *inferred* from a bare model reference: single-file backends such
|
||||
// as llama.cpp or whisper would otherwise be handed the snapshot directory
|
||||
// instead of the weight file and fail to load it. The importer relies on the
|
||||
// same gate, so both paths agree on which backends auto-materialize.
|
||||
var managedArtifactBackends = map[string]struct{}{
|
||||
"transformers": {}, "huggingface-embeddings": {}, "sentencetransformers": {},
|
||||
"transformers-musicgen": {}, "mamba": {}, "diffusers": {}, "qwen-asr": {},
|
||||
"fish-speech": {}, "nemo": {}, "voxcpm": {}, "qwen-tts": {},
|
||||
"liquid-audio": {}, "vllm": {}, "vllm-omni": {}, "sglang": {},
|
||||
}
|
||||
|
||||
// IsManagedArtifactBackend reports whether backend consumes a model as a
|
||||
// snapshot directory and is therefore eligible for inferred artifact
|
||||
// materialization. An explicit artifacts: block bypasses this gate; single-file
|
||||
// resolution handles the load path for single-file backends in that case.
|
||||
func IsManagedArtifactBackend(backend string) bool {
|
||||
_, ok := managedArtifactBackends[backend]
|
||||
return ok
|
||||
}
|
||||
|
||||
// PrimaryArtifactSpec returns the managed primary artifact to materialize for
|
||||
// this config. The boolean return is false when the config should stay on the
|
||||
// legacy path.
|
||||
@@ -24,6 +47,12 @@ func (c ModelConfig) PrimaryArtifactSpec(modelsPath string) (modelartifacts.Spec
|
||||
if len(c.DownloadFiles) > 0 {
|
||||
return modelartifacts.Spec{}, false, false, nil
|
||||
}
|
||||
// Only directory-consuming backends may have an artifact inferred from a
|
||||
// bare reference; single-file backends stay on the legacy download-to-file
|
||||
// path so the backend receives the weight file itself, not its directory.
|
||||
if !IsManagedArtifactBackend(c.Backend) {
|
||||
return modelartifacts.Spec{}, false, false, nil
|
||||
}
|
||||
|
||||
if modelsPath != "" {
|
||||
for _, candidate := range []string{
|
||||
|
||||
53
core/config/model_artifact_inference_test.go
Normal file
53
core/config/model_artifact_inference_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
)
|
||||
|
||||
var _ = Describe("PrimaryArtifactSpec backend gating", func() {
|
||||
parse := func(doc string) config.ModelConfig {
|
||||
var c config.ModelConfig
|
||||
Expect(yaml.Unmarshal([]byte(doc), &c)).To(Succeed())
|
||||
return c
|
||||
}
|
||||
|
||||
It("does not infer a managed artifact for a single-file backend", func() {
|
||||
// llama.cpp consumes a single GGUF file, not a snapshot directory.
|
||||
// A bare HuggingFace file reference must stay on the legacy
|
||||
// download-to-file path so the backend receives the file itself.
|
||||
c := parse("backend: llama-cpp\nparameters: {model: huggingface://owner/repo/model.gguf}\n")
|
||||
_, inferred, managed, err := c.PrimaryArtifactSpec("/models")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(managed).To(BeFalse())
|
||||
Expect(inferred).To(BeFalse())
|
||||
})
|
||||
|
||||
It("does not infer a managed artifact for a bare repo on a single-file backend", func() {
|
||||
c := parse("backend: llama-cpp\nparameters: {model: owner/repo}\n")
|
||||
_, _, managed, err := c.PrimaryArtifactSpec("/models")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(managed).To(BeFalse())
|
||||
})
|
||||
|
||||
It("infers a managed artifact for a directory-consuming backend", func() {
|
||||
c := parse("backend: transformers\nparameters: {model: huggingface://owner/repo/model.safetensors}\n")
|
||||
spec, inferred, managed, err := c.PrimaryArtifactSpec("/models")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(managed).To(BeTrue())
|
||||
Expect(inferred).To(BeTrue())
|
||||
Expect(spec.Source.Repo).To(Equal("owner/repo"))
|
||||
})
|
||||
|
||||
It("keeps explicit artifacts managed even on a single-file backend", func() {
|
||||
// An explicitly declared artifacts: block is a deliberate choice;
|
||||
// single-file resolution (PrimaryFile) handles the load path.
|
||||
c := parse("backend: llama-cpp\nartifacts:\n - source: {type: huggingface, repo: owner/repo}\nparameters: {model: owner/repo}\n")
|
||||
_, _, managed, err := c.PrimaryArtifactSpec("/models")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(managed).To(BeTrue())
|
||||
})
|
||||
})
|
||||
@@ -12,13 +12,6 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var managedArtifactBackends = map[string]struct{}{
|
||||
"transformers": {}, "huggingface-embeddings": {}, "sentencetransformers": {},
|
||||
"transformers-musicgen": {}, "mamba": {}, "diffusers": {}, "qwen-asr": {},
|
||||
"fish-speech": {}, "nemo": {}, "voxcpm": {}, "qwen-tts": {},
|
||||
"liquid-audio": {}, "vllm": {}, "vllm-omni": {}, "sglang": {},
|
||||
}
|
||||
|
||||
// AttachPrimaryArtifact adds the controller-managed source only when the
|
||||
// importer selected the same repository and a migrated backend.
|
||||
func AttachPrimaryArtifact(model gallery.ModelConfig, details Details) (gallery.ModelConfig, error) {
|
||||
@@ -29,7 +22,7 @@ func AttachPrimaryArtifact(model gallery.ModelConfig, details Details) (gallery.
|
||||
if err := yaml.Unmarshal([]byte(model.ConfigFile), &cfg); err != nil {
|
||||
return gallery.ModelConfig{}, err
|
||||
}
|
||||
if _, supported := managedArtifactBackends[cfg.Backend]; !supported {
|
||||
if !config.IsManagedArtifactBackend(cfg.Backend) {
|
||||
return model, nil
|
||||
}
|
||||
if len(cfg.Artifacts) != 0 || cfg.Model != details.HuggingFace.ModelID {
|
||||
|
||||
Reference in New Issue
Block a user