error.go

 1package anthropic
 2
 3import (
 4	"cmp"
 5	"errors"
 6	"io"
 7	"net/http"
 8	"regexp"
 9	"strconv"
10	"strings"
11
12	"charm.land/fantasy"
13	"github.com/charmbracelet/anthropic-sdk-go"
14)
15
16var anthropicContextPattern = regexp.MustCompile(`prompt is too long:\s*(\d+)\s*tokens?\s*>\s*(\d+)\s*maximum`)
17
18func toProviderErr(err error) error {
19	var apiErr *anthropic.Error
20	if errors.As(err, &apiErr) {
21		providerErr := &fantasy.ProviderError{
22			Title:           cmp.Or(fantasy.ErrorTitleForStatusCode(apiErr.StatusCode), "provider request failed"),
23			Message:         apiErr.Error(),
24			Cause:           apiErr,
25			URL:             apiErr.Request.URL.String(),
26			StatusCode:      apiErr.StatusCode,
27			RequestBody:     apiErr.DumpRequest(true),
28			ResponseHeaders: toHeaderMap(apiErr.Response.Header),
29			ResponseBody:    apiErr.DumpResponse(true),
30		}
31
32		parseContextTooLargeError(apiErr.Error(), providerErr)
33
34		return providerErr
35	}
36	// Wrap in a `ProviderError` so `.IsRetriable()` works.
37	if errors.Is(err, io.ErrUnexpectedEOF) {
38		return &fantasy.ProviderError{
39			Title:   "stream transport error",
40			Message: err.Error(),
41			Cause:   err,
42		}
43	}
44	return err
45}
46
47func parseContextTooLargeError(message string, providerErr *fantasy.ProviderError) {
48	matches := anthropicContextPattern.FindStringSubmatch(message)
49	if matches == nil {
50		return
51	}
52
53	providerErr.ContextTooLargeErr = true
54	providerErr.ContextUsedTokens, _ = strconv.Atoi(matches[1])
55	providerErr.ContextMaxTokens, _ = strconv.Atoi(matches[2])
56}
57
58func toHeaderMap(in http.Header) (out map[string]string) {
59	out = make(map[string]string, len(in))
60	for k, v := range in {
61		if l := len(v); l > 0 {
62			out[k] = v[l-1]
63			in[strings.ToLower(k)] = v
64		}
65	}
66	return out
67}