openai_gateway_responses_anthropic_native.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. package service
  2. // 国产供应商 Anthropic 协议账号的 Responses 入站反向路径。
  3. //
  4. // 客户端说 OpenAI Responses(/v1/responses,Codex 等)、上游是供应商原生
  5. // Anthropic 端点(api_protocol=anthropic)时的交叉组合:请求 Responses→Anthropic
  6. // 单次转换,响应 Anthropic 事件→Responses 事件转换。转换链与 Anthropic 平台的
  7. // gateway_forward_as_responses.go 完全一致(复用同一组 apicompat 状态机),仅上游
  8. // 发送/错误处理对齐 OpenAI 网关语义(模型映射、failover、transport error)。
  9. import (
  10. "bufio"
  11. "context"
  12. "encoding/json"
  13. "errors"
  14. "fmt"
  15. "io"
  16. "net/http"
  17. "strings"
  18. "time"
  19. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  20. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  21. "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
  22. "github.com/gin-gonic/gin"
  23. "github.com/tidwall/gjson"
  24. "go.uber.org/zap"
  25. )
  26. // forwardResponsesViaNativeAnthropic serves OpenAI /v1/responses clients through
  27. // a CN provider's native Anthropic endpoint.
  28. //
  29. // Conversion chain:
  30. //
  31. // Request: Responses → Anthropic (single conversion)
  32. // Response: Anthropic events → Responses events (stream state machine)
  33. func (s *OpenAIGatewayService) forwardResponsesViaNativeAnthropic(
  34. ctx context.Context,
  35. c *gin.Context,
  36. account *Account,
  37. body []byte,
  38. defaultMappedModel string,
  39. ) (*OpenAIForwardResult, error) {
  40. startTime := time.Now()
  41. // 1. Lower Codex client-side tools to function tools understood by Anthropic.
  42. adaptedBody, clientToolMapping, err := adaptResponsesClientToolsForAnthropic(body)
  43. if err != nil {
  44. writeResponsesError(c, http.StatusBadRequest, "invalid_request_error", "Failed to adapt request tools")
  45. return nil, fmt.Errorf("adapt responses client tools: %w", err)
  46. }
  47. // 2. Parse Responses request
  48. var responsesReq apicompat.ResponsesRequest
  49. if err := json.Unmarshal(adaptedBody, &responsesReq); err != nil {
  50. writeResponsesError(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  51. return nil, fmt.Errorf("parse responses request: %w", err)
  52. }
  53. originalModel := responsesReq.Model
  54. if strings.TrimSpace(originalModel) == "" {
  55. writeResponsesError(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  56. return nil, fmt.Errorf("missing model in request")
  57. }
  58. clientStream := responsesReq.Stream
  59. // 3. Convert Responses → Anthropic
  60. anthropicReq, err := apicompat.ResponsesToAnthropicRequest(&responsesReq)
  61. if err != nil {
  62. writeResponsesError(c, http.StatusBadRequest, "invalid_request_error", "Failed to convert request")
  63. return nil, fmt.Errorf("convert responses to anthropic: %w", err)
  64. }
  65. // 4. Model mapping(OpenAI 网关统一入口的映射语义)
  66. billingModel := resolveOpenAIForwardModel(account, originalModel, defaultMappedModel)
  67. upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
  68. anthropicReq.Model = upstreamModel
  69. reasoningEffort := ExtractResponsesReasoningEffortFromBody(body, upstreamModel, billingModel, originalModel)
  70. reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, billingModel)
  71. // 5. Force upstream streaming(客户端原始终决定响应格式;
  72. // 上游恒为流式,非流式由缓冲路径组装)。
  73. anthropicReq.Stream = true
  74. reqStream := true
  75. logger.L().Debug("openai responses: forwarding via native anthropic endpoint",
  76. zap.Int64("account_id", account.ID),
  77. zap.String("original_model", originalModel),
  78. zap.String("billing_model", billingModel),
  79. zap.String("upstream_model", upstreamModel),
  80. zap.Bool("client_stream", clientStream),
  81. )
  82. anthropicBody, err := json.Marshal(anthropicReq)
  83. if err != nil {
  84. return nil, fmt.Errorf("marshal anthropic request: %w", err)
  85. }
  86. // 与 /v1/messages 直通路径相同的 pre-filter。
  87. anthropicBody = StripEmptyTextBlocks(anthropicBody)
  88. anthropicBody = FilterWebSearchHistoryBlocks(anthropicBody, upstreamModel)
  89. anthropicBody = enforceCacheControlLimit(anthropicBody)
  90. apiKey := strings.TrimSpace(account.GetOpenAIProtocolAPIKey())
  91. if apiKey == "" {
  92. return nil, fmt.Errorf("account %d missing api_key", account.ID)
  93. }
  94. targetURL, err := s.nativeAnthropicTargetURL(account)
  95. if err != nil {
  96. return nil, err
  97. }
  98. proxyURL := ""
  99. if account.ProxyID != nil && account.Proxy != nil {
  100. proxyURL = account.Proxy.URL()
  101. }
  102. upstreamCtx, releaseUpstreamCtx := detachStreamUpstreamContext(ctx, reqStream)
  103. upstreamReq, _, err := s.buildNativeAnthropicUpstreamRequest(upstreamCtx, c, account, anthropicBody, apiKey, targetURL)
  104. releaseUpstreamCtx()
  105. if err != nil {
  106. return nil, fmt.Errorf("build upstream request: %w", err)
  107. }
  108. resp, err := s.doOpenAIUpstream(upstreamReq, proxyURL, account)
  109. if err != nil {
  110. return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, true)
  111. }
  112. defer func() { _ = resp.Body.Close() }()
  113. if resp.StatusCode >= 400 {
  114. respBody, upstreamMsg := s.readOpenAIUpstreamError(resp)
  115. if foErr := s.failoverOpenAIUpstreamHTTPError(ctx, c, account, resp, respBody, upstreamMsg, upstreamModel); foErr != nil {
  116. return nil, foErr
  117. }
  118. writeResponsesError(c, mapUpstreamStatusCode(resp.StatusCode), "server_error", upstreamMsg)
  119. return nil, fmt.Errorf("upstream error: %d %s", resp.StatusCode, upstreamMsg)
  120. }
  121. if clientStream {
  122. return s.handleResponsesStreamingFromNativeAnthropic(resp, c, originalModel, billingModel, upstreamModel, reasoningEffort, startTime, clientToolMapping)
  123. }
  124. return s.handleResponsesBufferedFromNativeAnthropic(resp, c, originalModel, billingModel, upstreamModel, reasoningEffort, startTime, clientToolMapping)
  125. }
  126. // handleResponsesBufferedFromNativeAnthropic reads Anthropic SSE events, assembles
  127. // the full response, then converts Anthropic → Responses.
  128. func (s *OpenAIGatewayService) handleResponsesBufferedFromNativeAnthropic(
  129. resp *http.Response,
  130. c *gin.Context,
  131. originalModel string,
  132. billingModel string,
  133. upstreamModel string,
  134. reasoningEffort *string,
  135. startTime time.Time,
  136. clientToolMapping apicompat.ResponsesClientToolMapping,
  137. ) (*OpenAIForwardResult, error) {
  138. requestID := resp.Header.Get("x-request-id")
  139. scanner := bufio.NewScanner(resp.Body)
  140. maxLineSize := defaultMaxLineSize
  141. if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
  142. maxLineSize = s.cfg.Gateway.MaxLineSize
  143. }
  144. scanner.Buffer(make([]byte, 0, 64*1024), maxLineSize)
  145. var finalResp *apicompat.AnthropicResponse
  146. var usage ClaudeUsage
  147. // 读间隔上限:上游挂住 SSE 时中止组装(缓冲路径尚未提交响应头,可回 502)。
  148. streamInterval := s.anthropicNativeStreamInterval()
  149. pump := newAnthropicNativeLinePump(scanner, streamInterval)
  150. defer pump.stop()
  151. logReadErr := func(err error) {
  152. if !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
  153. logger.L().Warn("openai responses via native anthropic buffered: read error",
  154. zap.Error(err),
  155. zap.String("request_id", requestID),
  156. )
  157. }
  158. }
  159. onIdle := func() (*OpenAIForwardResult, error) {
  160. _ = resp.Body.Close()
  161. logger.L().Warn("openai responses via native anthropic buffered: data interval timeout",
  162. zap.String("request_id", requestID),
  163. zap.Duration("interval", streamInterval),
  164. )
  165. writeResponsesError(c, http.StatusBadGateway, "server_error", "Upstream stream data interval timeout")
  166. return nil, fmt.Errorf("stream data interval timeout")
  167. }
  168. for {
  169. line, rerr := pump.next()
  170. if rerr != nil {
  171. if errors.Is(rerr, errAnthropicNativeStreamIdle) {
  172. return onIdle()
  173. }
  174. logReadErr(rerr)
  175. break
  176. }
  177. // SSE 规范允许 `event:xxx`(冒号后无空格):Kimi 等上游返回紧凑格式。
  178. if _, ok := extractOpenAISSEEventLine(line); !ok {
  179. continue
  180. }
  181. dataLine, rerr := pump.next()
  182. if rerr != nil {
  183. if errors.Is(rerr, errAnthropicNativeStreamIdle) {
  184. return onIdle()
  185. }
  186. logReadErr(rerr)
  187. break
  188. }
  189. payload, ok := extractOpenAISSEDataLine(dataLine)
  190. if !ok {
  191. continue
  192. }
  193. var event apicompat.AnthropicStreamEvent
  194. if err := json.Unmarshal([]byte(payload), &event); err != nil {
  195. continue
  196. }
  197. if event.Type == "message_start" && event.Message != nil {
  198. finalResp = event.Message
  199. mergeAnthropicUsage(&usage, event.Message.Usage)
  200. }
  201. if event.Type == "message_delta" {
  202. if event.Usage != nil {
  203. mergeAnthropicUsage(&usage, *event.Usage)
  204. }
  205. if event.Delta != nil && event.Delta.StopReason != "" && finalResp != nil {
  206. finalResp.StopReason = apicompat.AnthropicStopReasonPtr(event.Delta.StopReason)
  207. }
  208. }
  209. if event.Type == "content_block_start" && event.ContentBlock != nil && finalResp != nil {
  210. finalResp.Content = append(finalResp.Content, *event.ContentBlock)
  211. }
  212. if event.Type == "content_block_delta" && event.Delta != nil && finalResp != nil && event.Index != nil {
  213. idx := *event.Index
  214. if idx < len(finalResp.Content) {
  215. switch event.Delta.Type {
  216. case "text_delta":
  217. finalResp.Content[idx].Text += event.Delta.Text
  218. case "thinking_delta":
  219. finalResp.Content[idx].Thinking += event.Delta.Thinking
  220. case "input_json_delta":
  221. finalResp.Content[idx].Input = appendRawJSON(finalResp.Content[idx].Input, event.Delta.PartialJSON)
  222. }
  223. }
  224. }
  225. }
  226. if finalResp == nil {
  227. writeResponsesError(c, http.StatusBadGateway, "server_error", "Upstream stream ended without a response")
  228. return nil, fmt.Errorf("upstream stream ended without response")
  229. }
  230. if usage.InputTokens > 0 || usage.OutputTokens > 0 {
  231. finalResp.Usage = apicompat.AnthropicUsage{
  232. InputTokens: usage.InputTokens,
  233. OutputTokens: usage.OutputTokens,
  234. CacheCreationInputTokens: usage.CacheCreationInputTokens,
  235. CacheReadInputTokens: usage.CacheReadInputTokens,
  236. }
  237. }
  238. responsesResp := apicompat.AnthropicToResponsesResponse(finalResp)
  239. responsesResp.Model = originalModel
  240. if s.responseHeaderFilter != nil {
  241. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  242. }
  243. // 非流式响应必须是 application/json(上游被强制流式,透传头会污染)。
  244. c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
  245. if respBytes, err := json.Marshal(responsesResp); err == nil {
  246. respBytes = reverseToolNamesIfPresent(c, respBytes)
  247. respBytes, _, err = apicompat.RestoreResponsesClientToolPayload(respBytes, clientToolMapping)
  248. if err != nil {
  249. return nil, fmt.Errorf("restore responses client tools: %w", err)
  250. }
  251. c.Data(http.StatusOK, "application/json; charset=utf-8", respBytes)
  252. } else {
  253. c.JSON(http.StatusOK, responsesResp)
  254. }
  255. return &OpenAIForwardResult{
  256. RequestID: requestID,
  257. UpstreamHeaders: resp.Header,
  258. Usage: claudeUsageToOpenAIUsage(&usage),
  259. Model: originalModel,
  260. BillingModel: billingModel,
  261. UpstreamModel: upstreamModel,
  262. UpstreamEndpoint: "/v1/messages",
  263. ReasoningEffort: reasoningEffort,
  264. Stream: false,
  265. Duration: time.Since(startTime),
  266. }, nil
  267. }
  268. // handleResponsesStreamingFromNativeAnthropic reads Anthropic SSE events, converts
  269. // each to Responses SSE events, and writes them to the client.
  270. func (s *OpenAIGatewayService) handleResponsesStreamingFromNativeAnthropic(
  271. resp *http.Response,
  272. c *gin.Context,
  273. originalModel string,
  274. billingModel string,
  275. upstreamModel string,
  276. reasoningEffort *string,
  277. startTime time.Time,
  278. clientToolMapping apicompat.ResponsesClientToolMapping,
  279. ) (*OpenAIForwardResult, error) {
  280. requestID := resp.Header.Get("x-request-id")
  281. if s.responseHeaderFilter != nil {
  282. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  283. }
  284. c.Writer.Header().Set("Content-Type", "text/event-stream")
  285. c.Writer.Header().Set("Cache-Control", "no-cache")
  286. c.Writer.Header().Set("Connection", "keep-alive")
  287. c.Writer.Header().Set("X-Accel-Buffering", "no")
  288. c.Writer.WriteHeader(http.StatusOK)
  289. state := apicompat.NewAnthropicEventToResponsesState()
  290. state.Model = originalModel
  291. clientToolRestorer := apicompat.NewResponsesClientToolStreamRestorer(clientToolMapping)
  292. var usage ClaudeUsage
  293. var firstTokenMs *int
  294. firstChunk := true
  295. clientDisconnected := false
  296. scanner := bufio.NewScanner(resp.Body)
  297. maxLineSize := defaultMaxLineSize
  298. if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
  299. maxLineSize = s.cfg.Gateway.MaxLineSize
  300. }
  301. scanner.Buffer(make([]byte, 0, 64*1024), maxLineSize)
  302. resultWithUsage := func() *OpenAIForwardResult {
  303. return &OpenAIForwardResult{
  304. RequestID: requestID,
  305. UpstreamHeaders: resp.Header,
  306. Usage: claudeUsageToOpenAIUsage(&usage),
  307. Model: originalModel,
  308. BillingModel: billingModel,
  309. UpstreamModel: upstreamModel,
  310. UpstreamEndpoint: "/v1/messages",
  311. ReasoningEffort: reasoningEffort,
  312. Stream: true,
  313. Duration: time.Since(startTime),
  314. FirstTokenMs: firstTokenMs,
  315. ClientDisconnect: clientDisconnected,
  316. }
  317. }
  318. // 读间隔上限:上游挂住 SSE(不发数据也不断连)时结束转换循环。上游 ctx 为
  319. // WithoutCancel 且 http.Client 无整体 Timeout,无此界限则 scanner.Scan()
  320. // 永久阻塞(见 anthropic native pump 文件注释)。
  321. streamInterval := s.anthropicNativeStreamInterval()
  322. pump := newAnthropicNativeLinePump(scanner, streamInterval)
  323. defer pump.stop()
  324. logReadErr := func(err error) {
  325. if !errors.Is(err, io.EOF) && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
  326. logger.L().Warn("openai responses via native anthropic stream: read error",
  327. zap.Error(err),
  328. zap.String("request_id", requestID),
  329. )
  330. }
  331. }
  332. onIdle := func() (*OpenAIForwardResult, error) {
  333. _ = resp.Body.Close()
  334. logger.L().Warn("openai responses via native anthropic stream: data interval timeout",
  335. zap.String("request_id", requestID),
  336. zap.Duration("interval", streamInterval),
  337. )
  338. return resultWithUsage(), fmt.Errorf("stream data interval timeout")
  339. }
  340. // 与 CC 姊妹路径(handleCCStreamingFromNativeAnthropic.writeChunk)同语义:
  341. // 客户端断开后不再写出,但继续排水上游至流自然结束——Anthropic 的最终
  342. // output_tokens 只在末尾 message_delta 携带,提前退出会把整段生成记成 ~1
  343. // token,payg 上游照常计费而平台漏记。状态机照常推进以保证 finalize 一致。
  344. processAnthropicEvent := func(event *apicompat.AnthropicStreamEvent) {
  345. if firstChunk {
  346. firstChunk = false
  347. ms := int(time.Since(startTime).Milliseconds())
  348. firstTokenMs = &ms
  349. }
  350. if event.Type == "message_delta" && event.Usage != nil {
  351. mergeAnthropicUsage(&usage, *event.Usage)
  352. }
  353. if event.Type == "message_start" && event.Message != nil {
  354. mergeAnthropicUsage(&usage, event.Message.Usage)
  355. }
  356. events := apicompat.AnthropicEventToResponsesEvents(event, state)
  357. if clientDisconnected {
  358. return
  359. }
  360. for _, evt := range events {
  361. payload, err := json.Marshal(evt)
  362. if err != nil {
  363. continue
  364. }
  365. payload = reverseToolNamesIfPresent(c, payload)
  366. payloads, _, err := clientToolRestorer.RestoreEvent(payload)
  367. if err != nil {
  368. continue
  369. }
  370. for _, restored := range payloads {
  371. eventType := gjson.GetBytes(restored, "type").String()
  372. if _, err := fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", eventType, restored); err != nil {
  373. clientDisconnected = true
  374. return
  375. }
  376. }
  377. }
  378. if len(events) > 0 {
  379. c.Writer.Flush()
  380. }
  381. }
  382. for {
  383. line, rerr := pump.next()
  384. if rerr != nil {
  385. if errors.Is(rerr, errAnthropicNativeStreamIdle) {
  386. return onIdle()
  387. }
  388. logReadErr(rerr)
  389. break
  390. }
  391. if _, ok := extractOpenAISSEEventLine(line); !ok {
  392. continue
  393. }
  394. dataLine, rerr := pump.next()
  395. if rerr != nil {
  396. if errors.Is(rerr, errAnthropicNativeStreamIdle) {
  397. return onIdle()
  398. }
  399. logReadErr(rerr)
  400. break
  401. }
  402. payload, ok := extractOpenAISSEDataLine(dataLine)
  403. if !ok {
  404. continue
  405. }
  406. var event apicompat.AnthropicStreamEvent
  407. if err := json.Unmarshal([]byte(payload), &event); err != nil {
  408. continue
  409. }
  410. processAnthropicEvent(&event)
  411. }
  412. // Finalize state machine(客户端已断开时仍推进,保证 usage 汇总完整;仅在
  413. // 客户端仍连接时写出)。终态帧与逐事件路径一致过工具名反转与客户端工具还原,
  414. // 避免流截断时终态帧携带改写后的工具名。
  415. if finalEvents := apicompat.FinalizeAnthropicResponsesStream(state); len(finalEvents) > 0 && !clientDisconnected {
  416. wrote := false
  417. for _, evt := range finalEvents {
  418. payload, err := json.Marshal(evt)
  419. if err != nil {
  420. continue
  421. }
  422. payload = reverseToolNamesIfPresent(c, payload)
  423. payloads, _, err := clientToolRestorer.RestoreEvent(payload)
  424. if err != nil {
  425. continue
  426. }
  427. for _, restored := range payloads {
  428. eventType := gjson.GetBytes(restored, "type").String()
  429. if _, err := fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", eventType, restored); err != nil {
  430. clientDisconnected = true
  431. break
  432. }
  433. wrote = true
  434. }
  435. if clientDisconnected {
  436. break
  437. }
  438. }
  439. if wrote {
  440. c.Writer.Flush()
  441. }
  442. }
  443. return resultWithUsage(), nil
  444. }