gateway_forward_as_chat_completions.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. package service
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "net/http"
  11. "strings"
  12. "time"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  15. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  16. "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
  17. "github.com/gin-gonic/gin"
  18. "github.com/tidwall/gjson"
  19. "go.uber.org/zap"
  20. )
  21. // ForwardAsChatCompletions accepts an OpenAI Chat Completions API request body,
  22. // converts it to Anthropic Messages format (chained via Responses format),
  23. // forwards to the Anthropic upstream, and converts the response back to Chat
  24. // Completions format. This enables Chat Completions clients to access Anthropic
  25. // models through Anthropic platform groups.
  26. func (s *GatewayService) ForwardAsChatCompletions(
  27. ctx context.Context,
  28. c *gin.Context,
  29. account *Account,
  30. body []byte,
  31. parsed *ParsedRequest,
  32. ) (*ForwardResult, error) {
  33. startTime := time.Now()
  34. // 1. Parse Chat Completions request
  35. var ccReq apicompat.ChatCompletionsRequest
  36. if err := json.Unmarshal(body, &ccReq); err != nil {
  37. return nil, fmt.Errorf("parse chat completions request: %w", err)
  38. }
  39. originalModel := ccReq.Model
  40. clientStream := ccReq.Stream
  41. includeUsage := ccReq.StreamOptions != nil && ccReq.StreamOptions.IncludeUsage
  42. // 2. Convert CC → Responses → Anthropic (chained conversion)
  43. responsesReq, err := apicompat.ChatCompletionsToResponses(&ccReq)
  44. if err != nil {
  45. return nil, fmt.Errorf("convert chat completions to responses: %w", err)
  46. }
  47. anthropicReq, err := apicompat.ResponsesToAnthropicRequest(responsesReq)
  48. if err != nil {
  49. return nil, fmt.Errorf("convert responses to anthropic: %w", err)
  50. }
  51. // 3. Force upstream streaming
  52. anthropicReq.Stream = true
  53. reqStream := true
  54. // 4. Model mapping
  55. mappedModel := originalModel
  56. if account.Type == AccountTypeAPIKey || account.Type == AccountTypeServiceAccount {
  57. mappedModel = account.GetMappedModel(originalModel)
  58. }
  59. if mappedModel == originalModel && account.Platform == PlatformAnthropic && account.Type == AccountTypeServiceAccount {
  60. normalized := normalizeVertexAnthropicModelID(claude.NormalizeModelID(originalModel))
  61. if normalized != originalModel {
  62. mappedModel = normalized
  63. }
  64. } else if mappedModel == originalModel && account.Platform == PlatformAnthropic && account.Type != AccountTypeAPIKey {
  65. normalized := claude.NormalizeModelID(originalModel)
  66. if normalized != originalModel {
  67. mappedModel = normalized
  68. }
  69. }
  70. anthropicReq.Model = mappedModel
  71. logger.L().Debug("gateway forward_as_chat_completions: model mapping applied",
  72. zap.Int64("account_id", account.ID),
  73. zap.String("original_model", originalModel),
  74. zap.String("mapped_model", mappedModel),
  75. zap.Bool("client_stream", clientStream),
  76. )
  77. // 5. Marshal Anthropic request body
  78. anthropicBody, err := json.Marshal(anthropicReq)
  79. if err != nil {
  80. return nil, fmt.Errorf("marshal anthropic request: %w", err)
  81. }
  82. // 6. Apply Claude Code mimicry for OAuth accounts.
  83. // Chat Completions 协议进来的请求永远不是 Claude Code 客户端,所以对 OAuth 账号
  84. // 必须完整执行 /v1/messages 主路径上的伪装链路(system 重写 + normalize + metadata 注入),
  85. // 否则会被 Anthropic 判为第三方应用并扣 extra usage。
  86. // 见 applyClaudeCodeOAuthMimicryToBody 的 godoc。
  87. isClaudeCode := false
  88. shouldMimicClaudeCode := account.IsOAuth() && !isClaudeCode
  89. if shouldMimicClaudeCode {
  90. anthropicBody = s.applyClaudeCodeOAuthMimicryToBody(ctx, c, account, anthropicBody, anthropicReq.System, mappedModel)
  91. }
  92. // 7. Enforce cache_control block limit
  93. anthropicBody = enforceCacheControlLimit(anthropicBody)
  94. // 8. Get access token
  95. token, tokenType, err := s.GetAccessToken(ctx, account)
  96. if err != nil {
  97. return nil, fmt.Errorf("get access token: %w", err)
  98. }
  99. // 9. Get proxy URL
  100. proxyURL := ""
  101. if account.ProxyID != nil && account.Proxy != nil {
  102. proxyURL = account.Proxy.URL()
  103. }
  104. // 10. Build upstream request
  105. upstreamCtx, releaseUpstreamCtx := detachStreamUpstreamContext(ctx, reqStream)
  106. upstreamReq, _, err := s.buildUpstreamRequest(upstreamCtx, c, account, anthropicBody, token, tokenType, mappedModel, reqStream, shouldMimicClaudeCode)
  107. releaseUpstreamCtx()
  108. if err != nil {
  109. return nil, fmt.Errorf("build upstream request: %w", err)
  110. }
  111. // 11. Send request
  112. resp, err := s.httpUpstream.DoWithTLS(WithAccountTrafficRequest(upstreamReq, account), proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
  113. if err != nil {
  114. if resp != nil && resp.Body != nil {
  115. _ = resp.Body.Close()
  116. }
  117. return nil, s.handleUpstreamTransportError(ctx, c, account, err, OpsUpstreamErrorEvent{
  118. UpstreamURL: safeUpstreamURL(upstreamReq.URL.String()),
  119. })
  120. }
  121. defer func() { _ = resp.Body.Close() }()
  122. // 12. Handle error response with failover
  123. if resp.StatusCode >= 400 {
  124. respBody, _ := s.readUpstreamErrorBody(resp)
  125. _ = resp.Body.Close()
  126. resp.Body = io.NopCloser(bytes.NewReader(respBody))
  127. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  128. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  129. if s.shouldFailoverUpstreamError(resp.StatusCode) {
  130. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  131. ProxyID: opsUpstreamProxyID(account),
  132. ProxyName: opsUpstreamProxyName(account),
  133. Platform: account.Platform,
  134. AccountID: account.ID,
  135. AccountName: account.Name,
  136. UpstreamStatusCode: resp.StatusCode,
  137. UpstreamRequestID: resp.Header.Get("x-request-id"),
  138. Kind: "failover",
  139. Message: upstreamMsg,
  140. })
  141. shouldDisable := false
  142. if s.rateLimitService != nil {
  143. shouldDisable = s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody, mappedModel)
  144. }
  145. return nil, &UpstreamFailoverError{
  146. StatusCode: resp.StatusCode,
  147. ResponseBody: respBody,
  148. RetryableOnSameAccount: !shouldDisable && account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode),
  149. }
  150. }
  151. writeGatewayCCError(c, mapUpstreamStatusCode(resp.StatusCode), "server_error", upstreamMsg)
  152. return nil, fmt.Errorf("upstream error: %d %s", resp.StatusCode, upstreamMsg)
  153. }
  154. // 13. Extract reasoning effort from CC request body
  155. reasoningEffort := extractCCReasoningEffortFromBody(body, mappedModel, originalModel)
  156. // 国产模型默认 effort 补充:本路径是客户端 CC 请求 → Anthropic 上游,
  157. // 如果上游是 passback-required 国产模型 (Kimi-anthropic / GLM-anthropic / MiniMax)
  158. // 且客户端在 body 里传了 thinking.type=enabled,补中默认 effort。
  159. reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, mappedModel)
  160. // 14. Handle normal response
  161. // Read Anthropic SSE → convert to Responses events → convert to CC format
  162. var result *ForwardResult
  163. var handleErr error
  164. if clientStream {
  165. result, handleErr = s.handleCCStreamingFromAnthropic(resp, c, originalModel, mappedModel, reasoningEffort, startTime, includeUsage)
  166. } else {
  167. result, handleErr = s.handleCCBufferedFromAnthropic(resp, c, originalModel, mappedModel, reasoningEffort, startTime)
  168. }
  169. return result, handleErr
  170. }
  171. // extractCCReasoningEffortFromBody reads reasoning effort from a Chat Completions
  172. // request body. It checks both nested (reasoning.effort) and flat (reasoning_effort)
  173. // formats used by OpenAI-compatible clients.
  174. func extractCCReasoningEffortFromBody(body []byte, modelCandidates ...string) *string {
  175. raw := strings.TrimSpace(gjson.GetBytes(body, "reasoning.effort").String())
  176. if raw == "" {
  177. raw = strings.TrimSpace(gjson.GetBytes(body, "reasoning_effort").String())
  178. }
  179. if raw == "" {
  180. return nil
  181. }
  182. model := firstNonEmpty(modelCandidates...)
  183. if model == "" {
  184. model = strings.TrimSpace(gjson.GetBytes(body, "model").String())
  185. }
  186. normalized := normalizeOpenAIReasoningEffortForModel(raw, model)
  187. if normalized == "" {
  188. return nil
  189. }
  190. return &normalized
  191. }
  192. // handleCCBufferedFromAnthropic reads Anthropic SSE events, assembles the full
  193. // response, then converts Anthropic → Responses → Chat Completions.
  194. func (s *GatewayService) handleCCBufferedFromAnthropic(
  195. resp *http.Response,
  196. c *gin.Context,
  197. originalModel string,
  198. mappedModel string,
  199. reasoningEffort *string,
  200. startTime time.Time,
  201. ) (*ForwardResult, error) {
  202. requestID := resp.Header.Get("x-request-id")
  203. scanner := bufio.NewScanner(resp.Body)
  204. maxLineSize := defaultMaxLineSize
  205. if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
  206. maxLineSize = s.cfg.Gateway.MaxLineSize
  207. }
  208. scanner.Buffer(make([]byte, 0, 64*1024), maxLineSize)
  209. var finalResp *apicompat.AnthropicResponse
  210. var usage ClaudeUsage
  211. for scanner.Scan() {
  212. line := scanner.Text()
  213. // SSE 规范允许 `event:xxx`(冒号后无空格):Kimi 等 Anthropic 兼容上游
  214. // 返回紧凑格式,严格匹配 "event: " 会丢弃全部事件(#4653 同根因)。
  215. if _, ok := extractOpenAISSEEventLine(line); !ok {
  216. continue
  217. }
  218. if !scanner.Scan() {
  219. break
  220. }
  221. payload, ok := extractOpenAISSEDataLine(scanner.Text())
  222. if !ok {
  223. continue
  224. }
  225. var event apicompat.AnthropicStreamEvent
  226. if err := json.Unmarshal([]byte(payload), &event); err != nil {
  227. continue
  228. }
  229. // message_start carries the initial response structure and cache usage
  230. if event.Type == "message_start" && event.Message != nil {
  231. finalResp = event.Message
  232. mergeAnthropicUsage(&usage, event.Message.Usage)
  233. }
  234. // message_delta carries final usage and stop_reason
  235. if event.Type == "message_delta" {
  236. if event.Usage != nil {
  237. mergeAnthropicUsage(&usage, *event.Usage)
  238. }
  239. if event.Delta != nil && event.Delta.StopReason != "" && finalResp != nil {
  240. finalResp.StopReason = apicompat.AnthropicStopReasonPtr(event.Delta.StopReason)
  241. }
  242. }
  243. if event.Type == "content_block_start" && event.ContentBlock != nil && finalResp != nil {
  244. finalResp.Content = append(finalResp.Content, *event.ContentBlock)
  245. }
  246. if event.Type == "content_block_delta" && event.Delta != nil && finalResp != nil && event.Index != nil {
  247. idx := *event.Index
  248. if idx < len(finalResp.Content) {
  249. switch event.Delta.Type {
  250. case "text_delta":
  251. finalResp.Content[idx].Text += event.Delta.Text
  252. case "thinking_delta":
  253. finalResp.Content[idx].Thinking += event.Delta.Thinking
  254. case "input_json_delta":
  255. finalResp.Content[idx].Input = appendRawJSON(finalResp.Content[idx].Input, event.Delta.PartialJSON)
  256. }
  257. }
  258. }
  259. }
  260. if err := scanner.Err(); err != nil {
  261. if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
  262. logger.L().Warn("forward_as_cc buffered: read error",
  263. zap.Error(err),
  264. zap.String("request_id", requestID),
  265. )
  266. }
  267. }
  268. if finalResp == nil {
  269. writeGatewayCCError(c, http.StatusBadGateway, "server_error", "Upstream stream ended without a response")
  270. return nil, fmt.Errorf("upstream stream ended without response")
  271. }
  272. // Update usage from accumulated delta
  273. if usage.InputTokens > 0 || usage.OutputTokens > 0 {
  274. finalResp.Usage = apicompat.AnthropicUsage{
  275. InputTokens: usage.InputTokens,
  276. OutputTokens: usage.OutputTokens,
  277. CacheCreationInputTokens: usage.CacheCreationInputTokens,
  278. CacheReadInputTokens: usage.CacheReadInputTokens,
  279. }
  280. }
  281. // Chain: Anthropic → Responses → Chat Completions
  282. responsesResp := apicompat.AnthropicToResponsesResponse(finalResp)
  283. ccResp := apicompat.ResponsesToChatCompletions(responsesResp, originalModel)
  284. if s.responseHeaderFilter != nil {
  285. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  286. }
  287. // 非流式响应必须是 application/json。上游被强制流式后会返回
  288. // Content-Type: text/event-stream,经 WriteFilteredHeaders 透传后会污染
  289. // 响应头;而 c.Data/c.JSON 走 Gin 的 writeContentType(仅当头不存在时才设置),
  290. // 无法覆盖已存在的 SSE 头。这里显式 Set 强制改回 JSON,避免下游中间层
  291. // (如 new-api)按 Content-Type 误判为流式。
  292. c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
  293. // Marshal then bytes-replace so tool name mapping is reversed at byte level
  294. // (parity with Parrot non-stream flow that marshals → restore → emit).
  295. if respBytes, err := json.Marshal(ccResp); err == nil {
  296. respBytes = reverseToolNamesIfPresent(c, respBytes)
  297. c.Data(http.StatusOK, "application/json; charset=utf-8", respBytes)
  298. } else {
  299. c.JSON(http.StatusOK, ccResp)
  300. }
  301. return &ForwardResult{
  302. RequestID: requestID,
  303. UpstreamHeaders: resp.Header,
  304. Usage: usage,
  305. Model: originalModel,
  306. UpstreamModel: mappedModel,
  307. ReasoningEffort: reasoningEffort,
  308. Stream: false,
  309. Duration: time.Since(startTime),
  310. }, nil
  311. }
  312. // handleCCStreamingFromAnthropic reads Anthropic SSE events, converts each
  313. // to Responses events, then to Chat Completions chunks, and writes them.
  314. func (s *GatewayService) handleCCStreamingFromAnthropic(
  315. resp *http.Response,
  316. c *gin.Context,
  317. originalModel string,
  318. mappedModel string,
  319. reasoningEffort *string,
  320. startTime time.Time,
  321. includeUsage bool,
  322. ) (*ForwardResult, error) {
  323. requestID := resp.Header.Get("x-request-id")
  324. if s.responseHeaderFilter != nil {
  325. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  326. }
  327. c.Writer.Header().Set("Content-Type", "text/event-stream")
  328. c.Writer.Header().Set("Cache-Control", "no-cache")
  329. c.Writer.Header().Set("Connection", "keep-alive")
  330. c.Writer.Header().Set("X-Accel-Buffering", "no")
  331. c.Writer.WriteHeader(http.StatusOK)
  332. // Use Anthropic→Responses state machine, then convert Responses→CC
  333. anthState := apicompat.NewAnthropicEventToResponsesState()
  334. anthState.Model = originalModel
  335. ccState := apicompat.NewResponsesEventToChatState()
  336. ccState.Model = originalModel
  337. ccState.IncludeUsage = includeUsage
  338. var usage ClaudeUsage
  339. var firstTokenMs *int
  340. firstChunk := true
  341. scanner := bufio.NewScanner(resp.Body)
  342. maxLineSize := defaultMaxLineSize
  343. if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
  344. maxLineSize = s.cfg.Gateway.MaxLineSize
  345. }
  346. scanner.Buffer(make([]byte, 0, 64*1024), maxLineSize)
  347. resultWithUsage := func() *ForwardResult {
  348. return &ForwardResult{
  349. RequestID: requestID,
  350. UpstreamHeaders: resp.Header,
  351. Usage: usage,
  352. Model: originalModel,
  353. UpstreamModel: mappedModel,
  354. ReasoningEffort: reasoningEffort,
  355. Stream: true,
  356. Duration: time.Since(startTime),
  357. FirstTokenMs: firstTokenMs,
  358. }
  359. }
  360. writeChunk := func(chunk apicompat.ChatCompletionsChunk) bool {
  361. sse, err := apicompat.ChatChunkToSSE(chunk)
  362. if err != nil {
  363. return false
  364. }
  365. // Reverse tool name mapping: fake → real, per-chunk bytes.Replace.
  366. // c 可能持有请求侧注入的 ToolNameRewrite;无则仅做静态前缀还原。
  367. out := string(reverseToolNamesIfPresent(c, []byte(sse)))
  368. if _, err := fmt.Fprint(c.Writer, out); err != nil {
  369. return true // client disconnected
  370. }
  371. return false
  372. }
  373. processAnthropicEvent := func(event *apicompat.AnthropicStreamEvent) bool {
  374. if firstChunk {
  375. firstChunk = false
  376. ms := int(time.Since(startTime).Milliseconds())
  377. firstTokenMs = &ms
  378. }
  379. // Extract usage from message_delta
  380. if event.Type == "message_delta" && event.Usage != nil {
  381. mergeAnthropicUsage(&usage, *event.Usage)
  382. }
  383. // Also capture usage from message_start (carries cache fields)
  384. if event.Type == "message_start" && event.Message != nil {
  385. mergeAnthropicUsage(&usage, event.Message.Usage)
  386. }
  387. // Chain: Anthropic event → Responses events → CC chunks
  388. responsesEvents := apicompat.AnthropicEventToResponsesEvents(event, anthState)
  389. for _, resEvt := range responsesEvents {
  390. ccChunks := apicompat.ResponsesEventToChatChunks(&resEvt, ccState)
  391. for _, chunk := range ccChunks {
  392. if disconnected := writeChunk(chunk); disconnected {
  393. return true
  394. }
  395. }
  396. }
  397. c.Writer.Flush()
  398. return false
  399. }
  400. for scanner.Scan() {
  401. line := scanner.Text()
  402. // 与缓冲路径一致:接受 SSE 紧凑格式(冒号后无空格,#4653 同根因)。
  403. if _, ok := extractOpenAISSEEventLine(line); !ok {
  404. continue
  405. }
  406. if !scanner.Scan() {
  407. break
  408. }
  409. payload, ok := extractOpenAISSEDataLine(scanner.Text())
  410. if !ok {
  411. continue
  412. }
  413. var event apicompat.AnthropicStreamEvent
  414. if err := json.Unmarshal([]byte(payload), &event); err != nil {
  415. continue
  416. }
  417. if processAnthropicEvent(&event) {
  418. return resultWithUsage(), nil
  419. }
  420. }
  421. if err := scanner.Err(); err != nil {
  422. if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
  423. logger.L().Warn("forward_as_cc stream: read error",
  424. zap.Error(err),
  425. zap.String("request_id", requestID),
  426. )
  427. }
  428. }
  429. // Finalize both state machines
  430. finalResEvents := apicompat.FinalizeAnthropicResponsesStream(anthState)
  431. for _, resEvt := range finalResEvents {
  432. ccChunks := apicompat.ResponsesEventToChatChunks(&resEvt, ccState)
  433. for _, chunk := range ccChunks {
  434. writeChunk(chunk) //nolint:errcheck
  435. }
  436. }
  437. finalCCChunks := apicompat.FinalizeResponsesChatStream(ccState)
  438. for _, chunk := range finalCCChunks {
  439. writeChunk(chunk) //nolint:errcheck
  440. }
  441. // Write [DONE] marker
  442. fmt.Fprint(c.Writer, "data: [DONE]\n\n") //nolint:errcheck
  443. c.Writer.Flush()
  444. return resultWithUsage(), nil
  445. }
  446. // writeGatewayCCError writes an error in OpenAI Chat Completions format for
  447. // the Anthropic-upstream CC forwarding path.
  448. func writeGatewayCCError(c *gin.Context, statusCode int, errType, message string) {
  449. MarkResponseCommitted(c)
  450. c.JSON(statusCode, gin.H{
  451. "error": gin.H{
  452. "type": errType,
  453. "message": message,
  454. },
  455. })
  456. }