openai_gateway_messages_chat_fallback.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "strings"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  11. "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
  12. "github.com/gin-gonic/gin"
  13. "github.com/tidwall/gjson"
  14. "go.uber.org/zap"
  15. )
  16. // forwardAnthropicViaRawChatCompletions serves /v1/messages clients through
  17. // an OpenAI-compatible upstream that only supports /v1/chat/completions.
  18. //
  19. // Conversion chain (direct, no Responses intermediary):
  20. //
  21. // Request: Anthropic Messages → Chat Completions (AnthropicToChatCompletionsRequest)
  22. // Response: CC chunk/response → Anthropic events/response (direct bridge)
  23. //
  24. // This is the /v1/messages counterpart of forwardResponsesViaRawChatCompletions
  25. // (which serves /v1/responses clients). Unlike the Responses path, the direct
  26. // bridge skips the Responses API intermediate representation entirely — every
  27. // streaming token runs through a single state machine instead of two.
  28. func (s *OpenAIGatewayService) forwardAnthropicViaRawChatCompletions(
  29. ctx context.Context,
  30. c *gin.Context,
  31. account *Account,
  32. body []byte,
  33. defaultMappedModel string,
  34. ) (*OpenAIForwardResult, error) {
  35. startTime := time.Now()
  36. // 1. Parse Anthropic request
  37. var anthropicReq apicompat.AnthropicRequest
  38. if err := json.Unmarshal(body, &anthropicReq); err != nil {
  39. writeAnthropicError(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  40. return nil, fmt.Errorf("parse anthropic request: %w", err)
  41. }
  42. originalModel := anthropicReq.Model
  43. if strings.TrimSpace(originalModel) == "" {
  44. writeAnthropicError(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  45. return nil, fmt.Errorf("missing model in request")
  46. }
  47. applyOpenAICompatModelNormalization(&anthropicReq)
  48. clientStream := anthropicReq.Stream
  49. // 2. Anthropic → Chat Completions (direct, no Responses intermediary)
  50. chatReq, err := apicompat.AnthropicToChatCompletionsRequest(&anthropicReq)
  51. if err != nil {
  52. writeAnthropicError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
  53. return nil, fmt.Errorf("convert anthropic to chat completions: %w", err)
  54. }
  55. billingModel := resolveOpenAIForwardModel(account, anthropicReq.Model, defaultMappedModel)
  56. upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
  57. chatReq.Model = upstreamModel
  58. chatReq.ReasoningEffort = openAICompatAnthropicReasoningEffort(&anthropicReq, upstreamModel, chatReq.ReasoningEffort)
  59. chatReq.Stream = clientStream
  60. if clientStream {
  61. chatReq.StreamOptions = &apicompat.ChatStreamOptions{IncludeUsage: true}
  62. }
  63. convertedEffort := chatReq.ReasoningEffort
  64. reasoningEffort := &convertedEffort
  65. reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, billingModel)
  66. serviceTier := extractOpenAIServiceTierFromBody(body)
  67. chatBody, err := json.Marshal(chatReq)
  68. if err != nil {
  69. return nil, fmt.Errorf("marshal chat completions request: %w", err)
  70. }
  71. if normalizedBody, normalized := NormalizeGLMOpenAIReasoningEffort(chatBody, upstreamModel); normalized {
  72. chatBody = normalizedBody
  73. }
  74. if account.Platform == PlatformOpenAI {
  75. policyBody, changed, policyErr := ApplyOpenAIReasoningEffortPolicyFromContext(ctx, chatBody)
  76. if policyErr != nil {
  77. if IsReasoningEffortPolicyDenied(policyErr) {
  78. MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalPolicyDenied)
  79. writeAnthropicError(c, http.StatusForbidden, "forbidden_error", policyErr.Error())
  80. }
  81. return nil, policyErr
  82. }
  83. if changed {
  84. chatBody = policyBody
  85. if effectiveEffort := strings.TrimSpace(gjson.GetBytes(chatBody, "reasoning_effort").String()); effectiveEffort != "" {
  86. reasoningEffort = &effectiveEffort
  87. }
  88. }
  89. }
  90. // Unlike forwardResponsesViaRawChatCompletions, applyOpenAIFastPolicyToBody
  91. // is intentionally skipped: Anthropic Messages bodies carry no service_tier,
  92. // so the converted Chat Completions body never contains one and the policy
  93. // would always be a no-op on this path.
  94. logger.L().Debug("openai messages: forwarding via raw chat completions",
  95. zap.Int64("account_id", account.ID),
  96. zap.String("original_model", originalModel),
  97. zap.String("billing_model", billingModel),
  98. zap.String("upstream_model", upstreamModel),
  99. zap.Bool("stream", clientStream),
  100. )
  101. // 3. Build and send upstream request via the shared CC pipeline
  102. apiKey, targetURL, err := s.resolveCCFallbackTarget(account)
  103. if err != nil {
  104. return nil, err
  105. }
  106. resp, err := s.sendCCUpstreamRequest(ctx, c, account, targetURL, chatBody, clientStream, apiKey, account.GetOpenAIUserAgent(), "")
  107. if err != nil {
  108. return nil, err
  109. }
  110. defer func() { _ = resp.Body.Close() }()
  111. // 4. Handle error responses
  112. if resp.StatusCode >= 400 {
  113. respBody, upstreamMsg := s.readOpenAIUpstreamError(resp)
  114. if foErr := s.failoverOpenAIUpstreamHTTPError(ctx, c, account, resp, respBody, upstreamMsg, upstreamModel); foErr != nil {
  115. return nil, foErr
  116. }
  117. // Non-failover error: return Anthropic-formatted error to client via the
  118. // shared compat handler (passthrough rules, ops recording, cyber_policy).
  119. return s.handleAnthropicErrorResponse(resp, c, account, billingModel)
  120. }
  121. // 5. Convert response
  122. if clientStream {
  123. return s.streamChatCompletionsAsAnthropic(c, resp, originalModel, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime)
  124. }
  125. return s.bufferChatCompletionsAsAnthropic(c, resp, originalModel, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime)
  126. }
  127. func (s *OpenAIGatewayService) bufferChatCompletionsAsAnthropic(
  128. c *gin.Context,
  129. resp *http.Response,
  130. originalModel string,
  131. billingModel string,
  132. upstreamModel string,
  133. reasoningEffort *string,
  134. serviceTier *string,
  135. startTime time.Time,
  136. ) (*OpenAIForwardResult, error) {
  137. requestID := resp.Header.Get("x-request-id")
  138. ccResp, usage, err := s.readCCUpstreamJSONResponse(c, resp, writeAnthropicError)
  139. if err != nil {
  140. return nil, err
  141. }
  142. anthropicResp := apicompat.ChatCompletionsResponseToAnthropic(ccResp, originalModel)
  143. if s.responseHeaderFilter != nil {
  144. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  145. }
  146. c.JSON(http.StatusOK, anthropicResp)
  147. return &OpenAIForwardResult{
  148. RequestID: requestID,
  149. UpstreamHeaders: resp.Header,
  150. Usage: usage,
  151. Model: originalModel,
  152. BillingModel: billingModel,
  153. UpstreamModel: upstreamModel,
  154. ReasoningEffort: reasoningEffort,
  155. UpstreamResponseServiceTier: observedUpstreamResponseServiceTier(c),
  156. ServiceTier: resolvedOpenAIUpstreamServiceTier(c, serviceTier),
  157. Stream: false,
  158. Duration: time.Since(startTime),
  159. }, nil
  160. }
  161. func (s *OpenAIGatewayService) streamChatCompletionsAsAnthropic(
  162. c *gin.Context,
  163. resp *http.Response,
  164. originalModel string,
  165. billingModel string,
  166. upstreamModel string,
  167. reasoningEffort *string,
  168. serviceTier *string,
  169. startTime time.Time,
  170. ) (*OpenAIForwardResult, error) {
  171. requestID := resp.Header.Get("x-request-id")
  172. writeStreamHeaders := s.newStreamHeaderWriter(c, resp.Header)
  173. anthropicState := apicompat.NewChatCompletionsToAnthropicStreamState(originalModel)
  174. clientDisconnected := false
  175. // 与 responses 兄弟不同:客户端断开后仍继续做事件转换(喂 anthropicState),
  176. // 仅跳过写出,保证 finalize 阶段的 usage 汇总不受断开影响。
  177. emitChunk := func(chunk *apicompat.ChatCompletionsChunk) {
  178. // CC chunk → Anthropic events (direct, single state machine)
  179. anthropicEvents := apicompat.ChatCompletionsChunkToAnthropicEvents(chunk, anthropicState)
  180. if clientDisconnected {
  181. return
  182. }
  183. for _, aEvt := range anthropicEvents {
  184. sse, err := apicompat.ResponsesAnthropicEventToSSE(aEvt)
  185. if err != nil {
  186. continue
  187. }
  188. writeStreamHeaders()
  189. if _, err := fmt.Fprint(c.Writer, sse); err != nil {
  190. clientDisconnected = true
  191. break
  192. }
  193. }
  194. if !clientDisconnected && len(anthropicEvents) > 0 {
  195. c.Writer.Flush()
  196. }
  197. }
  198. scan := s.scanCCStream(c, resp, "openai messages chat fallback", requestID, startTime, emitChunk)
  199. usage := scan.Usage
  200. if scan.Err != nil {
  201. // Broken upstream read: skip finalization so no synthetic message_stop
  202. // masks the truncation, and surface the error to flag usage incomplete
  203. // (mirrors forwardResponsesViaRawChatCompletions).
  204. return &OpenAIForwardResult{
  205. RequestID: requestID,
  206. UpstreamHeaders: resp.Header,
  207. Usage: usage,
  208. Model: originalModel,
  209. BillingModel: billingModel,
  210. UpstreamModel: upstreamModel,
  211. ReasoningEffort: reasoningEffort,
  212. UpstreamResponseServiceTier: observedUpstreamResponseServiceTier(c),
  213. ServiceTier: resolvedOpenAIUpstreamServiceTier(c, serviceTier),
  214. Stream: true,
  215. Duration: time.Since(startTime),
  216. FirstTokenMs: scan.FirstTokenMs,
  217. ClientDisconnect: clientDisconnected,
  218. }, fmt.Errorf("stream usage incomplete: %w", scan.Err)
  219. }
  220. // Finalize: close open blocks + emit message_delta/message_stop.
  221. finalEvents := apicompat.FinalizeChatCompletionsAnthropicStream(anthropicState)
  222. if !clientDisconnected {
  223. for _, aEvt := range finalEvents {
  224. sse, err := apicompat.ResponsesAnthropicEventToSSE(aEvt)
  225. if err != nil {
  226. continue
  227. }
  228. writeStreamHeaders()
  229. if _, err := fmt.Fprint(c.Writer, sse); err != nil {
  230. clientDisconnected = true
  231. break
  232. }
  233. }
  234. c.Writer.Flush()
  235. }
  236. if !scan.SawDone {
  237. logCCStreamMissingDoneSentinel("openai messages chat fallback", requestID)
  238. }
  239. return &OpenAIForwardResult{
  240. RequestID: requestID,
  241. UpstreamHeaders: resp.Header,
  242. Usage: usage,
  243. Model: originalModel,
  244. BillingModel: billingModel,
  245. UpstreamModel: upstreamModel,
  246. ReasoningEffort: reasoningEffort,
  247. UpstreamResponseServiceTier: observedUpstreamResponseServiceTier(c),
  248. ServiceTier: resolvedOpenAIUpstreamServiceTier(c, serviceTier),
  249. Stream: true,
  250. Duration: time.Since(startTime),
  251. FirstTokenMs: scan.FirstTokenMs,
  252. ClientDisconnect: clientDisconnected,
  253. }, nil
  254. }