Files
CLIProxyAPI/internal/api/server.go
Luis Pater 1483c31c73 Refactor API handlers organization and simplify error response handling
- Modularized handlers into dedicated packages (`gemini`, `claude`, `cli`) for better structure.
- Centralized `ErrorResponse` and `ErrorDetail` types under `handlers` package for reuse.
- Updated all handlers to utilize the shared `ErrorResponse` model.
- Introduced specialization of handler structs (`GeminiAPIHandlers`, `ClaudeCodeAPIHandlers`, `GeminiCLIAPIHandlers`) for improved clarity and separation of concerns.
- Refactored `getClient` logic with additional properties and better state management.

Refactor `translator` package by modularizing code for `claude` and `gemini`

- Moved Claude-specific logic (`PrepareClaudeRequest`, `ConvertCliToClaude`) to `translator/claude/code`.
- Moved Gemini-specific logic (`FixCLIToolResponse`) to `translator/gemini/cli` for better package structure.
- Updated affected handler imports and method references.

Add comprehensive package-level documentation across key modules

- Introduced detailed package-level documentation for core modules: `auth`, `client`, `cmd`, `handlers`, `util`, `watcher`, `config`, `translator`, and `api`.
- Enhanced code readability and maintainability by clarifying the purpose and functionality of each package.
- Aligned documentation style and tone with existing codebase conventions.

Refactor API handlers and translator modules for improved clarity and consistency

- Standardized handler struct names (`GeminiAPIHandlers`, `ClaudeCodeAPIHandlers`, `GeminiCLIAPIHandlers`, `OpenAIAPIHandlers`) and updated related comments.
- Fixed unnecessary `else` blocks in streaming logic for cleaner error handling.
- Renamed variables for better readability (`responseIdResult` to `responseIDResult`, `activationUrl` to `activationURL`, etc.).
- Addressed minor inconsistencies in API handler comments and SSE header initialization.
- Improved modularization of `claude` and `gemini` translator components.

Standardize configuration field naming for consistency across modules

- Renamed `ProxyUrl` to `ProxyURL`, `ApiKeys` to `APIKeys`, and `ConfigQuotaExceeded` to `QuotaExceeded`.
- Updated all relevant references and comments in `config`, `auth`, `api`, `util`, and `watcher`.
- Ensured consistent casing for `GlAPIKey` debug logs.
2025-08-05 23:11:31 +08:00

214 lines
6.1 KiB
Go

// Package api provides the HTTP API server implementation for the CLI Proxy API.
// It includes the main server struct, routing setup, middleware for CORS and authentication,
// and integration with various AI API handlers (OpenAI, Claude, Gemini).
// The server supports hot-reloading of clients and configuration.
package api
import (
"context"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/api/handlers"
"github.com/luispater/CLIProxyAPI/internal/api/handlers/claude"
"github.com/luispater/CLIProxyAPI/internal/api/handlers/gemini"
"github.com/luispater/CLIProxyAPI/internal/api/handlers/gemini/cli"
"github.com/luispater/CLIProxyAPI/internal/api/handlers/openai"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
log "github.com/sirupsen/logrus"
"net/http"
"strings"
)
// Server represents the main API server.
// It encapsulates the Gin engine, HTTP server, handlers, and configuration.
type Server struct {
engine *gin.Engine
server *http.Server
handlers *handlers.APIHandlers
cfg *config.Config
}
// NewServer creates and initializes a new API server instance.
// It sets up the Gin engine, middleware, routes, and handlers.
func NewServer(cfg *config.Config, cliClients []*client.Client) *Server {
// Set gin mode
if !cfg.Debug {
gin.SetMode(gin.ReleaseMode)
}
// Create gin engine
engine := gin.New()
// Add middleware
engine.Use(gin.Logger())
engine.Use(gin.Recovery())
engine.Use(corsMiddleware())
// Create server instance
s := &Server{
engine: engine,
handlers: handlers.NewAPIHandlers(cliClients, cfg),
cfg: cfg,
}
// Setup routes
s.setupRoutes()
// Create HTTP server
s.server = &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
Handler: engine,
}
return s
}
// setupRoutes configures the API routes for the server.
// It defines the endpoints and associates them with their respective handlers.
func (s *Server) setupRoutes() {
openaiHandlers := openai.NewOpenAIAPIHandlers(s.handlers)
geminiHandlers := gemini.NewGeminiAPIHandlers(s.handlers)
geminiCLIHandlers := cli.NewGeminiCLIAPIHandlers(s.handlers)
claudeCodeHandlers := claude.NewClaudeCodeAPIHandlers(s.handlers)
// OpenAI compatible API routes
v1 := s.engine.Group("/v1")
v1.Use(AuthMiddleware(s.cfg))
{
v1.GET("/models", openaiHandlers.Models)
v1.POST("/chat/completions", openaiHandlers.ChatCompletions)
v1.POST("/messages", claudeCodeHandlers.ClaudeMessages)
}
// Gemini compatible API routes
v1beta := s.engine.Group("/v1beta")
v1beta.Use(AuthMiddleware(s.cfg))
{
v1beta.GET("/models", geminiHandlers.GeminiModels)
v1beta.POST("/models/:action", geminiHandlers.GeminiHandler)
v1beta.GET("/models/:action", geminiHandlers.GeminiGetHandler)
}
// Root endpoint
s.engine.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "CLI Proxy API Server",
"version": "1.0.0",
"endpoints": []string{
"POST /v1/chat/completions",
"GET /v1/models",
},
})
})
s.engine.POST("/v1internal:method", geminiCLIHandlers.CLIHandler)
}
// Start begins listening for and serving HTTP requests.
// It's a blocking call and will only return on an unrecoverable error.
func (s *Server) Start() error {
log.Debugf("Starting API server on %s", s.server.Addr)
// Start the HTTP server.
if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("failed to start HTTP server: %v", err)
}
return nil
}
// Stop gracefully shuts down the API server without interrupting any
// active connections.
func (s *Server) Stop(ctx context.Context) error {
log.Debug("Stopping API server...")
// Shutdown the HTTP server.
if err := s.server.Shutdown(ctx); err != nil {
return fmt.Errorf("failed to shutdown HTTP server: %v", err)
}
log.Debug("API server stopped")
return nil
}
// corsMiddleware returns a Gin middleware handler that adds CORS headers
// to every response, allowing cross-origin requests.
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// UpdateClients updates the server's client list and configuration
func (s *Server) UpdateClients(clients []*client.Client, cfg *config.Config) {
s.cfg = cfg
s.handlers.UpdateClients(clients, cfg)
log.Infof("server clients and configuration updated: %d clients", len(clients))
}
// AuthMiddleware returns a Gin middleware handler that authenticates requests
// using API keys. If no API keys are configured, it allows all requests.
func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
if len(cfg.APIKeys) == 0 {
c.Next()
return
}
// Get the Authorization header
authHeader := c.GetHeader("Authorization")
authHeaderGoogle := c.GetHeader("X-Goog-Api-Key")
authHeaderAnthropic := c.GetHeader("X-Api-Key")
// Get the API key from the query parameter
apiKeyQuery, _ := c.GetQuery("key")
if authHeader == "" && authHeaderGoogle == "" && authHeaderAnthropic == "" && apiKeyQuery == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Missing API key",
})
return
}
// Extract the API key
parts := strings.Split(authHeader, " ")
var apiKey string
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
apiKey = parts[1]
} else {
apiKey = authHeader
}
// Find the API key in the in-memory list
var foundKey string
for i := range cfg.APIKeys {
if cfg.APIKeys[i] == apiKey || cfg.APIKeys[i] == authHeaderGoogle || cfg.APIKeys[i] == authHeaderAnthropic || cfg.APIKeys[i] == apiKeyQuery {
foundKey = cfg.APIKeys[i]
break
}
}
if foundKey == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid API key",
})
return
}
// Store the API key and user in the context
c.Set("apiKey", foundKey)
c.Next()
}
}