Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

540 строки
14 KiB

  1. package helper
  2. import (
  3. "fmt"
  4. "io"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "sync"
  9. "sync/atomic"
  10. "testing"
  11. "time"
  12. "github.com/QuantumNous/new-api/constant"
  13. relaycommon "github.com/QuantumNous/new-api/relay/common"
  14. "github.com/QuantumNous/new-api/setting/operation_setting"
  15. "github.com/gin-gonic/gin"
  16. "github.com/stretchr/testify/assert"
  17. "github.com/stretchr/testify/require"
  18. )
  19. func init() {
  20. gin.SetMode(gin.TestMode)
  21. }
  22. func setupStreamTest(t *testing.T, body io.Reader) (*gin.Context, *http.Response, *relaycommon.RelayInfo) {
  23. t.Helper()
  24. oldTimeout := constant.StreamingTimeout
  25. constant.StreamingTimeout = 30
  26. t.Cleanup(func() {
  27. constant.StreamingTimeout = oldTimeout
  28. })
  29. recorder := httptest.NewRecorder()
  30. c, _ := gin.CreateTestContext(recorder)
  31. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  32. resp := &http.Response{
  33. Body: io.NopCloser(body),
  34. }
  35. info := &relaycommon.RelayInfo{
  36. ChannelMeta: &relaycommon.ChannelMeta{},
  37. }
  38. return c, resp, info
  39. }
  40. func buildSSEBody(n int) string {
  41. var b strings.Builder
  42. for i := 0; i < n; i++ {
  43. fmt.Fprintf(&b, "data: {\"id\":%d,\"choices\":[{\"delta\":{\"content\":\"token_%d\"}}]}\n", i, i)
  44. }
  45. b.WriteString("data: [DONE]\n")
  46. return b.String()
  47. }
  48. // slowReader wraps a reader and injects a delay before each Read call,
  49. // simulating a slow upstream that trickles data.
  50. type slowReader struct {
  51. r io.Reader
  52. delay time.Duration
  53. }
  54. func (s *slowReader) Read(p []byte) (int, error) {
  55. time.Sleep(s.delay)
  56. return s.r.Read(p)
  57. }
  58. // ---------- Basic correctness ----------
  59. func TestStreamScannerHandler_NilInputs(t *testing.T) {
  60. t.Parallel()
  61. recorder := httptest.NewRecorder()
  62. c, _ := gin.CreateTestContext(recorder)
  63. c.Request = httptest.NewRequest(http.MethodPost, "/", nil)
  64. info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{}}
  65. StreamScannerHandler(c, nil, info, func(data string) bool { return true })
  66. StreamScannerHandler(c, &http.Response{Body: io.NopCloser(strings.NewReader(""))}, info, nil)
  67. }
  68. func TestStreamScannerHandler_EmptyBody(t *testing.T) {
  69. t.Parallel()
  70. c, resp, info := setupStreamTest(t, strings.NewReader(""))
  71. var called atomic.Bool
  72. StreamScannerHandler(c, resp, info, func(data string) bool {
  73. called.Store(true)
  74. return true
  75. })
  76. assert.False(t, called.Load(), "handler should not be called for empty body")
  77. }
  78. func TestStreamScannerHandler_NonPositiveStreamingTimeoutDoesNotPanic(t *testing.T) {
  79. oldTimeout := constant.StreamingTimeout
  80. constant.StreamingTimeout = 0
  81. t.Cleanup(func() {
  82. constant.StreamingTimeout = oldTimeout
  83. })
  84. recorder := httptest.NewRecorder()
  85. c, _ := gin.CreateTestContext(recorder)
  86. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  87. resp := &http.Response{Body: io.NopCloser(strings.NewReader(""))}
  88. info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{}}
  89. require.NotPanics(t, func() {
  90. StreamScannerHandler(c, resp, info, func(data string) bool { return true })
  91. })
  92. }
  93. func TestStreamScannerHandler_1000Chunks(t *testing.T) {
  94. t.Parallel()
  95. const numChunks = 1000
  96. body := buildSSEBody(numChunks)
  97. c, resp, info := setupStreamTest(t, strings.NewReader(body))
  98. var count atomic.Int64
  99. StreamScannerHandler(c, resp, info, func(data string) bool {
  100. count.Add(1)
  101. return true
  102. })
  103. assert.Equal(t, int64(numChunks), count.Load())
  104. assert.Equal(t, numChunks, info.ReceivedResponseCount)
  105. }
  106. func TestStreamScannerHandler_10000Chunks(t *testing.T) {
  107. t.Parallel()
  108. const numChunks = 10000
  109. body := buildSSEBody(numChunks)
  110. c, resp, info := setupStreamTest(t, strings.NewReader(body))
  111. var count atomic.Int64
  112. start := time.Now()
  113. StreamScannerHandler(c, resp, info, func(data string) bool {
  114. count.Add(1)
  115. return true
  116. })
  117. elapsed := time.Since(start)
  118. assert.Equal(t, int64(numChunks), count.Load())
  119. assert.Equal(t, numChunks, info.ReceivedResponseCount)
  120. t.Logf("10000 chunks processed in %v", elapsed)
  121. }
  122. func TestStreamScannerHandler_OrderPreserved(t *testing.T) {
  123. t.Parallel()
  124. const numChunks = 500
  125. body := buildSSEBody(numChunks)
  126. c, resp, info := setupStreamTest(t, strings.NewReader(body))
  127. var mu sync.Mutex
  128. received := make([]string, 0, numChunks)
  129. StreamScannerHandler(c, resp, info, func(data string) bool {
  130. mu.Lock()
  131. received = append(received, data)
  132. mu.Unlock()
  133. return true
  134. })
  135. require.Equal(t, numChunks, len(received))
  136. for i := 0; i < numChunks; i++ {
  137. expected := fmt.Sprintf("{\"id\":%d,\"choices\":[{\"delta\":{\"content\":\"token_%d\"}}]}", i, i)
  138. assert.Equal(t, expected, received[i], "chunk %d out of order", i)
  139. }
  140. }
  141. func TestStreamScannerHandler_DoneStopsScanner(t *testing.T) {
  142. t.Parallel()
  143. body := buildSSEBody(50) + "data: should_not_appear\n"
  144. c, resp, info := setupStreamTest(t, strings.NewReader(body))
  145. var count atomic.Int64
  146. StreamScannerHandler(c, resp, info, func(data string) bool {
  147. count.Add(1)
  148. return true
  149. })
  150. assert.Equal(t, int64(50), count.Load(), "data after [DONE] must not be processed")
  151. }
  152. func TestStreamScannerHandler_HandlerFailureStops(t *testing.T) {
  153. t.Parallel()
  154. const numChunks = 200
  155. body := buildSSEBody(numChunks)
  156. c, resp, info := setupStreamTest(t, strings.NewReader(body))
  157. const failAt = 50
  158. var count atomic.Int64
  159. StreamScannerHandler(c, resp, info, func(data string) bool {
  160. n := count.Add(1)
  161. return n < failAt
  162. })
  163. // The worker stops at failAt; the scanner may have read ahead,
  164. // but the handler should not be called beyond failAt.
  165. assert.Equal(t, int64(failAt), count.Load())
  166. }
  167. func TestStreamScannerHandler_SkipsNonDataLines(t *testing.T) {
  168. t.Parallel()
  169. var b strings.Builder
  170. b.WriteString(": comment line\n")
  171. b.WriteString("event: message\n")
  172. b.WriteString("id: 12345\n")
  173. b.WriteString("retry: 5000\n")
  174. for i := 0; i < 100; i++ {
  175. fmt.Fprintf(&b, "data: payload_%d\n", i)
  176. b.WriteString(": interleaved comment\n")
  177. }
  178. b.WriteString("data: [DONE]\n")
  179. c, resp, info := setupStreamTest(t, strings.NewReader(b.String()))
  180. var count atomic.Int64
  181. StreamScannerHandler(c, resp, info, func(data string) bool {
  182. count.Add(1)
  183. return true
  184. })
  185. assert.Equal(t, int64(100), count.Load())
  186. }
  187. func TestStreamScannerHandler_DataWithExtraSpaces(t *testing.T) {
  188. t.Parallel()
  189. body := "data: {\"trimmed\":true} \ndata: [DONE]\n"
  190. c, resp, info := setupStreamTest(t, strings.NewReader(body))
  191. var got string
  192. StreamScannerHandler(c, resp, info, func(data string) bool {
  193. got = data
  194. return true
  195. })
  196. assert.Equal(t, "{\"trimmed\":true}", got)
  197. }
  198. // ---------- Decoupling: scanner not blocked by slow handler ----------
  199. func TestStreamScannerHandler_ScannerDecoupledFromSlowHandler(t *testing.T) {
  200. t.Parallel()
  201. // Strategy: use a slow upstream (io.Pipe, 10ms per chunk) AND a slow handler (20ms per chunk).
  202. // If the scanner were synchronously coupled to the handler, total time would be
  203. // ~numChunks * (10ms + 20ms) = 30ms * 50 = 1500ms.
  204. // With decoupling, total time should be closer to
  205. // ~numChunks * max(10ms, 20ms) = 20ms * 50 = 1000ms
  206. // because the scanner reads ahead into the buffer while the handler processes.
  207. const numChunks = 50
  208. const upstreamDelay = 10 * time.Millisecond
  209. const handlerDelay = 20 * time.Millisecond
  210. pr, pw := io.Pipe()
  211. go func() {
  212. defer pw.Close()
  213. for i := 0; i < numChunks; i++ {
  214. fmt.Fprintf(pw, "data: {\"id\":%d}\n", i)
  215. time.Sleep(upstreamDelay)
  216. }
  217. fmt.Fprint(pw, "data: [DONE]\n")
  218. }()
  219. recorder := httptest.NewRecorder()
  220. c, _ := gin.CreateTestContext(recorder)
  221. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  222. oldTimeout := constant.StreamingTimeout
  223. constant.StreamingTimeout = 30
  224. t.Cleanup(func() { constant.StreamingTimeout = oldTimeout })
  225. resp := &http.Response{Body: pr}
  226. info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{}}
  227. var count atomic.Int64
  228. start := time.Now()
  229. done := make(chan struct{})
  230. go func() {
  231. StreamScannerHandler(c, resp, info, func(data string) bool {
  232. time.Sleep(handlerDelay)
  233. count.Add(1)
  234. return true
  235. })
  236. close(done)
  237. }()
  238. select {
  239. case <-done:
  240. case <-time.After(15 * time.Second):
  241. t.Fatal("StreamScannerHandler did not complete in time")
  242. }
  243. elapsed := time.Since(start)
  244. assert.Equal(t, int64(numChunks), count.Load())
  245. coupledTime := time.Duration(numChunks) * (upstreamDelay + handlerDelay)
  246. t.Logf("elapsed=%v, coupled_estimate=%v", elapsed, coupledTime)
  247. // If decoupled, elapsed should be well under the coupled estimate.
  248. assert.Less(t, elapsed, coupledTime*85/100,
  249. "decoupled elapsed time (%v) should be significantly less than coupled estimate (%v)", elapsed, coupledTime)
  250. }
  251. func TestStreamScannerHandler_SlowUpstreamFastHandler(t *testing.T) {
  252. t.Parallel()
  253. const numChunks = 50
  254. body := buildSSEBody(numChunks)
  255. reader := &slowReader{r: strings.NewReader(body), delay: 2 * time.Millisecond}
  256. c, resp, info := setupStreamTest(t, reader)
  257. var count atomic.Int64
  258. start := time.Now()
  259. done := make(chan struct{})
  260. go func() {
  261. StreamScannerHandler(c, resp, info, func(data string) bool {
  262. count.Add(1)
  263. return true
  264. })
  265. close(done)
  266. }()
  267. select {
  268. case <-done:
  269. case <-time.After(15 * time.Second):
  270. t.Fatal("timed out with slow upstream")
  271. }
  272. elapsed := time.Since(start)
  273. assert.Equal(t, int64(numChunks), count.Load())
  274. t.Logf("slow upstream (%d chunks, 2ms/read): %v", numChunks, elapsed)
  275. }
  276. // ---------- Ping tests ----------
  277. func TestStreamScannerHandler_PingSentDuringSlowUpstream(t *testing.T) {
  278. t.Parallel()
  279. setting := operation_setting.GetGeneralSetting()
  280. oldEnabled := setting.PingIntervalEnabled
  281. oldSeconds := setting.PingIntervalSeconds
  282. setting.PingIntervalEnabled = true
  283. setting.PingIntervalSeconds = 1
  284. t.Cleanup(func() {
  285. setting.PingIntervalEnabled = oldEnabled
  286. setting.PingIntervalSeconds = oldSeconds
  287. })
  288. // Create a reader that delivers data slowly: one chunk every 500ms over 3.5 seconds.
  289. // The ping interval is 1s, so we should see at least 2 pings.
  290. pr, pw := io.Pipe()
  291. go func() {
  292. defer pw.Close()
  293. for i := 0; i < 7; i++ {
  294. fmt.Fprintf(pw, "data: chunk_%d\n", i)
  295. time.Sleep(500 * time.Millisecond)
  296. }
  297. fmt.Fprint(pw, "data: [DONE]\n")
  298. }()
  299. recorder := httptest.NewRecorder()
  300. c, _ := gin.CreateTestContext(recorder)
  301. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  302. oldTimeout := constant.StreamingTimeout
  303. constant.StreamingTimeout = 30
  304. t.Cleanup(func() {
  305. constant.StreamingTimeout = oldTimeout
  306. })
  307. resp := &http.Response{Body: pr}
  308. info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{}}
  309. var count atomic.Int64
  310. done := make(chan struct{})
  311. go func() {
  312. StreamScannerHandler(c, resp, info, func(data string) bool {
  313. count.Add(1)
  314. return true
  315. })
  316. close(done)
  317. }()
  318. select {
  319. case <-done:
  320. case <-time.After(15 * time.Second):
  321. t.Fatal("timed out waiting for stream to finish")
  322. }
  323. assert.Equal(t, int64(7), count.Load())
  324. body := recorder.Body.String()
  325. pingCount := strings.Count(body, ": PING")
  326. t.Logf("received %d pings in response body", pingCount)
  327. assert.GreaterOrEqual(t, pingCount, 2,
  328. "expected at least 2 pings during 3.5s stream with 1s interval; got %d", pingCount)
  329. }
  330. func TestStreamScannerHandler_PingDisabledByRelayInfo(t *testing.T) {
  331. t.Parallel()
  332. setting := operation_setting.GetGeneralSetting()
  333. oldEnabled := setting.PingIntervalEnabled
  334. oldSeconds := setting.PingIntervalSeconds
  335. setting.PingIntervalEnabled = true
  336. setting.PingIntervalSeconds = 1
  337. t.Cleanup(func() {
  338. setting.PingIntervalEnabled = oldEnabled
  339. setting.PingIntervalSeconds = oldSeconds
  340. })
  341. pr, pw := io.Pipe()
  342. go func() {
  343. defer pw.Close()
  344. for i := 0; i < 5; i++ {
  345. fmt.Fprintf(pw, "data: chunk_%d\n", i)
  346. time.Sleep(500 * time.Millisecond)
  347. }
  348. fmt.Fprint(pw, "data: [DONE]\n")
  349. }()
  350. recorder := httptest.NewRecorder()
  351. c, _ := gin.CreateTestContext(recorder)
  352. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  353. oldTimeout := constant.StreamingTimeout
  354. constant.StreamingTimeout = 30
  355. t.Cleanup(func() {
  356. constant.StreamingTimeout = oldTimeout
  357. })
  358. resp := &http.Response{Body: pr}
  359. info := &relaycommon.RelayInfo{
  360. DisablePing: true,
  361. ChannelMeta: &relaycommon.ChannelMeta{},
  362. }
  363. var count atomic.Int64
  364. done := make(chan struct{})
  365. go func() {
  366. StreamScannerHandler(c, resp, info, func(data string) bool {
  367. count.Add(1)
  368. return true
  369. })
  370. close(done)
  371. }()
  372. select {
  373. case <-done:
  374. case <-time.After(15 * time.Second):
  375. t.Fatal("timed out")
  376. }
  377. assert.Equal(t, int64(5), count.Load())
  378. body := recorder.Body.String()
  379. pingCount := strings.Count(body, ": PING")
  380. assert.Equal(t, 0, pingCount, "pings should be disabled when DisablePing=true")
  381. }
  382. func TestStreamScannerHandler_PingInterleavesWithSlowUpstream(t *testing.T) {
  383. t.Parallel()
  384. setting := operation_setting.GetGeneralSetting()
  385. oldEnabled := setting.PingIntervalEnabled
  386. oldSeconds := setting.PingIntervalSeconds
  387. setting.PingIntervalEnabled = true
  388. setting.PingIntervalSeconds = 1
  389. t.Cleanup(func() {
  390. setting.PingIntervalEnabled = oldEnabled
  391. setting.PingIntervalSeconds = oldSeconds
  392. })
  393. // Slow upstream + slow handler. Total stream takes ~5 seconds.
  394. // The ping goroutine stays alive as long as the scanner is reading,
  395. // so pings should fire between data writes.
  396. pr, pw := io.Pipe()
  397. go func() {
  398. defer pw.Close()
  399. for i := 0; i < 10; i++ {
  400. fmt.Fprintf(pw, "data: chunk_%d\n", i)
  401. time.Sleep(500 * time.Millisecond)
  402. }
  403. fmt.Fprint(pw, "data: [DONE]\n")
  404. }()
  405. recorder := httptest.NewRecorder()
  406. c, _ := gin.CreateTestContext(recorder)
  407. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  408. oldTimeout := constant.StreamingTimeout
  409. constant.StreamingTimeout = 30
  410. t.Cleanup(func() {
  411. constant.StreamingTimeout = oldTimeout
  412. })
  413. resp := &http.Response{Body: pr}
  414. info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{}}
  415. var count atomic.Int64
  416. done := make(chan struct{})
  417. go func() {
  418. StreamScannerHandler(c, resp, info, func(data string) bool {
  419. count.Add(1)
  420. return true
  421. })
  422. close(done)
  423. }()
  424. select {
  425. case <-done:
  426. case <-time.After(15 * time.Second):
  427. t.Fatal("timed out")
  428. }
  429. assert.Equal(t, int64(10), count.Load())
  430. body := recorder.Body.String()
  431. pingCount := strings.Count(body, ": PING")
  432. t.Logf("received %d pings interleaved with 10 chunks over 5s", pingCount)
  433. assert.GreaterOrEqual(t, pingCount, 3,
  434. "expected at least 3 pings during 5s stream with 1s ping interval; got %d", pingCount)
  435. }