diff --git a/core/gallery/backends_test.go b/core/gallery/backends_test.go index 96ffe0fe521e..626cbf1da7f4 100644 --- a/core/gallery/backends_test.go +++ b/core/gallery/backends_test.go @@ -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 ( diff --git a/core/gallery/gallery.go b/core/gallery/gallery.go index 6add8cfa73f8..035bfcde8af5 100644 --- a/core/gallery/gallery.go +++ b/core/gallery/gallery.go @@ -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) { diff --git a/core/gallery/gallery_test.go b/core/gallery/gallery_test.go index 3ba65f2d9a25..e2be9b19ba40 100644 --- a/core/gallery/gallery_test.go +++ b/core/gallery/gallery_test.go @@ -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")) + }) + }) }) diff --git a/core/services/backends_test.go b/core/services/backends_test.go index 077b2b182468..d1ebe77298e4 100644 --- a/core/services/backends_test.go +++ b/core/services/backends_test.go @@ -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() { diff --git a/core/services/models.go b/core/services/models.go index 383dee9b6e11..820e847b5ae3 100644 --- a/core/services/models.go +++ b/core/services/models.go @@ -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 ( diff --git a/go.mod b/go.mod index b8158af19472..89c7054c483d 100644 --- a/go.mod +++ b/go.mod @@ -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 ( diff --git a/go.sum b/go.sum index 43471df06aa1..1f6f28ecf7a5 100644 --- a/go.sum +++ b/go.sum @@ -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=