Files
gpt-load/internal/proxy/server.go
T

374 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package proxy provides high-performance OpenAI multi-key proxy server
package proxy
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"gpt-load/internal/channel"
"gpt-load/internal/config"
"gpt-load/internal/encryption"
app_errors "gpt-load/internal/errors"
"gpt-load/internal/keypool"
"gpt-load/internal/models"
"gpt-load/internal/response"
"gpt-load/internal/services"
"gpt-load/internal/utils"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// ProxyServer represents the proxy server
type ProxyServer struct {
keyProvider *keypool.KeyProvider
groupManager *services.GroupManager
subGroupManager *services.SubGroupManager
settingsManager *config.SystemSettingsManager
channelFactory *channel.Factory
requestLogService *services.RequestLogService
encryptionSvc encryption.Service
}
// NewProxyServer creates a new proxy server
func NewProxyServer(
keyProvider *keypool.KeyProvider,
groupManager *services.GroupManager,
subGroupManager *services.SubGroupManager,
settingsManager *config.SystemSettingsManager,
channelFactory *channel.Factory,
requestLogService *services.RequestLogService,
encryptionSvc encryption.Service,
) (*ProxyServer, error) {
return &ProxyServer{
keyProvider: keyProvider,
groupManager: groupManager,
subGroupManager: subGroupManager,
settingsManager: settingsManager,
channelFactory: channelFactory,
requestLogService: requestLogService,
encryptionSvc: encryptionSvc,
}, nil
}
// HandleProxy is the main entry point for proxy requests, refactored based on the stable .bak logic.
func (ps *ProxyServer) HandleProxy(c *gin.Context) {
startTime := time.Now()
groupName := c.Param("group_name")
originalGroup, err := ps.groupManager.GetGroupByName(groupName)
if err != nil {
response.Error(c, app_errors.ParseDBError(err))
return
}
// Select sub-group if this is an aggregate group
subGroupName, err := ps.subGroupManager.SelectSubGroup(originalGroup)
if err != nil {
logrus.WithFields(logrus.Fields{
"aggregate_group": originalGroup.Name,
"error": err,
}).Error("Failed to select sub-group from aggregate")
response.Error(c, app_errors.NewAPIError(app_errors.ErrNoKeysAvailable, "No available sub-groups"))
return
}
group := originalGroup
if subGroupName != "" {
group, err = ps.groupManager.GetGroupByName(subGroupName)
if err != nil {
response.Error(c, app_errors.ParseDBError(err))
return
}
}
channelHandler, err := ps.channelFactory.GetChannel(group)
if err != nil {
response.Error(c, app_errors.NewAPIError(app_errors.ErrInternalServer, fmt.Sprintf("Failed to get channel for group '%s': %v", groupName, err)))
return
}
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
logrus.Errorf("Failed to read request body: %v", err)
response.Error(c, app_errors.NewAPIError(app_errors.ErrBadRequest, "Failed to read request body"))
return
}
c.Request.Body.Close()
finalBodyBytes, err := ps.applyParamOverrides(bodyBytes, group)
if err != nil {
response.Error(c, app_errors.NewAPIError(app_errors.ErrInternalServer, fmt.Sprintf("Failed to apply parameter overrides: %v", err)))
return
}
isStream := channelHandler.IsStreamRequest(c, bodyBytes)
ps.executeRequestWithRetry(c, channelHandler, originalGroup, group, finalBodyBytes, isStream, startTime, 0)
}
// executeRequestWithRetry is the core recursive function for handling requests and retries.
func (ps *ProxyServer) executeRequestWithRetry(
c *gin.Context,
channelHandler channel.ChannelProxy,
originalGroup *models.Group,
group *models.Group,
bodyBytes []byte,
isStream bool,
startTime time.Time,
retryCount int,
) {
cfg := group.EffectiveConfig
apiKey, err := ps.keyProvider.SelectKey(group.ID)
if err != nil {
logrus.Errorf("Failed to select a key for group %s on attempt %d: %v", group.Name, retryCount+1, err)
response.Error(c, app_errors.NewAPIError(app_errors.ErrNoKeysAvailable, err.Error()))
ps.logRequest(c, originalGroup, group, nil, startTime, http.StatusServiceUnavailable, err, isStream, "", channelHandler, bodyBytes, models.RequestTypeFinal)
return
}
upstreamURL, err := channelHandler.BuildUpstreamURL(c.Request.URL, originalGroup.Name)
if err != nil {
response.Error(c, app_errors.NewAPIError(app_errors.ErrInternalServer, fmt.Sprintf("Failed to build upstream URL: %v", err)))
return
}
var ctx context.Context
var cancel context.CancelFunc
if isStream {
ctx, cancel = context.WithCancel(c.Request.Context())
} else {
timeout := time.Duration(cfg.RequestTimeout) * time.Second
ctx, cancel = context.WithTimeout(c.Request.Context(), timeout)
}
defer cancel()
req, err := http.NewRequestWithContext(ctx, c.Request.Method, upstreamURL, bytes.NewReader(bodyBytes))
if err != nil {
logrus.Errorf("Failed to create upstream request: %v", err)
response.Error(c, app_errors.ErrInternalServer)
return
}
req.ContentLength = int64(len(bodyBytes))
req.Header = c.Request.Header.Clone()
// Clean up client auth key
req.Header.Del("Authorization")
req.Header.Del("X-Api-Key")
req.Header.Del("X-Goog-Api-Key")
// Apply model redirection
finalBodyBytes, err := channelHandler.ApplyModelRedirect(req, bodyBytes, group)
if err != nil {
response.Error(c, app_errors.NewAPIError(app_errors.ErrBadRequest, err.Error()))
ps.logRequest(c, originalGroup, group, apiKey, startTime, http.StatusBadRequest, err, isStream, upstreamURL, channelHandler, bodyBytes, models.RequestTypeFinal)
return
}
// Update request body if it was modified by redirection
if !bytes.Equal(finalBodyBytes, bodyBytes) {
req.Body = io.NopCloser(bytes.NewReader(finalBodyBytes))
req.ContentLength = int64(len(finalBodyBytes))
}
channelHandler.ModifyRequest(req, apiKey, group)
// Apply custom header rules
if len(group.HeaderRuleList) > 0 {
headerCtx := utils.NewHeaderVariableContextFromGin(c, group, apiKey)
utils.ApplyHeaderRules(req, group.HeaderRuleList, headerCtx)
}
var client *http.Client
if isStream {
client = channelHandler.GetStreamClient()
req.Header.Set("X-Accel-Buffering", "no")
} else {
client = channelHandler.GetHTTPClient()
}
resp, err := client.Do(req)
if resp != nil {
defer resp.Body.Close()
}
// Unified error handling for retries.
// Retry policy is fully defined by group.FailoverStatusCodeMatcher (derived from EffectiveConfig).
shouldRetryByStatus := resp != nil && shouldFailoverOnStatusCode(resp.StatusCode, group)
if err != nil || shouldRetryByStatus {
if err != nil && app_errors.IsIgnorableError(err) {
logrus.Debugf("Client-side ignorable error for key %s, aborting retries: %v", utils.MaskAPIKey(apiKey.KeyValue), err)
ps.logRequest(c, originalGroup, group, apiKey, startTime, 499, err, isStream, upstreamURL, channelHandler, bodyBytes, models.RequestTypeFinal)
return
}
var statusCode int
var errorMessage string
var parsedError string
if err != nil {
statusCode = 500
errorMessage = err.Error()
parsedError = errorMessage
logrus.Debugf("Request failed (attempt %d/%d) for key %s: %v", retryCount+1, cfg.MaxRetries, utils.MaskAPIKey(apiKey.KeyValue), err)
} else {
// Retryable upstream response (HTTP status code matched failover policy)
statusCode = resp.StatusCode
errorBody, readErr := io.ReadAll(resp.Body)
if readErr != nil {
logrus.Errorf("Failed to read error body: %v", readErr)
errorBody = []byte("Failed to read error body")
}
errorBody = handleGzipCompression(resp, errorBody)
errorMessage = string(errorBody)
parsedError = app_errors.ParseUpstreamError(errorBody)
logrus.Debugf("Request failed with status %d (attempt %d/%d) for key %s. Parsed Error: %s", statusCode, retryCount+1, cfg.MaxRetries, utils.MaskAPIKey(apiKey.KeyValue), parsedError)
}
// 上游 key 可能出现在错误文本中(如 Gemini 通道将 key 放入 URL query,
// 传输层错误会把完整 URL 带入 err.Error()),返回客户端和落库前先脱敏
errorMessage = utils.RedactSecret(errorMessage, apiKey.KeyValue)
parsedError = utils.RedactSecret(parsedError, apiKey.KeyValue)
// 使用解析后的错误信息更新密钥状态
ps.keyProvider.UpdateStatus(apiKey, group, false, parsedError)
// 判断是否为最后一次尝试
isLastAttempt := retryCount >= cfg.MaxRetries
requestType := models.RequestTypeRetry
if isLastAttempt {
requestType = models.RequestTypeFinal
}
ps.logRequest(c, originalGroup, group, apiKey, startTime, statusCode, errors.New(parsedError), isStream, upstreamURL, channelHandler, bodyBytes, requestType)
// 如果是最后一次尝试,直接返回错误,不再递归
if isLastAttempt {
var errorJSON map[string]any
if err := json.Unmarshal([]byte(errorMessage), &errorJSON); err == nil {
c.JSON(statusCode, errorJSON)
} else {
response.Error(c, app_errors.NewAPIErrorWithUpstream(statusCode, "UPSTREAM_ERROR", errorMessage))
}
return
}
ps.executeRequestWithRetry(c, channelHandler, originalGroup, group, bodyBytes, isStream, startTime, retryCount+1)
return
}
// ps.keyProvider.UpdateStatus(apiKey, group, true) // 请求成功不再重置成功次数,减少IO消耗
logrus.Debugf("Request for group %s succeeded on attempt %d with key %s", group.Name, retryCount+1, utils.MaskAPIKey(apiKey.KeyValue))
// Check if this is a model list request (needs special handling)
if shouldInterceptModelList(c.Request.URL.Path, c.Request.Method) {
ps.handleModelListResponse(c, resp, group, channelHandler)
} else {
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
}
}
c.Status(resp.StatusCode)
if isStream {
ps.handleStreamingResponse(c, resp)
} else {
ps.handleNormalResponse(c, resp)
}
}
ps.logRequest(c, originalGroup, group, apiKey, startTime, resp.StatusCode, nil, isStream, upstreamURL, channelHandler, bodyBytes, models.RequestTypeFinal)
}
func shouldFailoverOnStatusCode(statusCode int, group *models.Group) bool {
if group == nil {
return false
}
return group.FailoverStatusCodeMatcher.Match(statusCode)
}
// logRequest is a helper function to create and record a request log.
func (ps *ProxyServer) logRequest(
c *gin.Context,
originalGroup *models.Group,
group *models.Group,
apiKey *models.APIKey,
startTime time.Time,
statusCode int,
finalError error,
isStream bool,
upstreamAddr string,
channelHandler channel.ChannelProxy,
bodyBytes []byte,
requestType string,
) {
if ps.requestLogService == nil {
return
}
var requestBodyToLog, userAgent string
if group.EffectiveConfig.EnableRequestBodyLogging {
requestBodyToLog = utils.TruncateString(string(bodyBytes), 65000)
userAgent = c.Request.UserAgent()
}
duration := time.Since(startTime).Milliseconds()
logEntry := &models.RequestLog{
GroupID: group.ID,
GroupName: group.Name,
IsSuccess: finalError == nil && statusCode < 400,
SourceIP: c.ClientIP(),
StatusCode: statusCode,
RequestPath: utils.TruncateString(c.Request.URL.String(), 500),
Duration: duration,
UserAgent: userAgent,
RequestType: requestType,
IsStream: isStream,
UpstreamAddr: utils.TruncateString(upstreamAddr, 500),
RequestBody: requestBodyToLog,
}
// Set parent group
if originalGroup != nil && originalGroup.GroupType == "aggregate" && originalGroup.ID != group.ID {
logEntry.ParentGroupID = originalGroup.ID
logEntry.ParentGroupName = originalGroup.Name
}
if channelHandler != nil && bodyBytes != nil {
logEntry.Model = channelHandler.ExtractModel(c, bodyBytes)
}
if apiKey != nil {
// 加密密钥值用于日志存储
encryptedKeyValue, err := ps.encryptionSvc.Encrypt(apiKey.KeyValue)
if err != nil {
logrus.WithError(err).Error("Failed to encrypt key value for logging")
logEntry.KeyValue = "failed-to-encryption"
} else {
logEntry.KeyValue = encryptedKeyValue
}
// 添加 KeyHash 用于反查
logEntry.KeyHash = ps.encryptionSvc.Hash(apiKey.KeyValue)
}
if finalError != nil {
logEntry.ErrorMessage = finalError.Error()
}
if err := ps.requestLogService.Record(logEntry); err != nil {
logrus.Errorf("Failed to record request log: %v", err)
}
}