| package management |
|
|
| import ( |
| "encoding/json" |
| "fmt" |
| "strings" |
|
|
| "github.com/gin-gonic/gin" |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| ) |
|
|
| |
|
|
| 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"` |
| } |
|
|
| |
| 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"}) |
| } |
|
|
| |
| 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, |
| ) |
| } |
|
|
| |
| 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, |
| ) |
| } |
|
|
| |
| 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, |
| ) |
| } |
|
|
| |
| 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) { |
| |
| |
| |
| |
|
|
| if val := c.Query("api-key"); val != "" { |
| out := make([]config.ClaudeKey, 0, len(h.cfg.ClaudeKey)) |
| for _, v := range h.cfg.ClaudeKey { |
| |
| if v.APIKey == val { |
| continue |
| } |
| |
| 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 |
| } |
| |
| |
| |
| |
|
|
| 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"}) |
| } |
|
|
| |
| 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) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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 |
| } |
| 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, |
| ) |
| } |
|
|
| |
| 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, |
| ) |
| } |
|
|
| |
| |
| 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) |
| } |
|
|
| |
| |
| 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) |
| } |
|
|
| |
| 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 |
| } |
| |
| 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) |
|
|
| |
| 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) |
| |
| 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 |
| } |
|
|
| |
| 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}) |
| } |
|
|
| |
| 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}) |
| } |
|
|
| |
| func (h *Handler) PutAmpUpstreamURL(c *gin.Context) { |
| genericUpdateField(h, c, func(v string) { h.cfg.AmpCode.UpstreamURL = strings.TrimSpace(v) }) |
| } |
|
|
| |
| func (h *Handler) DeleteAmpUpstreamURL(c *gin.Context) { |
| h.cfg.AmpCode.UpstreamURL = "" |
| h.persist(c) |
| } |
|
|
| |
| 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}) |
| } |
|
|
| |
| func (h *Handler) PutAmpUpstreamAPIKey(c *gin.Context) { |
| genericUpdateField(h, c, func(v string) { h.cfg.AmpCode.UpstreamAPIKey = strings.TrimSpace(v) }) |
| } |
|
|
| |
| func (h *Handler) DeleteAmpUpstreamAPIKey(c *gin.Context) { |
| h.cfg.AmpCode.UpstreamAPIKey = "" |
| h.persist(c) |
| } |
|
|
| |
| 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}) |
| } |
|
|
| |
| func (h *Handler) PutAmpRestrictManagementToLocalhost(c *gin.Context) { |
| genericUpdateField(h, c, func(v bool) { h.cfg.AmpCode.RestrictManagementToLocalhost = v }) |
| } |
|
|
| |
| 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}) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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}) |
| } |
|
|
| |
| func (h *Handler) PutAmpForceModelMappings(c *gin.Context) { |
| genericUpdateField(h, c, func(v bool) { h.cfg.AmpCode.ForceModelMappings = v }) |
| } |
|
|
| |
| 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}) |
| } |
|
|
| |
| 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 |
| } |
| |
| normalized := normalizeAmpUpstreamAPIKeyEntries(body.Value) |
| h.cfg.AmpCode.UpstreamAPIKeys = normalized |
| h.persist(c) |
| } |
|
|
| |
| |
| 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) |
| } |
|
|
| |
| |
| |
| |
| 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 |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|