forked from charmbracelet/fantasy
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.go
More file actions
314 lines (284 loc) · 9.8 KB
/
Copy patherrors.go
File metadata and controls
314 lines (284 loc) · 9.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
package fantasy
import (
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/charmbracelet/x/exp/slice"
"golang.org/x/net/http2"
)
// Error is a custom error type for the fantasy package.
type Error struct {
Message string
Title string
Cause error
}
func (err *Error) Error() string {
if err.Title == "" {
return err.Message
}
return fmt.Sprintf("%s: %s", err.Title, err.Message)
}
func (err Error) Unwrap() error {
return err.Cause
}
// ProviderError represents an error returned by an external provider.
type ProviderError struct {
Message string
Title string
Cause error
URL string
StatusCode int
RequestBody []byte
ResponseHeaders map[string]string
ResponseBody []byte
ContextUsedTokens int
ContextMaxTokens int
ContextTooLargeErr bool
// AuthError marks the error as an authentication failure a provider
// flagged as resolvable by refreshing credentials (e.g. re-running an
// interactive login). It covers auth failures that do not carry an HTTP
// 401 status, so a caller-supplied OnAuthRefresh hook still engages.
AuthError bool
// TransientError marks a temporary server-side failure worth retrying
// despite carrying no retryable HTTP status code. Mid-stream SSE error
// events ride inside an already-successful 200 response, so the status
// code alone cannot signal that a retry may succeed.
TransientError bool
}
func (m *ProviderError) Error() string {
if m.Title == "" {
return m.Message
}
return fmt.Sprintf("%s: %s", m.Title, m.Message)
}
// Unwrap returns the underlying cause so errors.Is and errors.As can
// inspect the wrapped error (e.g. an HTTP/2 transport error).
func (m *ProviderError) Unwrap() error {
return m.Cause
}
// IsRetryable reports whether the error should be retried.
// It returns true if the error is flagged as transient, if the underlying
// cause is io.ErrUnexpectedEOF, if the "x-should-retry" response header
// evaluates to true, if the HTTP status code indicates a retryable
// condition (408, 409, 429, or any 5xx), or if the cause is a transient
// HTTP/2 transport error.
func (m *ProviderError) IsRetryable() bool {
if m.TransientError {
return true
}
// We're mostly mimicking OpenAI's Go SDK here:
// https://github.com/openai/openai-go/blob/b9d280a37149430982e9dfeed16c41d27d45cfc5/internal/requestconfig/requestconfig.go#L244
if errors.Is(m.Cause, io.ErrUnexpectedEOF) {
return true
}
if IsTransportError(m.Cause) {
return true
}
if m.shouldRetryHeader() {
return true
}
return m.StatusCode == http.StatusRequestTimeout ||
m.StatusCode == http.StatusConflict ||
m.StatusCode == http.StatusTooManyRequests ||
m.StatusCode >= http.StatusInternalServerError
}
func (m *ProviderError) shouldRetryHeader() bool {
if m.ResponseHeaders == nil {
return false
}
for k, v := range m.ResponseHeaders {
if strings.EqualFold(k, "x-should-retry") {
b, _ := strconv.ParseBool(v)
return b
}
}
return false
}
// IsContextTooLarge checks if the error is due to the context exceeding the model's limit.
func (m *ProviderError) IsContextTooLarge() bool {
return m.ContextTooLargeErr || m.ContextMaxTokens > 0 || m.ContextUsedTokens > 0
}
// NewIncompleteStreamError returns a retryable ProviderError indicating that
// an upstream stream closed cleanly without delivering its terminal signal
// (finish_reason, stop_reason, response.completed, candidate.finishReason,
// etc.). The cause is io.ErrUnexpectedEOF so ProviderError.IsRetryable()
// engages and the retry middleware re-runs the step.
func NewIncompleteStreamError() *ProviderError {
return &ProviderError{
Title: "stream transport error",
Message: io.ErrUnexpectedEOF.Error(),
Cause: io.ErrUnexpectedEOF,
}
}
// http2TransportErrorFragments are message fragments that identify a
// transient HTTP/2 transport failure. Go's standard library bundles its
// own copy of the http2 package whose error types are unexported, so they
// cannot be matched with errors.As. We fall back to matching these stable
// fragments, which both the stdlib and x/net/http2 use. The list is kept
// tight to avoid misclassifying application-level errors as transport
// failures.
var http2TransportErrorFragments = []string{
"stream error:", // RST_STREAM: INTERNAL_ERROR, REFUSED_STREAM, CANCEL, etc.
"connection error:", // connection-level protocol error
}
// IsTransportError reports whether err or any error in its chain is a
// transient transport-level failure that is safe to retry on a fresh
// connection. In practice these are HTTP/2 stream resets, connection
// errors, and GOAWAY frames, which originate from the transport rather
// than the application.
//
// x/net/http2 error types are matched by type; Go's stdlib-bundled http2
// uses unexported types, so those are matched by their message fragments.
func IsTransportError(err error) bool {
if err == nil {
return false
}
var (
streamErr http2.StreamError
connErr http2.ConnectionError
goAwayErr http2.GoAwayError
)
if errors.As(err, &streamErr) ||
errors.As(err, &connErr) ||
errors.As(err, &goAwayErr) {
return true
}
// Wrapped errors embed the inner message, so scanning the top-level
// string covers the whole chain.
msg := err.Error()
for _, fragment := range http2TransportErrorFragments {
if strings.Contains(msg, fragment) {
return true
}
}
return false
}
// NewTransportError wraps a transient transport error into a retryable
// ProviderError with a human-friendly title and message.
func NewTransportError(err error) *ProviderError {
return &ProviderError{
Title: "stream transport error",
Message: extractHTTP2ErrorMessage(err),
Cause: err,
}
}
// TransientStreamErrorTypes are provider error "type" (or "code") values
// that name a temporary server-side condition worth retrying. Mid-stream
// SSE error events ride inside an already-successful 200 response, so the
// HTTP status code cannot signal retryability; providers classify the
// payload against this set and set ProviderError.TransientError.
//
// This is the canonical list. Providers parse their SDK-specific error
// shapes but defer the transient/permanent policy decision here.
var TransientStreamErrorTypes = map[string]bool{
"server_error": true,
"internal_error": true,
"overloaded_error": true,
"api_error": true,
"rate_limit_error": true,
}
// WrapTransportError wraps a transient transport failure in a retryable
// ProviderError so callers get a clean message and .IsRetryable() reports
// true. It recognizes an unexpected mid-stream EOF and HTTP/2 stream,
// connection, and GOAWAY resets. Any other error is returned unchanged.
//
// This is the canonical entry point for provider error handlers: they can
// hand off whatever the transport surfaced without re-encoding which
// failures count as transient.
func WrapTransportError(err error) error {
switch {
case errors.Is(err, io.ErrUnexpectedEOF):
return &ProviderError{
Title: "stream transport error",
Message: err.Error(),
Cause: err,
}
case IsTransportError(err):
return NewTransportError(err)
default:
return err
}
}
// extractHTTP2ErrorMessage locates the HTTP/2 error fragment within a
// possibly-wrapped error message and returns a concise, cleaned form for
// display. It falls back to the full message when no fragment is found.
//
// "stream error: stream ID 27; INTERNAL_ERROR; received from peer" → "INTERNAL_ERROR (received from peer)"
// "stream error: stream ID 5; REFUSED_STREAM" → "REFUSED_STREAM"
// "http2: connection error: INTERNAL_ERROR" → "INTERNAL_ERROR"
func extractHTTP2ErrorMessage(err error) string {
msg := err.Error()
for _, fragment := range http2TransportErrorFragments {
if i := strings.Index(msg, fragment); i != -1 {
return cleanHTTP2ErrorMessage(msg[i:])
}
}
return msg
}
// cleanHTTP2ErrorMessage trims the verbose framing from an HTTP/2 error
// string that begins at a known fragment. "stream error: stream ID N; CODE"
// collapses to "CODE" (with any trailing cause in parentheses), and
// "connection error: CODE" collapses to "CODE".
func cleanHTTP2ErrorMessage(msg string) string {
// "stream error: stream ID N; CODE[; cause]".
if idx := strings.Index(msg, "; "); idx != -1 {
rest := msg[idx+2:]
code, cause, hasCause := strings.Cut(rest, "; ")
if hasCause {
return fmt.Sprintf("%s (%s)", code, cause)
}
return code
}
// "connection error: CODE".
if _, code, ok := strings.Cut(msg, ": "); ok {
return code
}
return msg
}
// RetryError represents an error that occurred during retry operations.
type RetryError struct {
Errors []error
}
func (e *RetryError) Error() string {
if err, ok := slice.Last(e.Errors); ok {
return fmt.Sprintf("retry error: %v", err)
}
return "retry error: no underlying errors"
}
func (e RetryError) Unwrap() error {
if err, ok := slice.Last(e.Errors); ok {
return err
}
return nil
}
// ErrorTitleForStatusCode returns a human-readable title for a given HTTP status code.
func ErrorTitleForStatusCode(statusCode int) string {
return strings.ToLower(http.StatusText(statusCode))
}
// NoObjectGeneratedError is returned when object generation fails
// due to parsing errors, validation errors, or model failures.
type NoObjectGeneratedError struct {
RawText string
ParseError error
ValidationError error
Usage Usage
FinishReason FinishReason
}
// Error implements the error interface.
func (e *NoObjectGeneratedError) Error() string {
if e.ValidationError != nil {
return fmt.Sprintf("object validation failed: %v", e.ValidationError)
}
if e.ParseError != nil {
return fmt.Sprintf("failed to parse object: %v", e.ParseError)
}
return "failed to generate object"
}
// IsNoObjectGeneratedError checks if an error is of type NoObjectGeneratedError.
func IsNoObjectGeneratedError(err error) bool {
var target *NoObjectGeneratedError
return errors.As(err, &target)
}