feat(gallery): add nanbeige4.1-3b meta entry with hardware-resolved variants

Adds the first real meta entry to the gallery index. It resolves to the
Q8_0 build on hosts with at least 6GiB of VRAM and to the Q4_K_M build
everywhere else, installing either payload under the stable name
nanbeige4.1-3b.

The entry carries a url equal to its final candidate's url. LocalAI
releases that predate candidates support parse the index non-strictly
and drop the key silently, so without that url they would list the entry
and install nothing. A regression spec parses the index the way those
releases do and asserts every meta entry stays installable for them.

Also teaches core/schema/gallery-model.schema.json about candidates. The
schema sets additionalProperties: false at the top level, so an author
following CONTRIBUTING.md and adding the yaml-language-server comment
would otherwise get a validation error on this entry.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-07-18 11:41:16 +00:00
1 parent 98a0e3d7fa
commit d0d441bb43
3 files changed
+102

No files matched your search

+37
View File
@@ -368,3 +368,40 @@ var _ = Describe("InstallModelFromGallery with meta entries", func() {
Expect(record.URLs).To(ConsistOf("https://example.invalid/qwen3"))
})
})
var _ = Describe("legacy client compatibility", func() {
It("exposes a url on every meta entry to clients that ignore candidates", func() {
data, err := os.ReadFile(filepath.Join("..", "..", "gallery", "index.yaml"))
Expect(err).ToNot(HaveOccurred())
// Parse exactly as an older LocalAI release would: non-strictly, with
// no knowledge of the candidates key.
var legacy []struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
}
Expect(yaml.Unmarshal(data, &legacy)).To(Succeed())
var current []gallery.GalleryModel
Expect(yaml.Unmarshal(data, &current)).To(Succeed())
urlByName := map[string]string{}
for _, e := range legacy {
urlByName[e.Name] = e.URL
}
metaCount := 0
for _, e := range current {
if !e.IsMeta() {
continue
}
metaCount++
// Without a url an old client lists the entry and installs an
// empty model, because it silently drops candidates.
Expect(urlByName[e.Name]).ToNot(BeEmpty(),
"meta entry %q is invisible payload-wise to older clients", e.Name)
}
Expect(metaCount).To(BeNumerically(">", 0),
"expected at least one meta entry in the gallery index")
})
})
+36
View File
@@ -56,6 +56,42 @@
"additionalProperties": false
}
},
"candidates": {
"type": "array",
"description": "Ordered variant list of a meta entry. The first candidate the host satisfies is installed, under the meta entry's own name. An entry carrying candidates must not also carry files or config_file.",
"minItems": 1,
"items": {
"type": "object",
"required": ["model"],
"properties": {
"model": {
"type": "string",
"description": "Name of a concrete (non-meta) gallery entry"
},
"capability": {
"type": "string",
"description": "Host capability this candidate requires, matched exactly and case-sensitively (for example metal, nvidia-cuda-12). Absent matches any host."
},
"min_vram": {
"type": "string",
"description": "Authored VRAM floor, for example 20GiB. Authoritative: inference never overwrites it."
},
"backend": {
"type": "string",
"description": "Denormalized by the nightly job for display. Never authored by hand."
},
"quantization": {
"type": "string",
"description": "Denormalized by the nightly job for display. Never authored by hand."
},
"inferred_min_vram": {
"type": "string",
"description": "Denormalized by the nightly job as a fallback floor. Never authored by hand; min_vram wins."
}
},
"additionalProperties": false
}
},
"prompt_templates": {
"type": "array",
"description": "Prompt templates written as .tmpl files",