openai_gateway_count_tokens.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. package handler
  2. import (
  3. "net/http"
  4. "strconv"
  5. "strings"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/domain"
  8. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. "github.com/tidwall/gjson"
  12. "go.uber.org/zap"
  13. )
  14. // ResponsesInputTokens handles native OpenAI POST
  15. // /v1/responses/input_tokens requests without routing them through the normal
  16. // Responses generation and usage-recording pipeline.
  17. func (h *OpenAIGatewayHandler) ResponsesInputTokens(c *gin.Context) {
  18. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  19. if !ok {
  20. h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  21. return
  22. }
  23. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  24. if !ok {
  25. h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  26. return
  27. }
  28. reqLog := requestLogger(
  29. c,
  30. "handler.openai_gateway.responses_input_tokens",
  31. zap.Int64("user_id", subject.UserID),
  32. zap.Int64("api_key_id", apiKey.ID),
  33. zap.Any("group_id", apiKey.GroupID),
  34. )
  35. if !h.ensureResponsesDependencies(c, reqLog) {
  36. return
  37. }
  38. body, err := readLenientJSONRequestBodyWithPrealloc(c.Request, h.cfg)
  39. if err != nil {
  40. if maxErr, ok := extractMaxBytesError(err); ok {
  41. h.errorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
  42. return
  43. }
  44. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
  45. return
  46. }
  47. if len(body) == 0 || !gjson.ValidBytes(body) {
  48. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  49. return
  50. }
  51. modelResult := gjson.GetBytes(body, "model")
  52. if !modelResult.Exists() || modelResult.Type != gjson.String || strings.TrimSpace(modelResult.String()) == "" {
  53. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  54. return
  55. }
  56. reqModel := strings.TrimSpace(modelResult.String())
  57. ensureCompositeTargetPlatform(c, apiKey, reqModel)
  58. if !openAICompatibleTextTargetAllowed(c, apiKey, reqModel) {
  59. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
  60. return
  61. }
  62. setOpsRequestContext(c, reqModel, false)
  63. setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(false, false)))
  64. if decision := h.checkSecurityAudit(c, reqLog, apiKey, subject, service.ContentModerationProtocolOpenAIResponses, reqModel, body); decision != nil && !decision.AllowNextStage {
  65. h.openAISecurityAuditError(c, decision)
  66. return
  67. }
  68. subscription, _ := middleware2.GetSubscriptionFromContext(c)
  69. if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription, service.QuotaPlatform(c.Request.Context(), apiKey)); err != nil {
  70. reqLog.Info("openai_input_tokens.billing_eligibility_check_failed", zap.Error(err))
  71. status, code, message, retryAfter := billingErrorDetails(err)
  72. if retryAfter > 0 {
  73. c.Header("Retry-After", strconv.Itoa(retryAfter))
  74. }
  75. h.errorResponse(c, status, code, message)
  76. return
  77. }
  78. channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel)
  79. routingModel := reqModel
  80. forwardBody := body
  81. if channelMapping.Mapped {
  82. routingModel = channelMapping.MappedModel
  83. forwardBody = h.gatewayService.ReplaceModelInBody(body, routingModel)
  84. }
  85. // Token counting is not billed, so it must not be excluded by the profit gate.
  86. c.Request = c.Request.WithContext(service.WithOpenAIProfitControlSuppressed(c.Request.Context()))
  87. requestPlatform := openAICompatibleRequestPlatform(c.Request.Context(), apiKey)
  88. sessionHash := h.gatewayService.GenerateSessionHash(c, body)
  89. requestStart := time.Now()
  90. account, err := h.gatewayService.SelectAccountForTokenCount(
  91. c.Request.Context(),
  92. apiKey.GroupID,
  93. sessionHash,
  94. routingModel,
  95. service.OpenAIEndpointCapabilityChatCompletions,
  96. requestPlatform,
  97. )
  98. service.SetOpsLatencyMs(c, service.OpsAuthLatencyMsKey, time.Since(requestStart).Milliseconds())
  99. if err != nil {
  100. reqLog.Warn("openai_input_tokens.account_select_failed", zap.Error(openAICompatibleSelectionErrorForLog(err, requestPlatform)))
  101. cls := classifyOpenAICompatibleNoAccountErrorFromGin(c, h.gatewayService, apiKey, routingModel, reqModel)
  102. if !cls.ModelNotFound {
  103. markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
  104. }
  105. h.errorResponse(c, cls.Status, cls.ErrType, cls.Message)
  106. return
  107. }
  108. if account == nil {
  109. cls := classifyOpenAICompatibleNoAccountErrorFromGin(c, h.gatewayService, apiKey, routingModel, reqModel)
  110. if !cls.ModelNotFound {
  111. markOpsRoutingCapacityLimited(c)
  112. }
  113. h.errorResponse(c, cls.Status, cls.ErrType, cls.Message)
  114. return
  115. }
  116. setOpsSelectedAccount(c, account.ID, account.Platform)
  117. if err := h.gatewayService.ForwardResponsesInputTokens(c.Request.Context(), c, account, forwardBody); err != nil {
  118. reqLog.Error("openai_input_tokens.forward_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  119. }
  120. }
  121. // GrokCountTokens handles Anthropic-compatible count_tokens requests locally.
  122. // The route middleware already authenticates the API key and resolves the
  123. // group; this handler intentionally does not select an account or check billing.
  124. func (h *OpenAIGatewayHandler) GrokCountTokens(c *gin.Context) {
  125. body, err := readLenientJSONRequestBodyWithPrealloc(c.Request, h.cfg)
  126. if err != nil {
  127. if maxErr, ok := extractMaxBytesError(err); ok {
  128. h.anthropicErrorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
  129. return
  130. }
  131. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
  132. return
  133. }
  134. if len(body) == 0 {
  135. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
  136. return
  137. }
  138. bodyRef := service.NewRequestBodyRef(body)
  139. parsedReq, err := service.ParseGatewayRequest(bodyRef, domain.PlatformAnthropic)
  140. if err != nil {
  141. logRequestBodyParseFailure(requestLogger(c, "handler.openai_gateway.grok_count_tokens"), body, err)
  142. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  143. return
  144. }
  145. if parsedReq.Model == "" {
  146. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  147. return
  148. }
  149. estimated, err := service.EstimateGrokCountTokens(parsedReq.Body.Bytes())
  150. if err != nil {
  151. requestLogger(c, "handler.openai_gateway.grok_count_tokens").Warn("grok_count_tokens.local_estimate_failed", zap.Error(err))
  152. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  153. return
  154. }
  155. setOpsRequestContext(c, parsedReq.Model, false)
  156. setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(false, false)))
  157. c.JSON(http.StatusOK, gin.H{"input_tokens": estimated})
  158. }
  159. // CountTokens handles Anthropic-compatible POST /v1/messages/count_tokens for OpenAI groups.
  160. // It validates billing and routes to an OpenAI token-count bridge without recording usage.
  161. func (h *OpenAIGatewayHandler) CountTokens(c *gin.Context) {
  162. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  163. if !ok {
  164. h.anthropicErrorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  165. return
  166. }
  167. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  168. if !ok {
  169. h.anthropicErrorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  170. return
  171. }
  172. reqLog := requestLogger(
  173. c,
  174. "handler.openai_gateway.count_tokens",
  175. zap.Int64("user_id", subject.UserID),
  176. zap.Int64("api_key_id", apiKey.ID),
  177. zap.Any("group_id", apiKey.GroupID),
  178. )
  179. if !allowOpenAICompatibleMessagesDispatch(c, apiKey) {
  180. h.anthropicErrorResponse(c, http.StatusForbidden, "permission_error",
  181. "This group does not allow /v1/messages dispatch")
  182. return
  183. }
  184. if !h.ensureResponsesDependencies(c, reqLog) {
  185. return
  186. }
  187. body, err := readLenientJSONRequestBodyWithPrealloc(c.Request, h.cfg)
  188. if err != nil {
  189. if maxErr, ok := extractMaxBytesError(err); ok {
  190. h.anthropicErrorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
  191. return
  192. }
  193. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
  194. return
  195. }
  196. if len(body) == 0 {
  197. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
  198. return
  199. }
  200. bodyRef := service.NewRequestBodyRef(body)
  201. parsedReq, err := service.ParseGatewayRequest(bodyRef, domain.PlatformAnthropic)
  202. if err != nil {
  203. logRequestBodyParseFailure(reqLog, body, err)
  204. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  205. return
  206. }
  207. body = parsedReq.Body.Bytes()
  208. if parsedReq.Model == "" {
  209. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  210. return
  211. }
  212. reqModel := parsedReq.Model
  213. ensureCompositeTargetPlatform(c, apiKey, reqModel)
  214. // composite+grok 在路由层已分流到 GrokCountTokens,这里可达的目标平台是
  215. // openai 与 CN 供应商;CN 账号由 ForwardCountTokensAsAnthropic 本地估算。
  216. if !openAICompatibleTextTargetAllowed(c, apiKey, reqModel) {
  217. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
  218. return
  219. }
  220. routingModel := service.NormalizeOpenAICompatRequestedModel(reqModel)
  221. preferredMappedModel := resolveOpenAIMessagesDispatchMappedModel(c, apiKey, reqModel)
  222. reqLog = reqLog.With(zap.String("model", reqModel), zap.Bool("stream", parsedReq.Stream))
  223. setOpsRequestContext(c, reqModel, false)
  224. setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(false, false)))
  225. channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel)
  226. mappedBodyForMessages := newOpenAIModelMappedBodyCache(body, h.gatewayService.ReplaceModelInBody)
  227. subscription, _ := middleware2.GetSubscriptionFromContext(c)
  228. if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription, service.QuotaPlatform(c.Request.Context(), apiKey)); err != nil {
  229. reqLog.Info("openai_count_tokens.billing_eligibility_check_failed", zap.Error(err))
  230. status, code, message, retryAfter := billingErrorDetails(err)
  231. if retryAfter > 0 {
  232. c.Header("Retry-After", strconv.Itoa(retryAfter))
  233. }
  234. h.anthropicErrorResponse(c, status, code, message)
  235. return
  236. }
  237. requestStart := time.Now()
  238. // count_tokens 不计费:显式豁免利润门,避免高倍率账号池被门排除后连
  239. // token 计数都返回 no available accounts。
  240. c.Request = c.Request.WithContext(service.WithOpenAIProfitControlSuppressed(c.Request.Context()))
  241. sessionHash := h.gatewayService.GenerateSessionHash(c, body)
  242. currentRoutingModel := routingModel
  243. if preferredMappedModel != "" {
  244. currentRoutingModel = preferredMappedModel
  245. }
  246. account, err := h.gatewayService.SelectAccountForTokenCount(
  247. c.Request.Context(),
  248. apiKey.GroupID,
  249. sessionHash,
  250. currentRoutingModel,
  251. service.OpenAIEndpointCapabilityChatCompletions,
  252. openAICompatibleRequestPlatform(c.Request.Context(), apiKey),
  253. )
  254. service.SetOpsLatencyMs(c, service.OpsAuthLatencyMsKey, time.Since(requestStart).Milliseconds())
  255. if err != nil {
  256. requestPlatform := openAICompatibleRequestPlatform(c.Request.Context(), apiKey)
  257. reqLog.Warn("openai_count_tokens.account_select_failed", zap.Error(openAICompatibleSelectionErrorForLog(err, requestPlatform)))
  258. cls := classifyOpenAICompatibleNoAccountErrorFromGin(c, h.gatewayService, apiKey, currentRoutingModel, reqModel)
  259. if !cls.ModelNotFound {
  260. markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
  261. }
  262. h.anthropicErrorResponse(c, cls.Status, cls.ErrType, cls.Message)
  263. return
  264. }
  265. if account == nil {
  266. cls := classifyOpenAICompatibleNoAccountErrorFromGin(c, h.gatewayService, apiKey, currentRoutingModel, reqModel)
  267. if !cls.ModelNotFound {
  268. markOpsRoutingCapacityLimited(c)
  269. }
  270. h.anthropicErrorResponse(c, cls.Status, cls.ErrType, cls.Message)
  271. return
  272. }
  273. setOpsSelectedAccount(c, account.ID, account.Platform)
  274. forwardBody := mappedBodyForMessages(channelMapping.Mapped, channelMapping.MappedModel)
  275. defaultMappedModel := preferredMappedModel
  276. if err := h.gatewayService.ForwardCountTokensAsAnthropic(c.Request.Context(), c, account, forwardBody, defaultMappedModel); err != nil {
  277. reqLog.Error("openai_count_tokens.forward_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  278. }
  279. }