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}