Add management API handlers for config and auth file management
Some checks failed
docker-image / docker (push) Has been cancelled
goreleaser / goreleaser (push) Has been cancelled

- Implemented CRUD operations for authentication files.
- Added endpoints for managing API keys, quotas, proxy settings, and other configurations.
- Enhanced management access with robust validation, remote access control, and persistence support.
- Updated README with new configuration details.

Fixed OpenAI Chat Completions for codex
This commit is contained in:
Luis Pater
2025-08-31 04:14:43 +08:00
parent b1254106ee
commit db43930b98
15 changed files with 1848 additions and 31 deletions

View File

@@ -0,0 +1,139 @@
package management
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
// List auth files
func (h *Handler) ListAuthFiles(c *gin.Context) {
entries, err := os.ReadDir(h.cfg.AuthDir)
if err != nil {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to read auth dir: %v", err)})
return
}
files := make([]gin.H, 0)
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if !strings.HasSuffix(strings.ToLower(name), ".json") {
continue
}
if info, errInfo := e.Info(); errInfo == nil {
files = append(files, gin.H{"name": name, "size": info.Size(), "modtime": info.ModTime()})
}
}
c.JSON(200, gin.H{"files": files})
}
// Download single auth file by name
func (h *Handler) DownloadAuthFile(c *gin.Context) {
name := c.Query("name")
if name == "" || strings.Contains(name, string(os.PathSeparator)) {
c.JSON(400, gin.H{"error": "invalid name"})
return
}
if !strings.HasSuffix(strings.ToLower(name), ".json") {
c.JSON(400, gin.H{"error": "name must end with .json"})
return
}
full := filepath.Join(h.cfg.AuthDir, name)
data, err := os.ReadFile(full)
if err != nil {
if os.IsNotExist(err) {
c.JSON(404, gin.H{"error": "file not found"})
} else {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to read file: %v", err)})
}
return
}
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", name))
c.Data(200, "application/json", data)
}
// Upload auth file: multipart or raw JSON with ?name=
func (h *Handler) UploadAuthFile(c *gin.Context) {
if file, err := c.FormFile("file"); err == nil && file != nil {
name := filepath.Base(file.Filename)
if !strings.HasSuffix(strings.ToLower(name), ".json") {
c.JSON(400, gin.H{"error": "file must be .json"})
return
}
dst := filepath.Join(h.cfg.AuthDir, name)
if errSave := c.SaveUploadedFile(file, dst); errSave != nil {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to save file: %v", errSave)})
return
}
c.JSON(200, gin.H{"status": "ok"})
return
}
name := c.Query("name")
if name == "" || strings.Contains(name, string(os.PathSeparator)) {
c.JSON(400, gin.H{"error": "invalid name"})
return
}
if !strings.HasSuffix(strings.ToLower(name), ".json") {
c.JSON(400, gin.H{"error": "name must end with .json"})
return
}
data, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(400, gin.H{"error": "failed to read body"})
return
}
dst := filepath.Join(h.cfg.AuthDir, filepath.Base(name))
if errWrite := os.WriteFile(dst, data, 0o600); errWrite != nil {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to write file: %v", errWrite)})
return
}
c.JSON(200, gin.H{"status": "ok"})
}
// Delete auth files: single by name or all
func (h *Handler) DeleteAuthFile(c *gin.Context) {
if all := c.Query("all"); all == "true" || all == "1" || all == "*" {
entries, err := os.ReadDir(h.cfg.AuthDir)
if err != nil {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to read auth dir: %v", err)})
return
}
deleted := 0
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if !strings.HasSuffix(strings.ToLower(name), ".json") {
continue
}
full := filepath.Join(h.cfg.AuthDir, name)
if err = os.Remove(full); err == nil {
deleted++
}
}
c.JSON(200, gin.H{"status": "ok", "deleted": deleted})
return
}
name := c.Query("name")
if name == "" || strings.Contains(name, string(os.PathSeparator)) {
c.JSON(400, gin.H{"error": "invalid name"})
return
}
full := filepath.Join(h.cfg.AuthDir, filepath.Base(name))
if err := os.Remove(full); err != nil {
if os.IsNotExist(err) {
c.JSON(404, gin.H{"error": "file not found"})
} else {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to remove file: %v", err)})
}
return
}
c.JSON(200, gin.H{"status": "ok"})
}

View File

@@ -0,0 +1,41 @@
package management
import (
"github.com/gin-gonic/gin"
)
// Debug
func (h *Handler) GetDebug(c *gin.Context) { c.JSON(200, gin.H{"debug": h.cfg.Debug}) }
func (h *Handler) PutDebug(c *gin.Context) { h.updateBoolField(c, func(v bool) { h.cfg.Debug = v }) }
// Request log
func (h *Handler) GetRequestLog(c *gin.Context) { c.JSON(200, gin.H{"request-log": h.cfg.RequestLog}) }
func (h *Handler) PutRequestLog(c *gin.Context) {
h.updateBoolField(c, func(v bool) { h.cfg.RequestLog = v })
}
// Request retry
func (h *Handler) GetRequestRetry(c *gin.Context) {
c.JSON(200, gin.H{"request-retry": h.cfg.RequestRetry})
}
func (h *Handler) PutRequestRetry(c *gin.Context) {
h.updateIntField(c, func(v int) { h.cfg.RequestRetry = v })
}
// Allow localhost unauthenticated
func (h *Handler) GetAllowLocalhost(c *gin.Context) {
c.JSON(200, gin.H{"allow-localhost-unauthenticated": h.cfg.AllowLocalhostUnauthenticated})
}
func (h *Handler) PutAllowLocalhost(c *gin.Context) {
h.updateBoolField(c, func(v bool) { h.cfg.AllowLocalhostUnauthenticated = v })
}
// Proxy URL
func (h *Handler) GetProxyURL(c *gin.Context) { c.JSON(200, gin.H{"proxy-url": h.cfg.ProxyURL}) }
func (h *Handler) PutProxyURL(c *gin.Context) {
h.updateStringField(c, func(v string) { h.cfg.ProxyURL = v })
}
func (h *Handler) DeleteProxyURL(c *gin.Context) {
h.cfg.ProxyURL = ""
h.persist(c)
}

View File

@@ -0,0 +1,252 @@
package management
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/config"
)
// Generic helpers for list[string]
func (h *Handler) putStringList(c *gin.Context, set func([]string)) {
data, err := c.GetRawData()
if err != nil {
c.JSON(400, gin.H{"error": "failed to read body"})
return
}
var arr []string
if err = json.Unmarshal(data, &arr); err != nil {
var obj struct {
Items []string `json:"items"`
}
if err2 := json.Unmarshal(data, &obj); err2 != nil || len(obj.Items) == 0 {
c.JSON(400, gin.H{"error": "invalid body"})
return
}
arr = obj.Items
}
set(arr)
h.persist(c)
}
func (h *Handler) patchStringList(c *gin.Context, target *[]string) {
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
h.persist(c)
return
}
if body.Old != nil && body.New != nil {
for i := range *target {
if (*target)[i] == *body.Old {
(*target)[i] = *body.New
h.persist(c)
return
}
}
*target = append(*target, *body.New)
h.persist(c)
return
}
c.JSON(400, gin.H{"error": "missing fields"})
}
func (h *Handler) deleteFromStringList(c *gin.Context, target *[]string) {
if idxStr := c.Query("index"); idxStr != "" {
var idx int
_, err := fmt.Sscanf(idxStr, "%d", &idx)
if err == nil && idx >= 0 && idx < len(*target) {
*target = append((*target)[:idx], (*target)[idx+1:]...)
h.persist(c)
return
}
}
if val := c.Query("value"); val != "" {
out := make([]string, 0, len(*target))
for _, v := range *target {
if v != val {
out = append(out, v)
}
}
*target = out
h.persist(c)
return
}
c.JSON(400, gin.H{"error": "missing index or value"})
}
// api-keys
func (h *Handler) GetAPIKeys(c *gin.Context) { c.JSON(200, gin.H{"api-keys": h.cfg.APIKeys}) }
func (h *Handler) PutAPIKeys(c *gin.Context) {
h.putStringList(c, func(v []string) { h.cfg.APIKeys = v })
}
func (h *Handler) PatchAPIKeys(c *gin.Context) { h.patchStringList(c, &h.cfg.APIKeys) }
func (h *Handler) DeleteAPIKeys(c *gin.Context) { h.deleteFromStringList(c, &h.cfg.APIKeys) }
// generative-language-api-key
func (h *Handler) GetGlKeys(c *gin.Context) {
c.JSON(200, gin.H{"generative-language-api-key": h.cfg.GlAPIKey})
}
func (h *Handler) PutGlKeys(c *gin.Context) {
h.putStringList(c, func(v []string) { h.cfg.GlAPIKey = v })
}
func (h *Handler) PatchGlKeys(c *gin.Context) { h.patchStringList(c, &h.cfg.GlAPIKey) }
func (h *Handler) DeleteGlKeys(c *gin.Context) { h.deleteFromStringList(c, &h.cfg.GlAPIKey) }
// 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) {
data, err := c.GetRawData()
if err != nil {
c.JSON(400, gin.H{"error": "failed to read body"})
return
}
var arr []config.ClaudeKey
if err = json.Unmarshal(data, &arr); err != nil {
var obj struct {
Items []config.ClaudeKey `json:"items"`
}
if err2 := json.Unmarshal(data, &obj); err2 != nil || len(obj.Items) == 0 {
c.JSON(400, gin.H{"error": "invalid body"})
return
}
arr = obj.Items
}
h.cfg.ClaudeKey = arr
h.persist(c)
}
func (h *Handler) PatchClaudeKey(c *gin.Context) {
var body struct {
Index *int `json:"index"`
Match *string `json:"match"`
Value *config.ClaudeKey `json:"value"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
c.JSON(400, gin.H{"error": "invalid body"})
return
}
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.ClaudeKey) {
h.cfg.ClaudeKey[*body.Index] = *body.Value
h.persist(c)
return
}
if body.Match != nil {
for i := range h.cfg.ClaudeKey {
if h.cfg.ClaudeKey[i].APIKey == *body.Match {
h.cfg.ClaudeKey[i] = *body.Value
h.persist(c)
return
}
}
}
c.JSON(404, gin.H{"error": "item not found"})
}
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 {
out = append(out, v)
}
}
h.cfg.ClaudeKey = out
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.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": h.cfg.OpenAICompatibility})
}
func (h *Handler) PutOpenAICompat(c *gin.Context) {
data, err := c.GetRawData()
if err != nil {
c.JSON(400, gin.H{"error": "failed to read body"})
return
}
var arr []config.OpenAICompatibility
if err = json.Unmarshal(data, &arr); err != nil {
var obj struct {
Items []config.OpenAICompatibility `json:"items"`
}
if err2 := json.Unmarshal(data, &obj); err2 != nil || len(obj.Items) == 0 {
c.JSON(400, gin.H{"error": "invalid body"})
return
}
arr = obj.Items
}
h.cfg.OpenAICompatibility = arr
h.persist(c)
}
func (h *Handler) PatchOpenAICompat(c *gin.Context) {
var body struct {
Name *string `json:"name"`
Index *int `json:"index"`
Value *config.OpenAICompatibility `json:"value"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
c.JSON(400, gin.H{"error": "invalid body"})
return
}
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.OpenAICompatibility) {
h.cfg.OpenAICompatibility[*body.Index] = *body.Value
h.persist(c)
return
}
if body.Name != nil {
for i := range h.cfg.OpenAICompatibility {
if h.cfg.OpenAICompatibility[i].Name == *body.Name {
h.cfg.OpenAICompatibility[i] = *body.Value
h.persist(c)
return
}
}
}
c.JSON(404, gin.H{"error": "item not found"})
}
func (h *Handler) DeleteOpenAICompat(c *gin.Context) {
if name := c.Query("name"); name != "" {
out := make([]config.OpenAICompatibility, 0, len(h.cfg.OpenAICompatibility))
for _, v := range h.cfg.OpenAICompatibility {
if v.Name != name {
out = append(out, v)
}
}
h.cfg.OpenAICompatibility = out
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.OpenAICompatibility) {
h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:idx], h.cfg.OpenAICompatibility[idx+1:]...)
h.persist(c)
return
}
}
c.JSON(400, gin.H{"error": "missing name or index"})
}

View File

@@ -0,0 +1,140 @@
// Package management provides the management API handlers and middleware
// for configuring the server and managing auth files.
package management
import (
"fmt"
"net/http"
"strings"
"sync"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/config"
"golang.org/x/crypto/bcrypt"
)
// Handler aggregates config reference, persistence path and helpers.
type Handler struct {
cfg *config.Config
configFilePath string
mu sync.Mutex
}
// NewHandler creates a new management handler instance.
func NewHandler(cfg *config.Config, configFilePath string) *Handler {
return &Handler{cfg: cfg, configFilePath: configFilePath}
}
// SetConfig updates the in-memory config reference when the server hot-reloads.
func (h *Handler) SetConfig(cfg *config.Config) { h.cfg = cfg }
// Middleware enforces access control for management endpoints.
// All requests (local and remote) require a valid management key.
// Additionally, remote access requires allow-remote-management=true.
func (h *Handler) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
clientIP := c.ClientIP()
// Remote access control: when not loopback, must be enabled
if !(clientIP == "127.0.0.1" || clientIP == "::1") {
allowRemote := h.cfg.RemoteManagement.AllowRemote
if !allowRemote {
allowRemote = true
}
if !allowRemote {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "remote management disabled"})
return
}
}
secret := h.cfg.RemoteManagement.SecretKey
if secret == "" {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "remote management key not set"})
return
}
// Accept either Authorization: Bearer <key> or X-Management-Key
var provided string
if ah := c.GetHeader("Authorization"); ah != "" {
parts := strings.SplitN(ah, " ", 2)
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
provided = parts[1]
} else {
provided = ah
}
}
if provided == "" {
provided = c.GetHeader("X-Management-Key")
}
if provided == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing management key"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(secret), []byte(provided)); err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid management key"})
return
}
c.Next()
}
}
// persist saves the current in-memory config to disk.
func (h *Handler) persist(c *gin.Context) bool {
h.mu.Lock()
defer h.mu.Unlock()
// Preserve comments when writing
if err := config.SaveConfigPreserveComments(h.configFilePath, h.cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save config: %v", err)})
return false
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
return true
}
// Helper methods for simple types
func (h *Handler) updateBoolField(c *gin.Context, set func(bool)) {
var body struct {
Value *bool `json:"value"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
var m map[string]any
if err2 := c.ShouldBindJSON(&m); err2 == nil {
for _, v := range m {
if b, ok := v.(bool); ok {
set(b)
h.persist(c)
return
}
}
}
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
set(*body.Value)
h.persist(c)
}
func (h *Handler) updateIntField(c *gin.Context, set func(int)) {
var body struct {
Value *int `json:"value"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
set(*body.Value)
h.persist(c)
}
func (h *Handler) updateStringField(c *gin.Context, set func(string)) {
var body struct {
Value *string `json:"value"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
set(*body.Value)
h.persist(c)
}

View File

@@ -0,0 +1,18 @@
package management
import "github.com/gin-gonic/gin"
// Quota exceeded toggles
func (h *Handler) GetSwitchProject(c *gin.Context) {
c.JSON(200, gin.H{"switch-project": h.cfg.QuotaExceeded.SwitchProject})
}
func (h *Handler) PutSwitchProject(c *gin.Context) {
h.updateBoolField(c, func(v bool) { h.cfg.QuotaExceeded.SwitchProject = v })
}
func (h *Handler) GetSwitchPreviewModel(c *gin.Context) {
c.JSON(200, gin.H{"switch-preview-model": h.cfg.QuotaExceeded.SwitchPreviewModel})
}
func (h *Handler) PutSwitchPreviewModel(c *gin.Context) {
h.updateBoolField(c, func(v bool) { h.cfg.QuotaExceeded.SwitchPreviewModel = v })
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/luispater/CLIProxyAPI/internal/api/handlers"
"github.com/luispater/CLIProxyAPI/internal/api/handlers/claude"
"github.com/luispater/CLIProxyAPI/internal/api/handlers/gemini"
managementHandlers "github.com/luispater/CLIProxyAPI/internal/api/handlers/management"
"github.com/luispater/CLIProxyAPI/internal/api/handlers/openai"
"github.com/luispater/CLIProxyAPI/internal/api/middleware"
"github.com/luispater/CLIProxyAPI/internal/config"
@@ -40,6 +41,12 @@ type Server struct {
// requestLogger is the request logger instance for dynamic configuration updates.
requestLogger *logging.FileRequestLogger
// configFilePath is the absolute path to the YAML config file for persistence.
configFilePath string
// management handler
mgmt *managementHandlers.Handler
}
// NewServer creates and initializes a new API server instance.
@@ -51,7 +58,7 @@ type Server struct {
//
// Returns:
// - *Server: A new server instance
func NewServer(cfg *config.Config, cliClients []interfaces.Client) *Server {
func NewServer(cfg *config.Config, cliClients []interfaces.Client, configFilePath string) *Server {
// Set gin mode
if !cfg.Debug {
gin.SetMode(gin.ReleaseMode)
@@ -72,11 +79,14 @@ func NewServer(cfg *config.Config, cliClients []interfaces.Client) *Server {
// Create server instance
s := &Server{
engine: engine,
handlers: handlers.NewBaseAPIHandlers(cliClients, cfg),
cfg: cfg,
requestLogger: requestLogger,
engine: engine,
handlers: handlers.NewBaseAPIHandlers(cliClients, cfg),
cfg: cfg,
requestLogger: requestLogger,
configFilePath: configFilePath,
}
// Initialize management handler
s.mgmt = managementHandlers.NewHandler(cfg, configFilePath)
// Setup routes
s.setupRoutes()
@@ -130,6 +140,68 @@ func (s *Server) setupRoutes() {
})
})
s.engine.POST("/v1internal:method", geminiCLIHandlers.CLIHandler)
// Management API routes (delegated to management handlers)
// New logic: if remote-management-key is empty, do not expose any management endpoint (404).
if s.cfg.RemoteManagement.SecretKey != "" {
mgmt := s.engine.Group("/v0/management")
mgmt.Use(s.mgmt.Middleware())
{
mgmt.GET("/debug", s.mgmt.GetDebug)
mgmt.PUT("/debug", s.mgmt.PutDebug)
mgmt.PATCH("/debug", s.mgmt.PutDebug)
mgmt.GET("/proxy-url", s.mgmt.GetProxyURL)
mgmt.PUT("/proxy-url", s.mgmt.PutProxyURL)
mgmt.PATCH("/proxy-url", s.mgmt.PutProxyURL)
mgmt.DELETE("/proxy-url", s.mgmt.DeleteProxyURL)
mgmt.GET("/quota-exceeded/switch-project", s.mgmt.GetSwitchProject)
mgmt.PUT("/quota-exceeded/switch-project", s.mgmt.PutSwitchProject)
mgmt.PATCH("/quota-exceeded/switch-project", s.mgmt.PutSwitchProject)
mgmt.GET("/quota-exceeded/switch-preview-model", s.mgmt.GetSwitchPreviewModel)
mgmt.PUT("/quota-exceeded/switch-preview-model", s.mgmt.PutSwitchPreviewModel)
mgmt.PATCH("/quota-exceeded/switch-preview-model", s.mgmt.PutSwitchPreviewModel)
mgmt.GET("/api-keys", s.mgmt.GetAPIKeys)
mgmt.PUT("/api-keys", s.mgmt.PutAPIKeys)
mgmt.PATCH("/api-keys", s.mgmt.PatchAPIKeys)
mgmt.DELETE("/api-keys", s.mgmt.DeleteAPIKeys)
mgmt.GET("/generative-language-api-key", s.mgmt.GetGlKeys)
mgmt.PUT("/generative-language-api-key", s.mgmt.PutGlKeys)
mgmt.PATCH("/generative-language-api-key", s.mgmt.PatchGlKeys)
mgmt.DELETE("/generative-language-api-key", s.mgmt.DeleteGlKeys)
mgmt.GET("/request-log", s.mgmt.GetRequestLog)
mgmt.PUT("/request-log", s.mgmt.PutRequestLog)
mgmt.PATCH("/request-log", s.mgmt.PutRequestLog)
mgmt.GET("/request-retry", s.mgmt.GetRequestRetry)
mgmt.PUT("/request-retry", s.mgmt.PutRequestRetry)
mgmt.PATCH("/request-retry", s.mgmt.PutRequestRetry)
mgmt.GET("/allow-localhost-unauthenticated", s.mgmt.GetAllowLocalhost)
mgmt.PUT("/allow-localhost-unauthenticated", s.mgmt.PutAllowLocalhost)
mgmt.PATCH("/allow-localhost-unauthenticated", s.mgmt.PutAllowLocalhost)
mgmt.GET("/claude-api-key", s.mgmt.GetClaudeKeys)
mgmt.PUT("/claude-api-key", s.mgmt.PutClaudeKeys)
mgmt.PATCH("/claude-api-key", s.mgmt.PatchClaudeKey)
mgmt.DELETE("/claude-api-key", s.mgmt.DeleteClaudeKey)
mgmt.GET("/openai-compatibility", s.mgmt.GetOpenAICompat)
mgmt.PUT("/openai-compatibility", s.mgmt.PutOpenAICompat)
mgmt.PATCH("/openai-compatibility", s.mgmt.PatchOpenAICompat)
mgmt.DELETE("/openai-compatibility", s.mgmt.DeleteOpenAICompat)
mgmt.GET("/auth-files", s.mgmt.ListAuthFiles)
mgmt.GET("/auth-files/download", s.mgmt.DownloadAuthFile)
mgmt.POST("/auth-files", s.mgmt.UploadAuthFile)
mgmt.DELETE("/auth-files", s.mgmt.DeleteAuthFile)
}
}
}
// unifiedModelsHandler creates a unified handler for the /v1/models endpoint
@@ -220,11 +292,26 @@ func (s *Server) UpdateClients(clients []interfaces.Client, cfg *config.Config)
log.Debugf("request logging updated from %t to %t", s.cfg.RequestLog, cfg.RequestLog)
}
// Update log level dynamically when debug flag changes
if s.cfg.Debug != cfg.Debug {
if cfg.Debug {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.InfoLevel)
}
log.Debugf("debug mode updated from %t to %t", s.cfg.Debug, cfg.Debug)
}
s.cfg = cfg
s.handlers.UpdateClients(clients, cfg)
if s.mgmt != nil {
s.mgmt.SetConfig(cfg)
}
log.Infof("server clients and configuration updated: %d clients", len(clients))
}
// (management handlers moved to internal/api/handlers/management)
// AuthMiddleware returns a Gin middleware handler that authenticates requests
// using API keys. If no API keys are configured, it allows all requests.
//