返回 DeepSeek-Reasonix
nonstream.go
根目录 / internal / provider / openai / nonstream.go
1 package openai
2
3 import (
4 "fmt"
5 "io"
6 "strings"
7
8 "reasonix/internal/provider"
9 )
10
11 // sseProbe watches the stream's lines so an incomplete stream can say whether
12 // the endpoint spoke SSE at all. A body with no `data:` line and a non-stream
13 // Content-Type is a gateway page or error document, not a dropped connection.
14 type sseProbe struct {
15 sawData bool
16 firstNonData string // first non-blank non-`data:` line, clipped for the error text
17 }
18
19 // dataLine reports whether line is an SSE `data:` line, recording the first
20 // other non-blank line for the diagnostic.
21 func (p *sseProbe) dataLine(line string) bool {
22 if line == "" {
23 return false
24 }
25 if !strings.HasPrefix(line, "data:") {
26 if p.firstNonData == "" {
27 p.firstNonData = clipDiagnosticLine(line)
28 }
29 return false
30 }
31 p.sawData = true
32 return true
33 }
34
35 // incompleteErr is the error for a stream that ended without a terminal event.
36 func (p *sseProbe) incompleteErr(name, contentType string) error {
37 ct := strings.TrimSpace(contentType)
38 if !p.sawData && !isEventStreamContentType(ct) {
39 return fmt.Errorf("%s: stream ended before any SSE event (Content-Type %q, first line %q) — the endpoint returned a non-streaming response, e.g. a gateway landing page. Check that request_url/base_url points to a full API endpoint such as /v1/chat/completions or /v1/responses, not a gateway root: %w",
40 name, ct, p.firstNonData, provider.ErrNonStreamingResponse)
41 }
42 return fmt.Errorf("%s: stream ended before completion: %w", name, io.ErrUnexpectedEOF)
43 }
44
45 // isEventStreamContentType reports whether a Content-Type claims a stream. An
46 // absent header counts as one: some gateways stream without it, so this only
47 // shapes the diagnostic and never rejects a response.
48 func isEventStreamContentType(contentType string) bool {
49 ct := strings.ToLower(strings.TrimSpace(contentType))
50 return ct == "" || strings.Contains(ct, "event-stream") || strings.Contains(ct, "ndjson")
51 }
52
53 // clipDiagnosticLine bounds a non-SSE line before it rides in an error
54 // message, so a multi-KB HTML page cannot bloat the surface text.
55 func clipDiagnosticLine(line string) string {
56 const maxLen = 120
57 if len(line) <= maxLen {
58 return line
59 }
60 return line[:maxLen] + "…"
61 }
62
62 lines GO