Skip to content

fix: prevent DefaultConfig template values from leaking into user model_list entries#724

Merged
yinwm merged 1 commit intosipeed:mainfrom
mqyang56:fix/model-list-default-value-leak
Feb 24, 2026
Merged

fix: prevent DefaultConfig template values from leaking into user model_list entries#724
yinwm merged 1 commit intosipeed:mainfrom
mqyang56:fix/model-list-default-value-leak

Conversation

@mqyang56
Copy link

Summary

Fixes #721

When DefaultConfig() pre-populates ModelList with ~18 template entries, Go's JSON decoder reuses the existing slice backing-array elements rather than zero-initializing them. This means fields absent from the user's JSON (e.g. api_base) silently inherit the template value at the same index position — for example, ModelList[0] is the Zhipu glm-4.7 entry with APIBase: "https://open.bigmodel.cn/api/paas/v4", which leaks into any user-defined model at index 0 that omits api_base.

Fix: pre-scan the JSON into a zero-value Config to count user-provided model_list entries. Only reset cfg.ModelList = nil when the user provides >0 entries, ensuring clean zero-value initialization during the real unmarshal. When the user provides 0 entries, the DefaultConfig built-in list is preserved as a fallback.

Behavior change

Scenario Before After
User model_list has 0 entries DefaultConfig list used (correct) DefaultConfig list used (correct)
User model_list has ≥1 entries, some fields omitted Omitted fields leaked from DefaultConfig template Omitted fields are zero-valued (correct)

Test plan

  • All existing pkg/config tests pass
  • Full project builds cleanly (go build ./...)
  • Manual: set a single model_list entry with no api_base → confirm the model entry has empty api_base instead of the Zhipu template URL

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings February 24, 2026 10:00
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a config-loading bug where DefaultConfig()’s pre-populated ModelList entries could leak template field values (e.g., api_base) into user-provided model_list entries due to encoding/json reusing slice backing arrays during unmarshaling.

Changes:

  • Pre-scan the config JSON into a temporary Config to detect whether the user provided any model_list entries.
  • If the user provided entries, reset cfg.ModelList to nil before the real unmarshal to force zero-value initialization of ModelConfig elements.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +508 to +512
var tmp Config
if err := json.Unmarshal(data, &tmp); err != nil {
return nil, err
}
if len(tmp.ModelList) > 0 {
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pre-scan unmarshals the entire Config twice just to detect whether model_list has entries. That means running all nested decoding/custom unmarshaling twice (e.g., AgentModelConfig/FlexibleStringSlice) and doing extra allocations. Consider unmarshaling only the presence/length of model_list using a small struct with json.RawMessage/[]json.RawMessage (or *[]... to detect presence) so the main unmarshal runs only once.

Suggested change
var tmp Config
if err := json.Unmarshal(data, &tmp); err != nil {
return nil, err
}
if len(tmp.ModelList) > 0 {
type modelListEnvelope struct {
ModelList *[]json.RawMessage `json:"model_list"`
}
var pre modelListEnvelope
if err := json.Unmarshal(data, &pre); err != nil {
return nil, err
}
if pre.ModelList != nil && len(*pre.ModelList) > 0 {

Copilot uses AI. Check for mistakes.
Comment on lines +502 to +507
// Pre-scan the JSON to check how many model_list entries the user provided.
// Go's JSON decoder reuses existing slice backing-array elements rather than
// zero-initializing them, so fields absent from the user's JSON (e.g. api_base)
// would silently inherit values from the DefaultConfig template at the same
// index position. We only reset cfg.ModelList when the user actually provides
// entries; when count is 0 we keep DefaultConfig's built-in list as fallback.
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic len(tmp.ModelList) > 0 can’t distinguish between (a) model_list missing and (b) model_list present but empty ([]) or null—all yield length 0 in tmp. If the intended behavior is “missing/empty keeps DefaultConfig model list as fallback”, the subsequent json.Unmarshal(data, cfg) will still clear the default slice when model_list: [] is explicitly present. Either adjust the comment/PR semantics to state the fallback only applies when the field is omitted, or change the pre-scan to detect field presence (e.g., *[]json.RawMessage or map[string]json.RawMessage) and handle empty-vs-missing explicitly.

Suggested change
// Pre-scan the JSON to check how many model_list entries the user provided.
// Go's JSON decoder reuses existing slice backing-array elements rather than
// zero-initializing them, so fields absent from the user's JSON (e.g. api_base)
// would silently inherit values from the DefaultConfig template at the same
// index position. We only reset cfg.ModelList when the user actually provides
// entries; when count is 0 we keep DefaultConfig's built-in list as fallback.
// Pre-scan the JSON to check whether the user provided any model_list entries.
// Go's JSON decoder reuses existing slice backing-array elements rather than
// zero-initializing them, so fields absent from the user's JSON (e.g. api_base)
// would silently inherit values from the DefaultConfig template at the same
// index position. We only reset cfg.ModelList when the user actually provides
// one or more entries. If model_list is omitted entirely, DefaultConfig's
// built-in list is kept as a fallback; if model_list is present (even if empty
// or null), the subsequent unmarshal will override and disable that fallback.

Copilot uses AI. Check for mistakes.
Comment on lines +502 to 516
// Pre-scan the JSON to check how many model_list entries the user provided.
// Go's JSON decoder reuses existing slice backing-array elements rather than
// zero-initializing them, so fields absent from the user's JSON (e.g. api_base)
// would silently inherit values from the DefaultConfig template at the same
// index position. We only reset cfg.ModelList when the user actually provides
// entries; when count is 0 we keep DefaultConfig's built-in list as fallback.
var tmp Config
if err := json.Unmarshal(data, &tmp); err != nil {
return nil, err
}
if len(tmp.ModelList) > 0 {
cfg.ModelList = nil
}

if err := json.Unmarshal(data, cfg); err != nil {
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change fixes a subtle encoding/json slice-reuse bug but currently has no direct regression test. Please add a LoadConfig unit test that writes a config with model_list containing a single entry that omits api_base, calls LoadConfig, and asserts the resulting cfg.ModelList[0].APIBase is empty (and does not inherit the DefaultConfig template value). A second assertion for the “model_list omitted” case preserving DefaultConfig can help lock in the intended fallback behavior.

Copilot uses AI. Check for mistakes.
Comment on lines 619 to 623
func (c *Config) GetModelConfig(modelName string) (*ModelConfig, error) {
matches := c.findMatches(modelName)
if len(matches) == 0 {
return nil, fmt.Errorf("model %q not found in model_list or providers", modelName)
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetModelConfig reports "not found in model_list or providers", but findMatches only searches ModelList and does not consult Providers (nor does it trigger conversion). This makes the error message misleading; either update it to only mention model_list, or extend the lookup to include a providers->model_list fallback if that behavior is desired.

Copilot uses AI. Check for mistakes.
@yinwm
Copy link
Collaborator

yinwm commented Feb 24, 2026

LGTM ✅

Copy link
Collaborator

@yinwm yinwm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - high quality bug fix. Accurately identifies a tricky Go encoding/json behavior and fixes it correctly with clear fallback semantics.

@yinwm yinwm merged commit 9cc0f8e into sipeed:main Feb 24, 2026
5 of 6 checks passed
@yinwm
Copy link
Collaborator

yinwm commented Feb 24, 2026

thanks for the pr

@Orgmar
Copy link
Contributor

Orgmar commented Feb 25, 2026

@mqyang56 That DefaultConfig leak was a sneaky one. Slice reuse during JSON decode silently inheriting template values at the same index is the kind of bug that's easy to miss. Clean fix with the pre-scan approach.

We're building a PicoClaw Dev Group on Discord for contributors to connect and collaborate. If you're interested, drop an email to support@sipeed.com with the subject [Join PicoClaw Dev Group] mqyang56 and we'll send the invite link your way.

hyperwd pushed a commit to hyperwd/picoclaw that referenced this pull request Mar 5, 2026
…lue-leak

fix: prevent DefaultConfig template values from leaking into user model_list entries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] DefaultConfig ModelList template values leak into user model_list entries via JSON decoder slice reuse

4 participants