| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- package service
- import (
- "bufio"
- "context"
- "errors"
- "io"
- "net/http"
- "net/http/httptest"
- "testing"
- "time"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- "github.com/tidwall/gjson"
- )
- type openAICompatBufferedReadErrorCloser struct {
- err error
- }
- func (r *openAICompatBufferedReadErrorCloser) Read([]byte) (int, error) { return 0, r.err }
- func (r *openAICompatBufferedReadErrorCloser) Close() error { return nil }
- func TestChatCompletionsBufferedResponsesReadErrorReturnsFailover(t *testing.T) {
- gin.SetMode(gin.TestMode)
- readErrors := []struct {
- name string
- err error
- expectedCode string
- }{
- {name: "unexpected_eof", err: io.ErrUnexpectedEOF, expectedCode: OpenAIUpstreamStreamReadErrorCode},
- {name: "http2_reset", err: errors.New("stream error: stream ID 7; INTERNAL_ERROR; received from peer"), expectedCode: OpenAIUpstreamHTTP2StreamErrorCode},
- }
- for _, readError := range readErrors {
- t.Run(readError.name, func(t *testing.T) {
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
- resp := &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}, "X-Request-Id": []string{"upstream-rid"}},
- Body: &openAICompatBufferedReadErrorCloser{err: readError.err},
- }
- account := &Account{ID: 40, Name: "openai-oauth", Platform: PlatformOpenAI}
- result, err := (&OpenAIGatewayService{}).handleChatBufferedStreamingResponse(
- resp, c, account, "gpt-5.6-sol", "gpt-5.6-sol", "gpt-5.6-sol", time.Now(),
- )
- require.Error(t, err)
- require.Nil(t, result)
- var failoverErr *UpstreamFailoverError
- require.ErrorAs(t, err, &failoverErr)
- require.Equal(t, http.StatusBadGateway, failoverErr.StatusCode)
- require.Equal(t, "upstream-rid", failoverErr.ResponseHeaders.Get("x-request-id"))
- require.Equal(t, readError.expectedCode, gjson.GetBytes(failoverErr.ResponseBody, "error.code").String())
- require.Empty(t, rec.Body.String())
- require.False(t, c.Writer.Written())
- })
- }
- }
- func TestChatCompletionsBufferedResponsesReadErrorDoesNotFailoverAfterClientCancel(t *testing.T) {
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- requestContext, cancel := context.WithCancel(context.Background())
- cancel()
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil).WithContext(requestContext)
- resp := &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}},
- Body: &openAICompatBufferedReadErrorCloser{err: io.ErrUnexpectedEOF},
- }
- result, err := (&OpenAIGatewayService{}).handleChatBufferedStreamingResponse(
- resp,
- c,
- &Account{ID: 40, Name: "openai-oauth", Platform: PlatformOpenAI},
- "gpt-5.6-sol",
- "gpt-5.6-sol",
- "gpt-5.6-sol",
- time.Now(),
- )
- require.Error(t, err)
- require.Nil(t, result)
- var failoverErr *UpstreamFailoverError
- require.NotErrorAs(t, err, &failoverErr)
- require.Empty(t, rec.Body.String())
- }
- func TestChatCompletionsBufferedResponsesOversizedLineDoesNotFailover(t *testing.T) {
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
- resp := &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}},
- Body: &openAICompatBufferedReadErrorCloser{err: bufio.ErrTooLong},
- }
- result, err := (&OpenAIGatewayService{}).handleChatBufferedStreamingResponse(
- resp,
- c,
- &Account{ID: 40, Name: "openai-oauth", Platform: PlatformOpenAI},
- "gpt-5.6-sol",
- "gpt-5.6-sol",
- "gpt-5.6-sol",
- time.Now(),
- )
- require.ErrorIs(t, err, bufio.ErrTooLong)
- require.Nil(t, result)
- var failoverErr *UpstreamFailoverError
- require.NotErrorAs(t, err, &failoverErr)
- }
- func TestAnthropicBufferedResponsesReadErrorKeepsExistingBehavior(t *testing.T) {
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
- resp := &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}},
- Body: &openAICompatBufferedReadErrorCloser{err: io.ErrUnexpectedEOF},
- }
- result, err := (&OpenAIGatewayService{}).handleAnthropicBufferedStreamingResponse(
- resp,
- c,
- &Account{ID: 40, Name: "openai-oauth", Platform: PlatformOpenAI},
- "gpt-5.6-sol",
- "gpt-5.6-sol",
- "gpt-5.6-sol",
- time.Now(),
- )
- require.ErrorIs(t, err, io.ErrUnexpectedEOF)
- require.Equal(t, io.ErrUnexpectedEOF, err, "Messages 路径必须保持原始读取错误,不引入 Chat failover 包装")
- require.Nil(t, result)
- var failoverErr *UpstreamFailoverError
- require.NotErrorAs(t, err, &failoverErr)
- }
|