feat(gallery): resolve meta model entries to hardware-appropriate variants at install

Meta gallery entries carry an ordered candidate list; at install time the
first candidate the host satisfies is resolved and its payload installed
under the meta's name, so the model keeps a stable name regardless of which
variant backs it. The resolution is recorded in the installed gallery
config so a reinstall honors a prior pin and operators can see the backing
variant.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto
2026-07-18 10:19:47 +00:00
parent ba33c21567
commit 49fa504160
3 changed files with 179 additions and 2 deletions

View File

@@ -41,3 +41,77 @@ candidates:
Expect(m.Candidates[1].Capability).To(BeEmpty())
})
})
var _ = Describe("ResolveMetaModel", func() {
gib := func(n uint64) uint64 { return n * 1024 * 1024 * 1024 }
newModel := func(name, url, description, icon string) *gallery.GalleryModel {
m := &gallery.GalleryModel{}
m.Name = name
m.URL = url
m.Description = description
m.Icon = icon
return m
}
var models []*gallery.GalleryModel
var meta *gallery.GalleryModel
BeforeEach(func() {
concreteVLLM := newModel("qwen3-8b-vllm-awq", "file://vllm.yaml", "AWQ variant", "vllm.png")
concreteGGUF := newModel("qwen3-8b-gguf-q4", "file://gguf.yaml", "GGUF variant", "gguf.png")
meta = newModel("qwen3-8b", "file://gguf.yaml", "Qwen3 8B", "qwen.png")
meta.Tags = []string{"llm"}
meta.Candidates = []gallery.Candidate{
{Model: "qwen3-8b-vllm-awq", Capability: "nvidia", MinVRAM: "20GiB"},
{Model: "qwen3-8b-gguf-q4"},
}
models = []*gallery.GalleryModel{concreteVLLM, concreteGGUF, meta}
})
It("installs the concrete payload under the meta's name", func() {
resolved, candidate, err := gallery.ResolveMetaModel(models, meta, gallery.ResolveEnv{Capability: "nvidia", VRAM: gib(24)}, "")
Expect(err).ToNot(HaveOccurred())
Expect(candidate.Model).To(Equal("qwen3-8b-vllm-awq"))
Expect(resolved.Name).To(Equal("qwen3-8b"))
Expect(resolved.URL).To(Equal("file://vllm.yaml"))
})
It("presents the meta's metadata, not the variant's", func() {
resolved, _, err := gallery.ResolveMetaModel(models, meta, gallery.ResolveEnv{Capability: "nvidia", VRAM: gib(24)}, "")
Expect(err).ToNot(HaveOccurred())
Expect(resolved.Description).To(Equal("Qwen3 8B"))
Expect(resolved.Icon).To(Equal("qwen.png"))
Expect(resolved.Tags).To(ConsistOf("llm"))
})
It("keeps the name stable when a variant is pinned", func() {
resolved, candidate, err := gallery.ResolveMetaModel(models, meta, gallery.ResolveEnv{Capability: "nvidia", VRAM: gib(24)}, "qwen3-8b-gguf-q4")
Expect(err).ToNot(HaveOccurred())
Expect(candidate.Model).To(Equal("qwen3-8b-gguf-q4"))
Expect(resolved.Name).To(Equal("qwen3-8b"))
Expect(resolved.URL).To(Equal("file://gguf.yaml"))
})
It("errors when a candidate references a missing entry", func() {
meta.Candidates = []gallery.Candidate{{Model: "does-not-exist"}}
_, _, err := gallery.ResolveMetaModel(models, meta, gallery.ResolveEnv{Capability: "default", VRAM: gib(8)}, "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("does-not-exist"))
})
It("refuses a candidate that is itself a meta entry", func() {
nested := newModel("nested", "", "", "")
nested.Candidates = []gallery.Candidate{{Model: "qwen3-8b-gguf-q4"}}
models = append(models, nested)
meta.Candidates = []gallery.Candidate{{Model: "nested"}}
_, _, err := gallery.ResolveMetaModel(models, meta, gallery.ResolveEnv{Capability: "default", VRAM: gib(8)}, "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("nested"))
})
It("surfaces a bad pin", func() {
_, _, err := gallery.ResolveMetaModel(models, meta, gallery.ResolveEnv{Capability: "nvidia", VRAM: gib(24)}, "nope")
Expect(err).To(MatchError(gallery.ErrPinNotFound))
})
})

View File

@@ -19,6 +19,7 @@ type ArtifactMaterializer interface {
type installOptions struct {
materializer ArtifactMaterializer
variant string
}
type InstallOption func(*installOptions)
@@ -31,6 +32,14 @@ func WithArtifactMaterializer(materializer ArtifactMaterializer) InstallOption {
}
}
// WithVariant pins a meta model entry to a specific candidate by name,
// bypassing hardware-based resolution. Ignored for non-meta entries.
func WithVariant(variant string) InstallOption {
return func(options *installOptions) {
options.variant = variant
}
}
func applyInstallOptions(options ...InstallOption) installOptions {
result := installOptions{materializer: modelartifacts.NewDefaultManager()}
for _, option := range options {

View File

@@ -60,6 +60,13 @@ type ModelConfig struct {
ConfigFile string `yaml:"config_file"`
Files []File `yaml:"files"`
PromptTemplates []PromptTemplate `yaml:"prompt_templates"`
// The fields below record how a meta entry was resolved, so a reinstall
// or upgrade can honor the same pin and so operators can see which
// variant a stable model name is actually backed by.
MetaName string `yaml:"meta_name,omitempty"`
ResolvedVariant string `yaml:"resolved_variant,omitempty"`
PinnedVariant string `yaml:"pinned_variant,omitempty"`
}
type File struct {
@@ -73,6 +80,50 @@ type PromptTemplate struct {
Content string `yaml:"content"`
}
// ResolveMetaModel turns a meta gallery entry into the concrete entry that
// should be installed on this host, returning it renamed to the meta's name
// and carrying the meta's presentation metadata.
//
// Why the metadata split: the payload (url, config_file, files, overrides)
// must come from the variant because that is what actually gets downloaded,
// while the presentation (name, description, icon, tags) must come from the
// meta so the installed model presents as the model rather than the variant.
func ResolveMetaModel(models []*GalleryModel, meta *GalleryModel, env ResolveEnv, pin string) (*GalleryModel, Candidate, error) {
candidate, err := ResolveCandidate(meta.Candidates, env, pin)
if err != nil {
return nil, Candidate{}, fmt.Errorf("resolving variant for model %q: %w", meta.Name, err)
}
// A pin is an operator override and deliberately bypasses the hardware
// checks, but a silent bypass makes a later out-of-memory failure
// impossible to trace back to the pin, so it is recorded loudly here.
if pin != "" {
if floor, declared, verr := candidate.EffectiveMinVRAM(); verr == nil && declared && env.VRAM < floor {
xlog.Warn("Pinned model variant declares more VRAM than this system reports; installing anyway because the pin overrides hardware resolution",
"model", meta.Name, "variant", candidate.Model, "required_vram", floor, "available_vram", env.VRAM)
}
}
concrete := FindGalleryElement(models, candidate.Model)
if concrete == nil {
return nil, Candidate{}, fmt.Errorf("model %q references variant %q which does not exist in any configured gallery", meta.Name, candidate.Model)
}
if concrete.IsMeta() {
return nil, Candidate{}, fmt.Errorf("model %q references variant %q which is itself a meta entry; meta entries may not nest", meta.Name, candidate.Model)
}
resolved := *concrete
resolved.Name = meta.Name
resolved.Description = meta.Description
resolved.Icon = meta.Icon
resolved.License = meta.License
resolved.URLs = meta.URLs
resolved.Tags = meta.Tags
resolved.Candidates = nil
return &resolved, candidate, nil
}
// Installs a model from the gallery
func InstallModelFromGallery(
ctx context.Context,
@@ -81,7 +132,9 @@ func InstallModelFromGallery(
modelLoader *model.ModelLoader,
name string, req GalleryModel, downloadStatus func(string, string, string, float64), enforceScan, automaticallyInstallBackend, requireBackendIntegrity bool, options ...InstallOption) error {
applyModel := func(model *GalleryModel) error {
installOpts := applyInstallOptions(options...)
applyModel := func(model *GalleryModel, record *ModelConfig) error {
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
var config ModelConfig
@@ -113,6 +166,12 @@ func InstallModelFromGallery(
return fmt.Errorf("invalid gallery model %+v", model)
}
if record != nil {
config.MetaName = record.MetaName
config.ResolvedVariant = record.ResolvedVariant
config.PinnedVariant = record.PinnedVariant
}
installName := model.Name
if req.Name != "" {
installName = req.Name
@@ -157,7 +216,42 @@ func InstallModelFromGallery(
return fmt.Errorf("no model found with name %q", name)
}
return applyModel(model)
// Meta-ness is checked before anything looks at the URL: a meta entry also
// carries a url as a fallback for older LocalAI releases that do not
// understand candidates, so carrying both is normal and meta wins here.
if !model.IsMeta() {
return applyModel(model, nil)
}
pin := installOpts.variant
// A previously recorded pin survives reinstalls and upgrades, so a user
// who deliberately chose a variant is not silently re-resolved onto a
// different one by a hardware or gallery change.
if pin == "" {
if previous, err := GetLocalModelConfiguration(systemState.Model.ModelsPath, model.Name); err == nil && previous != nil {
pin = previous.PinnedVariant
}
}
env := ResolveEnv{
Capability: systemState.DetectedCapability(),
VRAM: systemState.VRAM,
}
resolved, candidate, err := ResolveMetaModel(models, model, env, pin)
if err != nil {
return err
}
xlog.Info("Resolved meta model to variant",
"model", model.Name, "variant", candidate.Model,
"capability", env.Capability, "vram", env.VRAM, "pinned", pin != "")
return applyModel(resolved, &ModelConfig{
MetaName: model.Name,
ResolvedVariant: candidate.Model,
PinnedVariant: pin,
})
}
func InstallModel(ctx context.Context, systemState *system.SystemState, nameOverride string, galleryConfig *ModelConfig, configOverrides map[string]any, downloadStatus func(string, string, string, float64), enforceScan bool, options ...InstallOption) (*lconfig.ModelConfig, error) {