You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

267 line
9.1 KiB

  1. package openai
  2. import (
  3. "io"
  4. "net/http"
  5. "net/http/httptest"
  6. "strings"
  7. "testing"
  8. "github.com/QuantumNous/new-api/common"
  9. "github.com/QuantumNous/new-api/constant"
  10. "github.com/QuantumNous/new-api/dto"
  11. relaycommon "github.com/QuantumNous/new-api/relay/common"
  12. "github.com/QuantumNous/new-api/relay/helper"
  13. "github.com/QuantumNous/new-api/types"
  14. "github.com/gin-gonic/gin"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/require"
  17. )
  18. func TestResponsesStreamEventErrorParsing(t *testing.T) {
  19. t.Run("standalone error event", func(t *testing.T) {
  20. data := `{"type":"error","error":{"type":"too_many_requests","code":"too_many_requests","message":"Too Many Requests","param":null}}`
  21. var streamResp dto.ResponsesStreamResponse
  22. err := common.UnmarshalJsonStr(data, &streamResp)
  23. require.NoError(t, err)
  24. assert.Equal(t, "error", streamResp.Type)
  25. assert.NotNil(t, streamResp.Error)
  26. oaiErr := dto.GetOpenAIError(streamResp.Error)
  27. require.NotNil(t, oaiErr)
  28. assert.Equal(t, "too_many_requests", oaiErr.Type)
  29. assert.Equal(t, "Too Many Requests", oaiErr.Message)
  30. })
  31. t.Run("response.failed event", func(t *testing.T) {
  32. data := `{"type":"response.failed","response":{"id":"test-id","status":"failed","error":{"type":"server_error","message":"Internal server error"}}}`
  33. var streamResp dto.ResponsesStreamResponse
  34. err := common.UnmarshalJsonStr(data, &streamResp)
  35. require.NoError(t, err)
  36. assert.Equal(t, "response.failed", streamResp.Type)
  37. require.NotNil(t, streamResp.Response)
  38. oaiErr := streamResp.Response.GetOpenAIError()
  39. require.NotNil(t, oaiErr)
  40. assert.Equal(t, "server_error", oaiErr.Type)
  41. })
  42. t.Run("response.error event with error in response object", func(t *testing.T) {
  43. data := `{"type":"response.error","response":{"id":"test-id","status":"failed","error":{"type":"invalid_request_error","message":"Invalid model"}}}`
  44. var streamResp dto.ResponsesStreamResponse
  45. err := common.UnmarshalJsonStr(data, &streamResp)
  46. require.NoError(t, err)
  47. assert.Equal(t, "response.error", streamResp.Type)
  48. oaiErr := streamResp.Response.GetOpenAIError()
  49. require.NotNil(t, oaiErr)
  50. assert.Equal(t, "invalid_request_error", oaiErr.Type)
  51. })
  52. t.Run("normal event has no error", func(t *testing.T) {
  53. data := `{"type":"response.output_text.delta","delta":"hello"}`
  54. var streamResp dto.ResponsesStreamResponse
  55. err := common.UnmarshalJsonStr(data, &streamResp)
  56. require.NoError(t, err)
  57. assert.Equal(t, "response.output_text.delta", streamResp.Type)
  58. assert.Nil(t, streamResp.Error)
  59. })
  60. }
  61. func TestOaiResponsesHandlerAttachesUpstreamBodyOnInvalidJSON(t *testing.T) {
  62. gin.SetMode(gin.TestMode)
  63. w := httptest.NewRecorder()
  64. c, _ := gin.CreateTestContext(w)
  65. resp := &http.Response{
  66. StatusCode: http.StatusOK,
  67. Body: io.NopCloser(strings.NewReader(`{"incomplete":`)),
  68. }
  69. usage, err := OaiResponsesHandler(c, &relaycommon.RelayInfo{}, resp)
  70. require.Nil(t, usage)
  71. require.Error(t, err)
  72. assert.NotEmpty(t, err.UpstreamBody)
  73. assert.Contains(t, err.UpstreamBody, `{"incomplete":`)
  74. }
  75. func TestOaiResponsesStreamHandlerDoesNotForwardErrorEvent(t *testing.T) {
  76. gin.SetMode(gin.TestMode)
  77. oldTimeout := constant.StreamingTimeout
  78. constant.StreamingTimeout = 30
  79. t.Cleanup(func() {
  80. constant.StreamingTimeout = oldTimeout
  81. })
  82. w := httptest.NewRecorder()
  83. c, _ := gin.CreateTestContext(w)
  84. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
  85. errorEvent := `{"type":"error","error":{"type":"tokens","code":"rate_limit_exceeded","message":"Request too large","param":null},"sequence_number":2}`
  86. resp := &http.Response{
  87. StatusCode: http.StatusOK,
  88. Body: io.NopCloser(strings.NewReader("data: " + errorEvent + "\n\n")),
  89. }
  90. usage, err := OaiResponsesStreamHandler(c, &relaycommon.RelayInfo{}, resp)
  91. require.Nil(t, usage)
  92. require.Error(t, err)
  93. assert.Equal(t, http.StatusInternalServerError, err.StatusCode)
  94. assert.Contains(t, err.UpstreamBody, "rate_limit_exceeded")
  95. assert.NotContains(t, w.Body.String(), "rate_limit_exceeded")
  96. assert.Empty(t, w.Body.String())
  97. assert.Empty(t, w.Header().Get("Content-Type"))
  98. }
  99. func TestOaiStreamHandlerDoesNotForwardInitialErrorEvent(t *testing.T) {
  100. gin.SetMode(gin.TestMode)
  101. oldTimeout := constant.StreamingTimeout
  102. constant.StreamingTimeout = 30
  103. t.Cleanup(func() {
  104. constant.StreamingTimeout = oldTimeout
  105. })
  106. w := httptest.NewRecorder()
  107. c, _ := gin.CreateTestContext(w)
  108. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  109. errorEvent := `{"error":{"type":"tokens","code":"rate_limit_exceeded","message":"Request too large","param":null}}`
  110. resp := &http.Response{
  111. StatusCode: http.StatusOK,
  112. Body: io.NopCloser(strings.NewReader("data: " + errorEvent + "\n\n")),
  113. }
  114. info := &relaycommon.RelayInfo{
  115. RelayFormat: types.RelayFormatOpenAI,
  116. ChannelMeta: &relaycommon.ChannelMeta{
  117. UpstreamModelName: "gpt-test",
  118. },
  119. }
  120. usage, err := OaiStreamHandler(c, info, resp)
  121. require.Nil(t, usage)
  122. require.Error(t, err)
  123. assert.Contains(t, err.UpstreamBody, "rate_limit_exceeded")
  124. assert.NotContains(t, w.Body.String(), "rate_limit_exceeded")
  125. assert.Empty(t, w.Body.String())
  126. assert.Empty(t, w.Header().Get("Content-Type"))
  127. }
  128. func TestOaiStreamHandlerReturnsInitialErrorAfterPing(t *testing.T) {
  129. gin.SetMode(gin.TestMode)
  130. oldTimeout := constant.StreamingTimeout
  131. constant.StreamingTimeout = 30
  132. t.Cleanup(func() {
  133. constant.StreamingTimeout = oldTimeout
  134. })
  135. w := httptest.NewRecorder()
  136. c, _ := gin.CreateTestContext(w)
  137. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  138. require.NoError(t, helper.PingData(c))
  139. errorEvent := `{"error":{"type":"tokens","code":"rate_limit_exceeded","message":"Request too large","param":null}}`
  140. resp := &http.Response{
  141. StatusCode: http.StatusOK,
  142. Body: io.NopCloser(strings.NewReader("data: " + errorEvent + "\n\n")),
  143. }
  144. info := &relaycommon.RelayInfo{
  145. RelayFormat: types.RelayFormatOpenAI,
  146. ChannelMeta: &relaycommon.ChannelMeta{
  147. UpstreamModelName: "gpt-test",
  148. },
  149. }
  150. usage, err := OaiStreamHandler(c, info, resp)
  151. require.Nil(t, usage)
  152. require.Error(t, err)
  153. assert.Equal(t, http.StatusInternalServerError, err.StatusCode)
  154. assert.Contains(t, err.UpstreamBody, "rate_limit_exceeded")
  155. assert.Contains(t, w.Body.String(), ": PING")
  156. assert.NotContains(t, w.Body.String(), "rate_limit_exceeded")
  157. }
  158. func TestOaiStreamHandlerDoesNotRetryAfterNormalStreamData(t *testing.T) {
  159. gin.SetMode(gin.TestMode)
  160. oldTimeout := constant.StreamingTimeout
  161. constant.StreamingTimeout = 30
  162. t.Cleanup(func() {
  163. constant.StreamingTimeout = oldTimeout
  164. })
  165. w := httptest.NewRecorder()
  166. c, _ := gin.CreateTestContext(w)
  167. c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
  168. normalEvent := `{"id":"chatcmpl-test","object":"chat.completion.chunk","created":1,"model":"gpt-test","choices":[{"index":0,"delta":{"content":"hi"}}]}`
  169. errorEvent := `{"error":{"type":"tokens","code":"rate_limit_exceeded","message":"Request too large","param":null}}`
  170. resp := &http.Response{
  171. StatusCode: http.StatusOK,
  172. Body: io.NopCloser(strings.NewReader("data: " + normalEvent + "\n\ndata: " + errorEvent + "\n\n")),
  173. }
  174. info := &relaycommon.RelayInfo{
  175. RelayFormat: types.RelayFormatOpenAI,
  176. ChannelMeta: &relaycommon.ChannelMeta{
  177. UpstreamModelName: "gpt-test",
  178. },
  179. }
  180. usage, err := OaiStreamHandler(c, info, resp)
  181. require.Nil(t, err)
  182. require.NotNil(t, usage)
  183. assert.Contains(t, w.Body.String(), "hi")
  184. assert.NotContains(t, w.Body.String(), "rate_limit_exceeded")
  185. }
  186. func TestOaiResponsesStreamHandlerDoesNotRetryAfterNormalStreamData(t *testing.T) {
  187. gin.SetMode(gin.TestMode)
  188. oldTimeout := constant.StreamingTimeout
  189. constant.StreamingTimeout = 30
  190. t.Cleanup(func() {
  191. constant.StreamingTimeout = oldTimeout
  192. })
  193. w := httptest.NewRecorder()
  194. c, _ := gin.CreateTestContext(w)
  195. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
  196. normalEvent := `{"type":"response.completed","response":{"id":"resp_test","usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`
  197. errorEvent := `{"type":"error","error":{"type":"tokens","code":"rate_limit_exceeded","message":"Request too large","param":null},"sequence_number":2}`
  198. resp := &http.Response{
  199. StatusCode: http.StatusOK,
  200. Body: io.NopCloser(strings.NewReader("data: " + normalEvent + "\n\ndata: " + errorEvent + "\n\n")),
  201. }
  202. info := &relaycommon.RelayInfo{
  203. ChannelMeta: &relaycommon.ChannelMeta{
  204. UpstreamModelName: "gpt-test",
  205. },
  206. }
  207. usage, err := OaiResponsesStreamHandler(c, info, resp)
  208. require.Nil(t, err)
  209. require.NotNil(t, usage)
  210. assert.Contains(t, w.Body.String(), "response.completed")
  211. assert.NotContains(t, w.Body.String(), "rate_limit_exceeded")
  212. }
  213. func TestOaiResponsesToChatHandlerAttachesUpstreamBodyOnInvalidJSON(t *testing.T) {
  214. gin.SetMode(gin.TestMode)
  215. w := httptest.NewRecorder()
  216. c, _ := gin.CreateTestContext(w)
  217. resp := &http.Response{
  218. StatusCode: http.StatusOK,
  219. Body: io.NopCloser(strings.NewReader(`{"incomplete":`)),
  220. }
  221. usage, err := OaiResponsesToChatHandler(c, &relaycommon.RelayInfo{}, resp)
  222. require.Nil(t, usage)
  223. require.Error(t, err)
  224. assert.NotEmpty(t, err.UpstreamBody)
  225. assert.Contains(t, err.UpstreamBody, `{"incomplete":`)
  226. }