gateway_handler_chat_completions.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. package handler
  2. import (
  3. "context"
  4. "errors"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
  10. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  11. "github.com/Wei-Shaw/sub2api/internal/service"
  12. "github.com/gin-gonic/gin"
  13. "github.com/tidwall/gjson"
  14. "go.uber.org/zap"
  15. )
  16. // ChatCompletions handles OpenAI Chat Completions API endpoint for Anthropic platform groups.
  17. // POST /v1/chat/completions
  18. // This converts Chat Completions requests to Anthropic format (via Responses format chain),
  19. // forwards to Anthropic upstream, and converts responses back to Chat Completions format.
  20. func (h *GatewayHandler) ChatCompletions(c *gin.Context) {
  21. streamStarted := false
  22. requestStart := time.Now()
  23. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  24. if !ok {
  25. h.chatCompletionsErrorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  26. return
  27. }
  28. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  29. if !ok {
  30. h.chatCompletionsErrorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  31. return
  32. }
  33. reqLog := requestLogger(
  34. c,
  35. "handler.gateway.chat_completions",
  36. zap.Int64("user_id", subject.UserID),
  37. zap.Int64("api_key_id", apiKey.ID),
  38. zap.Any("group_id", apiKey.GroupID),
  39. )
  40. // Read request body
  41. body, err := readLenientJSONRequestBodyWithPrealloc(c.Request, h.cfg)
  42. if err != nil {
  43. if maxErr, ok := extractMaxBytesError(err); ok {
  44. h.chatCompletionsErrorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
  45. return
  46. }
  47. h.chatCompletionsErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
  48. return
  49. }
  50. if len(body) == 0 {
  51. h.chatCompletionsErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
  52. return
  53. }
  54. setOpsRequestContext(c, "", false)
  55. // Validate JSON
  56. if !gjson.ValidBytes(body) {
  57. logRequestBodyParseFailure(reqLog, body, nil)
  58. h.chatCompletionsErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  59. return
  60. }
  61. // Extract model and stream
  62. modelResult := gjson.GetBytes(body, "model")
  63. if !modelResult.Exists() || modelResult.Type != gjson.String || modelResult.String() == "" {
  64. h.chatCompletionsErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  65. return
  66. }
  67. reqModel := modelResult.String()
  68. bindRequestedReasoningEffort(c, body, reqModel)
  69. ensureCompositeTargetPlatform(c, apiKey, reqModel)
  70. if !compositeTargetPlatformResolved(c, apiKey, reqModel) {
  71. h.chatCompletionsErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by composite groups")
  72. return
  73. }
  74. reqStream, ok := parseOpenAICompatibleStream(body)
  75. if !ok {
  76. h.chatCompletionsErrorResponse(c, http.StatusBadRequest, "invalid_request_error", invalidStreamFieldTypeMessage)
  77. return
  78. }
  79. if service.IsGPTImageGenerationModel(reqModel) {
  80. h.chatCompletionsErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "This model is not supported on the Chat Completions endpoint")
  81. return
  82. }
  83. reqLog = reqLog.With(zap.String("model", reqModel), zap.Bool("stream", reqStream))
  84. setOpsRequestContext(c, reqModel, reqStream)
  85. setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(reqStream, false)))
  86. pricingCtx, pricingAt := service.WithGatewayTokenRequestPricing(c.Request.Context())
  87. c.Request = c.Request.WithContext(pricingCtx)
  88. // 解析渠道级模型映射
  89. channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel)
  90. // Claude Code only restriction
  91. if apiKey.Group != nil && apiKey.Group.ClaudeCodeOnly {
  92. h.chatCompletionsErrorResponse(c, http.StatusForbidden, "permission_error",
  93. "This group is restricted to Claude Code clients (/v1/messages only)")
  94. return
  95. }
  96. if decision := h.checkSecurityAudit(c, reqLog, apiKey, subject, service.ContentModerationProtocolOpenAIChat, reqModel, body); decision != nil && !decision.AllowNextStage {
  97. h.openAISecurityAuditError(c, decision)
  98. return
  99. }
  100. // Error passthrough binding
  101. if h.errorPassthroughService != nil {
  102. service.BindErrorPassthroughService(c, h.errorPassthroughService)
  103. }
  104. subscription, _ := middleware2.GetSubscriptionFromContext(c)
  105. service.SetOpsLatencyMs(c, service.OpsAuthLatencyMsKey, time.Since(requestStart).Milliseconds())
  106. userReleaseFunc, err := h.concurrencyHelper.AcquireUserSlotWithWait(c, subject.UserID, subject.Concurrency, reqStream, &streamStarted)
  107. if err != nil {
  108. reqLog.Warn("gateway.cc.user_slot_acquire_failed", zap.Error(err))
  109. h.handleConcurrencyError(c, err, "user", streamStarted)
  110. return
  111. }
  112. userReleaseFunc = wrapReleaseOnDone(c.Request.Context(), userReleaseFunc)
  113. if userReleaseFunc != nil {
  114. defer userReleaseFunc()
  115. }
  116. // 2. Re-check billing
  117. if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription, service.QuotaPlatform(c.Request.Context(), apiKey)); err != nil {
  118. reqLog.Info("gateway.cc.billing_check_failed", zap.Error(err))
  119. status, code, message, retryAfter := billingErrorDetails(err)
  120. if retryAfter > 0 {
  121. c.Header("Retry-After", strconv.Itoa(retryAfter))
  122. }
  123. h.chatCompletionsErrorResponse(c, status, code, message)
  124. return
  125. }
  126. // Parse request for session hash
  127. bodyRef := service.NewRequestBodyRef(body)
  128. parsedReq, _ := service.ParseGatewayRequest(bodyRef, "chat_completions")
  129. if parsedReq == nil {
  130. parsedReq = &service.ParsedRequest{Model: reqModel, Stream: reqStream, Body: bodyRef}
  131. }
  132. parsedReq.SessionContext = &service.SessionContext{
  133. ClientIP: ip.GetClientIP(c),
  134. UserAgent: c.GetHeader("User-Agent"),
  135. APIKeyID: apiKey.ID,
  136. }
  137. sessionHash := h.gatewayService.GenerateSessionHash(parsedReq)
  138. groupPlatform := effectiveAPIKeyPlatform(c, apiKey)
  139. selectionSessionHash := sessionHash
  140. if groupPlatform == service.PlatformGemini && selectionSessionHash != "" {
  141. selectionSessionHash = "gemini:" + selectionSessionHash
  142. }
  143. // 3. Account selection + failover loop
  144. fs := NewFailoverState(h.maxAccountSwitches, false)
  145. if groupPlatform == service.PlatformGemini {
  146. fs = NewFailoverState(h.maxAccountSwitchesGemini, false)
  147. }
  148. for {
  149. if c.Request.Context().Err() != nil {
  150. return
  151. }
  152. selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), apiKey.GroupID, selectionSessionHash, reqModel, fs.FailedAccountIDs, "", int64(0))
  153. if err != nil {
  154. if len(fs.FailedAccountIDs) == 0 {
  155. cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, reqModel, reqModel, groupPlatform)
  156. cls = classifySelectionFailureError(err, cls)
  157. if !cls.ModelNotFound {
  158. markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
  159. }
  160. message := cls.Message
  161. if !cls.ModelNotFound {
  162. message = "No available accounts: " + err.Error()
  163. }
  164. h.chatCompletionsErrorResponse(c, cls.Status, cls.ErrType, message)
  165. return
  166. }
  167. action := fs.HandleSelectionExhausted(c.Request.Context())
  168. switch action {
  169. case FailoverContinue:
  170. continue
  171. case FailoverCanceled:
  172. failoverClientGone(c)
  173. return
  174. default:
  175. if fs.LastFailoverErr != nil {
  176. h.handleCCFailoverExhausted(c, fs.LastFailoverErr, streamStarted)
  177. } else {
  178. h.chatCompletionsErrorResponse(c, http.StatusBadGateway, "server_error", "All available accounts exhausted")
  179. }
  180. return
  181. }
  182. }
  183. account := selection.Account
  184. setOpsSelectedAccount(c, account.ID, account.Platform)
  185. // 4. Acquire account concurrency slot
  186. accountReleaseFunc := selection.ReleaseFunc
  187. if !selection.Acquired {
  188. if selection.WaitPlan == nil {
  189. markOpsRoutingCapacityLimited(c)
  190. h.chatCompletionsErrorResponse(c, http.StatusServiceUnavailable, "api_error", "No available accounts")
  191. return
  192. }
  193. accountReleaseFunc, err = h.concurrencyHelper.AcquireAccountSlotWithWaitTimeout(
  194. c,
  195. account.ID,
  196. selection.WaitPlan.MaxConcurrency,
  197. selection.WaitPlan.Timeout,
  198. reqStream,
  199. &streamStarted,
  200. )
  201. if err != nil {
  202. reqLog.Warn("gateway.cc.account_slot_acquire_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  203. h.handleConcurrencyError(c, err, "account", streamStarted)
  204. return
  205. }
  206. }
  207. // 终检与准入后绑定使用选号结果携带的门(见 responses 同名注释)。
  208. admissionCtx := service.ContextWithSelectionProfitGate(c.Request.Context(), selection)
  209. latest, vetoed, reason := h.gatewayService.GatewayProfitControlVetoLatest(admissionCtx, account)
  210. if vetoed {
  211. if accountReleaseFunc != nil {
  212. accountReleaseFunc()
  213. }
  214. reqLog.Debug("gateway.cc.account_slot_profit_vetoed", zap.Int64("account_id", account.ID), zap.String("reason", reason))
  215. if fs.RecordProfitVeto(account.ID) == FailoverExhausted {
  216. reqLog.Warn("gateway.cc.profit_veto_attempts_exhausted", zap.Int("profit_veto_count", fs.ProfitVetoCount()))
  217. h.chatCompletionsErrorResponse(c, http.StatusServiceUnavailable, "api_error", profitVetoExhaustedMessage)
  218. return
  219. }
  220. continue
  221. }
  222. account = latest
  223. selection.Account = latest
  224. if selection.ProfitGateActive() {
  225. if err := h.gatewayService.BindStickySessionAfterProfitAdmission(admissionCtx, apiKey.GroupID, selectionSessionHash, account.ID); err != nil {
  226. reqLog.Warn("gateway.cc.bind_sticky_session_after_profit_admission_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  227. }
  228. }
  229. accountReleaseFunc = wrapReleaseOnDone(c.Request.Context(), accountReleaseFunc)
  230. if groupPlatform == service.PlatformGemini && account.Platform != service.PlatformGemini {
  231. if accountReleaseFunc != nil {
  232. accountReleaseFunc()
  233. }
  234. fs.FailedAccountIDs[account.ID] = struct{}{}
  235. continue
  236. }
  237. // 5. Forward request
  238. writerSizeBeforeForward := c.Writer.Size()
  239. forwardBody := body
  240. if channelMapping.Mapped {
  241. forwardBody = h.gatewayService.ReplaceModelInBody(body, channelMapping.MappedModel)
  242. }
  243. var result *service.ForwardResult
  244. setActualUpstreamEndpoint(c, "")
  245. if account.Platform == service.PlatformGemini {
  246. if h.geminiCompatService == nil {
  247. h.chatCompletionsErrorResponse(c, http.StatusBadGateway, "upstream_error", "Gemini compatibility service is not configured")
  248. if accountReleaseFunc != nil {
  249. accountReleaseFunc()
  250. }
  251. return
  252. }
  253. result, err = h.geminiCompatService.ForwardAsChatCompletions(c.Request.Context(), c, account, forwardBody)
  254. } else if shouldUseAntigravityCompat(account) {
  255. if h.antigravityGatewayService == nil {
  256. h.chatCompletionsErrorResponse(c, http.StatusBadGateway, "upstream_error", "Antigravity compatibility service is not configured")
  257. if accountReleaseFunc != nil {
  258. accountReleaseFunc()
  259. }
  260. return
  261. }
  262. setActualUpstreamEndpoint(c, EndpointAntigravityGenerateContent)
  263. result, err = h.antigravityGatewayService.ForwardAsChatCompletions(c.Request.Context(), c, account, forwardBody, parsedReq)
  264. } else {
  265. result, err = h.gatewayService.ForwardAsChatCompletions(c.Request.Context(), c, account, forwardBody, parsedReq)
  266. }
  267. if accountReleaseFunc != nil {
  268. accountReleaseFunc()
  269. }
  270. if err != nil {
  271. var failoverErr *service.UpstreamFailoverError
  272. if errors.As(err, &failoverErr) {
  273. if c.Writer.Size() != writerSizeBeforeForward {
  274. h.handleCCFailoverExhausted(c, failoverErr, true)
  275. return
  276. }
  277. action := fs.HandleFailoverError(c.Request.Context(), h.gatewayService, account.ID, account.Platform, account.GetPoolModeRetryCount(), failoverErr)
  278. switch action {
  279. case FailoverContinue:
  280. continue
  281. case FailoverExhausted:
  282. h.handleCCFailoverExhausted(c, fs.LastFailoverErr, streamStarted)
  283. return
  284. case FailoverCanceled:
  285. failoverClientGone(c)
  286. return
  287. }
  288. }
  289. upstreamErrorAlreadyCommunicated := gatewayForwardErrorAlreadyCommunicated(c, writerSizeBeforeForward, err)
  290. wroteFallback := false
  291. if !upstreamErrorAlreadyCommunicated {
  292. wroteFallback = h.ensureForwardErrorResponse(c, streamStarted)
  293. }
  294. reqLog.Error("gateway.cc.forward_failed",
  295. zap.Int64("account_id", account.ID),
  296. zap.Bool("fallback_error_response_written", wroteFallback),
  297. zap.Bool("upstream_error_response_already_written", upstreamErrorAlreadyCommunicated),
  298. zap.Error(err),
  299. )
  300. return
  301. }
  302. // 6. Record usage
  303. userAgent := c.GetHeader("User-Agent")
  304. clientIP := ip.GetClientIP(c)
  305. requestPayloadHash := service.HashUsageRequestPayload(body)
  306. inboundEndpoint := GetInboundEndpoint(c)
  307. upstreamEndpoint := GetUpstreamEndpoint(c, account.Platform)
  308. quotaPlatform := service.QuotaPlatform(c.Request.Context(), apiKey)
  309. sessionID := service.ExtractClientSessionID(c)
  310. stampForwardRequestedReasoningEffort(result, service.RequestedReasoningEffortFromContext(c.Request.Context()))
  311. h.submitUsageRecordTask(c.Request.Context(), func(ctx context.Context) {
  312. if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
  313. Result: result,
  314. QuotaPlatform: quotaPlatform,
  315. APIKey: apiKey,
  316. User: apiKey.User,
  317. Account: account,
  318. Subscription: subscription,
  319. PricingAt: pricingAt,
  320. InboundEndpoint: inboundEndpoint,
  321. UpstreamEndpoint: upstreamEndpoint,
  322. UserAgent: userAgent,
  323. IPAddress: clientIP,
  324. RequestPayloadHash: requestPayloadHash,
  325. APIKeyService: h.apiKeyService,
  326. SessionID: sessionID,
  327. ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
  328. }); err != nil {
  329. reqLog.Error("gateway.cc.record_usage_failed",
  330. zap.Int64("account_id", account.ID),
  331. zap.Error(err),
  332. )
  333. }
  334. })
  335. return
  336. }
  337. }
  338. // chatCompletionsErrorResponse writes an error in OpenAI Chat Completions format.
  339. func (h *GatewayHandler) chatCompletionsErrorResponse(c *gin.Context, status int, errType, message string) {
  340. c.JSON(status, gin.H{
  341. "error": gin.H{
  342. "type": errType,
  343. "message": message,
  344. },
  345. })
  346. }
  347. // handleCCFailoverExhausted writes a failover-exhausted error in CC format.
  348. func (h *GatewayHandler) handleCCFailoverExhausted(c *gin.Context, lastErr *service.UpstreamFailoverError, streamStarted bool) {
  349. if streamStarted {
  350. return
  351. }
  352. if lastErr != nil {
  353. copyFailoverRetryAfter(c, lastErr.ResponseHeaders)
  354. }
  355. if lastErr != nil && lastErr.IsCredentialFailure() {
  356. status, message := credentialFailoverClientResponse(lastErr)
  357. h.chatCompletionsErrorResponse(c, status, "server_error", message)
  358. return
  359. }
  360. if lastErr != nil && lastErr.IsOpenAICapacityShed() && strings.TrimSpace(lastErr.ClientMessage) != "" {
  361. status := lastErr.ClientStatusCode
  362. if status <= 0 {
  363. status = http.StatusServiceUnavailable
  364. }
  365. h.chatCompletionsErrorResponse(c, status, "server_error", lastErr.ClientMessage)
  366. return
  367. }
  368. statusCode := http.StatusBadGateway
  369. if lastErr != nil && lastErr.StatusCode > 0 {
  370. statusCode = lastErr.StatusCode
  371. }
  372. if lastErr != nil && service.IsOpenAISilentRefusalErrorBody(lastErr.ResponseBody) {
  373. service.SetOpsUpstreamError(c, statusCode, service.OpenAISilentRefusalClientMessage(), "")
  374. h.chatCompletionsErrorResponse(c, http.StatusBadGateway, "upstream_error", service.OpenAISilentRefusalClientMessage())
  375. return
  376. }
  377. h.chatCompletionsErrorResponse(c, statusCode, "server_error", "All available accounts exhausted")
  378. }