gateway_service_streaming_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. package service
  2. import (
  3. "context"
  4. "io"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. "github.com/gin-gonic/gin"
  11. "github.com/stretchr/testify/require"
  12. )
  13. type upstreamContextTestKey string
  14. func newStreamingResponseTestGatewayService() *GatewayService {
  15. return &GatewayService{
  16. cfg: &config.Config{
  17. Gateway: config.GatewayConfig{
  18. StreamDataIntervalTimeout: 0,
  19. MaxLineSize: defaultMaxLineSize,
  20. },
  21. },
  22. rateLimitService: &RateLimitService{},
  23. }
  24. }
  25. func TestGatewayService_StreamingReusesScannerBufferAndStillParsesUsage(t *testing.T) {
  26. gin.SetMode(gin.TestMode)
  27. svc := newStreamingResponseTestGatewayService()
  28. rec := httptest.NewRecorder()
  29. c, _ := gin.CreateTestContext(rec)
  30. c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
  31. pr, pw := io.Pipe()
  32. resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}, Body: pr}
  33. go func() {
  34. defer func() { _ = pw.Close() }()
  35. // Minimal SSE event to trigger parseSSEUsage
  36. _, _ = pw.Write([]byte("data: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":3}}}\n\n"))
  37. _, _ = pw.Write([]byte("data: {\"type\":\"message_delta\",\"usage\":{\"output_tokens\":7}}\n\n"))
  38. _, _ = pw.Write([]byte("data: [DONE]\n\n"))
  39. }()
  40. result, err := svc.handleStreamingResponse(context.Background(), resp, c, &Account{ID: 1}, time.Now(), "model", "model", false)
  41. _ = pr.Close()
  42. require.NoError(t, err)
  43. require.NotNil(t, result)
  44. require.NotNil(t, result.usage)
  45. require.Equal(t, 3, result.usage.InputTokens)
  46. require.Equal(t, 7, result.usage.OutputTokens)
  47. }
  48. func TestGatewayService_StreamingKeepaliveUsesIdleTimer(t *testing.T) {
  49. gin.SetMode(gin.TestMode)
  50. svc := newStreamingResponseTestGatewayService()
  51. svc.cfg.Gateway.StreamKeepaliveInterval = 1
  52. rec := httptest.NewRecorder()
  53. c, _ := gin.CreateTestContext(rec)
  54. c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
  55. pr, pw := io.Pipe()
  56. resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}, Body: pr}
  57. go func() {
  58. defer func() { _ = pw.Close() }()
  59. _, _ = pw.Write([]byte("data: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":1}}}\n\n"))
  60. time.Sleep(1100 * time.Millisecond)
  61. _, _ = pw.Write([]byte("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"))
  62. }()
  63. result, err := svc.handleStreamingResponse(context.Background(), resp, c, &Account{ID: 1}, time.Now(), "model", "model", false)
  64. _ = pr.Close()
  65. require.NoError(t, err)
  66. require.NotNil(t, result)
  67. require.Contains(t, rec.Body.String(), "event: ping")
  68. }
  69. func TestGatewayService_StreamingKeepaliveUsesNoopDeltaForAffectedClaudeCodeVersion(t *testing.T) {
  70. gin.SetMode(gin.TestMode)
  71. svc := newStreamingResponseTestGatewayService()
  72. svc.cfg.Gateway.StreamKeepaliveInterval = 1
  73. rec := httptest.NewRecorder()
  74. c, _ := gin.CreateTestContext(rec)
  75. c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
  76. c.Request.Header.Set("User-Agent", "claude-cli/2.1.198 (external, cli)")
  77. pr, pw := io.Pipe()
  78. resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}, Body: pr}
  79. go func() {
  80. defer func() { _ = pw.Close() }()
  81. _, _ = pw.Write([]byte("event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":1}}}\n\n"))
  82. _, _ = pw.Write([]byte("event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n"))
  83. time.Sleep(1100 * time.Millisecond)
  84. _, _ = pw.Write([]byte("event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n"))
  85. _, _ = pw.Write([]byte("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"))
  86. }()
  87. result, err := svc.handleStreamingResponse(context.Background(), resp, c, &Account{ID: 1}, time.Now(), "model", "model", false)
  88. _ = pr.Close()
  89. require.NoError(t, err)
  90. require.NotNil(t, result)
  91. body := rec.Body.String()
  92. require.Contains(t, body, "event: content_block_delta")
  93. require.Contains(t, body, `"delta":{"type":"text_delta","text":""}`)
  94. }
  95. func TestGatewayService_StreamingKeepaliveUsesNoopDeltaDuringToolUseForAffectedClaudeCodeVersion(t *testing.T) {
  96. gin.SetMode(gin.TestMode)
  97. svc := newStreamingResponseTestGatewayService()
  98. svc.cfg.Gateway.StreamKeepaliveInterval = 1
  99. rec := httptest.NewRecorder()
  100. c, _ := gin.CreateTestContext(rec)
  101. c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
  102. c.Request.Header.Set("User-Agent", "claude-cli/2.1.198 (external, cli)")
  103. pr, pw := io.Pipe()
  104. resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}, Body: pr}
  105. go func() {
  106. defer func() { _ = pw.Close() }()
  107. _, _ = pw.Write([]byte("event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":1}}}\n\n"))
  108. _, _ = pw.Write([]byte("event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_1\",\"name\":\"Edit\",\"input\":{}}}\n\n"))
  109. time.Sleep(1100 * time.Millisecond)
  110. _, _ = pw.Write([]byte("event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\n"))
  111. _, _ = pw.Write([]byte("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"))
  112. }()
  113. result, err := svc.handleStreamingResponse(context.Background(), resp, c, &Account{ID: 1}, time.Now(), "model", "model", false)
  114. _ = pr.Close()
  115. require.NoError(t, err)
  116. require.NotNil(t, result)
  117. body := rec.Body.String()
  118. require.Contains(t, body, "event: content_block_delta")
  119. require.Contains(t, body, `"index":1`)
  120. require.Contains(t, body, `"delta":{"type":"input_json_delta","partial_json":""}`)
  121. }
  122. func TestGatewayService_StreamingKeepaliveKeepsPingForOlderClaudeCodeVersion(t *testing.T) {
  123. gin.SetMode(gin.TestMode)
  124. svc := newStreamingResponseTestGatewayService()
  125. svc.cfg.Gateway.StreamKeepaliveInterval = 1
  126. rec := httptest.NewRecorder()
  127. c, _ := gin.CreateTestContext(rec)
  128. c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
  129. c.Request.Header.Set("User-Agent", "claude-cli/2.1.187 (external, cli)")
  130. pr, pw := io.Pipe()
  131. resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}, Body: pr}
  132. go func() {
  133. defer func() { _ = pw.Close() }()
  134. _, _ = pw.Write([]byte("event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":1}}}\n\n"))
  135. _, _ = pw.Write([]byte("event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n"))
  136. time.Sleep(1100 * time.Millisecond)
  137. _, _ = pw.Write([]byte("event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n"))
  138. _, _ = pw.Write([]byte("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"))
  139. }()
  140. result, err := svc.handleStreamingResponse(context.Background(), resp, c, &Account{ID: 1}, time.Now(), "model", "model", false)
  141. _ = pr.Close()
  142. require.NoError(t, err)
  143. require.NotNil(t, result)
  144. body := rec.Body.String()
  145. require.Contains(t, body, "event: ping")
  146. require.NotContains(t, body, `"delta":{"type":"text_delta","text":""}`)
  147. }
  148. func TestDetachUpstreamContextIgnoresClientCancel(t *testing.T) {
  149. parent, cancel := context.WithCancel(context.WithValue(context.Background(), upstreamContextTestKey("test-key"), "test-value"))
  150. upstreamCtx, release := detachUpstreamContext(parent)
  151. defer release()
  152. cancel()
  153. require.NoError(t, upstreamCtx.Err())
  154. require.Equal(t, "test-value", upstreamCtx.Value(upstreamContextTestKey("test-key")))
  155. }