返回 DeepSeek-Reasonix
nonstream_test.go
根目录 / internal / provider / openai / nonstream_test.go
1 package openai
2
3 import (
4 "context"
5 "errors"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11
12 "reasonix/internal/provider"
13 )
14
15 // TestStreamHTMLResponseDiagnostic reproduces #8781: a gateway that answers a
16 // POST to its root path with 200 + an HTML landing page (no SSE events at all).
17 // The stream must surface a diagnostic naming the non-streaming response and
18 // the Content-Type, instead of a bare "unexpected EOF" that reads like a
19 // dropped connection.
20 func TestStreamHTMLResponseDiagnostic(t *testing.T) {
21 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22 w.Header().Set("Content-Type", "text/html")
23 w.WriteHeader(http.StatusOK)
24 _, _ = io.WriteString(w, "<!doctype html><html><head><title>Gateway</title></head><body>Welcome</body></html>")
25 }))
26 defer srv.Close()
27
28 p, err := New(provider.Config{Name: "deepseek-responses", BaseURL: srv.URL, Model: "deepseek-v4", APIKey: "k"})
29 if err != nil {
30 t.Fatalf("New: %v", err)
31 }
32 ch, err := p.Stream(context.Background(), provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "hi"}}})
33 if err != nil {
34 t.Fatalf("Stream: %v", err)
35 }
36 var gotErr error
37 for chunk := range ch {
38 if chunk.Type == provider.ChunkError {
39 gotErr = chunk.Err
40 }
41 }
42 if gotErr == nil {
43 t.Fatal("expected a stream error for an HTML response, got none")
44 }
45 msg := gotErr.Error()
46 for _, want := range []string{
47 "non-streaming response",
48 "text/html",
49 "request_url/base_url",
50 "<!doctype html>",
51 } {
52 if !strings.Contains(msg, want) {
53 t.Errorf("error %q missing %q", msg, want)
54 }
55 }
56 if !errors.Is(gotErr, provider.ErrNonStreamingResponse) {
57 t.Errorf("error %v does not carry ErrNonStreamingResponse", gotErr)
58 }
59 if errors.Is(gotErr, io.ErrUnexpectedEOF) || provider.IsConnReset(gotErr) || provider.IsStreamInterrupted(gotErr) {
60 t.Errorf("error %v is classified as a dropped connection", gotErr)
61 }
62 }
63
64 // TestStreamHTMLResponseDiagnosticJSONError covers the sibling case where the
65 // root path answers 200 with a JSON error page rather than HTML.
66 func TestStreamHTMLResponseDiagnosticJSONError(t *testing.T) {
67 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
68 w.Header().Set("Content-Type", "application/json")
69 w.WriteHeader(http.StatusOK)
70 _, _ = io.WriteString(w, `{"error":"not found"}`)
71 }))
72 defer srv.Close()
73
74 p, err := New(provider.Config{Name: "gw", BaseURL: srv.URL, Model: "m", APIKey: "k"})
75 if err != nil {
76 t.Fatalf("New: %v", err)
77 }
78 ch, err := p.Stream(context.Background(), provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "hi"}}})
79 if err != nil {
80 t.Fatalf("Stream: %v", err)
81 }
82 var gotErr error
83 for chunk := range ch {
84 if chunk.Type == provider.ChunkError {
85 gotErr = chunk.Err
86 }
87 }
88 if gotErr == nil {
89 t.Fatal("expected a stream error for a JSON response, got none")
90 }
91 msg := gotErr.Error()
92 if !strings.Contains(msg, "non-streaming response") || !strings.Contains(msg, "application/json") {
93 t.Errorf("error %q missing non-streaming diagnostic", msg)
94 }
95 if !errors.Is(gotErr, provider.ErrNonStreamingResponse) || errors.Is(gotErr, io.ErrUnexpectedEOF) {
96 t.Errorf("error %v: want ErrNonStreamingResponse identity without io.ErrUnexpectedEOF", gotErr)
97 }
98 }
99
100 // TestStreamMissingContentTypeStillStreams guards the diagnostic: a gateway
101 // that streams SSE without setting Content-Type must keep working.
102 func TestStreamMissingContentTypeStillStreams(t *testing.T) {
103 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
104 w.WriteHeader(http.StatusOK) // no Content-Type header at all
105 _, _ = io.WriteString(w, "data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\ndata: [DONE]\n\n")
106 }))
107 defer srv.Close()
108
109 p, err := New(provider.Config{Name: "gw", BaseURL: srv.URL, Model: "m", APIKey: "k"})
110 if err != nil {
111 t.Fatalf("New: %v", err)
112 }
113 ch, err := p.Stream(context.Background(), provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "hi"}}})
114 if err != nil {
115 t.Fatalf("Stream: %v", err)
116 }
117 var got strings.Builder
118 for chunk := range ch {
119 if chunk.Type == provider.ChunkError {
120 t.Fatalf("unexpected stream error: %v", chunk.Err)
121 }
122 if chunk.Type == provider.ChunkText {
123 got.WriteString(chunk.Text)
124 }
125 }
126 if got.String() != "ok" {
127 t.Errorf("streamed text = %q, want %q", got.String(), "ok")
128 }
129 }
130
131 func TestIsEventStreamContentType(t *testing.T) {
132 tests := []struct {
133 in string
134 want bool
135 }{
136 {"text/event-stream", true},
137 {"text/event-stream; charset=utf-8", true},
138 {"application/x-ndjson", true},
139 {"text/html", false},
140 {"application/json", false},
141 {"", true}, // unknown: diagnostics only, never reject
142 }
143 for _, tc := range tests {
144 if got := isEventStreamContentType(tc.in); got != tc.want {
145 t.Errorf("isEventStreamContentType(%q) = %v, want %v", tc.in, got, tc.want)
146 }
147 }
148 }
149
149 lines GO