package management import ( "encoding/json" "fmt" "strings" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" ) // Patch structs for generic handlers type geminiKeyPatch struct { APIKey *string `json:"api-key"` Prefix *string `json:"prefix"` BaseURL *string `json:"base-url"` ProxyURL *string `json:"proxy-url"` Headers *map[string]string `json:"headers"` ExcludedModels *[]string `json:"excluded-models"` } type kiroKeyPatch struct { RefreshToken *string `json:"refresh-token"` ProfileARN *string `json:"profile-arn"` Region *string `json:"region"` Prefix *string `json:"prefix"` ProxyURL *string `json:"proxy-url"` CredentialsFile *string `json:"credentials-file"` KiroCliDBFile *string `json:"kiro-cli-db-file"` Models *[]config.KiroModel `json:"models"` Headers *map[string]string `json:"headers"` ExcludedModels *[]string `json:"excluded-models"` } type claudeKeyPatch struct { APIKey *string `json:"api-key"` APIKeyEntries *[]config.ClaudeAPIKeyEntry `json:"api-key-entries"` Prefix *string `json:"prefix"` BaseURL *string `json:"base-url"` ProxyURL *string `json:"proxy-url"` Models *[]config.ClaudeModel `json:"models"` Headers *map[string]string `json:"headers"` ExcludedModels *[]string `json:"excluded-models"` } type openAICompatPatch struct { Name *string `json:"name"` Prefix *string `json:"prefix"` BaseURL *string `json:"base-url"` APIKeyEntries *[]config.OpenAICompatibilityAPIKey `json:"api-key-entries"` Models *[]config.OpenAICompatibilityModel `json:"models"` Headers *map[string]string `json:"headers"` } type vertexCompatPatch struct { APIKey *string `json:"api-key"` Prefix *string `json:"prefix"` BaseURL *string `json:"base-url"` ProxyURL *string `json:"proxy-url"` Headers *map[string]string `json:"headers"` Models *[]config.VertexCompatModel `json:"models"` } type codexKeyPatch struct { APIKey *string `json:"api-key"` Prefix *string `json:"prefix"` BaseURL *string `json:"base-url"` ProxyURL *string `json:"proxy-url"` Models *[]config.CodexModel `json:"models"` Headers *map[string]string `json:"headers"` ExcludedModels *[]string `json:"excluded-models"` } // Special helper for string list patch (API Keys) due to unique Old/New format support func (h *Handler) patchStringList(c *gin.Context, target *[]string, after func()) { var body struct { Old *string `json:"old"` New *string `json:"new"` Index *int `json:"index"` Value *string `json:"value"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(400, gin.H{"error": "invalid body"}) return } if body.Index != nil && body.Value != nil && *body.Index >= 0 && *body.Index < len(*target) { (*target)[*body.Index] = *body.Value if after != nil { after() } h.persist(c) return } if body.Old != nil && body.New != nil { for i := range *target { if (*target)[i] == *body.Old { (*target)[i] = *body.New if after != nil { after() } h.persist(c) return } } *target = append(*target, *body.New) if after != nil { after() } h.persist(c) return } c.JSON(400, gin.H{"error": "missing fields"}) } // 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) { genericPutList(h, c, func(v []string) { h.cfg.APIKeys = append([]string(nil), v...) h.cfg.Access.Providers = nil }, nil, nil) } func (h *Handler) PatchAPIKeys(c *gin.Context) { h.patchStringList(c, &h.cfg.APIKeys, func() { h.cfg.Access.Providers = nil }) } func (h *Handler) DeleteAPIKeys(c *gin.Context) { genericDeleteList(h, c, func() []string { return h.cfg.APIKeys }, func(v []string) { h.cfg.APIKeys = v h.cfg.Access.Providers = nil }, func(item string, val string) bool { return strings.TrimSpace(item) == val }, "value", nil, ) } // gemini-api-key: []GeminiKey func (h *Handler) GetGeminiKeys(c *gin.Context) { c.JSON(200, gin.H{"gemini-api-key": h.cfg.GeminiKey}) } func (h *Handler) PutGeminiKeys(c *gin.Context) { genericPutList(h, c, func(v []config.GeminiKey) { h.cfg.GeminiKey = append([]config.GeminiKey(nil), v...) }, nil, h.cfg.SanitizeGeminiKeys) } func (h *Handler) PatchGeminiKey(c *gin.Context) { genericPatchList(h, c, func() []config.GeminiKey { return h.cfg.GeminiKey }, func(v []config.GeminiKey) { h.cfg.GeminiKey = v }, func(item config.GeminiKey, match string) bool { return item.APIKey == match }, func(item *config.GeminiKey, patch geminiKeyPatch) bool { if patch.APIKey != nil { trimmed := strings.TrimSpace(*patch.APIKey) if trimmed == "" { return false } item.APIKey = trimmed } if patch.Prefix != nil { item.Prefix = strings.TrimSpace(*patch.Prefix) } if patch.BaseURL != nil { item.BaseURL = strings.TrimSpace(*patch.BaseURL) } if patch.ProxyURL != nil { item.ProxyURL = strings.TrimSpace(*patch.ProxyURL) } if patch.Headers != nil { item.Headers = config.NormalizeHeaders(*patch.Headers) } if patch.ExcludedModels != nil { item.ExcludedModels = config.NormalizeExcludedModels(*patch.ExcludedModels) } return true }, nil, h.cfg.SanitizeGeminiKeys, ) } func (h *Handler) DeleteGeminiKey(c *gin.Context) { genericDeleteList(h, c, func() []config.GeminiKey { return h.cfg.GeminiKey }, func(v []config.GeminiKey) { h.cfg.GeminiKey = v }, func(item config.GeminiKey, val string) bool { return item.APIKey == val }, "api-key", h.cfg.SanitizeGeminiKeys, ) } // kiro-api-key: []KiroKey func (h *Handler) GetKiroKeys(c *gin.Context) { c.JSON(200, gin.H{"kiro-api-key": h.cfg.KiroKey}) } func (h *Handler) PutKiroKeys(c *gin.Context) { genericPutList(h, c, func(v []config.KiroKey) { h.cfg.KiroKey = v }, normalizeKiroKey, nil) } func (h *Handler) PatchKiroKey(c *gin.Context) { genericPatchList(h, c, func() []config.KiroKey { return h.cfg.KiroKey }, func(v []config.KiroKey) { h.cfg.KiroKey = v }, func(item config.KiroKey, match string) bool { return item.RefreshToken == match }, func(item *config.KiroKey, patch kiroKeyPatch) bool { if patch.RefreshToken != nil { item.RefreshToken = strings.TrimSpace(*patch.RefreshToken) } if patch.ProfileARN != nil { item.ProfileARN = strings.TrimSpace(*patch.ProfileARN) } if patch.Region != nil { item.Region = strings.TrimSpace(*patch.Region) } if patch.Prefix != nil { item.Prefix = strings.TrimSpace(*patch.Prefix) } if patch.ProxyURL != nil { item.ProxyURL = strings.TrimSpace(*patch.ProxyURL) } if patch.CredentialsFile != nil { item.CredentialsFile = strings.TrimSpace(*patch.CredentialsFile) } if patch.KiroCliDBFile != nil { item.KiroCliDBFile = strings.TrimSpace(*patch.KiroCliDBFile) } if patch.Models != nil { item.Models = append([]config.KiroModel(nil), (*patch.Models)...) } if patch.Headers != nil { item.Headers = config.NormalizeHeaders(*patch.Headers) } if patch.ExcludedModels != nil { item.ExcludedModels = config.NormalizeExcludedModels(*patch.ExcludedModels) } return true }, normalizeKiroKey, nil, ) } func (h *Handler) DeleteKiroKey(c *gin.Context) { genericDeleteList(h, c, func() []config.KiroKey { return h.cfg.KiroKey }, func(v []config.KiroKey) { h.cfg.KiroKey = v }, func(item config.KiroKey, val string) bool { return item.RefreshToken == val }, "refresh-token", nil, ) } // claude-api-key: []ClaudeKey func (h *Handler) GetClaudeKeys(c *gin.Context) { c.JSON(200, gin.H{"claude-api-key": h.cfg.ClaudeKey}) } func (h *Handler) PutClaudeKeys(c *gin.Context) { genericPutList(h, c, func(v []config.ClaudeKey) { h.cfg.ClaudeKey = v }, normalizeClaudeKey, h.cfg.SanitizeClaudeKeys) } func (h *Handler) PatchClaudeKey(c *gin.Context) { genericPatchList(h, c, func() []config.ClaudeKey { return h.cfg.ClaudeKey }, func(v []config.ClaudeKey) { h.cfg.ClaudeKey = v }, func(item config.ClaudeKey, match string) bool { if item.APIKey == match { return true } for _, entry := range item.APIKeyEntries { if entry.APIKey == match { return true } } return false }, func(item *config.ClaudeKey, patch claudeKeyPatch) bool { if patch.APIKey != nil { item.APIKey = strings.TrimSpace(*patch.APIKey) } if patch.APIKeyEntries != nil { item.APIKeyEntries = append([]config.ClaudeAPIKeyEntry(nil), (*patch.APIKeyEntries)...) } if patch.Prefix != nil { item.Prefix = strings.TrimSpace(*patch.Prefix) } if patch.BaseURL != nil { item.BaseURL = strings.TrimSpace(*patch.BaseURL) } if patch.ProxyURL != nil { item.ProxyURL = strings.TrimSpace(*patch.ProxyURL) } if patch.Models != nil { item.Models = append([]config.ClaudeModel(nil), (*patch.Models)...) } if patch.Headers != nil { item.Headers = config.NormalizeHeaders(*patch.Headers) } if patch.ExcludedModels != nil { item.ExcludedModels = config.NormalizeExcludedModels(*patch.ExcludedModels) } return true }, normalizeClaudeKey, h.cfg.SanitizeClaudeKeys, ) } func (h *Handler) DeleteClaudeKey(c *gin.Context) { // Custom logic needed for APIKeyEntries deletion support // existing handler removes either the whole key OR just an entry from APIKeyEntries // This is not supported by genericDeleteList (which removes items from the list). // So we keep custom implementation for now, but cleaner. if val := c.Query("api-key"); val != "" { out := make([]config.ClaudeKey, 0, len(h.cfg.ClaudeKey)) for _, v := range h.cfg.ClaudeKey { // Check if single API key matches if v.APIKey == val { continue } // Check if any API key in APIKeyEntries matches foundInEntries := false for _, entry := range v.APIKeyEntries { if entry.APIKey == val { foundInEntries = true break } } if !foundInEntries { out = append(out, v) } } h.cfg.ClaudeKey = out h.cfg.SanitizeClaudeKeys() h.persist(c) return } // Index deletion can use generic logic, but mix and match is messy. // Let's fallback to genericDeleteList for index, and manually handle api-key above. // Actually, if we use genericDeleteList, we can't handle the "partial delete" of APIKeyEntries. // So we keep the custom implementation for Claude Delete. if idxStr := c.Query("index"); idxStr != "" { var idx int _, err := fmt.Sscanf(idxStr, "%d", &idx) if err == nil && idx >= 0 && idx < len(h.cfg.ClaudeKey) { h.cfg.ClaudeKey = append(h.cfg.ClaudeKey[:idx], h.cfg.ClaudeKey[idx+1:]...) h.cfg.SanitizeClaudeKeys() h.persist(c) return } } c.JSON(400, gin.H{"error": "missing api-key or index"}) } // openai-compatibility: []OpenAICompatibility func (h *Handler) GetOpenAICompat(c *gin.Context) { c.JSON(200, gin.H{"openai-compatibility": normalizedOpenAICompatibilityEntries(h.cfg.OpenAICompatibility)}) } func (h *Handler) PutOpenAICompat(c *gin.Context) { // Custom Put because of filter logic (BaseURL check) // We can use genericPutList with normalize, but we need to remove items if BaseURL is empty. // genericPutList normalize works on *T. If we empty it, it stays. // So we can't use genericPutList if we need to filter *during* Put. // Let's keep custom implementation. // Or we can rely on SanitizeOpenAICompatibility to remove empty BaseURL entries? // Existing SanitizeOpenAICompatibility: // "SanitizeOpenAICompatibility providers: drop entries without base-url" // So yes, we can use genericPutList + Sanitize! genericPutList(h, c, func(v []config.OpenAICompatibility) { h.cfg.OpenAICompatibility = v }, normalizeOpenAICompatibilityEntry, h.cfg.SanitizeOpenAICompatibility) } func (h *Handler) PatchOpenAICompat(c *gin.Context) { genericPatchList(h, c, func() []config.OpenAICompatibility { return h.cfg.OpenAICompatibility }, func(v []config.OpenAICompatibility) { h.cfg.OpenAICompatibility = v }, func(item config.OpenAICompatibility, match string) bool { return item.Name == match }, func(item *config.OpenAICompatibility, patch openAICompatPatch) bool { if patch.Name != nil { item.Name = strings.TrimSpace(*patch.Name) } if patch.Prefix != nil { item.Prefix = strings.TrimSpace(*patch.Prefix) } if patch.BaseURL != nil { trimmed := strings.TrimSpace(*patch.BaseURL) if trimmed == "" { return false // Remove if base URL is cleared } item.BaseURL = trimmed } if patch.APIKeyEntries != nil { item.APIKeyEntries = append([]config.OpenAICompatibilityAPIKey(nil), (*patch.APIKeyEntries)...) } if patch.Models != nil { item.Models = append([]config.OpenAICompatibilityModel(nil), (*patch.Models)...) } if patch.Headers != nil { item.Headers = config.NormalizeHeaders(*patch.Headers) } return true }, normalizeOpenAICompatibilityEntry, h.cfg.SanitizeOpenAICompatibility, ) } func (h *Handler) DeleteOpenAICompat(c *gin.Context) { genericDeleteList(h, c, func() []config.OpenAICompatibility { return h.cfg.OpenAICompatibility }, func(v []config.OpenAICompatibility) { h.cfg.OpenAICompatibility = v }, func(item config.OpenAICompatibility, val string) bool { return item.Name == val }, "name", h.cfg.SanitizeOpenAICompatibility, ) } // vertex-api-key: []VertexCompatKey func (h *Handler) GetVertexCompatKeys(c *gin.Context) { c.JSON(200, gin.H{"vertex-api-key": h.cfg.VertexCompatAPIKey}) } func (h *Handler) PutVertexCompatKeys(c *gin.Context) { genericPutList(h, c, func(v []config.VertexCompatKey) { h.cfg.VertexCompatAPIKey = v }, normalizeVertexCompatKey, h.cfg.SanitizeVertexCompatKeys) } func (h *Handler) PatchVertexCompatKey(c *gin.Context) { genericPatchList(h, c, func() []config.VertexCompatKey { return h.cfg.VertexCompatAPIKey }, func(v []config.VertexCompatKey) { h.cfg.VertexCompatAPIKey = v }, func(item config.VertexCompatKey, match string) bool { return item.APIKey == match }, func(item *config.VertexCompatKey, patch vertexCompatPatch) bool { if patch.APIKey != nil { trimmed := strings.TrimSpace(*patch.APIKey) if trimmed == "" { return false } item.APIKey = trimmed } if patch.Prefix != nil { item.Prefix = strings.TrimSpace(*patch.Prefix) } if patch.BaseURL != nil { trimmed := strings.TrimSpace(*patch.BaseURL) if trimmed == "" { return false } item.BaseURL = trimmed } if patch.ProxyURL != nil { item.ProxyURL = strings.TrimSpace(*patch.ProxyURL) } if patch.Headers != nil { item.Headers = config.NormalizeHeaders(*patch.Headers) } if patch.Models != nil { item.Models = append([]config.VertexCompatModel(nil), (*patch.Models)...) } return true }, normalizeVertexCompatKey, h.cfg.SanitizeVertexCompatKeys, ) } func (h *Handler) DeleteVertexCompatKey(c *gin.Context) { genericDeleteList(h, c, func() []config.VertexCompatKey { return h.cfg.VertexCompatAPIKey }, func(v []config.VertexCompatKey) { h.cfg.VertexCompatAPIKey = v }, func(item config.VertexCompatKey, val string) bool { return item.APIKey == val }, "api-key", h.cfg.SanitizeVertexCompatKeys, ) } // oauth-excluded-models: map[string][]string // ... (Keeping as is, it's a map) func (h *Handler) GetOAuthExcludedModels(c *gin.Context) { c.JSON(200, gin.H{"oauth-excluded-models": config.NormalizeOAuthExcludedModels(h.cfg.OAuthExcludedModels)}) } func (h *Handler) PutOAuthExcludedModels(c *gin.Context) { data, err := c.GetRawData() if err != nil { c.JSON(400, gin.H{"error": "failed to read body"}) return } var entries map[string][]string if err = json.Unmarshal(data, &entries); err != nil { var wrapper struct { Items map[string][]string `json:"items"` } if err2 := json.Unmarshal(data, &wrapper); err2 != nil { c.JSON(400, gin.H{"error": "invalid body"}) return } entries = wrapper.Items } h.cfg.OAuthExcludedModels = config.NormalizeOAuthExcludedModels(entries) h.persist(c) } func (h *Handler) PatchOAuthExcludedModels(c *gin.Context) { var body struct { Provider *string `json:"provider"` Models []string `json:"models"` } if err := c.ShouldBindJSON(&body); err != nil || body.Provider == nil { c.JSON(400, gin.H{"error": "invalid body"}) return } provider := strings.ToLower(strings.TrimSpace(*body.Provider)) if provider == "" { c.JSON(400, gin.H{"error": "invalid provider"}) return } normalized := config.NormalizeExcludedModels(body.Models) if len(normalized) == 0 { if h.cfg.OAuthExcludedModels == nil { c.JSON(404, gin.H{"error": "provider not found"}) return } if _, ok := h.cfg.OAuthExcludedModels[provider]; !ok { c.JSON(404, gin.H{"error": "provider not found"}) return } delete(h.cfg.OAuthExcludedModels, provider) if len(h.cfg.OAuthExcludedModels) == 0 { h.cfg.OAuthExcludedModels = nil } h.persist(c) return } if h.cfg.OAuthExcludedModels == nil { h.cfg.OAuthExcludedModels = make(map[string][]string) } h.cfg.OAuthExcludedModels[provider] = normalized h.persist(c) } func (h *Handler) DeleteOAuthExcludedModels(c *gin.Context) { provider := strings.ToLower(strings.TrimSpace(c.Query("provider"))) if provider == "" { c.JSON(400, gin.H{"error": "missing provider"}) return } if h.cfg.OAuthExcludedModels == nil { c.JSON(404, gin.H{"error": "provider not found"}) return } if _, ok := h.cfg.OAuthExcludedModels[provider]; !ok { c.JSON(404, gin.H{"error": "provider not found"}) return } delete(h.cfg.OAuthExcludedModels, provider) if len(h.cfg.OAuthExcludedModels) == 0 { h.cfg.OAuthExcludedModels = nil } h.persist(c) } // oauth-model-alias: map[string][]OAuthModelAlias // ... (Keeping as is, it's a map) func (h *Handler) GetOAuthModelAlias(c *gin.Context) { c.JSON(200, gin.H{"oauth-model-alias": sanitizedOAuthModelAlias(h.cfg.OAuthModelAlias)}) } func (h *Handler) PutOAuthModelAlias(c *gin.Context) { data, err := c.GetRawData() if err != nil { c.JSON(400, gin.H{"error": "failed to read body"}) return } var entries map[string][]config.OAuthModelAlias if err = json.Unmarshal(data, &entries); err != nil { var wrapper struct { Items map[string][]config.OAuthModelAlias `json:"items"` } if err2 := json.Unmarshal(data, &wrapper); err2 != nil { c.JSON(400, gin.H{"error": "invalid body"}) return } entries = wrapper.Items } h.cfg.OAuthModelAlias = sanitizedOAuthModelAlias(entries) h.persist(c) } func (h *Handler) PatchOAuthModelAlias(c *gin.Context) { var body struct { Provider *string `json:"provider"` Channel *string `json:"channel"` Aliases []config.OAuthModelAlias `json:"aliases"` } if errBindJSON := c.ShouldBindJSON(&body); errBindJSON != nil { c.JSON(400, gin.H{"error": "invalid body"}) return } channelRaw := "" if body.Channel != nil { channelRaw = *body.Channel } else if body.Provider != nil { channelRaw = *body.Provider } channel := strings.ToLower(strings.TrimSpace(channelRaw)) if channel == "" { c.JSON(400, gin.H{"error": "invalid channel"}) return } normalizedMap := sanitizedOAuthModelAlias(map[string][]config.OAuthModelAlias{channel: body.Aliases}) normalized := normalizedMap[channel] if len(normalized) == 0 { if h.cfg.OAuthModelAlias == nil { c.JSON(404, gin.H{"error": "channel not found"}) return } if _, ok := h.cfg.OAuthModelAlias[channel]; !ok { c.JSON(404, gin.H{"error": "channel not found"}) return } delete(h.cfg.OAuthModelAlias, channel) if len(h.cfg.OAuthModelAlias) == 0 { h.cfg.OAuthModelAlias = nil } h.persist(c) return } if h.cfg.OAuthModelAlias == nil { h.cfg.OAuthModelAlias = make(map[string][]config.OAuthModelAlias) } h.cfg.OAuthModelAlias[channel] = normalized h.persist(c) } func (h *Handler) DeleteOAuthModelAlias(c *gin.Context) { channel := strings.ToLower(strings.TrimSpace(c.Query("channel"))) if channel == "" { channel = strings.ToLower(strings.TrimSpace(c.Query("provider"))) } if channel == "" { c.JSON(400, gin.H{"error": "missing channel"}) return } if h.cfg.OAuthModelAlias == nil { c.JSON(404, gin.H{"error": "channel not found"}) return } if _, ok := h.cfg.OAuthModelAlias[channel]; !ok { c.JSON(404, gin.H{"error": "channel not found"}) return } delete(h.cfg.OAuthModelAlias, channel) if len(h.cfg.OAuthModelAlias) == 0 { h.cfg.OAuthModelAlias = nil } h.persist(c) } // codex-api-key: []CodexKey func (h *Handler) GetCodexKeys(c *gin.Context) { c.JSON(200, gin.H{"codex-api-key": h.cfg.CodexKey}) } func (h *Handler) PutCodexKeys(c *gin.Context) { genericPutList(h, c, func(v []config.CodexKey) { h.cfg.CodexKey = v }, normalizeCodexKey, h.cfg.SanitizeCodexKeys) } func (h *Handler) PatchCodexKey(c *gin.Context) { genericPatchList(h, c, func() []config.CodexKey { return h.cfg.CodexKey }, func(v []config.CodexKey) { h.cfg.CodexKey = v }, func(item config.CodexKey, match string) bool { return item.APIKey == match }, func(item *config.CodexKey, patch codexKeyPatch) bool { if patch.APIKey != nil { item.APIKey = strings.TrimSpace(*patch.APIKey) } if patch.Prefix != nil { item.Prefix = strings.TrimSpace(*patch.Prefix) } if patch.BaseURL != nil { trimmed := strings.TrimSpace(*patch.BaseURL) if trimmed == "" { return false } item.BaseURL = trimmed } if patch.ProxyURL != nil { item.ProxyURL = strings.TrimSpace(*patch.ProxyURL) } if patch.Models != nil { item.Models = append([]config.CodexModel(nil), (*patch.Models)...) } if patch.Headers != nil { item.Headers = config.NormalizeHeaders(*patch.Headers) } if patch.ExcludedModels != nil { item.ExcludedModels = config.NormalizeExcludedModels(*patch.ExcludedModels) } return true }, normalizeCodexKey, h.cfg.SanitizeCodexKeys, ) } func (h *Handler) DeleteCodexKey(c *gin.Context) { genericDeleteList(h, c, func() []config.CodexKey { return h.cfg.CodexKey }, func(v []config.CodexKey) { h.cfg.CodexKey = v }, func(item config.CodexKey, val string) bool { return item.APIKey == val }, "api-key", h.cfg.SanitizeCodexKeys, ) } func normalizeOpenAICompatibilityEntry(entry *config.OpenAICompatibility) { if entry == nil { return } // Trim base-url; empty base-url indicates provider should be removed by sanitization entry.BaseURL = strings.TrimSpace(entry.BaseURL) entry.Headers = config.NormalizeHeaders(entry.Headers) existing := make(map[string]struct{}, len(entry.APIKeyEntries)) for i := range entry.APIKeyEntries { trimmed := strings.TrimSpace(entry.APIKeyEntries[i].APIKey) entry.APIKeyEntries[i].APIKey = trimmed if trimmed != "" { existing[trimmed] = struct{}{} } } } 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...) } normalizeOpenAICompatibilityEntry(©Entry) out[i] = copyEntry } return out } func normalizeKiroKey(entry *config.KiroKey) { if entry == nil { return } entry.RefreshToken = strings.TrimSpace(entry.RefreshToken) entry.ProfileARN = strings.TrimSpace(entry.ProfileARN) entry.Region = strings.TrimSpace(entry.Region) entry.Prefix = strings.TrimSpace(entry.Prefix) entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) entry.CredentialsFile = strings.TrimSpace(entry.CredentialsFile) entry.KiroCliDBFile = strings.TrimSpace(entry.KiroCliDBFile) entry.Headers = config.NormalizeHeaders(entry.Headers) entry.ExcludedModels = config.NormalizeExcludedModels(entry.ExcludedModels) if len(entry.Models) == 0 { return } normalized := make([]config.KiroModel, 0, len(entry.Models)) for i := range entry.Models { model := entry.Models[i] model.Name = strings.TrimSpace(model.Name) model.Alias = strings.TrimSpace(model.Alias) if model.Name == "" && model.Alias == "" { continue } normalized = append(normalized, model) } entry.Models = normalized } func normalizeClaudeKey(entry *config.ClaudeKey) { if entry == nil { return } entry.APIKey = strings.TrimSpace(entry.APIKey) entry.BaseURL = strings.TrimSpace(entry.BaseURL) entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) entry.Headers = config.NormalizeHeaders(entry.Headers) entry.ExcludedModels = config.NormalizeExcludedModels(entry.ExcludedModels) // Normalize APIKeyEntries if len(entry.APIKeyEntries) > 0 { normalizedEntries := make([]config.ClaudeAPIKeyEntry, 0, len(entry.APIKeyEntries)) for i := range entry.APIKeyEntries { apiKeyEntry := entry.APIKeyEntries[i] apiKeyEntry.APIKey = strings.TrimSpace(apiKeyEntry.APIKey) apiKeyEntry.ProxyURL = strings.TrimSpace(apiKeyEntry.ProxyURL) // Skip empty API keys if apiKeyEntry.APIKey != "" { normalizedEntries = append(normalizedEntries, apiKeyEntry) } } entry.APIKeyEntries = normalizedEntries } if len(entry.Models) == 0 { return } normalized := make([]config.ClaudeModel, 0, len(entry.Models)) for i := range entry.Models { model := entry.Models[i] model.Name = strings.TrimSpace(model.Name) model.Alias = strings.TrimSpace(model.Alias) if model.Name == "" && model.Alias == "" { continue } normalized = append(normalized, model) } entry.Models = normalized } func normalizeCodexKey(entry *config.CodexKey) { if entry == nil { return } entry.APIKey = strings.TrimSpace(entry.APIKey) entry.Prefix = strings.TrimSpace(entry.Prefix) entry.BaseURL = strings.TrimSpace(entry.BaseURL) entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) entry.Headers = config.NormalizeHeaders(entry.Headers) entry.ExcludedModels = config.NormalizeExcludedModels(entry.ExcludedModels) if len(entry.Models) == 0 { return } normalized := make([]config.CodexModel, 0, len(entry.Models)) for i := range entry.Models { model := entry.Models[i] model.Name = strings.TrimSpace(model.Name) model.Alias = strings.TrimSpace(model.Alias) if model.Name == "" && model.Alias == "" { continue } normalized = append(normalized, model) } entry.Models = normalized } func normalizeVertexCompatKey(entry *config.VertexCompatKey) { if entry == nil { return } entry.APIKey = strings.TrimSpace(entry.APIKey) entry.Prefix = strings.TrimSpace(entry.Prefix) entry.BaseURL = strings.TrimSpace(entry.BaseURL) entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) entry.Headers = config.NormalizeHeaders(entry.Headers) if len(entry.Models) == 0 { return } normalized := make([]config.VertexCompatModel, 0, len(entry.Models)) for i := range entry.Models { model := entry.Models[i] model.Name = strings.TrimSpace(model.Name) model.Alias = strings.TrimSpace(model.Alias) if model.Name == "" || model.Alias == "" { continue } normalized = append(normalized, model) } entry.Models = normalized } func sanitizedOAuthModelAlias(entries map[string][]config.OAuthModelAlias) map[string][]config.OAuthModelAlias { if len(entries) == 0 { return nil } copied := make(map[string][]config.OAuthModelAlias, len(entries)) for channel, aliases := range entries { if len(aliases) == 0 { continue } copied[channel] = append([]config.OAuthModelAlias(nil), aliases...) } if len(copied) == 0 { return nil } cfg := config.Config{OAuthModelAlias: copied} cfg.SanitizeOAuthModelAlias() if len(cfg.OAuthModelAlias) == 0 { return nil } return cfg.OAuthModelAlias } // GetAmpCode returns the complete ampcode configuration. func (h *Handler) GetAmpCode(c *gin.Context) { if h == nil || h.cfg == nil { c.JSON(200, gin.H{"ampcode": config.AmpCode{}}) return } c.JSON(200, gin.H{"ampcode": h.cfg.AmpCode}) } // GetAmpUpstreamURL returns the ampcode upstream URL. func (h *Handler) GetAmpUpstreamURL(c *gin.Context) { if h == nil || h.cfg == nil { c.JSON(200, gin.H{"upstream-url": ""}) return } c.JSON(200, gin.H{"upstream-url": h.cfg.AmpCode.UpstreamURL}) } // PutAmpUpstreamURL updates the ampcode upstream URL. func (h *Handler) PutAmpUpstreamURL(c *gin.Context) { genericUpdateField(h, c, func(v string) { h.cfg.AmpCode.UpstreamURL = strings.TrimSpace(v) }) } // DeleteAmpUpstreamURL clears the ampcode upstream URL. func (h *Handler) DeleteAmpUpstreamURL(c *gin.Context) { h.cfg.AmpCode.UpstreamURL = "" h.persist(c) } // GetAmpUpstreamAPIKey returns the ampcode upstream API key. func (h *Handler) GetAmpUpstreamAPIKey(c *gin.Context) { if h == nil || h.cfg == nil { c.JSON(200, gin.H{"upstream-api-key": ""}) return } c.JSON(200, gin.H{"upstream-api-key": h.cfg.AmpCode.UpstreamAPIKey}) } // PutAmpUpstreamAPIKey updates the ampcode upstream API key. func (h *Handler) PutAmpUpstreamAPIKey(c *gin.Context) { genericUpdateField(h, c, func(v string) { h.cfg.AmpCode.UpstreamAPIKey = strings.TrimSpace(v) }) } // DeleteAmpUpstreamAPIKey clears the ampcode upstream API key. func (h *Handler) DeleteAmpUpstreamAPIKey(c *gin.Context) { h.cfg.AmpCode.UpstreamAPIKey = "" h.persist(c) } // GetAmpRestrictManagementToLocalhost returns the localhost restriction setting. func (h *Handler) GetAmpRestrictManagementToLocalhost(c *gin.Context) { if h == nil || h.cfg == nil { c.JSON(200, gin.H{"restrict-management-to-localhost": true}) return } c.JSON(200, gin.H{"restrict-management-to-localhost": h.cfg.AmpCode.RestrictManagementToLocalhost}) } // PutAmpRestrictManagementToLocalhost updates the localhost restriction setting. func (h *Handler) PutAmpRestrictManagementToLocalhost(c *gin.Context) { genericUpdateField(h, c, func(v bool) { h.cfg.AmpCode.RestrictManagementToLocalhost = v }) } // GetAmpModelMappings returns the ampcode model mappings. func (h *Handler) GetAmpModelMappings(c *gin.Context) { if h == nil || h.cfg == nil { c.JSON(200, gin.H{"model-mappings": []config.AmpModelMapping{}}) return } c.JSON(200, gin.H{"model-mappings": h.cfg.AmpCode.ModelMappings}) } // PutAmpModelMappings replaces all ampcode model mappings. func (h *Handler) PutAmpModelMappings(c *gin.Context) { var body struct { Value []config.AmpModelMapping `json:"value"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(400, gin.H{"error": "invalid body"}) return } h.cfg.AmpCode.ModelMappings = body.Value h.persist(c) } // PatchAmpModelMappings adds or updates model mappings. func (h *Handler) PatchAmpModelMappings(c *gin.Context) { var body struct { Value []config.AmpModelMapping `json:"value"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(400, gin.H{"error": "invalid body"}) return } existing := make(map[string]int) for i, m := range h.cfg.AmpCode.ModelMappings { existing[strings.TrimSpace(m.From)] = i } for _, newMapping := range body.Value { from := strings.TrimSpace(newMapping.From) if idx, ok := existing[from]; ok { h.cfg.AmpCode.ModelMappings[idx] = newMapping } else { h.cfg.AmpCode.ModelMappings = append(h.cfg.AmpCode.ModelMappings, newMapping) existing[from] = len(h.cfg.AmpCode.ModelMappings) - 1 } } h.persist(c) } // DeleteAmpModelMappings removes specified model mappings by "from" field. func (h *Handler) DeleteAmpModelMappings(c *gin.Context) { var body struct { Value []string `json:"value"` } if err := c.ShouldBindJSON(&body); err != nil || len(body.Value) == 0 { h.cfg.AmpCode.ModelMappings = nil h.persist(c) return } toRemove := make(map[string]bool) for _, from := range body.Value { toRemove[strings.TrimSpace(from)] = true } newMappings := make([]config.AmpModelMapping, 0, len(h.cfg.AmpCode.ModelMappings)) for _, m := range h.cfg.AmpCode.ModelMappings { if !toRemove[strings.TrimSpace(m.From)] { newMappings = append(newMappings, m) } } h.cfg.AmpCode.ModelMappings = newMappings h.persist(c) } // GetAmpForceModelMappings returns whether model mappings are forced. func (h *Handler) GetAmpForceModelMappings(c *gin.Context) { if h == nil || h.cfg == nil { c.JSON(200, gin.H{"force-model-mappings": false}) return } c.JSON(200, gin.H{"force-model-mappings": h.cfg.AmpCode.ForceModelMappings}) } // PutAmpForceModelMappings updates the force model mappings setting. func (h *Handler) PutAmpForceModelMappings(c *gin.Context) { genericUpdateField(h, c, func(v bool) { h.cfg.AmpCode.ForceModelMappings = v }) } // GetAmpUpstreamAPIKeys returns the ampcode upstream API keys mapping. func (h *Handler) GetAmpUpstreamAPIKeys(c *gin.Context) { if h == nil || h.cfg == nil { c.JSON(200, gin.H{"upstream-api-keys": []config.AmpUpstreamAPIKeyEntry{}}) return } c.JSON(200, gin.H{"upstream-api-keys": h.cfg.AmpCode.UpstreamAPIKeys}) } // PutAmpUpstreamAPIKeys replaces all ampcode upstream API keys mappings. func (h *Handler) PutAmpUpstreamAPIKeys(c *gin.Context) { var body struct { Value []config.AmpUpstreamAPIKeyEntry `json:"value"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(400, gin.H{"error": "invalid body"}) return } // Normalize entries: trim whitespace, filter empty normalized := normalizeAmpUpstreamAPIKeyEntries(body.Value) h.cfg.AmpCode.UpstreamAPIKeys = normalized h.persist(c) } // PatchAmpUpstreamAPIKeys adds or updates upstream API keys entries. // Matching is done by upstream-api-key value. func (h *Handler) PatchAmpUpstreamAPIKeys(c *gin.Context) { var body struct { Value []config.AmpUpstreamAPIKeyEntry `json:"value"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(400, gin.H{"error": "invalid body"}) return } existing := make(map[string]int) for i, entry := range h.cfg.AmpCode.UpstreamAPIKeys { existing[strings.TrimSpace(entry.UpstreamAPIKey)] = i } for _, newEntry := range body.Value { upstreamKey := strings.TrimSpace(newEntry.UpstreamAPIKey) if upstreamKey == "" { continue } normalizedEntry := config.AmpUpstreamAPIKeyEntry{ UpstreamAPIKey: upstreamKey, APIKeys: normalizeAPIKeysList(newEntry.APIKeys), } if idx, ok := existing[upstreamKey]; ok { h.cfg.AmpCode.UpstreamAPIKeys[idx] = normalizedEntry } else { h.cfg.AmpCode.UpstreamAPIKeys = append(h.cfg.AmpCode.UpstreamAPIKeys, normalizedEntry) existing[upstreamKey] = len(h.cfg.AmpCode.UpstreamAPIKeys) - 1 } } h.persist(c) } // DeleteAmpUpstreamAPIKeys removes specified upstream API keys entries. // Body must be JSON: {"value": ["", ...]}. // If "value" is an empty array, clears all entries. // If JSON is invalid or "value" is missing/null, returns 400 and does not persist any change. func (h *Handler) DeleteAmpUpstreamAPIKeys(c *gin.Context) { var body struct { Value []string `json:"value"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(400, gin.H{"error": "invalid body"}) return } if body.Value == nil { c.JSON(400, gin.H{"error": "missing value"}) return } // Empty array means clear all if len(body.Value) == 0 { h.cfg.AmpCode.UpstreamAPIKeys = nil h.persist(c) return } toRemove := make(map[string]bool) for _, key := range body.Value { trimmed := strings.TrimSpace(key) if trimmed == "" { continue } toRemove[trimmed] = true } if len(toRemove) == 0 { c.JSON(400, gin.H{"error": "empty value"}) return } newEntries := make([]config.AmpUpstreamAPIKeyEntry, 0, len(h.cfg.AmpCode.UpstreamAPIKeys)) for _, entry := range h.cfg.AmpCode.UpstreamAPIKeys { if !toRemove[strings.TrimSpace(entry.UpstreamAPIKey)] { newEntries = append(newEntries, entry) } } h.cfg.AmpCode.UpstreamAPIKeys = newEntries h.persist(c) } // normalizeAmpUpstreamAPIKeyEntries normalizes a list of upstream API key entries. func normalizeAmpUpstreamAPIKeyEntries(entries []config.AmpUpstreamAPIKeyEntry) []config.AmpUpstreamAPIKeyEntry { if len(entries) == 0 { return nil } out := make([]config.AmpUpstreamAPIKeyEntry, 0, len(entries)) for _, entry := range entries { upstreamKey := strings.TrimSpace(entry.UpstreamAPIKey) if upstreamKey == "" { continue } apiKeys := normalizeAPIKeysList(entry.APIKeys) out = append(out, config.AmpUpstreamAPIKeyEntry{ UpstreamAPIKey: upstreamKey, APIKeys: apiKeys, }) } if len(out) == 0 { return nil } return out } // normalizeAPIKeysList trims and filters empty strings from a list of API keys. func normalizeAPIKeysList(keys []string) []string { if len(keys) == 0 { return nil } out := make([]string, 0, len(keys)) for _, k := range keys { trimmed := strings.TrimSpace(k) if trimmed != "" { out = append(out, trimmed) } } if len(out) == 0 { return nil } return out }