| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- package service
- import (
- "context"
- "errors"
- "io"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- type passthroughFlushTestWriter struct {
- gin.ResponseWriter
- recorder *httptest.ResponseRecorder
- failAfterWrites int
- successfulWrites int
- failedWrites int
- flushBodyLengths []int
- }
- func (w *passthroughFlushTestWriter) Write(data []byte) (int, error) {
- if w.failAfterWrites >= 0 && w.successfulWrites >= w.failAfterWrites {
- w.failedWrites++
- return 0, errors.New("client disconnected")
- }
- n, err := w.ResponseWriter.Write(data)
- if err == nil {
- w.successfulWrites++
- }
- return n, err
- }
- func (w *passthroughFlushTestWriter) WriteString(data string) (int, error) {
- return w.Write([]byte(data))
- }
- func (w *passthroughFlushTestWriter) Flush() {
- w.ResponseWriter.Flush()
- w.flushBodyLengths = append(w.flushBodyLengths, w.recorder.Body.Len())
- }
- type passthroughFlushTestErrorBody struct {
- payload []byte
- err error
- sent bool
- }
- func (r *passthroughFlushTestErrorBody) Read(p []byte) (int, error) {
- if !r.sent {
- r.sent = true
- return copy(p, r.payload), nil
- }
- return 0, r.err
- }
- func (r *passthroughFlushTestErrorBody) Close() error { return nil }
- func runPassthroughFlushTest(
- t *testing.T,
- body io.ReadCloser,
- failAfterWrites int,
- setups ...func(*gin.Context),
- ) (*openaiStreamingResultPassthrough, *httptest.ResponseRecorder, *passthroughFlushTestWriter, error) {
- t.Helper()
- gin.SetMode(gin.TestMode)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
- writer := &passthroughFlushTestWriter{
- ResponseWriter: c.Writer,
- recorder: recorder,
- failAfterWrites: failAfterWrites,
- }
- c.Writer = writer
- for _, setup := range setups {
- setup(c)
- }
- svc := &OpenAIGatewayService{cfg: &config.Config{
- Gateway: config.GatewayConfig{MaxLineSize: defaultMaxLineSize},
- }}
- resp := &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}},
- Body: body,
- }
- result, err := svc.handleStreamingResponsePassthrough(
- context.Background(),
- resp,
- c,
- &Account{ID: 1, Platform: PlatformOpenAI, Name: "flush-test"},
- time.Now(),
- "",
- "",
- )
- return result, recorder, writer, err
- }
- func TestOpenAIStreamingPassthroughFlushesAtCompleteEventBoundaries(t *testing.T) {
- firstEvent := "event: response.output_text.delta\n" +
- "id: event-1\n" +
- `data: {"type":"response.output_text.delta","delta":"hello"}` + "\n\n"
- heartbeat := ": keepalive\n\n"
- terminalEvent := "event: response.completed\n" +
- `data: {"type":"response.completed","response":{"id":"resp_flush","usage":{"input_tokens":3,"output_tokens":2,"total_tokens":5}}}` + "\n\n"
- upstream := firstEvent + heartbeat + terminalEvent
- result, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, upstream, recorder.Body.String())
- require.Equal(t, []int{
- len(firstEvent),
- len(firstEvent) + len(heartbeat),
- len(upstream),
- }, writer.flushBodyLengths)
- require.Equal(t, 3, result.usage.InputTokens)
- require.Equal(t, 2, result.usage.OutputTokens)
- }
- func TestOpenAIStreamingPassthroughKeepsPreamblePendingUntilFirstOutputBoundary(t *testing.T) {
- preamble := "event: response.created\n" +
- `data: {"type":"response.created","response":{"id":"resp_pending"}}` + "\n\n" +
- ": waiting\n\n"
- firstOutput := `data: {"type":"response.output_text.delta","delta":"ready"}` + "\n\n"
- terminalEvent := `data: {"type":"response.completed","response":{"id":"resp_pending","usage":{"input_tokens":4,"output_tokens":1,"total_tokens":5}}}` + "\n\n"
- upstream := preamble + firstOutput + terminalEvent
- _, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
- require.NoError(t, err)
- require.Equal(t, upstream, recorder.Body.String())
- require.Equal(t, []int{
- len(preamble) + len(firstOutput),
- len(upstream),
- }, writer.flushBodyLengths)
- }
- func TestOpenAIStreamingPassthroughFlushesTerminalEventAtEOFWithoutBlankLine(t *testing.T) {
- upstream := "event: response.completed\n" +
- `data: {"type":"response.completed","response":{"id":"resp_eof","usage":{"input_tokens":5,"output_tokens":2,"total_tokens":7}}}`
- wantBody := upstream + "\n"
- result, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, wantBody, recorder.Body.String())
- require.Equal(t, []int{len(wantBody)}, writer.flushBodyLengths)
- require.Equal(t, 5, result.usage.InputTokens)
- require.Equal(t, 2, result.usage.OutputTokens)
- }
- func TestOpenAIStreamingPassthroughFailedBeforeOutputCanStillFailOverWithoutFlush(t *testing.T) {
- upstream := "event: response.created\n" +
- `data: {"type":"response.created","response":{"id":"resp_failover"}}` + "\n\n" +
- "event: response.failed\n" +
- `data: {"type":"response.failed","error":{"code":"server_error","message":"upstream processing failed"}}` + "\n\n"
- _, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
- require.Error(t, err)
- var failoverErr *UpstreamFailoverError
- require.ErrorAs(t, err, &failoverErr)
- require.Empty(t, recorder.Body.String())
- require.Empty(t, writer.flushBodyLengths)
- }
- func TestOpenAIStreamingPassthroughNonRetryableFailedBeforeOutputFlushesAtBoundary(t *testing.T) {
- upstream := "event: response.failed\n" +
- `data: {"type":"response.failed","error":{"code":"content_policy","message":"request blocked by policy"},"usage":{"input_tokens":6,"output_tokens":0,"total_tokens":6}}` + "\n\n"
- result, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
- require.Error(t, err)
- var failoverErr *UpstreamFailoverError
- require.False(t, errors.As(err, &failoverErr))
- require.NotNil(t, result)
- require.Equal(t, upstream, recorder.Body.String())
- require.Equal(t, []int{len(upstream)}, writer.flushBodyLengths)
- require.Equal(t, 6, result.usage.InputTokens)
- require.Zero(t, result.usage.OutputTokens)
- }
- func TestOpenAIStreamingPassthroughBareErrorTerminatesBeforeDone(t *testing.T) {
- errorEvent := "event: error\n" +
- `data: {"type":"error","error":{"code":"content_policy","message":"request blocked by policy"},"usage":{"input_tokens":6,"output_tokens":0,"total_tokens":6}}` + "\n\n"
- upstream := errorEvent + "data: [DONE]\n\n"
- result, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
- require.Error(t, err)
- var failoverErr *UpstreamFailoverError
- require.False(t, errors.As(err, &failoverErr))
- require.NotNil(t, result)
- body := recorder.Body.String()
- require.NotContains(t, body, `"type":"error"`)
- require.Equal(t, 1, strings.Count(body, `"type":"response.failed"`))
- require.Contains(t, body, `"status":"failed"`)
- require.NotContains(t, body, "[DONE]")
- require.Equal(t, []int{len(body)}, writer.flushBodyLengths)
- require.Equal(t, 6, result.usage.InputTokens)
- require.Zero(t, result.usage.OutputTokens)
- }
- func TestOpenAIStreamingPassthroughBareErrorDrainsAuthoritativeFailedUsage(t *testing.T) {
- errorEvent := "event: error\n" +
- `data: {"type":"error","error":{"code":"content_policy","message":"request blocked by policy"}}` + "\n\n"
- failedEvent := "event: response.failed\n" +
- `data: {"type":"response.failed","response":{"id":"resp_failed","usage":{"input_tokens":9,"output_tokens":2},"error":{"code":"content_policy","message":"request blocked by policy"}}}` + "\n\n"
- upstream := errorEvent + failedEvent + "data: [DONE]\n\n"
- result, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
- require.Error(t, err)
- require.NotNil(t, result)
- body := recorder.Body.String()
- require.NotContains(t, body, `"type":"error"`)
- require.Equal(t, 1, strings.Count(body, `"type":"response.failed"`))
- require.Contains(t, body, `"id":"resp_failed"`)
- require.NotContains(t, body, "[DONE]")
- require.Equal(t, []int{len(body)}, writer.flushBodyLengths)
- require.Equal(t, 9, result.usage.InputTokens)
- require.Equal(t, 2, result.usage.OutputTokens)
- }
- func TestOpenAIStreamingPassthroughFailedAfterOutputFlushesAtBoundaryAndKeepsUsage(t *testing.T) {
- firstOutput := `data: {"type":"response.output_text.delta","delta":"partial"}` + "\n\n"
- failedEvent := "event: response.failed\n" +
- `data: {"type":"response.failed","error":{"code":"server_error","message":"upstream processing failed"},"usage":{"input_tokens":7,"output_tokens":2,"total_tokens":9}}` + "\n\n"
- upstream := firstOutput + failedEvent
- result, recorder, writer, err := runPassthroughFlushTest(t, io.NopCloser(strings.NewReader(upstream)), -1)
- require.Error(t, err)
- var failoverErr *UpstreamFailoverError
- require.False(t, errors.As(err, &failoverErr))
- require.NotNil(t, result)
- require.Equal(t, upstream, recorder.Body.String())
- require.Equal(t, []int{len(firstOutput), len(upstream)}, writer.flushBodyLengths)
- require.Equal(t, 7, result.usage.InputTokens)
- require.Equal(t, 2, result.usage.OutputTokens)
- }
- func TestOpenAIStreamingPassthroughClientDisconnectStillDrainsTerminalUsage(t *testing.T) {
- firstOutput := `data: {"type":"response.output_text.delta","delta":"partial"}` + "\n\n"
- terminalEvent := `data: {"type":"response.completed","response":{"id":"resp_drain","usage":{"input_tokens":11,"output_tokens":4,"total_tokens":15}}}` + "\n\n"
- result, recorder, writer, err := runPassthroughFlushTest(
- t,
- io.NopCloser(strings.NewReader(firstOutput+terminalEvent)),
- 2,
- )
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, firstOutput, recorder.Body.String())
- require.Equal(t, []int{len(firstOutput)}, writer.flushBodyLengths)
- require.Equal(t, 1, writer.failedWrites)
- require.Equal(t, 11, result.usage.InputTokens)
- require.Equal(t, 4, result.usage.OutputTokens)
- }
- func TestOpenAIStreamingPassthroughScannerErrorFlushesWrittenResidual(t *testing.T) {
- upstream := []byte(`data: {"type":"response.output_text.delta","delta":"partial"}`)
- readErr := errors.New("upstream read failed")
- _, recorder, writer, err := runPassthroughFlushTest(t, &passthroughFlushTestErrorBody{
- payload: upstream,
- err: readErr,
- }, -1)
- require.ErrorIs(t, err, readErr)
- wantBody := string(upstream) + "\n"
- require.Equal(t, wantBody, recorder.Body.String())
- require.Equal(t, []int{len(wantBody)}, writer.flushBodyLengths)
- }
- func TestOpenAIStreamingPassthroughNamespaceRestoreErrorFlushesWrittenResidualOnce(t *testing.T) {
- writtenPrefix := `data: {"type":"response.output_text.delta","delta":"prefix"}` + "\n"
- overflowData := `data: {"type":"response.output_text.delta","delta":"not-written","overflow":1e1000}`
- _, recorder, writer, err := runPassthroughFlushTest(
- t,
- io.NopCloser(strings.NewReader(writtenPrefix+overflowData)),
- -1,
- func(c *gin.Context) {
- setOpenAIResponsesNamespaceNames(c, map[string]apicompat.ResponsesNamespaceName{
- "collaboration__spawn_agent": {Namespace: "collaboration", Name: "spawn_agent"},
- })
- },
- )
- require.ErrorContains(t, err, "restore OpenAI passthrough namespace response")
- require.Equal(t, writtenPrefix, recorder.Body.String())
- require.Equal(t, []int{len(writtenPrefix)}, writer.flushBodyLengths)
- }
- func TestOpenAIStreamingPassthroughBlankWriteFailureDoesNotFlushAndStillDrainsUsage(t *testing.T) {
- writtenDataLine := `data: {"type":"response.output_text.delta","delta":"partial"}` + "\n"
- terminalEvent := `data: {"type":"response.completed","response":{"id":"resp_blank_failure","usage":{"input_tokens":13,"output_tokens":5,"total_tokens":18}}}` + "\n\n"
- result, recorder, writer, err := runPassthroughFlushTest(
- t,
- io.NopCloser(strings.NewReader(writtenDataLine+"\n"+terminalEvent)),
- 1,
- )
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, writtenDataLine, recorder.Body.String())
- require.Empty(t, writer.flushBodyLengths)
- require.Equal(t, 1, writer.successfulWrites)
- require.Equal(t, 1, writer.failedWrites)
- require.Equal(t, 13, result.usage.InputTokens)
- require.Equal(t, 5, result.usage.OutputTokens)
- }
|