openai_gateway_cc_pipeline.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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/logger"
  15. "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
  16. "github.com/gin-gonic/gin"
  17. "go.uber.org/zap"
  18. )
  19. // 本文件收敛三个 CC(Chat Completions)forwarder 之间重复的 HTTP 管线与 SSE
  20. // 循环骨架(PR #3802 遗留项):
  21. //
  22. // - forwardAsRawChatCompletions (原生 CC 直转)
  23. // - forwardResponsesViaRawChatCompletions(/v1/responses → CC 回退)
  24. // - forwardAnthropicViaRawChatCompletions(/v1/messages → CC 回退)
  25. //
  26. // 以及 messages / chat_completions 两条 Responses 主路径中逐字相同的错误处理块。
  27. // 所有 helper 都是对既有内联代码的等价提取,不改变任何行为;各路径的差异
  28. // (GLM effort 归一化、fast policy、Grok 分支、ClientDisconnect 语义等)仍留在
  29. // 调用方,属于有意保留的行为差异,不在此强行统一。
  30. // newUpstreamSSEScanner 构造读取上游 SSE 流的行扫描器,按配置放大单行上限。
  31. func (s *OpenAIGatewayService) newUpstreamSSEScanner(r io.Reader) *bufio.Scanner {
  32. scanner := bufio.NewScanner(r)
  33. maxLineSize := defaultMaxLineSize
  34. if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
  35. maxLineSize = s.cfg.Gateway.MaxLineSize
  36. }
  37. scanner.Buffer(make([]byte, 0, 64*1024), maxLineSize)
  38. return scanner
  39. }
  40. // newStreamHeaderWriter 返回幂等的 SSE 响应头写入闭包:首次调用时透传过滤后的
  41. // 上游响应头并写入标准 SSE 头 + 200 状态码,后续调用为 no-op。延迟到首个事件
  42. // 写出前才提交响应头,使上游早期失败仍可改走 failover 或非流式错误响应。
  43. func (s *OpenAIGatewayService) newStreamHeaderWriter(c *gin.Context, upstream http.Header) func() {
  44. headersWritten := false
  45. return func() {
  46. if headersWritten {
  47. return
  48. }
  49. headersWritten = true
  50. if s.responseHeaderFilter != nil {
  51. responseheaders.WriteFilteredHeaders(c.Writer.Header(), upstream, s.responseHeaderFilter)
  52. }
  53. c.Writer.Header().Set("Content-Type", "text/event-stream")
  54. c.Writer.Header().Set("Cache-Control", "no-cache")
  55. c.Writer.Header().Set("Connection", "keep-alive")
  56. c.Writer.Header().Set("X-Accel-Buffering", "no")
  57. c.Writer.WriteHeader(http.StatusOK)
  58. }
  59. }
  60. // readOpenAIUpstreamError 读取上游错误体并把 resp.Body 回卷为可重读的副本
  61. // (下游 handleXxxErrorResponse 需要再次读取),返回原始错误体与脱敏后的
  62. // 上游错误消息。
  63. func (s *OpenAIGatewayService) readOpenAIUpstreamError(resp *http.Response) ([]byte, string) {
  64. respBody := s.readUpstreamErrorBody(resp)
  65. _ = resp.Body.Close()
  66. resp.Body = io.NopCloser(bytes.NewReader(respBody))
  67. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  68. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  69. return respBody, upstreamMsg
  70. }
  71. // failoverOpenAIUpstreamHTTPError 对 >=400 的上游响应做 failover 判定:命中时
  72. // 记录 ops 事件、执行账号级错误处置并返回 *UpstreamFailoverError;未命中返回
  73. // nil,调用方继续走各自端点格式的非 failover 错误处理链。
  74. func (s *OpenAIGatewayService) failoverOpenAIUpstreamHTTPError(
  75. ctx context.Context,
  76. c *gin.Context,
  77. account *Account,
  78. resp *http.Response,
  79. respBody []byte,
  80. upstreamMsg string,
  81. upstreamModel string,
  82. ) *UpstreamFailoverError {
  83. shouldFailover := s.shouldFailoverOpenAIUpstreamResponse(account, resp.StatusCode, upstreamMsg, respBody)
  84. tempUnscheduled := false
  85. if c != nil && account != nil && account.Platform != PlatformGrok && !shouldFailover && !IsResponseCommitted(c) && s.rateLimitService != nil {
  86. tempUnscheduled = s.rateLimitService.CheckErrorPolicy(ctx, account, resp.StatusCode, respBody, upstreamModel) == ErrorPolicyTempUnscheduled
  87. shouldFailover = tempUnscheduled
  88. }
  89. if account != nil && account.Platform == PlatformGrok {
  90. shouldFailover = s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody)
  91. }
  92. if account != nil && account.Platform == PlatformGrok {
  93. s.handleGrokAccountUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
  94. }
  95. if !shouldFailover {
  96. return nil
  97. }
  98. upstreamDetail := ""
  99. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  100. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  101. if maxBytes <= 0 {
  102. maxBytes = 2048
  103. }
  104. upstreamDetail = truncateString(string(respBody), maxBytes)
  105. }
  106. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  107. ProxyID: opsUpstreamProxyID(account),
  108. ProxyName: opsUpstreamProxyName(account),
  109. Platform: account.Platform,
  110. AccountID: account.ID,
  111. AccountName: account.Name,
  112. UpstreamStatusCode: resp.StatusCode,
  113. UpstreamRequestID: resp.Header.Get("x-request-id"),
  114. Kind: "failover",
  115. Message: upstreamMsg,
  116. Detail: upstreamDetail,
  117. })
  118. shouldDisable := tempUnscheduled
  119. if account.Platform != PlatformGrok && !tempUnscheduled {
  120. shouldDisable = s.handleOpenAIAccountUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody, upstreamModel)
  121. }
  122. return s.newOpenAIAccountFailoverError(
  123. account,
  124. resp.StatusCode,
  125. resp.Header,
  126. respBody,
  127. upstreamMsg,
  128. shouldDisable,
  129. !shouldDisable && account.IsPoolMode() && (account.IsPoolModeRetryableStatus(resp.StatusCode) || isOpenAITransientProcessingError(resp.StatusCode, upstreamMsg, respBody)),
  130. )
  131. }
  132. // openAIChatCompletionsTargetURL 解析账号的(非 Grok)Chat Completions 上游端点。
  133. func (s *OpenAIGatewayService) openAIChatCompletionsTargetURL(account *Account) (string, error) {
  134. baseURL := account.GetOpenAIBaseURL()
  135. if baseURL == "" {
  136. baseURL = "https://api.openai.com"
  137. }
  138. validatedURL, err := s.validateUpstreamBaseURL(baseURL)
  139. if err != nil {
  140. return "", fmt.Errorf("invalid base_url: %w", err)
  141. }
  142. return buildOpenAIChatCompletionsURL(validatedURL), nil
  143. }
  144. // resolveCCFallbackTarget 解析两条 CC 回退路径共用的账号凭证与上游端点
  145. // (回退路径仅面向 APIKey 账号,凭证恒为 openai api_key)。
  146. func (s *OpenAIGatewayService) resolveCCFallbackTarget(account *Account) (apiKey string, targetURL string, err error) {
  147. apiKey = strings.TrimSpace(account.GetOpenAIProtocolAPIKey())
  148. if apiKey == "" {
  149. return "", "", fmt.Errorf("account %d missing api_key", account.ID)
  150. }
  151. targetURL, err = s.openAIChatCompletionsTargetURL(account)
  152. if err != nil {
  153. return "", "", err
  154. }
  155. return apiKey, targetURL, nil
  156. }
  157. // sendCCUpstreamRequest 构建并发送 CC 上游请求:分离的上游 context、OpenAI HTTP
  158. // profile、标准头(含流式 Accept 切换)、客户端 header 白名单透传、自定义 UA 与
  159. // 账号级 header 覆写,最后经代理发出。传输层失败(DNS/TCP/TLS,无 HTTP 响应)
  160. // 统一由 handleOpenAIUpstreamTransportError 归一为 failover。
  161. //
  162. // userAgent 为空时保留默认 UA;Grok 的默认 UA 兜底由调用方解析后传入。
  163. func (s *OpenAIGatewayService) sendCCUpstreamRequest(
  164. ctx context.Context,
  165. c *gin.Context,
  166. account *Account,
  167. targetURL string,
  168. body []byte,
  169. stream bool,
  170. bearerToken string,
  171. userAgent string,
  172. grokCacheIdentity string,
  173. ) (*http.Response, error) {
  174. upstreamCtx, releaseUpstreamCtx := detachUpstreamContext(ctx)
  175. upstreamReq, err := http.NewRequestWithContext(upstreamCtx, http.MethodPost, targetURL, bytes.NewReader(body))
  176. releaseUpstreamCtx()
  177. if err != nil {
  178. return nil, fmt.Errorf("build upstream request: %w", err)
  179. }
  180. // 记录本次实际选择的协议端点,供错误日志和用量日志在没有
  181. // OpenAIForwardResult(例如 503/传输失败)时使用。每次发送都覆盖,
  182. // 避免 Gin context 在账号 failover 尝试之间残留旧端点。
  183. SetActualOpenAIUpstreamEndpoint(c, "/v1/chat/completions")
  184. upstreamReq = upstreamReq.WithContext(WithHTTPUpstreamProfile(upstreamReq.Context(), HTTPUpstreamProfileOpenAI))
  185. upstreamReq.Header.Set("Content-Type", "application/json")
  186. upstreamReq.Header.Set("Authorization", "Bearer "+bearerToken)
  187. if stream {
  188. upstreamReq.Header.Set("Accept", "text/event-stream")
  189. } else {
  190. upstreamReq.Header.Set("Accept", "application/json")
  191. }
  192. // 透传白名单中的客户端 header。详见 openaiCCRawAllowedHeaders 的设计说明。
  193. for key, values := range c.Request.Header {
  194. lowerKey := strings.ToLower(key)
  195. if openaiCCRawAllowedHeaders[lowerKey] {
  196. for _, v := range values {
  197. upstreamReq.Header.Add(key, v)
  198. }
  199. }
  200. }
  201. if userAgent != "" {
  202. upstreamReq.Header.Set("user-agent", userAgent)
  203. }
  204. if account.Platform == PlatformGrok {
  205. if account.IsGrokOAuth() {
  206. applyGrokCLIHeaders(upstreamReq.Header)
  207. }
  208. applyGrokCacheHeaders(upstreamReq.Header, grokCacheIdentity)
  209. }
  210. // 账号级请求头覆写:放在所有内置默认头(含 Grok CLI 身份头)之后应用,
  211. // 使配置值获得除共享传输层强制头之外的最高优先级。
  212. account.ApplyHeaderOverrides(upstreamReq.Header)
  213. applyOpenCodeSessionHeader(c, account, targetURL, upstreamReq.Header)
  214. proxyURL := ""
  215. if account.ProxyID != nil && account.Proxy != nil {
  216. proxyURL = account.Proxy.URL()
  217. }
  218. resp, err := s.doOpenAIUpstream(upstreamReq, proxyURL, account)
  219. if err != nil {
  220. return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, false)
  221. }
  222. return resp, nil
  223. }
  224. // ccStreamScanState 是 scanCCStream 返回的读取状态快照。
  225. type ccStreamScanState struct {
  226. // Usage 为 include_usage chunk 中最近一次出现的用量(上游可能重复发送,
  227. // 总是保留最新值);终态事件中的用量由调用方在 finalize 阶段自行覆盖。
  228. Usage OpenAIUsage
  229. // FirstTokenMs 为首个实际输出 chunk(排除 usage-only chunk)的到达时延。
  230. FirstTokenMs *int
  231. // SawDone 表示上游发出了 [DONE] 哨兵。
  232. SawDone bool
  233. // Err 为 scanner 读错误(客户端 context 取消不属于此类,会原样带出)。
  234. // 非 nil 时调用方必须跳过 finalize 并返回 usage-incomplete 错误,避免
  235. // 把上游截断伪装成正常收尾。
  236. Err error
  237. }
  238. // scanCCStream 驱动两条 CC 回退路径共享的 SSE 读循环:提取 data 行、在 [DONE]
  239. // 哨兵处停止、保留最新 usage、记录首 token 时延,并把每个解析成功的 chunk 交给
  240. // emit 回调做各自的协议转换与写出。读错误按既有约定过滤 context 取消类噪声后
  241. // 记入 Warn 日志。
  242. func (s *OpenAIGatewayService) scanCCStream(
  243. c *gin.Context,
  244. resp *http.Response,
  245. logPrefix string,
  246. requestID string,
  247. startTime time.Time,
  248. emit func(*apicompat.ChatCompletionsChunk),
  249. ) ccStreamScanState {
  250. var st ccStreamScanState
  251. scanner := s.newUpstreamSSEScanner(resp.Body)
  252. for scanner.Scan() {
  253. line := scanner.Text()
  254. payload, ok := extractOpenAISSEDataLine(line)
  255. if !ok {
  256. continue
  257. }
  258. payload = strings.TrimSpace(payload)
  259. if payload == "" {
  260. continue
  261. }
  262. if payload == "[DONE]" {
  263. st.SawDone = true
  264. break
  265. }
  266. // 观察上游 CC chunk 回显的 model / service_tier(计费以回显为准)。
  267. // CC chunk 无 type 字段,按 untyped payload 观察(上游约束:只有终止
  268. // 事件与无类型 body 报告实际处理档位)。
  269. if observer := upstreamResponseModelObserverFromContext(c); observer != nil {
  270. observer.ObserveOpenAI([]byte(payload), "")
  271. }
  272. if u := extractCCStreamUsage(payload); u != nil {
  273. st.Usage = *u
  274. }
  275. var chunk apicompat.ChatCompletionsChunk
  276. if err := json.Unmarshal([]byte(payload), &chunk); err != nil {
  277. logger.L().Warn(logPrefix+": failed to parse chat stream chunk",
  278. zap.Error(err),
  279. zap.String("request_id", requestID),
  280. )
  281. continue
  282. }
  283. if st.FirstTokenMs == nil && !isOpenAIChatUsageOnlyStreamChunk(payload) && chatChunkStartsResponsesOutput(&chunk) {
  284. ms := int(time.Since(startTime).Milliseconds())
  285. st.FirstTokenMs = &ms
  286. }
  287. emit(&chunk)
  288. }
  289. if err := scanner.Err(); err != nil {
  290. if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
  291. logger.L().Warn(logPrefix+": stream read error",
  292. zap.Error(err),
  293. zap.String("request_id", requestID),
  294. )
  295. }
  296. st.Err = err
  297. }
  298. return st
  299. }
  300. // logCCStreamMissingDoneSentinel 记录"上游未发 [DONE] 哨兵即结束"的 debug 日志。
  301. func logCCStreamMissingDoneSentinel(logPrefix, requestID string) {
  302. logger.L().Debug(logPrefix+": upstream stream ended without done sentinel",
  303. zap.String("request_id", requestID),
  304. )
  305. }
  306. // readCCUpstreamJSONResponse 读取并解析 CC 非流式 JSON 响应,失败时以调用方
  307. // 端点格式回写错误;成功时顺带提取 usage。
  308. func (s *OpenAIGatewayService) readCCUpstreamJSONResponse(
  309. c *gin.Context,
  310. resp *http.Response,
  311. writeError compatErrorWriter,
  312. ) (*apicompat.ChatCompletionsResponse, OpenAIUsage, error) {
  313. respBody, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, openAITooLargeError)
  314. if err != nil {
  315. if !errors.Is(err, ErrUpstreamResponseBodyTooLarge) {
  316. writeError(c, http.StatusBadGateway, "api_error", "Failed to read upstream response")
  317. }
  318. return nil, OpenAIUsage{}, fmt.Errorf("read upstream body: %w", err)
  319. }
  320. var ccResp apicompat.ChatCompletionsResponse
  321. if err := json.Unmarshal(respBody, &ccResp); err != nil {
  322. writeError(c, http.StatusBadGateway, "api_error", "Failed to parse upstream response")
  323. return nil, OpenAIUsage{}, fmt.Errorf("parse chat completions response: %w", err)
  324. }
  325. // 观察上游 CC JSON 回显的 model / service_tier(计费以回显为准)。
  326. // CC JSON 无 type 字段,按 untyped payload 观察(上游约束)。
  327. if observer := upstreamResponseModelObserverFromContext(c); observer != nil {
  328. observer.ObserveOpenAI(respBody, "")
  329. }
  330. usage := OpenAIUsage{}
  331. if parsed, ok := extractOpenAIUsageFromJSONBytes(respBody); ok {
  332. usage = parsed
  333. }
  334. return &ccResp, usage, nil
  335. }
  336. // writeOpenAIResponsesFallbackError 以 /v1/responses 回退路径的既有错误格式回写
  337. // (裸 error 对象;不调用 MarkResponseCommitted,与原内联写法保持一致)。
  338. func writeOpenAIResponsesFallbackError(c *gin.Context, statusCode int, errType, message string) {
  339. c.JSON(statusCode, gin.H{
  340. "error": gin.H{
  341. "type": errType,
  342. "message": message,
  343. },
  344. })
  345. }