diff --git a/errors.go b/errors.go index f9228de08e182020dcadf04e9d498655917ece1f..a925d4c43dce6d668bc1cac17b9a2a7e96b447fb 100644 --- a/errors.go +++ b/errors.go @@ -3,7 +3,9 @@ package fantasy import ( "errors" "fmt" + "io" "net/http" + "strconv" "strings" "github.com/charmbracelet/x/exp/slice" @@ -51,9 +53,36 @@ func (m *ProviderError) Error() string { return fmt.Sprintf("%s: %s", m.Title, m.Message) } -// IsRetryable checks if the error is retryable based on the status code. +// IsRetryable reports whether the error should be retried. +// It returns true if the underlying cause is io.ErrUnexpectedEOF, if the +// "x-should-retry" response header evaluates to true, or if the HTTP status +// code indicates a retryable condition (408, 409, 429, or any 5xx). func (m *ProviderError) IsRetryable() bool { - return m.StatusCode == http.StatusRequestTimeout || m.StatusCode == http.StatusConflict || m.StatusCode == http.StatusTooManyRequests + // 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 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.