antigravity_gateway_upstream.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. package service
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "strings"
  11. "sync/atomic"
  12. "time"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  15. "github.com/gin-gonic/gin"
  16. )
  17. // ForwardUpstream 使用 base_url + /v1/messages + 双 header 认证透传上游 Claude 请求
  18. func (s *AntigravityGatewayService) ForwardUpstream(ctx context.Context, c *gin.Context, account *Account, body []byte) (*ForwardResult, error) {
  19. beginUpstreamResponseModelObservation(c)
  20. startTime := time.Now()
  21. sessionID := getSessionID(c)
  22. prefix := logPrefix(sessionID, account.Name)
  23. // 获取上游配置
  24. baseURL := strings.TrimSpace(account.GetCredential("base_url"))
  25. apiKey := strings.TrimSpace(account.GetCredential("api_key"))
  26. if baseURL == "" || apiKey == "" {
  27. return nil, fmt.Errorf("upstream account missing base_url or api_key")
  28. }
  29. baseURL = strings.TrimSuffix(baseURL, "/")
  30. // 解析请求获取模型信息
  31. var claudeReq antigravity.ClaudeRequest
  32. if err := json.Unmarshal(body, &claudeReq); err != nil {
  33. return nil, fmt.Errorf("parse claude request: %w", err)
  34. }
  35. if strings.TrimSpace(claudeReq.Model) == "" {
  36. return nil, fmt.Errorf("missing model")
  37. }
  38. originalModel := claudeReq.Model
  39. // 构建上游请求 URL
  40. upstreamURL := baseURL + "/v1/messages"
  41. // 能力维度 sanitize:Anthropic-compatible 上游透传路径也需要保证 body↔beta header
  42. // 对称。客户端 anthropic-beta header 不含 context-management-2025-06-27 但 body 带
  43. // context_management 时 strip,与 Anthropic 直连 / Bedrock / Vertex 路径保持一致。
  44. clientBeta := c.GetHeader("anthropic-beta")
  45. if sanitized, changed := sanitizeAnthropicBodyForBetaTokens(body, clientBeta); changed {
  46. body = sanitized
  47. }
  48. // 创建请求
  49. req, err := http.NewRequestWithContext(ctx, http.MethodPost, upstreamURL, bytes.NewReader(body))
  50. if err != nil {
  51. return nil, fmt.Errorf("create upstream request: %w", err)
  52. }
  53. // 设置请求头
  54. req.Header.Set("Content-Type", "application/json")
  55. req.Header.Set("Authorization", "Bearer "+apiKey)
  56. req.Header.Set("x-api-key", apiKey) // Claude API 兼容
  57. // 透传 Claude 相关 headers
  58. if v := c.GetHeader("anthropic-version"); v != "" {
  59. req.Header.Set("anthropic-version", v)
  60. }
  61. if v := clientBeta; v != "" {
  62. req.Header.Set("anthropic-beta", v)
  63. }
  64. // 代理 URL
  65. proxyURL := ""
  66. if account.ProxyID != nil && account.Proxy != nil {
  67. proxyURL = account.Proxy.URL()
  68. }
  69. // 发送请求
  70. resp, err := s.httpUpstream.Do(WithAccountTrafficRequest(req, account), proxyURL, account.ID, account.Concurrency)
  71. if err != nil {
  72. logger.LegacyPrintf("service.antigravity_gateway", "%s upstream request failed: %v", prefix, err)
  73. return nil, fmt.Errorf("upstream request failed: %w", err)
  74. }
  75. defer func() { _ = resp.Body.Close() }()
  76. // 处理错误响应
  77. if resp.StatusCode >= 400 {
  78. respBody := s.readUpstreamErrorBody(resp)
  79. // 429 错误时标记账号限流
  80. if resp.StatusCode == http.StatusTooManyRequests {
  81. s.handleUpstreamError(ctx, prefix, account, resp.StatusCode, resp.Header, respBody, originalModel, 0, "", false)
  82. }
  83. // 透传上游错误
  84. c.Header("Content-Type", resp.Header.Get("Content-Type"))
  85. c.Status(resp.StatusCode)
  86. _, _ = c.Writer.Write(respBody)
  87. return &ForwardResult{
  88. Model: originalModel,
  89. }, nil
  90. }
  91. // 处理成功响应(流式/非流式)
  92. var usage *ClaudeUsage
  93. var firstTokenMs *int
  94. var clientDisconnect bool
  95. if claudeReq.Stream {
  96. // 流式响应:透传
  97. c.Header("Content-Type", "text/event-stream")
  98. c.Header("Cache-Control", "no-cache")
  99. c.Header("Connection", "keep-alive")
  100. c.Header("X-Accel-Buffering", "no")
  101. c.Status(http.StatusOK)
  102. streamRes := s.streamUpstreamResponse(c, resp, startTime)
  103. usage = streamRes.usage
  104. firstTokenMs = streamRes.firstTokenMs
  105. clientDisconnect = streamRes.clientDisconnect
  106. } else {
  107. // 非流式响应:直接透传
  108. respBody, err := io.ReadAll(resp.Body)
  109. if err != nil {
  110. return nil, fmt.Errorf("read upstream response: %w", err)
  111. }
  112. // 提取 usage
  113. upstreamResponseModelObserverFromContext(c).ObserveAnthropic(respBody)
  114. usage = s.extractClaudeUsage(respBody)
  115. c.Header("Content-Type", resp.Header.Get("Content-Type"))
  116. c.Status(http.StatusOK)
  117. _, _ = c.Writer.Write(respBody)
  118. }
  119. // 构建计费结果
  120. duration := time.Since(startTime)
  121. logger.LegacyPrintf("service.antigravity_gateway", "%s status=success duration_ms=%d", prefix, duration.Milliseconds())
  122. return &ForwardResult{
  123. Model: originalModel,
  124. UpstreamResponseModel: observedUpstreamResponseModel(c),
  125. UpstreamResponseModelConflict: observedUpstreamResponseModelConflict(c),
  126. Stream: claudeReq.Stream,
  127. Duration: duration,
  128. FirstTokenMs: firstTokenMs,
  129. ClientDisconnect: clientDisconnect,
  130. UpstreamHeaders: resp.Header,
  131. Usage: ClaudeUsage{
  132. InputTokens: usage.InputTokens,
  133. OutputTokens: usage.OutputTokens,
  134. CacheReadInputTokens: usage.CacheReadInputTokens,
  135. CacheCreationInputTokens: usage.CacheCreationInputTokens,
  136. },
  137. }, nil
  138. }
  139. // streamUpstreamResponse 透传上游 SSE 流并提取 Claude usage
  140. func (s *AntigravityGatewayService) streamUpstreamResponse(c *gin.Context, resp *http.Response, startTime time.Time) *antigravityStreamResult {
  141. usage := &ClaudeUsage{}
  142. var firstTokenMs *int
  143. scanner := bufio.NewScanner(resp.Body)
  144. maxLineSize := defaultMaxLineSize
  145. if s.settingService.cfg != nil && s.settingService.cfg.Gateway.MaxLineSize > 0 {
  146. maxLineSize = s.settingService.cfg.Gateway.MaxLineSize
  147. }
  148. scanner.Buffer(make([]byte, 64*1024), maxLineSize)
  149. type scanEvent struct {
  150. line string
  151. err error
  152. }
  153. events := make(chan scanEvent, 16)
  154. done := make(chan struct{})
  155. sendEvent := func(ev scanEvent) bool {
  156. select {
  157. case events <- ev:
  158. return true
  159. case <-done:
  160. return false
  161. }
  162. }
  163. var lastReadAt int64
  164. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  165. go func() {
  166. defer close(events)
  167. for scanner.Scan() {
  168. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  169. if !sendEvent(scanEvent{line: scanner.Text()}) {
  170. return
  171. }
  172. }
  173. if err := scanner.Err(); err != nil {
  174. _ = sendEvent(scanEvent{err: err})
  175. }
  176. }()
  177. defer close(done)
  178. streamInterval := time.Duration(0)
  179. if s.settingService.cfg != nil && s.settingService.cfg.Gateway.StreamDataIntervalTimeout > 0 {
  180. streamInterval = time.Duration(s.settingService.cfg.Gateway.StreamDataIntervalTimeout) * time.Second
  181. }
  182. var intervalTicker *time.Ticker
  183. if streamInterval > 0 {
  184. intervalTicker = time.NewTicker(streamInterval)
  185. defer intervalTicker.Stop()
  186. }
  187. var intervalCh <-chan time.Time
  188. if intervalTicker != nil {
  189. intervalCh = intervalTicker.C
  190. }
  191. // 下游 keepalive:防止代理/Cloudflare Tunnel 因连接空闲而断开
  192. keepaliveInterval := time.Duration(0)
  193. if s.settingService.cfg != nil && s.settingService.cfg.Gateway.StreamKeepaliveInterval > 0 {
  194. keepaliveInterval = time.Duration(s.settingService.cfg.Gateway.StreamKeepaliveInterval) * time.Second
  195. }
  196. var keepaliveTicker *time.Ticker
  197. if keepaliveInterval > 0 {
  198. keepaliveTicker = time.NewTicker(keepaliveInterval)
  199. defer keepaliveTicker.Stop()
  200. }
  201. var keepaliveCh <-chan time.Time
  202. if keepaliveTicker != nil {
  203. keepaliveCh = keepaliveTicker.C
  204. }
  205. lastDataAt := time.Now()
  206. flusher, _ := c.Writer.(http.Flusher)
  207. cw := newAntigravityClientWriter(c.Writer, flusher, "antigravity upstream")
  208. for {
  209. select {
  210. case ev, ok := <-events:
  211. if !ok {
  212. return &antigravityStreamResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: cw.Disconnected()}
  213. }
  214. if ev.err != nil {
  215. if disconnect, handled := handleStreamReadError(ev.err, cw.Disconnected(), "antigravity upstream"); handled {
  216. return &antigravityStreamResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: disconnect}
  217. }
  218. logger.LegacyPrintf("service.antigravity_gateway", "Stream read error (antigravity upstream): %v", ev.err)
  219. return &antigravityStreamResult{usage: usage, firstTokenMs: firstTokenMs}
  220. }
  221. lastDataAt = time.Now()
  222. line := ev.line
  223. if data, ok := extractAnthropicSSEDataLine(line); ok {
  224. upstreamResponseModelObserverFromContext(c).ObserveAnthropic([]byte(strings.TrimSpace(data)))
  225. }
  226. // 记录首 token 时间
  227. if firstTokenMs == nil && len(line) > 0 {
  228. ms := int(time.Since(startTime).Milliseconds())
  229. firstTokenMs = &ms
  230. }
  231. // 尝试从 message_delta 或 message_stop 事件提取 usage
  232. s.extractSSEUsage(line, usage)
  233. // 透传行
  234. cw.Fprintf("%s\n", line)
  235. case <-intervalCh:
  236. lastRead := time.Unix(0, atomic.LoadInt64(&lastReadAt))
  237. if time.Since(lastRead) < streamInterval {
  238. continue
  239. }
  240. if cw.Disconnected() {
  241. logger.LegacyPrintf("service.antigravity_gateway", "Upstream timeout after client disconnect (antigravity upstream), returning collected usage")
  242. return &antigravityStreamResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}
  243. }
  244. logger.LegacyPrintf("service.antigravity_gateway", "Stream data interval timeout (antigravity upstream)")
  245. return &antigravityStreamResult{usage: usage, firstTokenMs: firstTokenMs}
  246. case <-keepaliveCh:
  247. if cw.Disconnected() {
  248. continue
  249. }
  250. if time.Since(lastDataAt) < keepaliveInterval {
  251. continue
  252. }
  253. // SSE ping 事件:Anthropic 原生格式,客户端会正确处理,
  254. // 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开
  255. if !cw.Fprintf("event: ping\ndata: {\"type\": \"ping\"}\n\n") {
  256. logger.LegacyPrintf("service.antigravity_gateway", "Client disconnected during keepalive ping (antigravity upstream), continuing to drain upstream for billing")
  257. continue
  258. }
  259. }
  260. }
  261. }
  262. // extractSSEUsage 从 SSE data 行中提取 Claude usage(用于流式透传场景)
  263. //
  264. // Anthropic streaming 的 usage 字段分布在两类事件中:
  265. // - message_start:嵌套在 event.message.usage(input_tokens、cache_creation_input_tokens、
  266. // cache_read_input_tokens 等输入侧字段)
  267. // - message_delta:位于顶层 event.usage(流结束时的最终 output_tokens)
  268. //
  269. // 仅读取顶层 event.usage 会漏掉 message_start 的输入侧字段,导致流式透传请求落库的
  270. // usage_logs 记录 input_tokens=0。
  271. func (s *AntigravityGatewayService) extractSSEUsage(line string, usage *ClaudeUsage) {
  272. if !strings.HasPrefix(line, "data: ") {
  273. return
  274. }
  275. dataStr := strings.TrimPrefix(line, "data: ")
  276. var event map[string]any
  277. if json.Unmarshal([]byte(dataStr), &event) != nil {
  278. return
  279. }
  280. var u map[string]any
  281. if eventType, _ := event["type"].(string); eventType == "message_start" {
  282. if msg, ok := event["message"].(map[string]any); ok {
  283. u, _ = msg["usage"].(map[string]any)
  284. }
  285. } else {
  286. u, _ = event["usage"].(map[string]any)
  287. }
  288. if u == nil {
  289. return
  290. }
  291. if v, ok := u["input_tokens"].(float64); ok && int(v) > 0 {
  292. usage.InputTokens = int(v)
  293. }
  294. if v, ok := u["output_tokens"].(float64); ok && int(v) > 0 {
  295. usage.OutputTokens = int(v)
  296. }
  297. if v, ok := u["cache_read_input_tokens"].(float64); ok && int(v) > 0 {
  298. usage.CacheReadInputTokens = int(v)
  299. }
  300. if v, ok := u["cache_creation_input_tokens"].(float64); ok && int(v) > 0 {
  301. usage.CacheCreationInputTokens = int(v)
  302. }
  303. // 解析嵌套的 cache_creation 对象中的 5m/1h 明细
  304. if cc, ok := u["cache_creation"].(map[string]any); ok {
  305. if v, ok := cc["ephemeral_5m_input_tokens"].(float64); ok {
  306. usage.CacheCreation5mTokens = int(v)
  307. }
  308. if v, ok := cc["ephemeral_1h_input_tokens"].(float64); ok {
  309. usage.CacheCreation1hTokens = int(v)
  310. }
  311. }
  312. }
  313. // extractClaudeUsage 从非流式 Claude 响应提取 usage
  314. func (s *AntigravityGatewayService) extractClaudeUsage(body []byte) *ClaudeUsage {
  315. usage := &ClaudeUsage{}
  316. var resp map[string]any
  317. if json.Unmarshal(body, &resp) != nil {
  318. return usage
  319. }
  320. if u, ok := resp["usage"].(map[string]any); ok {
  321. if v, ok := u["input_tokens"].(float64); ok {
  322. usage.InputTokens = int(v)
  323. }
  324. if v, ok := u["output_tokens"].(float64); ok {
  325. usage.OutputTokens = int(v)
  326. }
  327. if v, ok := u["cache_read_input_tokens"].(float64); ok {
  328. usage.CacheReadInputTokens = int(v)
  329. }
  330. if v, ok := u["cache_creation_input_tokens"].(float64); ok {
  331. usage.CacheCreationInputTokens = int(v)
  332. }
  333. // 解析嵌套的 cache_creation 对象中的 5m/1h 明细
  334. if cc, ok := u["cache_creation"].(map[string]any); ok {
  335. if v, ok := cc["ephemeral_5m_input_tokens"].(float64); ok {
  336. usage.CacheCreation5mTokens = int(v)
  337. }
  338. if v, ok := cc["ephemeral_1h_input_tokens"].(float64); ok {
  339. usage.CacheCreation1hTokens = int(v)
  340. }
  341. }
  342. }
  343. return usage
  344. }