API / internal /api /handlers /management /config_lists.go
sshinmen's picture
Refactor auth handlers and executors
9b1c663
Raw
History Blame Contribute Delete
37.4 kB
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(&copyEntry)
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": ["<upstream-api-key>", ...]}.
// 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
}