errors.go

  1package fantasy
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7	"net/http"
  8	"strconv"
  9	"strings"
 10
 11	"github.com/charmbracelet/x/exp/slice"
 12)
 13
 14// Error is a custom error type for the fantasy package.
 15type Error struct {
 16	Message string
 17	Title   string
 18	Cause   error
 19}
 20
 21func (err *Error) Error() string {
 22	if err.Title == "" {
 23		return err.Message
 24	}
 25	return fmt.Sprintf("%s: %s", err.Title, err.Message)
 26}
 27
 28func (err Error) Unwrap() error {
 29	return err.Cause
 30}
 31
 32// ProviderError represents an error returned by an external provider.
 33type ProviderError struct {
 34	Message string
 35	Title   string
 36	Cause   error
 37
 38	URL             string
 39	StatusCode      int
 40	RequestBody     []byte
 41	ResponseHeaders map[string]string
 42	ResponseBody    []byte
 43
 44	ContextUsedTokens  int
 45	ContextMaxTokens   int
 46	ContextTooLargeErr bool
 47}
 48
 49func (m *ProviderError) Error() string {
 50	if m.Title == "" {
 51		return m.Message
 52	}
 53	return fmt.Sprintf("%s: %s", m.Title, m.Message)
 54}
 55
 56// IsRetryable reports whether the error should be retried.
 57// It returns true if the underlying cause is io.ErrUnexpectedEOF, if the
 58// "x-should-retry" response header evaluates to true, or if the HTTP status
 59// code indicates a retryable condition (408, 409, 429, or any 5xx).
 60func (m *ProviderError) IsRetryable() bool {
 61	// We're mostly mimicking OpenAI's Go SDK here:
 62	// https://github.com/openai/openai-go/blob/b9d280a37149430982e9dfeed16c41d27d45cfc5/internal/requestconfig/requestconfig.go#L244
 63	if errors.Is(m.Cause, io.ErrUnexpectedEOF) {
 64		return true
 65	}
 66	if m.shouldRetryHeader() {
 67		return true
 68	}
 69	return m.StatusCode == http.StatusRequestTimeout ||
 70		m.StatusCode == http.StatusConflict ||
 71		m.StatusCode == http.StatusTooManyRequests ||
 72		m.StatusCode >= http.StatusInternalServerError
 73}
 74
 75func (m *ProviderError) shouldRetryHeader() bool {
 76	if m.ResponseHeaders == nil {
 77		return false
 78	}
 79	for k, v := range m.ResponseHeaders {
 80		if strings.EqualFold(k, "x-should-retry") {
 81			b, _ := strconv.ParseBool(v)
 82			return b
 83		}
 84	}
 85	return false
 86}
 87
 88// IsContextTooLarge checks if the error is due to the context exceeding the model's limit.
 89func (m *ProviderError) IsContextTooLarge() bool {
 90	return m.ContextTooLargeErr || m.ContextMaxTokens > 0 || m.ContextUsedTokens > 0
 91}
 92
 93// RetryError represents an error that occurred during retry operations.
 94type RetryError struct {
 95	Errors []error
 96}
 97
 98func (e *RetryError) Error() string {
 99	if err, ok := slice.Last(e.Errors); ok {
100		return fmt.Sprintf("retry error: %v", err)
101	}
102	return "retry error: no underlying errors"
103}
104
105func (e RetryError) Unwrap() error {
106	if err, ok := slice.Last(e.Errors); ok {
107		return err
108	}
109	return nil
110}
111
112// ErrorTitleForStatusCode returns a human-readable title for a given HTTP status code.
113func ErrorTitleForStatusCode(statusCode int) string {
114	return strings.ToLower(http.StatusText(statusCode))
115}
116
117// NoObjectGeneratedError is returned when object generation fails
118// due to parsing errors, validation errors, or model failures.
119type NoObjectGeneratedError struct {
120	RawText         string
121	ParseError      error
122	ValidationError error
123	Usage           Usage
124	FinishReason    FinishReason
125}
126
127// Error implements the error interface.
128func (e *NoObjectGeneratedError) Error() string {
129	if e.ValidationError != nil {
130		return fmt.Sprintf("object validation failed: %v", e.ValidationError)
131	}
132	if e.ParseError != nil {
133		return fmt.Sprintf("failed to parse object: %v", e.ParseError)
134	}
135	return "failed to generate object"
136}
137
138// IsNoObjectGeneratedError checks if an error is of type NoObjectGeneratedError.
139func IsNoObjectGeneratedError(err error) bool {
140	var target *NoObjectGeneratedError
141	return errors.As(err, &target)
142}