fix(gallery): Use YAML v3 to avoid merging maps with incompatible keys (#8580)

Signed-off-by: Richard Palethorpe <io@richiejp.com>
This commit is contained in:
Richard Palethorpe
2026-02-16 13:10:19 +00:00
committed by GitHub
parent 109f29cc24
commit 074a982853
7 changed files with 64 additions and 9 deletions

View File

@@ -12,7 +12,7 @@ import (
"github.com/mudler/LocalAI/pkg/system"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
const (

View File

@@ -16,7 +16,7 @@ import (
"github.com/mudler/LocalAI/pkg/xsync"
"github.com/mudler/xlog"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
func GetGalleryConfigFromURL[T any](url string, basePath string) (T, error) {

View File

@@ -4,11 +4,12 @@ import (
"os"
"path/filepath"
"dario.cat/mergo"
"github.com/mudler/LocalAI/core/config"
. "github.com/mudler/LocalAI/core/gallery"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
var _ = Describe("Gallery", func() {
@@ -462,4 +463,60 @@ var _ = Describe("Gallery", func() {
Expect(result).To(BeNil())
})
})
Describe("YAML merge with nested maps", func() {
It("should handle YAML anchors and merges with nested overrides (regression test for nanbeige4.1)", func() {
// This tests the fix for the panic that occurred with yaml.v2:
// yaml.v2 produces map[interface{}]interface{} for nested maps
// which caused mergo.Merge to panic with "value of type interface {} is not assignable to type string"
// The exact YAML structure from gallery/index.yaml nanbeige4.1 entries
yamlContent := `---
- &nanbeige4
name: "nanbeige4.1-3b-q8"
overrides:
parameters:
model: nanbeige4.1-3b-q8_0.gguf
- !!merge <<: *nanbeige4
name: "nanbeige4.1-3b-q4"
overrides:
parameters:
model: nanbeige4.1-3b-q4_k_m.gguf
`
var models []GalleryModel
err := yaml.Unmarshal([]byte(yamlContent), &models)
Expect(err).NotTo(HaveOccurred())
Expect(models).To(HaveLen(2))
// Verify first model
Expect(models[0].Name).To(Equal("nanbeige4.1-3b-q8"))
Expect(models[0].Overrides).NotTo(BeNil())
Expect(models[0].Overrides["parameters"]).To(BeAssignableToTypeOf(map[string]interface{}{}))
params := models[0].Overrides["parameters"].(map[string]interface{})
Expect(params["model"]).To(Equal("nanbeige4.1-3b-q8_0.gguf"))
// Verify second model (merged)
Expect(models[1].Name).To(Equal("nanbeige4.1-3b-q4"))
Expect(models[1].Overrides).NotTo(BeNil())
Expect(models[1].Overrides["parameters"]).To(BeAssignableToTypeOf(map[string]interface{}{}))
params = models[1].Overrides["parameters"].(map[string]interface{})
Expect(params["model"]).To(Equal("nanbeige4.1-3b-q4_k_m.gguf"))
// Simulate the mergo.Merge call that was failing in models.go:251
// This should not panic with yaml.v3
configMap := make(map[string]interface{})
configMap["name"] = "test"
configMap["backend"] = "llama-cpp"
configMap["parameters"] = map[string]interface{}{
"model": "original.gguf",
}
err = mergo.Merge(&configMap, models[1].Overrides, mergo.WithOverride)
Expect(err).NotTo(HaveOccurred())
Expect(configMap["parameters"]).NotTo(BeNil())
// Verify the merge worked correctly
mergedParams := configMap["parameters"].(map[string]interface{})
Expect(mergedParams["model"]).To(Equal("nanbeige4.1-3b-q4_k_m.gguf"))
})
})
})

View File

@@ -12,7 +12,7 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
var _ = Describe("InstallExternalBackend", func() {

View File

@@ -13,7 +13,7 @@ import (
"github.com/mudler/LocalAI/pkg/system"
"github.com/mudler/LocalAI/pkg/utils"
"github.com/mudler/xlog"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
const (

4
go.mod
View File

@@ -61,7 +61,7 @@ require (
go.opentelemetry.io/otel/metric v1.40.0
go.opentelemetry.io/otel/sdk/metric v1.40.0
google.golang.org/grpc v1.78.0
gopkg.in/yaml.v2 v2.4.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
oras.land/oras-go/v2 v2.6.0
)
@@ -75,7 +75,7 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (

2
go.sum
View File

@@ -509,8 +509,6 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mudler/cogito v0.8.1 h1:66qPJkAMrq/Vo8AC/PvXWuVxYPhi7X2DQuJIilL8+3I=
github.com/mudler/cogito v0.8.1/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
github.com/mudler/cogito v0.8.2-0.20260214201734-da0d4ceb2b44 h1:joGszpItINnZdoL/0p2077Wz2xnxMGRSRgYN5mS7I4c=
github.com/mudler/cogito v0.8.2-0.20260214201734-da0d4ceb2b44/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
github.com/mudler/edgevpn v0.31.1 h1:7qegiDWd0kAg6ljhNHxqvp8hbo/6BbzSdbb7/2WZfiY=