diff --git a/core/gallery/empty_base_install_test.go b/core/gallery/empty_base_install_test.go index 6b104e233f02..051c10907bd0 100644 --- a/core/gallery/empty_base_install_test.go +++ b/core/gallery/empty_base_install_test.go @@ -236,8 +236,8 @@ var _ = Describe("InstallModelFromGallery with an empty base config", func() { Expect(install(e.Name, gallery.GalleryModel{})).To(Succeed()) cfg := installedConfig(e.Name) Expect(cfg["name"]).To(Equal(e.Name)) - // The catalog's own overrides, verbatim, laid over the empty base. - Expect(cfg["parameters"]).To(Equal(e.Overrides["parameters"])) + // The catalog's model override survives inference-default enrichment. + Expect(cfg["parameters"]).To(HaveKeyWithValue("model", "LiquidAI_LFM2-1.2B-RAG-Q4_K_M.gguf")) Expect(cfg["known_usecases"]).To(Equal(e.Overrides["known_usecases"])) }) }) diff --git a/core/gallery/inference_defaults_install_test.go b/core/gallery/inference_defaults_install_test.go new file mode 100644 index 000000000000..50303a2a5a5f --- /dev/null +++ b/core/gallery/inference_defaults_install_test.go @@ -0,0 +1,90 @@ +package gallery_test + +import ( + "context" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" + + "github.com/mudler/LocalAI/core/gallery" + "github.com/mudler/LocalAI/pkg/modelartifacts" + "github.com/mudler/LocalAI/pkg/system" +) + +var _ = Describe("gallery inference defaults", func() { + readPersistedConfig := func(modelsPath, name string) map[string]any { + data, err := os.ReadFile(filepath.Join(modelsPath, name+".yaml")) + Expect(err).NotTo(HaveOccurred()) + persisted := map[string]any{} + Expect(yaml.Unmarshal(data, &persisted)).To(Succeed()) + return persisted + } + + expectNestedDefaults := func(persisted map[string]any) { + Expect(persisted).NotTo(HaveKey("temperature")) + Expect(persisted).NotTo(HaveKey("top_p")) + parameters, ok := persisted["parameters"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(parameters).To(HaveKeyWithValue("temperature", 0.7)) + Expect(parameters).To(HaveKeyWithValue("top_p", 0.42)) + Expect(parameters).To(HaveKeyWithValue("top_k", 20)) + Expect(parameters).To(HaveKeyWithValue("min_p", 0)) + Expect(parameters).To(HaveKeyWithValue("repeat_penalty", 1)) + Expect(parameters).To(HaveKeyWithValue("presence_penalty", 1.5)) + } + + It("persists defaults under parameters after artifact binding", func() { + modelsPath := GinkgoT().TempDir() + state, err := system.GetSystemState(system.WithModelPath(modelsPath)) + Expect(err).NotTo(HaveOccurred()) + resolved := modelartifacts.Spec{ + Name: "model", Target: "model", + Source: modelartifacts.Source{Type: "huggingface", Repo: "owner/qwen3.5-model", Revision: "main"}, + Resolved: &modelartifacts.Resolved{ + Endpoint: "https://huggingface.co", + Revision: "0123456789abcdef0123456789abcdef01234567", + CacheKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, + } + fake := &fakeArtifactMaterializer{result: modelartifacts.Result{Spec: resolved}} + definition := &gallery.ModelConfig{Name: "qwen3.5-artifact", ConfigFile: ` +backend: transformers +artifacts: + - name: model + target: model + source: {type: huggingface, repo: owner/qwen3.5-model} +parameters: + model: owner/qwen3.5-model + top_p: 0.42 +`} + + _, err = gallery.InstallModel(context.Background(), state, "", definition, nil, nil, false, + gallery.WithArtifactMaterializer(fake)) + Expect(err).NotTo(HaveOccurred()) + expectNestedDefaults(readPersistedConfig(modelsPath, definition.Name)) + }) + + It("persists defaults under parameters when the entry declares files", func() { + modelsPath := GinkgoT().TempDir() + state, err := system.GetSystemState(system.WithModelPath(modelsPath)) + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(filepath.Join(modelsPath, "weights.gguf"), []byte("weights"), 0644)).To(Succeed()) + definition := &gallery.ModelConfig{ + Name: "qwen3.5-files", + ConfigFile: ` +backend: llama-cpp +parameters: + model: weights.gguf + top_p: 0.42 +`, + Files: []gallery.File{{Filename: "weights.gguf", URI: "https://example.com/weights.gguf"}}, + } + + _, err = gallery.InstallModel(context.Background(), state, "", definition, nil, nil, false) + Expect(err).NotTo(HaveOccurred()) + expectNestedDefaults(readPersistedConfig(modelsPath, definition.Name)) + }) +}) diff --git a/core/gallery/models.go b/core/gallery/models.go index f3184648b15e..28d2435712ec 100644 --- a/core/gallery/models.go +++ b/core/gallery/models.go @@ -622,35 +622,41 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver lconfig.ApplyInferenceDefaults(&modelConfig, name, modelConfig.Model) // Merge inference defaults into configMap so they are persisted without losing unknown fields. + defaults := make(map[string]any) if modelConfig.Temperature != nil { - if _, exists := configMap["temperature"]; !exists { - configMap["temperature"] = *modelConfig.Temperature - } + defaults["temperature"] = *modelConfig.Temperature } if modelConfig.TopP != nil { - if _, exists := configMap["top_p"]; !exists { - configMap["top_p"] = *modelConfig.TopP - } + defaults["top_p"] = *modelConfig.TopP } if modelConfig.TopK != nil { - if _, exists := configMap["top_k"]; !exists { - configMap["top_k"] = *modelConfig.TopK - } + defaults["top_k"] = *modelConfig.TopK } if modelConfig.MinP != nil { - if _, exists := configMap["min_p"]; !exists { - configMap["min_p"] = *modelConfig.MinP - } + defaults["min_p"] = *modelConfig.MinP } if modelConfig.RepeatPenalty != 0 { - if _, exists := configMap["repeat_penalty"]; !exists { - configMap["repeat_penalty"] = modelConfig.RepeatPenalty - } + defaults["repeat_penalty"] = modelConfig.RepeatPenalty } if modelConfig.PresencePenalty != 0 { - if _, exists := configMap["presence_penalty"]; !exists { - configMap["presence_penalty"] = modelConfig.PresencePenalty + defaults["presence_penalty"] = modelConfig.PresencePenalty + } + if len(defaults) > 0 { + parameters, ok := configMap["parameters"].(map[string]any) + if !ok { + parameters = make(map[string]any) + configMap["parameters"] = parameters } + for key, value := range defaults { + if _, exists := parameters[key]; !exists { + parameters[key] = value + } + } + } + + updatedConfigYAML, err = yaml.Marshal(configMap) + if err != nil { + return nil, fmt.Errorf("failed to marshal config with inference defaults: %v", err) } if valid, err := modelConfig.Validate(); !valid {