Skip to content

Commit b743f48

Browse files
committed
fix(gallery): write installed inference defaults under parameters so the loader reads them back (#11230)
ModelConfig embeds schema.PredictionOptions with `yaml:"parameters"`, so the loader only reads temperature/top_p/top_k/min_p/repeat_penalty/ presence_penalty from the parameters: submap. The gallery installer wrote those family defaults at the top level of the model YAML, where nothing reads them back, leaving every persisted default inert on reload. Merge them into the parameters: submap instead (preserving any values the config already sets there), and add a regression test that installs a qwen3.5 model and asserts the defaults round-trip through the typed loader rather than landing as inert top-level keys. The test is fully offline: the definition declares no files, so no download is attempted. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Anai-Guo <antai12232931@outlook.com>
1 parent a6cf67c commit b743f48

2 files changed

Lines changed: 87 additions & 13 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package gallery_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/mudler/LocalAI/core/config"
9+
. "github.com/mudler/LocalAI/core/gallery"
10+
"github.com/mudler/LocalAI/pkg/system"
11+
. "github.com/onsi/ginkgo/v2"
12+
. "github.com/onsi/gomega"
13+
"gopkg.in/yaml.v3"
14+
)
15+
16+
var _ = Describe("gallery inference-default persistence", func() {
17+
It("persists inference defaults under parameters so the loader reads them back", func() {
18+
modelsPath, err := os.MkdirTemp("", "inference-defaults")
19+
Expect(err).ToNot(HaveOccurred())
20+
defer os.RemoveAll(modelsPath)
21+
22+
systemState, err := system.GetSystemState(system.WithModelPath(modelsPath))
23+
Expect(err).ToNot(HaveOccurred())
24+
25+
// A qwen3.5 name makes ApplyInferenceDefaults fill in the recommended
26+
// sampling parameters (repeat_penalty=1, presence_penalty=1.5, min_p=0).
27+
// Those belong under the parameters: key — ModelConfig embeds
28+
// schema.PredictionOptions with `yaml:"parameters"`, so the loader only
29+
// reads them from that submap (#11230). The install is fully offline:
30+
// the definition declares no files, so InstallModel just writes the YAML.
31+
definition := &ModelConfig{ConfigFile: `backend: transformers
32+
parameters:
33+
model: owner/repo
34+
`}
35+
36+
_, err = InstallModel(context.TODO(), systemState, "qwen3.5-managed", definition, map[string]any{}, func(string, string, string, float64) {}, false)
37+
Expect(err).ToNot(HaveOccurred())
38+
39+
data, err := os.ReadFile(filepath.Join(modelsPath, "qwen3.5-managed.yaml"))
40+
Expect(err).ToNot(HaveOccurred())
41+
42+
// The defaults must survive a round-trip through the typed loader.
43+
var reloaded config.ModelConfig
44+
Expect(yaml.Unmarshal(data, &reloaded)).To(Succeed())
45+
Expect(reloaded.PresencePenalty).To(BeNumerically("==", 1.5))
46+
Expect(reloaded.RepeatPenalty).To(BeNumerically("==", 1))
47+
Expect(reloaded.MinP).NotTo(BeNil())
48+
Expect(reloaded.Temperature).NotTo(BeNil())
49+
50+
// They must live under parameters:, never at the top level, or they are
51+
// silently dropped on reload.
52+
var raw map[string]any
53+
Expect(yaml.Unmarshal(data, &raw)).To(Succeed())
54+
Expect(raw).NotTo(HaveKey("presence_penalty"))
55+
Expect(raw).NotTo(HaveKey("repeat_penalty"))
56+
Expect(raw).NotTo(HaveKey("min_p"))
57+
parameters, ok := raw["parameters"].(map[string]any)
58+
Expect(ok).To(BeTrue())
59+
Expect(parameters).To(HaveKey("presence_penalty"))
60+
Expect(parameters).To(HaveKey("repeat_penalty"))
61+
Expect(parameters).To(HaveKey("min_p"))
62+
})
63+
})

core/gallery/models.go

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -270,36 +270,47 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver
270270
lconfig.ApplyInferenceDefaults(&modelConfig, name, modelConfig.Model)
271271

272272
// Merge inference defaults into configMap so they are persisted without losing unknown fields.
273+
// These sampling parameters live under the `parameters:` key on disk: ModelConfig
274+
// embeds schema.PredictionOptions with `yaml:"parameters"`, so the loader only reads
275+
// them from that submap. Writing them at the top level produced keys the loader never
276+
// read back, leaving the persisted defaults inert on reload (#11230).
277+
params, ok := configMap["parameters"].(map[string]any)
278+
if !ok {
279+
params = make(map[string]any)
280+
}
273281
if modelConfig.Temperature != nil {
274-
if _, exists := configMap["temperature"]; !exists {
275-
configMap["temperature"] = *modelConfig.Temperature
282+
if _, exists := params["temperature"]; !exists {
283+
params["temperature"] = *modelConfig.Temperature
276284
}
277285
}
278286
if modelConfig.TopP != nil {
279-
if _, exists := configMap["top_p"]; !exists {
280-
configMap["top_p"] = *modelConfig.TopP
287+
if _, exists := params["top_p"]; !exists {
288+
params["top_p"] = *modelConfig.TopP
281289
}
282290
}
283291
if modelConfig.TopK != nil {
284-
if _, exists := configMap["top_k"]; !exists {
285-
configMap["top_k"] = *modelConfig.TopK
292+
if _, exists := params["top_k"]; !exists {
293+
params["top_k"] = *modelConfig.TopK
286294
}
287295
}
288296
if modelConfig.MinP != nil {
289-
if _, exists := configMap["min_p"]; !exists {
290-
configMap["min_p"] = *modelConfig.MinP
297+
if _, exists := params["min_p"]; !exists {
298+
params["min_p"] = *modelConfig.MinP
291299
}
292300
}
293301
if modelConfig.RepeatPenalty != 0 {
294-
if _, exists := configMap["repeat_penalty"]; !exists {
295-
configMap["repeat_penalty"] = modelConfig.RepeatPenalty
302+
if _, exists := params["repeat_penalty"]; !exists {
303+
params["repeat_penalty"] = modelConfig.RepeatPenalty
296304
}
297305
}
298306
if modelConfig.PresencePenalty != 0 {
299-
if _, exists := configMap["presence_penalty"]; !exists {
300-
configMap["presence_penalty"] = modelConfig.PresencePenalty
307+
if _, exists := params["presence_penalty"]; !exists {
308+
params["presence_penalty"] = modelConfig.PresencePenalty
301309
}
302310
}
311+
if len(params) > 0 {
312+
configMap["parameters"] = params
313+
}
303314

304315
// Re-marshal from configMap to preserve unknown fields
305316
updatedConfigYAML, err = yaml.Marshal(configMap)
@@ -494,4 +505,4 @@ func SafetyScanGalleryModel(galleryModel *GalleryModel) error {
494505
}
495506
}
496507
return nil
497-
}
508+
}

0 commit comments

Comments
 (0)