Compare commits

...

1 Commits

Author SHA1 Message Date
localai-org-maint-bot
bc8113a8d6 fix(gallery): persist inference defaults under parameters
Gallery installs merged family defaults at the YAML root and only re-marshaled them on the artifact path. Persist the defaults in the loader-visible parameters map for every install path while preserving authored overrides.

Assisted-by: Codex:gpt-5
2026-07-31 18:06:29 +00:00
3 changed files with 115 additions and 19 deletions

View File

@@ -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"]))
})
})

View File

@@ -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))
})
})

View File

@@ -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 {