diff --git a/internal/api/handlers/management/config_basic.go b/internal/api/handlers/management/config_basic.go index 8f57171e..0f76b5a3 100644 --- a/internal/api/handlers/management/config_basic.go +++ b/internal/api/handlers/management/config_basic.go @@ -17,7 +17,6 @@ func (h *Handler) GetConfig(c *gin.Context) { return } cfgCopy := *h.cfg - cfgCopy.GlAPIKey = geminiKeyStringsFromConfig(h.cfg) c.JSON(200, &cfgCopy) } diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index 71193084..c5cbfa43 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -147,7 +147,6 @@ func (h *Handler) applyLegacyKeys(keys []string) { } } h.cfg.GeminiKey = newList - h.cfg.GlAPIKey = sanitized h.cfg.SanitizeGeminiKeys() } @@ -409,15 +408,14 @@ func (h *Handler) PutOpenAICompat(c *gin.Context) { } arr = obj.Items } - arr = migrateLegacyOpenAICompatibilityKeys(arr) - // Filter out providers with empty base-url -> remove provider entirely filtered := make([]config.OpenAICompatibility, 0, len(arr)) for i := range arr { + normalizeOpenAICompatibilityEntry(&arr[i]) if strings.TrimSpace(arr[i].BaseURL) != "" { filtered = append(filtered, arr[i]) } } - h.cfg.OpenAICompatibility = migrateLegacyOpenAICompatibilityKeys(filtered) + h.cfg.OpenAICompatibility = filtered h.cfg.SanitizeOpenAICompatibility() h.persist(c) } @@ -431,7 +429,6 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) { c.JSON(400, gin.H{"error": "invalid body"}) return } - h.cfg.OpenAICompatibility = migrateLegacyOpenAICompatibilityKeys(h.cfg.OpenAICompatibility) normalizeOpenAICompatibilityEntry(body.Value) // If base-url becomes empty, delete the provider instead of updating if strings.TrimSpace(body.Value.BaseURL) == "" { @@ -731,28 +728,6 @@ func normalizeOpenAICompatibilityEntry(entry *config.OpenAICompatibility) { existing[trimmed] = struct{}{} } } - if len(entry.APIKeys) == 0 { - return - } - for _, legacyKey := range entry.APIKeys { - trimmed := strings.TrimSpace(legacyKey) - if trimmed == "" { - continue - } - if _, ok := existing[trimmed]; ok { - continue - } - entry.APIKeyEntries = append(entry.APIKeyEntries, config.OpenAICompatibilityAPIKey{APIKey: trimmed}) - existing[trimmed] = struct{}{} - } - entry.APIKeys = nil -} - -func migrateLegacyOpenAICompatibilityKeys(entries []config.OpenAICompatibility) []config.OpenAICompatibility { - for i := range entries { - normalizeOpenAICompatibilityEntry(&entries[i]) - } - return entries } func normalizedOpenAICompatibilityEntries(entries []config.OpenAICompatibility) []config.OpenAICompatibility { @@ -765,9 +740,6 @@ func normalizedOpenAICompatibilityEntries(entries []config.OpenAICompatibility) 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 } diff --git a/internal/api/server.go b/internal/api/server.go index 119df848..6866b4cf 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -938,11 +938,7 @@ func (s *Server) UpdateClients(cfg *config.Config) { openAICompatCount := 0 for i := range cfg.OpenAICompatibility { entry := cfg.OpenAICompatibility[i] - if len(entry.APIKeyEntries) > 0 { - openAICompatCount += len(entry.APIKeyEntries) - continue - } - openAICompatCount += len(entry.APIKeys) + openAICompatCount += len(entry.APIKeyEntries) } total := authFiles + geminiAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + vertexAICompatCount + openAICompatCount diff --git a/internal/config/config.go b/internal/config/config.go index afe498de..b256cdd8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -58,9 +58,6 @@ type Config struct { // GeminiKey defines Gemini API key configurations with optional routing overrides. GeminiKey []GeminiKey `yaml:"gemini-api-key" json:"gemini-api-key"` - // GlAPIKey exposes the legacy generative language API key list for backward compatibility. - GlAPIKey []string `yaml:"generative-language-api-key" json:"generative-language-api-key"` - // Codex defines a list of Codex API key configurations as specified in the YAML configuration file. CodexKey []CodexKey `yaml:"codex-api-key" json:"codex-api-key"` @@ -170,6 +167,17 @@ type PayloadModelRule struct { Protocol string `yaml:"protocol" json:"protocol"` } +type legacyConfigData struct { + LegacyGeminiKeys []string `yaml:"generative-language-api-key"` + OpenAICompat []legacyOpenAICompatibility `yaml:"openai-compatibility"` +} + +type legacyOpenAICompatibility struct { + Name string `yaml:"name"` + BaseURL string `yaml:"base-url"` + APIKeys []string `yaml:"api-keys"` +} + // ClaudeKey represents the configuration for a Claude API key, // including the API key itself and an optional base URL for the API endpoint. type ClaudeKey struct { @@ -250,10 +258,6 @@ type OpenAICompatibility struct { // BaseURL is the base URL for the external OpenAI-compatible API endpoint. BaseURL string `yaml:"base-url" json:"base-url"` - // APIKeys are the authentication keys for accessing the external API services. - // Deprecated: Use APIKeyEntries instead to support per-key proxy configuration. - APIKeys []string `yaml:"api-keys,omitempty" json:"api-keys,omitempty"` - // APIKeyEntries defines API keys with optional per-key proxy configuration. APIKeyEntries []OpenAICompatibilityAPIKey `yaml:"api-key-entries,omitempty" json:"api-key-entries,omitempty"` @@ -333,6 +337,12 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) { return nil, fmt.Errorf("failed to parse config file: %w", err) } + var legacy legacyConfigData + if errLegacy := yaml.Unmarshal(data, &legacy); errLegacy == nil { + cfg.migrateLegacyGeminiKeys(legacy.LegacyGeminiKeys) + cfg.migrateLegacyOpenAICompatibilityKeys(legacy.OpenAICompat) + } + // Hash remote management key if plaintext is detected (nested) // We consider a value to be already hashed if it looks like a bcrypt hash ($2a$, $2b$, or $2y$ prefix). if cfg.RemoteManagement.SecretKey != "" && !looksLikeBcrypt(cfg.RemoteManagement.SecretKey) { @@ -451,22 +461,94 @@ func (cfg *Config) SanitizeGeminiKeys() { out = append(out, entry) } cfg.GeminiKey = out +} - if len(cfg.GlAPIKey) > 0 { - for _, raw := range cfg.GlAPIKey { - key := strings.TrimSpace(raw) - if key == "" { - continue - } - if _, exists := seen[key]; exists { - continue - } - cfg.GeminiKey = append(cfg.GeminiKey, GeminiKey{APIKey: key}) - seen[key] = struct{}{} +func (cfg *Config) migrateLegacyGeminiKeys(legacy []string) { + if cfg == nil || len(legacy) == 0 { + return + } + seen := make(map[string]struct{}, len(cfg.GeminiKey)) + for i := range cfg.GeminiKey { + key := strings.TrimSpace(cfg.GeminiKey[i].APIKey) + if key == "" { + continue + } + seen[key] = struct{}{} + } + for _, raw := range legacy { + key := strings.TrimSpace(raw) + if key == "" { + continue + } + if _, exists := seen[key]; exists { + continue + } + cfg.GeminiKey = append(cfg.GeminiKey, GeminiKey{APIKey: key}) + seen[key] = struct{}{} + } +} + +func (cfg *Config) migrateLegacyOpenAICompatibilityKeys(legacy []legacyOpenAICompatibility) { + if cfg == nil || len(cfg.OpenAICompatibility) == 0 || len(legacy) == 0 { + return + } + lookup := make(map[string]*OpenAICompatibility, len(cfg.OpenAICompatibility)) + for i := range cfg.OpenAICompatibility { + if key := legacyOpenAICompatKey(cfg.OpenAICompatibility[i].Name, cfg.OpenAICompatibility[i].BaseURL); key != "" { + lookup[key] = &cfg.OpenAICompatibility[i] } } + for _, legacyEntry := range legacy { + if len(legacyEntry.APIKeys) == 0 { + continue + } + key := legacyOpenAICompatKey(legacyEntry.Name, legacyEntry.BaseURL) + if key == "" { + continue + } + target := lookup[key] + if target == nil { + continue + } + mergeLegacyOpenAICompatAPIKeys(target, legacyEntry.APIKeys) + } +} - cfg.GlAPIKey = nil +func mergeLegacyOpenAICompatAPIKeys(entry *OpenAICompatibility, keys []string) { + if entry == nil || len(keys) == 0 { + return + } + existing := make(map[string]struct{}, len(entry.APIKeyEntries)) + for i := range entry.APIKeyEntries { + key := strings.TrimSpace(entry.APIKeyEntries[i].APIKey) + if key == "" { + continue + } + existing[key] = struct{}{} + } + for _, raw := range keys { + key := strings.TrimSpace(raw) + if key == "" { + continue + } + if _, ok := existing[key]; ok { + continue + } + entry.APIKeyEntries = append(entry.APIKeyEntries, OpenAICompatibilityAPIKey{APIKey: key}) + existing[key] = struct{}{} + } +} + +func legacyOpenAICompatKey(name, baseURL string) string { + trimmedName := strings.ToLower(strings.TrimSpace(name)) + if trimmedName != "" { + return "name:" + trimmedName + } + trimmedBase := strings.ToLower(strings.TrimSpace(baseURL)) + if trimmedBase != "" { + return "base:" + trimmedBase + } + return "" } func syncInlineAccessProvider(cfg *Config) { @@ -605,6 +687,7 @@ func SaveConfigPreserveComments(configFile string, cfg *Config) error { // Remove deprecated auth block before merging to avoid persisting it again. removeMapKey(original.Content[0], "auth") removeLegacyOpenAICompatAPIKeys(original.Content[0]) + removeMapKey(original.Content[0], "generative-language-api-key") pruneMappingToGeneratedKeys(original.Content[0], generated.Content[0], "oauth-excluded-models") // Merge generated into original in-place, preserving comments/order of existing nodes. diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index 7e6c2631..c582d1e3 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -1162,71 +1162,37 @@ func (w *Watcher) SnapshotCoreAuths() []*coreauth.Auth { // Handle new APIKeyEntries format (preferred) createdEntries := 0 - if len(compat.APIKeyEntries) > 0 { - for j := range compat.APIKeyEntries { - entry := &compat.APIKeyEntries[j] - key := strings.TrimSpace(entry.APIKey) - proxyURL := strings.TrimSpace(entry.ProxyURL) - idKind := fmt.Sprintf("openai-compatibility:%s", providerName) - id, token := idGen.next(idKind, key, base, proxyURL) - attrs := map[string]string{ - "source": fmt.Sprintf("config:%s[%s]", providerName, token), - "base_url": base, - "compat_name": compat.Name, - "provider_key": providerName, - } - if key != "" { - attrs["api_key"] = key - } - if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" { - attrs["models_hash"] = hash - } - addConfigHeadersToAttrs(compat.Headers, attrs) - a := &coreauth.Auth{ - ID: id, - Provider: providerName, - Label: compat.Name, - Status: coreauth.StatusActive, - ProxyURL: proxyURL, - Attributes: attrs, - CreatedAt: now, - UpdatedAt: now, - } - out = append(out, a) - createdEntries++ + for j := range compat.APIKeyEntries { + entry := &compat.APIKeyEntries[j] + key := strings.TrimSpace(entry.APIKey) + proxyURL := strings.TrimSpace(entry.ProxyURL) + idKind := fmt.Sprintf("openai-compatibility:%s", providerName) + id, token := idGen.next(idKind, key, base, proxyURL) + attrs := map[string]string{ + "source": fmt.Sprintf("config:%s[%s]", providerName, token), + "base_url": base, + "compat_name": compat.Name, + "provider_key": providerName, } - } else { - // Handle legacy APIKeys format for backward compatibility - for j := range compat.APIKeys { - key := strings.TrimSpace(compat.APIKeys[j]) - if key == "" { - continue - } - idKind := fmt.Sprintf("openai-compatibility:%s", providerName) - id, token := idGen.next(idKind, key, base) - attrs := map[string]string{ - "source": fmt.Sprintf("config:%s[%s]", providerName, token), - "base_url": base, - "compat_name": compat.Name, - "provider_key": providerName, - } + if key != "" { attrs["api_key"] = key - if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" { - attrs["models_hash"] = hash - } - addConfigHeadersToAttrs(compat.Headers, attrs) - a := &coreauth.Auth{ - ID: id, - Provider: providerName, - Label: compat.Name, - Status: coreauth.StatusActive, - Attributes: attrs, - CreatedAt: now, - UpdatedAt: now, - } - out = append(out, a) - createdEntries++ } + if hash := computeOpenAICompatModelsHash(compat.Models); hash != "" { + attrs["models_hash"] = hash + } + addConfigHeadersToAttrs(compat.Headers, attrs) + a := &coreauth.Auth{ + ID: id, + Provider: providerName, + Label: compat.Name, + Status: coreauth.StatusActive, + ProxyURL: proxyURL, + Attributes: attrs, + CreatedAt: now, + UpdatedAt: now, + } + out = append(out, a) + createdEntries++ } if createdEntries == 0 { idKind := fmt.Sprintf("openai-compatibility:%s", providerName) @@ -1530,12 +1496,7 @@ func BuildAPIKeyClients(cfg *config.Config) (int, int, int, int, int) { if len(cfg.OpenAICompatibility) > 0 { // Do not construct legacy clients for OpenAI-compat providers; these are handled by the stateless executor. for _, compatConfig := range cfg.OpenAICompatibility { - // Count from new APIKeyEntries format if present, otherwise fall back to legacy APIKeys - if len(compatConfig.APIKeyEntries) > 0 { - openAICompatCount += len(compatConfig.APIKeyEntries) - } else { - openAICompatCount += len(compatConfig.APIKeys) - } + openAICompatCount += len(compatConfig.APIKeyEntries) } } return geminiAPIKeyCount, vertexCompatAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount @@ -1612,24 +1573,9 @@ func describeOpenAICompatibilityUpdate(oldEntry, newEntry config.OpenAICompatibi } func countAPIKeys(entry config.OpenAICompatibility) int { - // Prefer new APIKeyEntries format - if len(entry.APIKeyEntries) > 0 { - count := 0 - for _, keyEntry := range entry.APIKeyEntries { - if strings.TrimSpace(keyEntry.APIKey) != "" { - count++ - } - } - return count - } - // Fall back to legacy APIKeys format - return countNonEmptyStrings(entry.APIKeys) -} - -func countNonEmptyStrings(values []string) int { count := 0 - for _, value := range values { - if strings.TrimSpace(value) != "" { + for _, keyEntry := range entry.APIKeyEntries { + if strings.TrimSpace(keyEntry.APIKey) != "" { count++ } } @@ -1754,9 +1700,6 @@ func buildConfigChangeDetails(oldCfg, newCfg *config.Config) []string { changes = append(changes, fmt.Sprintf("gemini[%d].excluded-models: updated (%d -> %d entries)", i, oldExcluded.count, newExcluded.count)) } } - if !reflect.DeepEqual(trimStrings(oldCfg.GlAPIKey), trimStrings(newCfg.GlAPIKey)) { - changes = append(changes, "generative-language-api-key: values updated (legacy view, redacted)") - } } // Claude keys (do not print key material)