Merge pull request #750 from router-for-me/config

fix(config): preserve original config structure and avoid default value pollution
This commit is contained in:
Luis Pater
2025-12-27 22:10:01 +08:00
committed by GitHub
6 changed files with 58 additions and 39 deletions
+1 -2
View File
@@ -13,8 +13,6 @@ Dockerfile
docs/* docs/*
README.md README.md
README_CN.md README_CN.md
MANAGEMENT_API.md
MANAGEMENT_API_CN.md
LICENSE LICENSE
# Runtime data folders (should be mounted as volumes) # Runtime data folders (should be mounted as volumes)
@@ -32,3 +30,4 @@ bin/*
.agent/* .agent/*
.bmad/* .bmad/*
_bmad/* _bmad/*
_bmad-output/*
+6 -1
View File
@@ -11,11 +11,15 @@ bin/*
logs/* logs/*
conv/* conv/*
temp/* temp/*
refs/*
# Storage backends
pgstore/* pgstore/*
gitstore/* gitstore/*
objectstore/* objectstore/*
# Static assets
static/* static/*
refs/*
# Authentication data # Authentication data
auths/* auths/*
@@ -35,6 +39,7 @@ GEMINI.md
.agent/* .agent/*
.bmad/* .bmad/*
_bmad/* _bmad/*
_bmad-output/*
# macOS # macOS
.DS_Store .DS_Store
+42 -26
View File
@@ -817,8 +817,8 @@ func getOrCreateMapValue(mapNode *yaml.Node, key string) *yaml.Node {
} }
// mergeMappingPreserve merges keys from src into dst mapping node while preserving // mergeMappingPreserve merges keys from src into dst mapping node while preserving
// key order and comments of existing keys in dst. Unknown keys from src are appended // key order and comments of existing keys in dst. New keys are only added if their
// to dst at the end, copying their node structure from src. // value is non-zero to avoid polluting the config with defaults.
func mergeMappingPreserve(dst, src *yaml.Node) { func mergeMappingPreserve(dst, src *yaml.Node) {
if dst == nil || src == nil { if dst == nil || src == nil {
return return
@@ -829,20 +829,19 @@ func mergeMappingPreserve(dst, src *yaml.Node) {
copyNodeShallow(dst, src) copyNodeShallow(dst, src)
return return
} }
// Build a lookup of existing keys in dst
for i := 0; i+1 < len(src.Content); i += 2 { for i := 0; i+1 < len(src.Content); i += 2 {
sk := src.Content[i] sk := src.Content[i]
sv := src.Content[i+1] sv := src.Content[i+1]
idx := findMapKeyIndex(dst, sk.Value) idx := findMapKeyIndex(dst, sk.Value)
if idx >= 0 { if idx >= 0 {
// Merge into existing value node // Merge into existing value node (always update, even to zero values)
dv := dst.Content[idx+1] dv := dst.Content[idx+1]
mergeNodePreserve(dv, sv) mergeNodePreserve(dv, sv)
} else { } else {
if shouldSkipEmptyCollectionOnPersist(sk.Value, sv) { // New key: only add if value is non-zero to avoid polluting config with defaults
if isZeroValueNode(sv) {
continue continue
} }
// Append new key/value pair by deep-copying from src
dst.Content = append(dst.Content, deepCopyNode(sk), deepCopyNode(sv)) dst.Content = append(dst.Content, deepCopyNode(sk), deepCopyNode(sv))
} }
} }
@@ -925,32 +924,49 @@ func findMapKeyIndex(mapNode *yaml.Node, key string) int {
return -1 return -1
} }
func shouldSkipEmptyCollectionOnPersist(key string, node *yaml.Node) bool { // isZeroValueNode returns true if the YAML node represents a zero/default value
switch key { // that should not be written as a new key to preserve config cleanliness.
case "generative-language-api-key", // For mappings and sequences, recursively checks if all children are zero values.
"gemini-api-key", func isZeroValueNode(node *yaml.Node) bool {
"vertex-api-key",
"claude-api-key",
"codex-api-key",
"openai-compatibility":
return isEmptyCollectionNode(node)
default:
return false
}
}
func isEmptyCollectionNode(node *yaml.Node) bool {
if node == nil { if node == nil {
return true return true
} }
switch node.Kind { switch node.Kind {
case yaml.SequenceNode:
return len(node.Content) == 0
case yaml.ScalarNode: case yaml.ScalarNode:
return node.Tag == "!!null" switch node.Tag {
default: case "!!bool":
return false return node.Value == "false"
case "!!int", "!!float":
return node.Value == "0" || node.Value == "0.0"
case "!!str":
return node.Value == ""
case "!!null":
return true
}
case yaml.SequenceNode:
if len(node.Content) == 0 {
return true
}
// Check if all elements are zero values
for _, child := range node.Content {
if !isZeroValueNode(child) {
return false
}
}
return true
case yaml.MappingNode:
if len(node.Content) == 0 {
return true
}
// Check if all values are zero values (values are at odd indices)
for i := 1; i < len(node.Content); i += 2 {
if !isZeroValueNode(node.Content[i]) {
return false
}
}
return true
} }
return false
} }
// deepCopyNode creates a deep copy of a yaml.Node graph. // deepCopyNode creates a deep copy of a yaml.Node graph.
+4 -4
View File
@@ -30,13 +30,13 @@ type SDKConfig struct {
// StreamingConfig holds server streaming behavior configuration. // StreamingConfig holds server streaming behavior configuration.
type StreamingConfig struct { type StreamingConfig struct {
// KeepAliveSeconds controls how often the server emits SSE heartbeats (": keep-alive\n\n"). // KeepAliveSeconds controls how often the server emits SSE heartbeats (": keep-alive\n\n").
// nil means default (15 seconds). <= 0 disables keep-alives. // <= 0 disables keep-alives. Default is 0.
KeepAliveSeconds *int `yaml:"keepalive-seconds,omitempty" json:"keepalive-seconds,omitempty"` KeepAliveSeconds int `yaml:"keepalive-seconds,omitempty" json:"keepalive-seconds,omitempty"`
// BootstrapRetries controls how many times the server may retry a streaming request before any bytes are sent, // BootstrapRetries controls how many times the server may retry a streaming request before any bytes are sent,
// to allow auth rotation / transient recovery. // to allow auth rotation / transient recovery.
// nil means default (2). 0 disables bootstrap retries. // <= 0 disables bootstrap retries. Default is 0.
BootstrapRetries *int `yaml:"bootstrap-retries,omitempty" json:"bootstrap-retries,omitempty"` BootstrapRetries int `yaml:"bootstrap-retries,omitempty" json:"bootstrap-retries,omitempty"`
} }
// AccessConfig groups request authentication providers. // AccessConfig groups request authentication providers.
+4 -4
View File
@@ -104,8 +104,8 @@ func BuildErrorResponseBody(status int, errText string) []byte {
// Returning 0 disables keep-alives (default when unset). // Returning 0 disables keep-alives (default when unset).
func StreamingKeepAliveInterval(cfg *config.SDKConfig) time.Duration { func StreamingKeepAliveInterval(cfg *config.SDKConfig) time.Duration {
seconds := defaultStreamingKeepAliveSeconds seconds := defaultStreamingKeepAliveSeconds
if cfg != nil && cfg.Streaming.KeepAliveSeconds != nil { if cfg != nil {
seconds = *cfg.Streaming.KeepAliveSeconds seconds = cfg.Streaming.KeepAliveSeconds
} }
if seconds <= 0 { if seconds <= 0 {
return 0 return 0
@@ -116,8 +116,8 @@ func StreamingKeepAliveInterval(cfg *config.SDKConfig) time.Duration {
// StreamingBootstrapRetries returns how many times a streaming request may be retried before any bytes are sent. // StreamingBootstrapRetries returns how many times a streaming request may be retried before any bytes are sent.
func StreamingBootstrapRetries(cfg *config.SDKConfig) int { func StreamingBootstrapRetries(cfg *config.SDKConfig) int {
retries := defaultStreamingBootstrapRetries retries := defaultStreamingBootstrapRetries
if cfg != nil && cfg.Streaming.BootstrapRetries != nil { if cfg != nil {
retries = *cfg.Streaming.BootstrapRetries retries = cfg.Streaming.BootstrapRetries
} }
if retries < 0 { if retries < 0 {
retries = 0 retries = 0
@@ -94,10 +94,9 @@ func TestExecuteStreamWithAuthManager_RetriesBeforeFirstByte(t *testing.T) {
registry.GetGlobalRegistry().UnregisterClient(auth2.ID) registry.GetGlobalRegistry().UnregisterClient(auth2.ID)
}) })
bootstrapRetries := 1
handler := NewBaseAPIHandlers(&sdkconfig.SDKConfig{ handler := NewBaseAPIHandlers(&sdkconfig.SDKConfig{
Streaming: sdkconfig.StreamingConfig{ Streaming: sdkconfig.StreamingConfig{
BootstrapRetries: &bootstrapRetries, BootstrapRetries: 1,
}, },
}, manager) }, manager)
dataChan, errChan := handler.ExecuteStreamWithAuthManager(context.Background(), "openai", "test-model", []byte(`{"model":"test-model"}`), "") dataChan, errChan := handler.ExecuteStreamWithAuthManager(context.Background(), "openai", "test-model", []byte(`{"model":"test-model"}`), "")