mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
192 lines
7.3 KiB
Go
192 lines
7.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/domain"
|
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// GrokCountTokens handles Anthropic-compatible count_tokens requests locally.
|
|
// The route middleware already authenticates the API key and resolves the
|
|
// group; this handler intentionally does not select an account or check billing.
|
|
func (h *OpenAIGatewayHandler) GrokCountTokens(c *gin.Context) {
|
|
body, err := readLenientJSONRequestBodyWithPrealloc(c.Request, h.cfg)
|
|
if err != nil {
|
|
if maxErr, ok := extractMaxBytesError(err); ok {
|
|
h.anthropicErrorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
|
|
return
|
|
}
|
|
h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
|
|
return
|
|
}
|
|
if len(body) == 0 {
|
|
h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
|
|
return
|
|
}
|
|
|
|
bodyRef := service.NewRequestBodyRef(body)
|
|
parsedReq, err := service.ParseGatewayRequest(bodyRef, domain.PlatformAnthropic)
|
|
if err != nil {
|
|
logRequestBodyParseFailure(requestLogger(c, "handler.openai_gateway.grok_count_tokens"), body, err)
|
|
h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
|
|
return
|
|
}
|
|
if parsedReq.Model == "" {
|
|
h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
|
|
return
|
|
}
|
|
|
|
estimated, err := service.EstimateGrokCountTokens(parsedReq.Body.Bytes())
|
|
if err != nil {
|
|
requestLogger(c, "handler.openai_gateway.grok_count_tokens").Warn("grok_count_tokens.local_estimate_failed", zap.Error(err))
|
|
h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
|
|
return
|
|
}
|
|
|
|
setOpsRequestContext(c, parsedReq.Model, false)
|
|
setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(false, false)))
|
|
c.JSON(http.StatusOK, gin.H{"input_tokens": estimated})
|
|
}
|
|
|
|
// CountTokens handles Anthropic-compatible POST /v1/messages/count_tokens for OpenAI groups.
|
|
// It validates billing and routes to an OpenAI token-count bridge without taking concurrency slots
|
|
// or recording usage.
|
|
func (h *OpenAIGatewayHandler) CountTokens(c *gin.Context) {
|
|
apiKey, ok := middleware2.GetAPIKeyFromContext(c)
|
|
if !ok {
|
|
h.anthropicErrorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
|
|
return
|
|
}
|
|
|
|
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
|
if !ok {
|
|
h.anthropicErrorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
|
|
return
|
|
}
|
|
reqLog := requestLogger(
|
|
c,
|
|
"handler.openai_gateway.count_tokens",
|
|
zap.Int64("user_id", subject.UserID),
|
|
zap.Int64("api_key_id", apiKey.ID),
|
|
zap.Any("group_id", apiKey.GroupID),
|
|
)
|
|
|
|
if apiKey.Group != nil && !apiKey.Group.AllowMessagesDispatch {
|
|
h.anthropicErrorResponse(c, http.StatusForbidden, "permission_error",
|
|
"This group does not allow /v1/messages dispatch")
|
|
return
|
|
}
|
|
|
|
if !h.ensureResponsesDependencies(c, reqLog) {
|
|
return
|
|
}
|
|
|
|
body, err := readLenientJSONRequestBodyWithPrealloc(c.Request, h.cfg)
|
|
if err != nil {
|
|
if maxErr, ok := extractMaxBytesError(err); ok {
|
|
h.anthropicErrorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
|
|
return
|
|
}
|
|
h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
|
|
return
|
|
}
|
|
if len(body) == 0 {
|
|
h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
|
|
return
|
|
}
|
|
|
|
bodyRef := service.NewRequestBodyRef(body)
|
|
parsedReq, err := service.ParseGatewayRequest(bodyRef, domain.PlatformAnthropic)
|
|
if err != nil {
|
|
logRequestBodyParseFailure(reqLog, body, err)
|
|
h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
|
|
return
|
|
}
|
|
body = parsedReq.Body.Bytes()
|
|
if parsedReq.Model == "" {
|
|
h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
|
|
return
|
|
}
|
|
|
|
reqModel := parsedReq.Model
|
|
routingModel := service.NormalizeOpenAICompatRequestedModel(reqModel)
|
|
preferredMappedModel := resolveOpenAIMessagesDispatchMappedModel(apiKey, reqModel)
|
|
reqLog = reqLog.With(zap.String("model", reqModel), zap.Bool("stream", parsedReq.Stream))
|
|
|
|
setOpsRequestContext(c, reqModel, false)
|
|
setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(false, false)))
|
|
|
|
channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel)
|
|
mappedBodyForMessages := newOpenAIModelMappedBodyCache(body, h.gatewayService.ReplaceModelInBody)
|
|
|
|
subscription, _ := middleware2.GetSubscriptionFromContext(c)
|
|
if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription, service.QuotaPlatform(c.Request.Context(), apiKey)); err != nil {
|
|
reqLog.Info("openai_count_tokens.billing_eligibility_check_failed", zap.Error(err))
|
|
status, code, message, retryAfter := billingErrorDetails(err)
|
|
if retryAfter > 0 {
|
|
c.Header("Retry-After", strconv.Itoa(retryAfter))
|
|
}
|
|
h.anthropicErrorResponse(c, status, code, message)
|
|
return
|
|
}
|
|
|
|
requestStart := time.Now()
|
|
sessionHash := h.gatewayService.GenerateSessionHash(c, body)
|
|
currentRoutingModel := routingModel
|
|
if preferredMappedModel != "" {
|
|
currentRoutingModel = preferredMappedModel
|
|
}
|
|
selection, _, err := h.gatewayService.SelectAccountWithSchedulerForCapability(
|
|
c.Request.Context(),
|
|
apiKey.GroupID,
|
|
"",
|
|
sessionHash,
|
|
currentRoutingModel,
|
|
nil,
|
|
service.OpenAIUpstreamTransportAny,
|
|
service.OpenAIEndpointCapabilityChatCompletions,
|
|
false,
|
|
false,
|
|
false,
|
|
openAICompatibleRequestPlatform(apiKey),
|
|
)
|
|
service.SetOpsLatencyMs(c, service.OpsAuthLatencyMsKey, time.Since(requestStart).Milliseconds())
|
|
if err != nil {
|
|
requestPlatform := openAICompatibleRequestPlatform(apiKey)
|
|
reqLog.Warn("openai_count_tokens.account_select_failed", zap.Error(openAICompatibleSelectionErrorForLog(err, requestPlatform)))
|
|
cls := classifyOpenAICompatibleNoAccountErrorFromGin(c, h.gatewayService, apiKey, currentRoutingModel, reqModel)
|
|
if !cls.ModelNotFound {
|
|
markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
|
|
}
|
|
h.anthropicErrorResponse(c, cls.Status, cls.ErrType, cls.Message)
|
|
return
|
|
}
|
|
if selection == nil || selection.Account == nil {
|
|
cls := classifyOpenAICompatibleNoAccountErrorFromGin(c, h.gatewayService, apiKey, currentRoutingModel, reqModel)
|
|
if !cls.ModelNotFound {
|
|
markOpsRoutingCapacityLimited(c)
|
|
}
|
|
h.anthropicErrorResponse(c, cls.Status, cls.ErrType, cls.Message)
|
|
return
|
|
}
|
|
|
|
account := selection.Account
|
|
setOpsSelectedAccount(c, account.ID, account.Platform)
|
|
if selection.Acquired && selection.ReleaseFunc != nil {
|
|
defer selection.ReleaseFunc()
|
|
}
|
|
forwardBody := mappedBodyForMessages(channelMapping.Mapped, channelMapping.MappedModel)
|
|
defaultMappedModel := preferredMappedModel
|
|
|
|
if err := h.gatewayService.ForwardCountTokensAsAnthropic(c.Request.Context(), c, account, forwardBody, defaultMappedModel); err != nil {
|
|
reqLog.Error("openai_count_tokens.forward_failed", zap.Int64("account_id", account.ID), zap.Error(err))
|
|
}
|
|
}
|