errors.go

88 lines
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
package assistant

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
)

var (
	// ErrNoAPIKey is returned when no API key is provided.
	ErrNoAPIKey = errors.New("API key is required")

	// ErrRateLimited is returned when rate limited by the API.
	ErrRateLimited = errors.New("rate limited")

	// ErrEmptyResponse is returned when the API returns an empty response.
	ErrEmptyResponse = errors.New("empty response")
)

// APIError represents an error from the AI provider's API.
type APIError struct {
	StatusCode int    // HTTP status code
	Type       string // Error type from the API
	Message    string // Error message
}

// Error returns a formatted error string including the status code and message.
func (e *APIError) Error() string {
	if e.Type != "" {
		return fmt.Sprintf("API error %d (%s): %s", e.StatusCode, e.Type, e.Message)
	}
	return fmt.Sprintf("API error %d: %s", e.StatusCode, e.Message)
}

// IsRateLimited returns true if the error is a rate limit error.
func IsRateLimited(err error) bool {
	if errors.Is(err, ErrRateLimited) {
		return true
	}
	var apiErr *APIError
	if errors.As(err, &apiErr) {
		return apiErr.StatusCode == 429
	}
	return false
}

// IsAuthError returns true if the error is an authentication error.
func IsAuthError(err error) bool {
	if errors.Is(err, ErrNoAPIKey) {
		return true
	}
	var apiErr *APIError
	if errors.As(err, &apiErr) {
		return apiErr.StatusCode == 401 || apiErr.StatusCode == 403
	}
	return false
}

// ParseErrorResponse reads an HTTP error response body and constructs an APIError.
// Both Anthropic and OpenAI use the same {"error": {"type": "...", "message": "..."}} shape.
// If the body cannot be parsed, the raw body text is used as the error message.
//
// This is provider-facing: used by provider subpackages to handle API errors.
func ParseErrorResponse(resp *http.Response) error {
	defer resp.Body.Close()
	body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1MB max

	var errResp struct {
		Error struct {
			Type    string `json:"type"`
			Message string `json:"message"`
		} `json:"error"`
	}
	if json.Unmarshal(body, &errResp) == nil && errResp.Error.Message != "" {
		return &APIError{
			StatusCode: resp.StatusCode,
			Type:       errResp.Error.Type,
			Message:    errResp.Error.Message,
		}
	}

	return &APIError{
		StatusCode: resp.StatusCode,
		Message:    string(body),
	}
}