// Package handlers provides core API handler functionality for the CLI Proxy API server. // It includes common types, client management, load balancing, and error handling // shared across all API endpoint handlers (OpenAI, Claude, Gemini). package handlers import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" "golang.org/x/net/context" ) // ErrorResponse represents a standard error response format for the API. // It contains a single ErrorDetail field. type ErrorResponse struct { // Error contains detailed information about the error that occurred. Error ErrorDetail `json:"error"` } // ErrorDetail provides specific information about an error that occurred. // It includes a human-readable message, an error type, and an optional error code. type ErrorDetail struct { // Message is a human-readable message providing more details about the error. Message string `json:"message"` // Type is the category of error that occurred (e.g., "invalid_request_error"). Type string `json:"type"` // Code is a short code identifying the error, if applicable. Code string `json:"code,omitempty"` } // BaseAPIHandler contains the handlers for API endpoints. // It holds a pool of clients to interact with the backend service and manages // load balancing, client selection, and configuration. type BaseAPIHandler struct { // AuthManager manages auth lifecycle and execution in the new architecture. AuthManager *coreauth.Manager // Cfg holds the current application configuration. Cfg *config.Config } // NewBaseAPIHandlers creates a new API handlers instance. // It takes a slice of clients and configuration as input. // // Parameters: // - cliClients: A slice of AI service clients // - cfg: The application configuration // // Returns: // - *BaseAPIHandler: A new API handlers instance func NewBaseAPIHandlers(cfg *config.Config, authManager *coreauth.Manager) *BaseAPIHandler { return &BaseAPIHandler{ Cfg: cfg, AuthManager: authManager, } } // UpdateClients updates the handlers' client list and configuration. // This method is called when the configuration or authentication tokens change. // // Parameters: // - clients: The new slice of AI service clients // - cfg: The new application configuration func (h *BaseAPIHandler) UpdateClients(cfg *config.Config) { h.Cfg = cfg } // GetAlt extracts the 'alt' parameter from the request query string. // It checks both 'alt' and '$alt' parameters and returns the appropriate value. // // Parameters: // - c: The Gin context containing the HTTP request // // Returns: // - string: The alt parameter value, or empty string if it's "sse" func (h *BaseAPIHandler) GetAlt(c *gin.Context) string { var alt string var hasAlt bool alt, hasAlt = c.GetQuery("alt") if !hasAlt { alt, _ = c.GetQuery("$alt") } if alt == "sse" { return "" } return alt } // GetContextWithCancel creates a new context with cancellation capabilities. // It embeds the Gin context and the API handler into the new context for later use. // The returned cancel function also handles logging the API response if request logging is enabled. // // Parameters: // - handler: The API handler associated with the request. // - c: The Gin context of the current request. // - ctx: The parent context. // // Returns: // - context.Context: The new context with cancellation and embedded values. // - APIHandlerCancelFunc: A function to cancel the context and log the response. func (h *BaseAPIHandler) GetContextWithCancel(handler interfaces.APIHandler, c *gin.Context, ctx context.Context) (context.Context, APIHandlerCancelFunc) { newCtx, cancel := context.WithCancel(ctx) newCtx = context.WithValue(newCtx, "gin", c) newCtx = context.WithValue(newCtx, "handler", handler) return newCtx, func(params ...interface{}) { if h.Cfg.RequestLog { if len(params) == 1 { data := params[0] switch data.(type) { case []byte: c.Set("API_RESPONSE", data.([]byte)) case error: c.Set("API_RESPONSE", []byte(data.(error).Error())) case string: c.Set("API_RESPONSE", []byte(data.(string))) case bool: case nil: } } } cancel() } } // ExecuteWithAuthManager executes a non-streaming request via the core auth manager. // This path is the only supported execution route. func (h *BaseAPIHandler) ExecuteWithAuthManager(ctx context.Context, handlerType, modelName string, rawJSON []byte, alt string) ([]byte, *interfaces.ErrorMessage) { providers := util.GetProviderName(modelName, h.Cfg) if len(providers) == 0 { return nil, &interfaces.ErrorMessage{StatusCode: http.StatusBadRequest, Error: fmt.Errorf("unknown provider for model %s", modelName)} } req := coreexecutor.Request{ Model: modelName, Payload: cloneBytes(rawJSON), } opts := coreexecutor.Options{ Stream: false, Alt: alt, OriginalRequest: cloneBytes(rawJSON), SourceFormat: sdktranslator.FromString(handlerType), } resp, err := h.AuthManager.Execute(ctx, providers, req, opts) if err != nil { return nil, &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: err} } return cloneBytes(resp.Payload), nil } // ExecuteCountWithAuthManager executes a non-streaming request via the core auth manager. // This path is the only supported execution route. func (h *BaseAPIHandler) ExecuteCountWithAuthManager(ctx context.Context, handlerType, modelName string, rawJSON []byte, alt string) ([]byte, *interfaces.ErrorMessage) { providers := util.GetProviderName(modelName, h.Cfg) if len(providers) == 0 { return nil, &interfaces.ErrorMessage{StatusCode: http.StatusBadRequest, Error: fmt.Errorf("unknown provider for model %s", modelName)} } req := coreexecutor.Request{ Model: modelName, Payload: cloneBytes(rawJSON), } opts := coreexecutor.Options{ Stream: false, Alt: alt, OriginalRequest: cloneBytes(rawJSON), SourceFormat: sdktranslator.FromString(handlerType), } resp, err := h.AuthManager.ExecuteCount(ctx, providers, req, opts) if err != nil { return nil, &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: err} } return cloneBytes(resp.Payload), nil } // ExecuteStreamWithAuthManager executes a streaming request via the core auth manager. // This path is the only supported execution route. func (h *BaseAPIHandler) ExecuteStreamWithAuthManager(ctx context.Context, handlerType, modelName string, rawJSON []byte, alt string) (<-chan []byte, <-chan *interfaces.ErrorMessage) { providers := util.GetProviderName(modelName, h.Cfg) if len(providers) == 0 { errChan := make(chan *interfaces.ErrorMessage, 1) errChan <- &interfaces.ErrorMessage{StatusCode: http.StatusBadRequest, Error: fmt.Errorf("unknown provider for model %s", modelName)} close(errChan) return nil, errChan } req := coreexecutor.Request{ Model: modelName, Payload: cloneBytes(rawJSON), } opts := coreexecutor.Options{ Stream: true, Alt: alt, OriginalRequest: cloneBytes(rawJSON), SourceFormat: sdktranslator.FromString(handlerType), } chunks, err := h.AuthManager.ExecuteStream(ctx, providers, req, opts) if err != nil { errChan := make(chan *interfaces.ErrorMessage, 1) errChan <- &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: err} close(errChan) return nil, errChan } dataChan := make(chan []byte) errChan := make(chan *interfaces.ErrorMessage, 1) go func() { defer close(dataChan) defer close(errChan) for chunk := range chunks { if chunk.Err != nil { errChan <- &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: chunk.Err} return } if len(chunk.Payload) > 0 { dataChan <- cloneBytes(chunk.Payload) } } }() return dataChan, errChan } func cloneBytes(src []byte) []byte { if len(src) == 0 { return nil } dst := make([]byte, len(src)) copy(dst, src) return dst } // WriteErrorResponse writes an error message to the response writer using the HTTP status embedded in the message. func (h *BaseAPIHandler) WriteErrorResponse(c *gin.Context, msg *interfaces.ErrorMessage) { status := http.StatusInternalServerError if msg != nil && msg.StatusCode > 0 { status = msg.StatusCode } c.Status(status) if msg != nil && msg.Error != nil { _, _ = c.Writer.Write([]byte(msg.Error.Error())) } else { _, _ = c.Writer.Write([]byte(http.StatusText(status))) } } func (h *BaseAPIHandler) LoggingAPIResponseError(ctx context.Context, err *interfaces.ErrorMessage) { if h.Cfg.RequestLog { if ginContext, ok := ctx.Value("gin").(*gin.Context); ok { if apiResponseErrors, isExist := ginContext.Get("API_RESPONSE_ERROR"); isExist { if slicesAPIResponseError, isOk := apiResponseErrors.([]*interfaces.ErrorMessage); isOk { slicesAPIResponseError = append(slicesAPIResponseError, err) ginContext.Set("API_RESPONSE_ERROR", slicesAPIResponseError) } } else { // Create new response data entry ginContext.Set("API_RESPONSE_ERROR", []*interfaces.ErrorMessage{err}) } } } } // APIHandlerCancelFunc is a function type for canceling an API handler's context. // It can optionally accept parameters, which are used for logging the response. type APIHandlerCancelFunc func(params ...interface{})