antigravity_gateway_compat_stream.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. package service
  2. import (
  3. "bufio"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "strings"
  10. "time"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  14. "github.com/gin-gonic/gin"
  15. )
  16. type antigravityCompatStreamAdapter interface {
  17. Emit(*apicompat.AnthropicStreamEvent, *antigravityClientWriter)
  18. Finalize(*antigravityClientWriter)
  19. WriteError(*antigravityClientWriter, string)
  20. }
  21. type antigravityChatStreamAdapter struct {
  22. anthropicState *apicompat.AnthropicEventToResponsesState
  23. chatState *apicompat.ResponsesEventToChatState
  24. }
  25. func newAntigravityChatStreamAdapter(model string, includeUsage bool) *antigravityChatStreamAdapter {
  26. anthropicState := apicompat.NewAnthropicEventToResponsesState()
  27. anthropicState.Model = model
  28. chatState := apicompat.NewResponsesEventToChatState()
  29. chatState.Model = model
  30. chatState.IncludeUsage = includeUsage
  31. return &antigravityChatStreamAdapter{
  32. anthropicState: anthropicState,
  33. chatState: chatState,
  34. }
  35. }
  36. func (a *antigravityChatStreamAdapter) Emit(event *apicompat.AnthropicStreamEvent, writer *antigravityClientWriter) {
  37. for _, responseEvent := range apicompat.AnthropicEventToResponsesEvents(event, a.anthropicState) {
  38. a.emitResponseEvent(&responseEvent, writer)
  39. }
  40. }
  41. func (a *antigravityChatStreamAdapter) Finalize(writer *antigravityClientWriter) {
  42. for _, responseEvent := range apicompat.FinalizeAnthropicResponsesStream(a.anthropicState) {
  43. a.emitResponseEvent(&responseEvent, writer)
  44. }
  45. for _, chunk := range apicompat.FinalizeResponsesChatStream(a.chatState) {
  46. if data, err := apicompat.ChatChunkToSSE(chunk); err == nil {
  47. writer.Write([]byte(data))
  48. }
  49. }
  50. writer.Write([]byte("data: [DONE]\n\n"))
  51. }
  52. func (a *antigravityChatStreamAdapter) WriteError(writer *antigravityClientWriter, reason string) {
  53. writer.Fprintf("data: {\"error\":{\"message\":%q,\"type\":\"upstream_error\"}}\n\n", reason)
  54. }
  55. func (a *antigravityChatStreamAdapter) emitResponseEvent(event *apicompat.ResponsesStreamEvent, writer *antigravityClientWriter) {
  56. for _, chunk := range apicompat.ResponsesEventToChatChunks(event, a.chatState) {
  57. if data, err := apicompat.ChatChunkToSSE(chunk); err == nil {
  58. writer.Write([]byte(data))
  59. }
  60. }
  61. }
  62. type antigravityResponsesStreamAdapter struct {
  63. anthropicState *apicompat.AnthropicEventToResponsesState
  64. }
  65. func newAntigravityResponsesStreamAdapter(model string) *antigravityResponsesStreamAdapter {
  66. state := apicompat.NewAnthropicEventToResponsesState()
  67. state.Model = model
  68. return &antigravityResponsesStreamAdapter{anthropicState: state}
  69. }
  70. func (a *antigravityResponsesStreamAdapter) Emit(event *apicompat.AnthropicStreamEvent, writer *antigravityClientWriter) {
  71. for _, responseEvent := range apicompat.AnthropicEventToResponsesEvents(event, a.anthropicState) {
  72. a.emitResponseEvent(responseEvent, writer)
  73. }
  74. }
  75. func (a *antigravityResponsesStreamAdapter) Finalize(writer *antigravityClientWriter) {
  76. for _, responseEvent := range apicompat.FinalizeAnthropicResponsesStream(a.anthropicState) {
  77. a.emitResponseEvent(responseEvent, writer)
  78. }
  79. }
  80. func (a *antigravityResponsesStreamAdapter) WriteError(writer *antigravityClientWriter, reason string) {
  81. writer.Fprintf("event: error\ndata: {\"type\":\"error\",\"error\":{\"type\":\"upstream_error\",\"message\":%q}}\n\n", reason)
  82. }
  83. func (a *antigravityResponsesStreamAdapter) emitResponseEvent(event apicompat.ResponsesStreamEvent, writer *antigravityClientWriter) {
  84. if data, err := apicompat.ResponsesEventToSSE(event); err == nil {
  85. writer.Write([]byte(data))
  86. }
  87. }
  88. type antigravityCompatScanEvent struct {
  89. line string
  90. err error
  91. }
  92. type antigravityCompatStreamSession struct {
  93. processor *antigravity.StreamingProcessor
  94. adapter antigravityCompatStreamAdapter
  95. writer *antigravityClientWriter
  96. usage *ClaudeUsage
  97. pendingEvents []apicompat.AnthropicStreamEvent
  98. firstTokenMs *int
  99. startTime time.Time
  100. meaningfulData bool
  101. }
  102. func newAntigravityCompatStreamSession(
  103. model string,
  104. startTime time.Time,
  105. adapter antigravityCompatStreamAdapter,
  106. writer *antigravityClientWriter,
  107. ) *antigravityCompatStreamSession {
  108. return &antigravityCompatStreamSession{
  109. processor: antigravity.NewStreamingProcessor(model),
  110. adapter: adapter,
  111. writer: writer,
  112. usage: &ClaudeUsage{},
  113. startTime: startTime,
  114. }
  115. }
  116. func (s *antigravityCompatStreamSession) consume(line string) {
  117. claudeEvents := s.processor.ProcessLine(strings.TrimRight(line, "\r\n"))
  118. if len(claudeEvents) == 0 {
  119. return
  120. }
  121. s.consumeClaudeEvents(claudeEvents)
  122. }
  123. func (s *antigravityCompatStreamSession) hasMeaningfulData() bool {
  124. return s.meaningfulData
  125. }
  126. func (s *antigravityCompatStreamSession) finish() *antigravityStreamResult {
  127. finalEvents, usage := s.processor.Finish()
  128. mergeAntigravityCompatUsage(s.usage, usage)
  129. s.consumeClaudeEvents(finalEvents)
  130. s.adapter.Finalize(s.writer)
  131. return s.result(s.writer.Disconnected())
  132. }
  133. func (s *antigravityCompatStreamSession) collectResult(clientDisconnect bool) *antigravityStreamResult {
  134. _, usage := s.processor.Finish()
  135. mergeAntigravityCompatUsage(s.usage, usage)
  136. return s.result(clientDisconnect)
  137. }
  138. func (s *antigravityCompatStreamSession) result(clientDisconnect bool) *antigravityStreamResult {
  139. return &antigravityStreamResult{
  140. usage: s.usage,
  141. firstTokenMs: s.firstTokenMs,
  142. clientDisconnect: clientDisconnect,
  143. }
  144. }
  145. func (s *antigravityCompatStreamSession) consumeClaudeEvents(data []byte) {
  146. var eventType string
  147. for _, line := range strings.Split(string(data), "\n") {
  148. line = strings.TrimSpace(line)
  149. switch {
  150. case strings.HasPrefix(line, "event:"):
  151. eventType = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
  152. case strings.HasPrefix(line, "data:"):
  153. s.consumeClaudeData(eventType, strings.TrimSpace(strings.TrimPrefix(line, "data:")))
  154. }
  155. }
  156. }
  157. func (s *antigravityCompatStreamSession) consumeClaudeData(eventType, payload string) {
  158. var event apicompat.AnthropicStreamEvent
  159. if json.Unmarshal([]byte(payload), &event) != nil {
  160. return
  161. }
  162. if event.Type == "" {
  163. event.Type = eventType
  164. }
  165. if event.Usage != nil {
  166. mergeAnthropicUsage(s.usage, *event.Usage)
  167. }
  168. if event.Message != nil {
  169. mergeAnthropicUsage(s.usage, event.Message.Usage)
  170. }
  171. s.emitOrBuffer(event)
  172. }
  173. func (s *antigravityCompatStreamSession) emitOrBuffer(event apicompat.AnthropicStreamEvent) {
  174. if s.meaningfulData {
  175. s.adapter.Emit(&event, s.writer)
  176. return
  177. }
  178. s.pendingEvents = append(s.pendingEvents, event)
  179. if !isMeaningfulAntigravityCompatEvent(&event) {
  180. return
  181. }
  182. s.meaningfulData = true
  183. ms := int(time.Since(s.startTime).Milliseconds())
  184. s.firstTokenMs = &ms
  185. for i := range s.pendingEvents {
  186. s.adapter.Emit(&s.pendingEvents[i], s.writer)
  187. }
  188. s.pendingEvents = nil
  189. }
  190. func isMeaningfulAntigravityCompatEvent(event *apicompat.AnthropicStreamEvent) bool {
  191. if event == nil {
  192. return false
  193. }
  194. if event.Type == "message_stop" {
  195. return true
  196. }
  197. if event.ContentBlock != nil {
  198. block := event.ContentBlock
  199. return block.Type == "tool_use" ||
  200. block.Text != "" ||
  201. block.Thinking != "" ||
  202. block.Signature != "" ||
  203. block.Source != nil
  204. }
  205. if event.Delta != nil {
  206. delta := event.Delta
  207. return delta.Text != "" ||
  208. delta.PartialJSON != "" ||
  209. delta.Thinking != "" ||
  210. delta.Signature != "" ||
  211. delta.StopReason != ""
  212. }
  213. return false
  214. }
  215. func mergeAntigravityCompatUsage(dst *ClaudeUsage, src *antigravity.ClaudeUsage) {
  216. if dst == nil || src == nil {
  217. return
  218. }
  219. dst.InputTokens = src.InputTokens
  220. dst.OutputTokens = src.OutputTokens
  221. dst.CacheCreationInputTokens = src.CacheCreationInputTokens
  222. dst.CacheReadInputTokens = src.CacheReadInputTokens
  223. dst.ImageOutputTokens = src.ImageOutputTokens
  224. }
  225. func (s *AntigravityGatewayService) handleAntigravityCompatStream(
  226. c *gin.Context,
  227. resp *http.Response,
  228. startTime time.Time,
  229. originalModel string,
  230. adapter antigravityCompatStreamAdapter,
  231. prefix string,
  232. ) (*antigravityStreamResult, error) {
  233. flusher, ok := c.Writer.(http.Flusher)
  234. if !ok {
  235. return nil, errors.New("streaming not supported")
  236. }
  237. writer := newAntigravityClientWriter(c.Writer, flusher, prefix)
  238. writer.beforeFirstWrite = func() {
  239. c.Header("Content-Type", "text/event-stream")
  240. c.Header("Cache-Control", "no-cache")
  241. c.Header("Connection", "keep-alive")
  242. c.Header("X-Accel-Buffering", "no")
  243. c.Status(http.StatusOK)
  244. }
  245. session := newAntigravityCompatStreamSession(originalModel, startTime, adapter, writer)
  246. events, stopScanner, maxLineSize := s.startAntigravityCompatScanner(resp.Body)
  247. defer stopScanner()
  248. timeout := s.antigravityCompatStreamTimeout()
  249. timeoutTimer, timeoutCh := newAntigravityCompatTimer(timeout)
  250. if timeoutTimer != nil {
  251. defer timeoutTimer.Stop()
  252. }
  253. keepaliveTicker, keepaliveCh := s.newAntigravityCompatKeepaliveTicker()
  254. if keepaliveTicker != nil {
  255. defer keepaliveTicker.Stop()
  256. }
  257. for {
  258. select {
  259. case event, open := <-events:
  260. if !open {
  261. if !session.hasMeaningfulData() && !writer.Disconnected() {
  262. return nil, antigravityCompatEmptyStreamError()
  263. }
  264. return session.finish(), nil
  265. }
  266. if event.err != nil {
  267. return s.handleAntigravityCompatReadError(c, session, event.err, maxLineSize, prefix)
  268. }
  269. resetAntigravityCompatTimer(timeoutTimer, timeout)
  270. s.observeAntigravityGeminiSSELine(c, event.line)
  271. session.consume(event.line)
  272. case <-timeoutCh:
  273. if writer.Disconnected() {
  274. return session.collectResult(true), nil
  275. }
  276. if !session.hasMeaningfulData() {
  277. return nil, antigravityCompatEmptyStreamError()
  278. }
  279. logger.LegacyPrintf("service.antigravity_gateway", "Stream data interval timeout (%s)", prefix)
  280. writeAntigravityCompatStreamError(c, adapter, writer, "stream_timeout")
  281. return session.collectResult(false), fmt.Errorf("stream data interval timeout")
  282. case <-keepaliveCh:
  283. if session.hasMeaningfulData() && !writer.Disconnected() {
  284. writer.Write([]byte(": ping\n\n"))
  285. }
  286. }
  287. }
  288. }
  289. func (s *AntigravityGatewayService) startAntigravityCompatScanner(
  290. body io.Reader,
  291. ) (<-chan antigravityCompatScanEvent, func(), int) {
  292. maxLineSize := defaultMaxLineSize
  293. if s.settingService != nil && s.settingService.cfg != nil && s.settingService.cfg.Gateway.MaxLineSize > 0 {
  294. maxLineSize = s.settingService.cfg.Gateway.MaxLineSize
  295. }
  296. scanner := bufio.NewScanner(body)
  297. scanBuf := getSSEScannerBuf64K()
  298. scanner.Buffer(scanBuf[:0], maxLineSize)
  299. events := make(chan antigravityCompatScanEvent, 16)
  300. done := make(chan struct{})
  301. go func() {
  302. defer putSSEScannerBuf64K(scanBuf)
  303. defer close(events)
  304. send := func(event antigravityCompatScanEvent) bool {
  305. select {
  306. case events <- event:
  307. return true
  308. case <-done:
  309. return false
  310. }
  311. }
  312. for scanner.Scan() {
  313. if !send(antigravityCompatScanEvent{line: scanner.Text()}) {
  314. return
  315. }
  316. }
  317. if err := scanner.Err(); err != nil {
  318. send(antigravityCompatScanEvent{err: err})
  319. }
  320. }()
  321. return events, func() { close(done) }, maxLineSize
  322. }
  323. func (s *AntigravityGatewayService) antigravityCompatStreamTimeout() time.Duration {
  324. if s.settingService == nil || s.settingService.cfg == nil {
  325. return 0
  326. }
  327. return time.Duration(s.settingService.cfg.Gateway.StreamDataIntervalTimeout) * time.Second
  328. }
  329. func (s *AntigravityGatewayService) newAntigravityCompatKeepaliveTicker() (*time.Ticker, <-chan time.Time) {
  330. if s.settingService == nil || s.settingService.cfg == nil {
  331. return nil, nil
  332. }
  333. interval := time.Duration(s.settingService.cfg.Gateway.StreamKeepaliveInterval) * time.Second
  334. if interval <= 0 {
  335. return nil, nil
  336. }
  337. ticker := time.NewTicker(interval)
  338. return ticker, ticker.C
  339. }
  340. func newAntigravityCompatTimer(timeout time.Duration) (*time.Timer, <-chan time.Time) {
  341. if timeout <= 0 {
  342. return nil, nil
  343. }
  344. timer := time.NewTimer(timeout)
  345. return timer, timer.C
  346. }
  347. func resetAntigravityCompatTimer(timer *time.Timer, timeout time.Duration) {
  348. if timer == nil {
  349. return
  350. }
  351. if !timer.Stop() {
  352. select {
  353. case <-timer.C:
  354. default:
  355. }
  356. }
  357. timer.Reset(timeout)
  358. }
  359. func (s *AntigravityGatewayService) handleAntigravityCompatReadError(
  360. c *gin.Context,
  361. session *antigravityCompatStreamSession,
  362. err error,
  363. maxLineSize int,
  364. prefix string,
  365. ) (*antigravityStreamResult, error) {
  366. if !session.hasMeaningfulData() && !session.writer.Disconnected() {
  367. return nil, antigravityCompatEmptyStreamError()
  368. }
  369. if disconnect, handled := handleStreamReadError(err, session.writer.Disconnected(), prefix); handled {
  370. return session.collectResult(disconnect), nil
  371. }
  372. if errors.Is(err, bufio.ErrTooLong) {
  373. logger.LegacyPrintf("service.antigravity_gateway", "SSE line too long (%s): max_size=%d error=%v", prefix, maxLineSize, err)
  374. writeAntigravityCompatStreamError(c, session.adapter, session.writer, "response_too_large")
  375. return session.result(false), err
  376. }
  377. writeAntigravityCompatStreamError(c, session.adapter, session.writer, "stream_read_error")
  378. return nil, fmt.Errorf("stream read error: %w", err)
  379. }
  380. func writeAntigravityCompatStreamError(
  381. c *gin.Context,
  382. adapter antigravityCompatStreamAdapter,
  383. writer *antigravityClientWriter,
  384. reason string,
  385. ) {
  386. adapter.WriteError(writer, reason)
  387. MarkResponseCommitted(c)
  388. }
  389. func antigravityCompatEmptyStreamError() error {
  390. logger.LegacyPrintf("service.antigravity_gateway", "Empty Antigravity compatibility stream, triggering failover")
  391. return &UpstreamFailoverError{
  392. StatusCode: http.StatusBadGateway,
  393. ResponseBody: []byte(`{"error":"empty stream response from upstream"}`),
  394. RetryableOnSameAccount: true,
  395. }
  396. }
  397. func (s *AntigravityGatewayService) handleChatCompletionsStreamingFromAntigravity(
  398. c *gin.Context,
  399. resp *http.Response,
  400. startTime time.Time,
  401. originalModel string,
  402. includeUsage bool,
  403. ) (*antigravityStreamResult, error) {
  404. return s.handleAntigravityCompatStream(
  405. c,
  406. resp,
  407. startTime,
  408. originalModel,
  409. newAntigravityChatStreamAdapter(originalModel, includeUsage),
  410. "antigravity chat completions stream",
  411. )
  412. }
  413. func (s *AntigravityGatewayService) handleResponsesStreamingFromAntigravity(
  414. c *gin.Context,
  415. resp *http.Response,
  416. startTime time.Time,
  417. originalModel string,
  418. ) (*antigravityStreamResult, error) {
  419. return s.handleAntigravityCompatStream(
  420. c,
  421. resp,
  422. startTime,
  423. originalModel,
  424. newAntigravityResponsesStreamAdapter(originalModel),
  425. "antigravity responses stream",
  426. )
  427. }