openai_stream_read_error.go 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "strings"
  7. )
  8. const (
  9. // OpenAIUpstreamHTTP2StreamErrorCode is returned to OpenAI-compatible clients
  10. // when an upstream HTTP/2 response stream is reset after the request started.
  11. OpenAIUpstreamHTTP2StreamErrorCode = "upstream_http2_stream_error"
  12. OpenAIUpstreamStreamReadErrorCode = "upstream_stream_read_error"
  13. // OpenAIUpstreamStreamTruncatedCode is returned when an upstream SSE stream
  14. // closes *cleanly* before delivering any terminal signal. A clean EOF carries
  15. // no transport error, so without this classification a truncated generation is
  16. // indistinguishable from a successful one.
  17. OpenAIUpstreamStreamTruncatedCode = "upstream_stream_truncated"
  18. )
  19. // ErrOpenAIUpstreamStreamTruncated marks an upstream SSE stream that ended at
  20. // EOF — without a read error — before any terminal signal arrived.
  21. var ErrOpenAIUpstreamStreamTruncated = errors.New("upstream stream ended before any terminal chunk")
  22. type openAIUpstreamStreamReadError struct {
  23. cause error
  24. clientCode string
  25. clientMessage string
  26. }
  27. func (e *openAIUpstreamStreamReadError) Error() string {
  28. return fmt.Sprintf("stream usage incomplete: %v", e.cause)
  29. }
  30. func (e *openAIUpstreamStreamReadError) Unwrap() error { return e.cause }
  31. func newOpenAIUpstreamStreamReadError(err error) error {
  32. code, message := classifyOpenAIUpstreamStreamReadError(err)
  33. return &openAIUpstreamStreamReadError{
  34. cause: err,
  35. clientCode: code,
  36. clientMessage: message,
  37. }
  38. }
  39. // shouldClassifyOpenAIUpstreamStreamReadError excludes cancellation and
  40. // response-size enforcement from upstream retry.
  41. func shouldClassifyOpenAIUpstreamStreamReadError(err error, contexts ...context.Context) bool {
  42. if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, ErrUpstreamResponseBodyTooLarge) {
  43. return false
  44. }
  45. for _, ctx := range contexts {
  46. if ctx != nil && ctx.Err() != nil {
  47. return false
  48. }
  49. }
  50. return true
  51. }
  52. // OpenAIUpstreamStreamReadErrorDetails returns the stable, sanitized client
  53. // classification attached to an upstream stream read failure.
  54. func OpenAIUpstreamStreamReadErrorDetails(err error) (code, message string, ok bool) {
  55. var streamErr *openAIUpstreamStreamReadError
  56. if !errors.As(err, &streamErr) || streamErr == nil {
  57. return "", "", false
  58. }
  59. return streamErr.clientCode, streamErr.clientMessage, true
  60. }
  61. func classifyOpenAIUpstreamStreamReadError(err error) (code, message string) {
  62. if err != nil {
  63. if errors.Is(err, ErrOpenAIUpstreamStreamTruncated) {
  64. return OpenAIUpstreamStreamTruncatedCode, "Upstream response stream ended before completion"
  65. }
  66. lower := strings.ToLower(err.Error())
  67. // net/http's HTTP/2 stream error is unexported. Its stable text contains
  68. // "stream error: stream ID ..."; match only the transport signature and
  69. // never pass the original text to the client.
  70. if strings.Contains(lower, "stream error: stream id ") ||
  71. (strings.Contains(lower, "http2:") && strings.Contains(lower, "stream")) {
  72. return OpenAIUpstreamHTTP2StreamErrorCode, "Upstream HTTP/2 stream failed"
  73. }
  74. }
  75. return OpenAIUpstreamStreamReadErrorCode, "Upstream response stream was interrupted"
  76. }