error.go

 1package openai
 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/openai/openai-go/v2"
14)
15
16var openaiContextPattern = regexp.MustCompile(`maximum context length is (\d+) tokens.*?(?:resulted in|requested) (\d+) tokens`)
17
18func toProviderErr(err error) error {
19	var apiErr *openai.Error
20	if errors.As(err, &apiErr) {
21		message := toProviderErrMessage(apiErr)
22		providerErr := &fantasy.ProviderError{
23			Title:           cmp.Or(fantasy.ErrorTitleForStatusCode(apiErr.StatusCode), "provider request failed"),
24			Message:         message,
25			Cause:           apiErr,
26			URL:             apiErr.Request.URL.String(),
27			StatusCode:      apiErr.StatusCode,
28			RequestBody:     apiErr.DumpRequest(true),
29			ResponseHeaders: toHeaderMap(apiErr.Response.Header),
30			ResponseBody:    apiErr.DumpResponse(true),
31		}
32
33		parseContextTooLargeError(message, providerErr)
34
35		return providerErr
36	}
37	return err
38}
39
40func parseContextTooLargeError(message string, providerErr *fantasy.ProviderError) {
41	matches := openaiContextPattern.FindStringSubmatch(message)
42	if matches == nil {
43		return
44	}
45	providerErr.ContextTooLargeErr = true
46	providerErr.ContextMaxTokens, _ = strconv.Atoi(matches[1])
47	providerErr.ContextUsedTokens, _ = strconv.Atoi(matches[2])
48}
49
50func toProviderErrMessage(apiErr *openai.Error) string {
51	if apiErr.Message != "" {
52		return apiErr.Message
53	}
54
55	// For some OpenAI-compatible providers, the SDK is not always able to parse
56	// the error message correctly.
57	// Fallback to returning the raw response body in such cases.
58	data, _ := io.ReadAll(apiErr.Response.Body)
59	return string(data)
60}
61
62func toHeaderMap(in http.Header) (out map[string]string) {
63	out = make(map[string]string, len(in))
64	for k, v := range in {
65		if l := len(v); l > 0 {
66			out[k] = v[l-1]
67			in[strings.ToLower(k)] = v
68		}
69	}
70	return out
71}