openai_passthrough_preoutput_keepalive_test.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. package service
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "strings"
  6. "testing"
  7. "time"
  8. "github.com/gin-gonic/gin"
  9. "github.com/stretchr/testify/require"
  10. )
  11. // 这一组守住的是「首个可见输出之前下游不再静默」这条性质。
  12. //
  13. // 透传路径的 pendingLines 会把 response.created / response.in_progress 全部扣住,
  14. // 于是首个可见输出之前下游一个字节都收不到,连 HTTP 响应头都不会提交。推理模型
  15. // 思考数百秒时,中间层代理会按空闲超时把连接判死。Forward 路径早就用心跳解决了
  16. // 这个问题(见 openai_gateway_response_handling.go 中 lastDownstreamWriteAt 的注释),
  17. // 透传路径漏了。
  18. func newPassthroughKeepaliveTestContext(t *testing.T) (*gin.Context, *httptest.ResponseRecorder) {
  19. t.Helper()
  20. gin.SetMode(gin.TestMode)
  21. rec := httptest.NewRecorder()
  22. c, _ := gin.CreateTestContext(rec)
  23. // 刻意【不】调用 MarkOpenAICompactClientStream:普通 /v1/responses 透传不带
  24. // compact 标记,这正是它此前拿不到心跳的原因。
  25. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
  26. return c, rec
  27. }
  28. // startOpenAISSEKeepalive 必须在【没有】compact 标记时也能启动 ——
  29. // 否则普通透传请求依旧静默。
  30. func TestStartOpenAISSEKeepalive_WorksWithoutCompactMarker(t *testing.T) {
  31. c, rec := newPassthroughKeepaliveTestContext(t)
  32. // 对照:带 compact 标记检查的入口在这里应当直接 no-op。
  33. stop := StartOpenAICompactSSEKeepalive(c, keepaliveTestInterval)
  34. waitForKeepaliveBeats()
  35. stop()
  36. require.Zero(t, rec.Body.Len(), "无 compact 标记时 StartOpenAICompactSSEKeepalive 应当 no-op")
  37. // 内部入口不检查标记,应当真的开始打拍。
  38. c, rec = newPassthroughKeepaliveTestContext(t)
  39. stop = startOpenAISSEKeepalive(c, keepaliveTestInterval)
  40. defer stop()
  41. waitForKeepaliveBeats()
  42. require.True(t, StopOpenAICompactSSEKeepaliveCommitted(c), "心跳应当提交响应头")
  43. require.Equal(t, http.StatusOK, rec.Code)
  44. require.Equal(t, "text/event-stream", rec.Header().Get("Content-Type"))
  45. require.Equal(t, "no", rec.Header().Get("X-Accel-Buffering"))
  46. require.Contains(t, rec.Body.String(), ": keepalive\n\n")
  47. }
  48. // 🔴 最要紧的一条:心跳字节【不得】把请求判成「已向客户端写出语义响应」,
  49. // 否则上游 429/5xx 时不再换号 —— 这正是 #3887 加固的那条不变量,
  50. // 透传路径的 pre-output failover 完全依赖它。
  51. func TestPassthroughKeepaliveDoesNotBlockPreOutputFailover(t *testing.T) {
  52. c, rec := newPassthroughKeepaliveTestContext(t)
  53. stop := startOpenAISSEKeepalive(c, keepaliveTestInterval)
  54. defer stop()
  55. waitForKeepaliveBeats()
  56. require.True(t, StopOpenAICompactSSEKeepaliveCommitted(c))
  57. require.NotZero(t, rec.Body.Len(), "前提:心跳确实写出了字节")
  58. // 只有心跳字节时,仍应判定为「尚未向客户端输出」。
  59. require.False(t, openAIStreamClientOutputStarted(c, false),
  60. "心跳字节不构成语义输出,pre-output failover 必须仍然可用")
  61. // 写出一条真实事件之后,判定才翻转。
  62. _, err := c.Writer.Write([]byte("data: {\"type\":\"response.output_text.delta\"}\n\n"))
  63. require.NoError(t, err)
  64. require.True(t, openAIStreamClientOutputStarted(c, false),
  65. "真实语义输出之后应当判定为已输出")
  66. }
  67. // 停拍之后不得再有心跳字节写出 —— 主循环接管 ResponseWriter 的前提。
  68. func TestPassthroughKeepaliveStopsBeforeHandingOverWriter(t *testing.T) {
  69. c, rec := newPassthroughKeepaliveTestContext(t)
  70. stop := startOpenAISSEKeepalive(c, keepaliveTestInterval)
  71. waitForKeepaliveBeats()
  72. stop()
  73. before := rec.Body.String()
  74. waitForKeepaliveBeats()
  75. require.Equal(t, before, rec.Body.String(), "停拍后不应再有字节写出")
  76. // 停拍后主循环写出的内容不应被心跳穿插。
  77. _, err := c.Writer.Write([]byte("data: real\n\n"))
  78. require.NoError(t, err)
  79. waitForKeepaliveBeats()
  80. require.True(t, strings.HasSuffix(rec.Body.String(), "data: real\n\n"),
  81. "停拍后写入应当是响应体的最后一段")
  82. }
  83. // interval<=0(配置禁用)时行为与改动前完全一致:一个字节都不写。
  84. func TestPassthroughKeepaliveDisabledKeepsWriterUntouched(t *testing.T) {
  85. c, rec := newPassthroughKeepaliveTestContext(t)
  86. stop := startOpenAISSEKeepalive(c, 0)
  87. waitForKeepaliveBeats()
  88. stop()
  89. require.Zero(t, rec.Body.Len())
  90. require.False(t, StopOpenAICompactSSEKeepaliveCommitted(c))
  91. _ = time.Now
  92. }