diff --git a/internal/access/reconcile.go b/internal/access/reconcile.go index 7a69dd95..a1f153aa 100644 --- a/internal/access/reconcile.go +++ b/internal/access/reconcile.go @@ -79,51 +79,35 @@ func ReconcileProviders(oldCfg, newCfg *config.Config, existing []sdkaccess.Prov finalIDs[key] = struct{}{} } - if len(result) == 0 && len(newCfg.APIKeys) > 0 { - sdkConfig.SyncInlineAPIKeys(&newCfg.SDKConfig, newCfg.APIKeys) - if providerCfg := newCfg.ConfigAPIKeyProvider(); providerCfg != nil { - key := providerIdentifier(providerCfg) + if len(result) == 0 { + if inline := sdkConfig.MakeInlineAPIKeyProvider(newCfg.APIKeys); inline != nil { + key := providerIdentifier(inline) if key != "" { if oldCfgProvider, ok := oldCfgMap[key]; ok { - isAliased := oldCfgProvider == providerCfg - if !isAliased && providerConfigEqual(oldCfgProvider, providerCfg) { + if providerConfigEqual(oldCfgProvider, inline) { if existingProvider, okExisting := existingMap[key]; okExisting { result = append(result, existingProvider) - } else { - provider, buildErr := sdkaccess.BuildProvider(providerCfg, &newCfg.SDKConfig) - if buildErr != nil { - return nil, nil, nil, nil, buildErr - } - if _, existed := existingMap[key]; existed { - appendChange(&updated, key) - } else { - appendChange(&added, key) - } - result = append(result, provider) + finalIDs[key] = struct{}{} + goto inlineDone } - } else { - provider, buildErr := sdkaccess.BuildProvider(providerCfg, &newCfg.SDKConfig) - if buildErr != nil { - return nil, nil, nil, nil, buildErr - } - if _, existed := existingMap[key]; existed { - appendChange(&updated, key) - } else { - appendChange(&added, key) - } - result = append(result, provider) } - } else { - provider, buildErr := sdkaccess.BuildProvider(providerCfg, &newCfg.SDKConfig) - if buildErr != nil { - return nil, nil, nil, nil, buildErr - } - appendChange(&added, key) - result = append(result, provider) } + provider, buildErr := sdkaccess.BuildProvider(inline, &newCfg.SDKConfig) + if buildErr != nil { + return nil, nil, nil, nil, buildErr + } + if _, existed := existingMap[key]; existed { + appendChange(&updated, key) + } else if _, hadOld := oldCfgMap[key]; hadOld { + appendChange(&updated, key) + } else { + appendChange(&added, key) + } + result = append(result, provider) finalIDs[key] = struct{}{} } } + inlineDone: } removedSet := make(map[string]struct{}) @@ -192,7 +176,7 @@ func accessProviderMap(cfg *config.Config) map[string]*sdkConfig.AccessProvider result[key] = providerCfg } if len(result) == 0 && len(cfg.APIKeys) > 0 { - if provider := cfg.ConfigAPIKeyProvider(); provider != nil { + if provider := sdkConfig.MakeInlineAPIKeyProvider(cfg.APIKeys); provider != nil { if key := providerIdentifier(provider); key != "" { result[key] = provider } @@ -212,6 +196,11 @@ func collectProviderEntries(cfg *config.Config) []*sdkConfig.AccessProvider { entries = append(entries, providerCfg) } } + if len(entries) == 0 && len(cfg.APIKeys) > 0 { + if inline := sdkConfig.MakeInlineAPIKeyProvider(cfg.APIKeys); inline != nil { + entries = append(entries, inline) + } + } return entries } diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index 12fdf349..22bff1b3 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -7,7 +7,6 @@ import ( "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - sdkConfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" ) // Generic helpers for list[string] @@ -108,13 +107,16 @@ func (h *Handler) deleteFromStringList(c *gin.Context, target *[]string, after f // api-keys func (h *Handler) GetAPIKeys(c *gin.Context) { c.JSON(200, gin.H{"api-keys": h.cfg.APIKeys}) } func (h *Handler) PutAPIKeys(c *gin.Context) { - h.putStringList(c, func(v []string) { sdkConfig.SyncInlineAPIKeys(&h.cfg.SDKConfig, v) }, nil) + h.putStringList(c, func(v []string) { + h.cfg.APIKeys = append([]string(nil), v...) + h.cfg.Access.Providers = nil + }, nil) } func (h *Handler) PatchAPIKeys(c *gin.Context) { - h.patchStringList(c, &h.cfg.APIKeys, func() { sdkConfig.SyncInlineAPIKeys(&h.cfg.SDKConfig, h.cfg.APIKeys) }) + h.patchStringList(c, &h.cfg.APIKeys, func() { h.cfg.Access.Providers = nil }) } func (h *Handler) DeleteAPIKeys(c *gin.Context) { - h.deleteFromStringList(c, &h.cfg.APIKeys, func() { sdkConfig.SyncInlineAPIKeys(&h.cfg.SDKConfig, h.cfg.APIKeys) }) + h.deleteFromStringList(c, &h.cfg.APIKeys, func() { h.cfg.Access.Providers = nil }) } // generative-language-api-key @@ -203,7 +205,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) { // openai-compatibility: []OpenAICompatibility func (h *Handler) GetOpenAICompat(c *gin.Context) { - c.JSON(200, gin.H{"openai-compatibility": h.cfg.OpenAICompatibility}) + c.JSON(200, gin.H{"openai-compatibility": normalizedOpenAICompatibilityEntries(h.cfg.OpenAICompatibility)}) } func (h *Handler) PutOpenAICompat(c *gin.Context) { data, err := c.GetRawData() @@ -381,3 +383,22 @@ func normalizeOpenAICompatibilityEntry(entry *config.OpenAICompatibility) { } entry.APIKeys = nil } + +func normalizedOpenAICompatibilityEntries(entries []config.OpenAICompatibility) []config.OpenAICompatibility { + if len(entries) == 0 { + return nil + } + out := make([]config.OpenAICompatibility, len(entries)) + for i := range entries { + copyEntry := entries[i] + if len(copyEntry.APIKeyEntries) > 0 { + copyEntry.APIKeyEntries = append([]config.OpenAICompatibilityAPIKey(nil), copyEntry.APIKeyEntries...) + } + if len(copyEntry.APIKeys) > 0 { + copyEntry.APIKeys = append([]string(nil), copyEntry.APIKeys...) + } + normalizeOpenAICompatibilityEntry(©Entry) + out[i] = copyEntry + } + return out +} diff --git a/internal/config/config.go b/internal/config/config.go index de1d49eb..b9a34754 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -217,33 +217,12 @@ func syncInlineAccessProvider(cfg *Config) { if cfg == nil { return } - if len(cfg.Access.Providers) == 0 { - if len(cfg.APIKeys) == 0 { - return + if len(cfg.APIKeys) == 0 { + if provider := cfg.ConfigAPIKeyProvider(); provider != nil && len(provider.APIKeys) > 0 { + cfg.APIKeys = append([]string(nil), provider.APIKeys...) } - cfg.Access.Providers = append(cfg.Access.Providers, config.AccessProvider{ - Name: config.DefaultAccessProviderName, - Type: config.AccessProviderTypeConfigAPIKey, - APIKeys: append([]string(nil), cfg.APIKeys...), - }) - return } - provider := cfg.ConfigAPIKeyProvider() - if provider == nil { - if len(cfg.APIKeys) == 0 { - return - } - cfg.Access.Providers = append(cfg.Access.Providers, config.AccessProvider{ - Name: config.DefaultAccessProviderName, - Type: config.AccessProviderTypeConfigAPIKey, - APIKeys: append([]string(nil), cfg.APIKeys...), - }) - return - } - if len(provider.APIKeys) == 0 && len(cfg.APIKeys) > 0 { - provider.APIKeys = append([]string(nil), cfg.APIKeys...) - } - cfg.APIKeys = append([]string(nil), provider.APIKeys...) + cfg.Access.Providers = nil } // looksLikeBcrypt returns true if the provided string appears to be a bcrypt hash. @@ -264,6 +243,7 @@ func hashSecret(secret string) (string, error) { // SaveConfigPreserveComments writes the config back to YAML while preserving existing comments // and key ordering by loading the original file into a yaml.Node tree and updating values in-place. func SaveConfigPreserveComments(configFile string, cfg *Config) error { + persistCfg := sanitizeConfigForPersist(cfg) // Load original YAML as a node tree to preserve comments and ordering. data, err := os.ReadFile(configFile) if err != nil { @@ -282,7 +262,7 @@ func SaveConfigPreserveComments(configFile string, cfg *Config) error { } // Marshal the current cfg to YAML, then unmarshal to a yaml.Node we can merge from. - rendered, err := yaml.Marshal(cfg) + rendered, err := yaml.Marshal(persistCfg) if err != nil { return err } @@ -297,6 +277,9 @@ func SaveConfigPreserveComments(configFile string, cfg *Config) error { return fmt.Errorf("expected generated root mapping node") } + // Remove deprecated auth block before merging to avoid persisting it again. + removeMapKey(original.Content[0], "auth") + // Merge generated into original in-place, preserving comments/order of existing nodes. mergeMappingPreserve(original.Content[0], generated.Content[0]) @@ -315,6 +298,16 @@ func SaveConfigPreserveComments(configFile string, cfg *Config) error { return enc.Close() } +func sanitizeConfigForPersist(cfg *Config) *Config { + if cfg == nil { + return nil + } + clone := *cfg + clone.SDKConfig = cfg.SDKConfig + clone.SDKConfig.Access = config.AccessConfig{} + return &clone +} + // SaveConfigPreserveCommentsUpdateNestedScalar updates a nested scalar key path like ["a","b"] // while preserving comments and positions. func SaveConfigPreserveCommentsUpdateNestedScalar(configFile string, path []string, value string) error { @@ -517,3 +510,15 @@ func copyNodeShallow(dst, src *yaml.Node) { dst.Content = nil } } + +func removeMapKey(mapNode *yaml.Node, key string) { + if mapNode == nil || mapNode.Kind != yaml.MappingNode || key == "" { + return + } + for i := 0; i+1 < len(mapNode.Content); i += 2 { + if mapNode.Content[i] != nil && mapNode.Content[i].Value == key { + mapNode.Content = append(mapNode.Content[:i], mapNode.Content[i+2:]...) + return + } + } +} diff --git a/sdk/access/registry.go b/sdk/access/registry.go index f2eb4d1c..a29cdd96 100644 --- a/sdk/access/registry.go +++ b/sdk/access/registry.go @@ -74,10 +74,9 @@ func BuildProviders(root *config.SDKConfig) ([]Provider, error) { } providers = append(providers, provider) } - if len(providers) == 0 && len(root.APIKeys) > 0 { - config.SyncInlineAPIKeys(root, root.APIKeys) - if providerCfg := root.ConfigAPIKeyProvider(); providerCfg != nil { - provider, err := BuildProvider(providerCfg, root) + if len(providers) == 0 { + if inline := config.MakeInlineAPIKeyProvider(root.APIKeys); inline != nil { + provider, err := BuildProvider(inline, root) if err != nil { return nil, err } diff --git a/sdk/config/config.go b/sdk/config/config.go index 6170afb3..acb340ef 100644 --- a/sdk/config/config.go +++ b/sdk/config/config.go @@ -16,13 +16,13 @@ type SDKConfig struct { APIKeys []string `yaml:"api-keys" json:"api-keys"` // Access holds request authentication provider configuration. - Access AccessConfig `yaml:"auth" json:"auth"` + Access AccessConfig `yaml:"auth,omitempty" json:"auth,omitempty"` } // AccessConfig groups request authentication providers. type AccessConfig struct { // Providers lists configured authentication providers. - Providers []AccessProvider `yaml:"providers" json:"providers"` + Providers []AccessProvider `yaml:"providers,omitempty" json:"providers,omitempty"` } // AccessProvider describes a request authentication provider entry. @@ -51,27 +51,6 @@ const ( DefaultAccessProviderName = "config-inline" ) -// SyncInlineAPIKeys updates the inline API key provider and top-level APIKeys field. -func SyncInlineAPIKeys(cfg *SDKConfig, keys []string) { - if cfg == nil { - return - } - cloned := append([]string(nil), keys...) - cfg.APIKeys = cloned - if provider := cfg.ConfigAPIKeyProvider(); provider != nil { - if provider.Name == "" { - provider.Name = DefaultAccessProviderName - } - provider.APIKeys = cloned - return - } - cfg.Access.Providers = append(cfg.Access.Providers, AccessProvider{ - Name: DefaultAccessProviderName, - Type: AccessProviderTypeConfigAPIKey, - APIKeys: cloned, - }) -} - // ConfigAPIKeyProvider returns the first inline API key provider if present. func (c *SDKConfig) ConfigAPIKeyProvider() *AccessProvider { if c == nil { @@ -87,3 +66,17 @@ func (c *SDKConfig) ConfigAPIKeyProvider() *AccessProvider { } return nil } + +// MakeInlineAPIKeyProvider constructs an inline API key provider configuration. +// It returns nil when no keys are supplied. +func MakeInlineAPIKeyProvider(keys []string) *AccessProvider { + if len(keys) == 0 { + return nil + } + provider := &AccessProvider{ + Name: DefaultAccessProviderName, + Type: AccessProviderTypeConfigAPIKey, + APIKeys: append([]string(nil), keys...), + } + return provider +}