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:
+1
-2
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"}`), "")
|
||||||
|
|||||||
Reference in New Issue
Block a user