package openai import ( "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time" crawlembed "github.com/openclaw/crawlkit/embed") type APIError struct { Status int Type string Code string Message string RetryAfter time.Duration} func (e *APIError) Error() string { parts := []string{fmt.Sprintf("openai request status=%d", e.Status)} if e.Type != "" { parts = append(parts, "type="+e.Type) } if e.Code != "" { parts = append(parts, "code="+e.Code) } if e.Message != "" { parts = append(parts, "message="+e.Message) } return strings.Join(parts, " ")} func apiErrorFromHTTP(status int, header http.Header, body []byte, now time.Time) *APIError { apiErr := &APIError{ Status: status, Message: strings.TrimSpace(string(body)), RetryAfter: parseRetryAfter(header.Get("Retry-After"), now), } var parsed summaryResponse if err := json.Unmarshal(body, &parsed); err == nil && parsed.Error != nil { apiErr.Message = parsed.Error.Message apiErr.Type = parsed.Error.Type apiErr.Code = parsed.Error.Code } return apiErr} func (e *APIError) Retryable() bool { if e == nil { return false } switch e.Status { case http.StatusRequestTimeout, http.StatusTooManyRequests: return e.Type != "insufficient_quota" && e.Code != "insufficient_quota" case http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout: return true default: return false }} func (e *APIError) IsOverloaded() bool { return e != nil && (e.Type == "overloaded_error" || (e.Status == http.StatusServiceUnavailable && e.Code == "overloaded"))} func AsAPIError(err error) *APIError { if err == nil { return nil } var apiErr *APIError if errors.As(err, &apiErr) { return apiErr } return nil} func apiErrorFromEmbed(err error, now time.Time) *APIError { var httpErr *crawlembed.HTTPError if !errors.As(err, &httpErr) { return nil } apiErr := &APIError{ Status: httpErr.StatusCode, Message: strings.TrimSpace(httpErr.Body), RetryAfter: parseRetryAfter(httpErr.Header.Get("Retry-After"), now), } var parsed embeddingResponse if jerr := json.Unmarshal([]byte(httpErr.Body), &parsed); jerr == nil && parsed.Error != nil { apiErr.Message = parsed.Error.Message apiErr.Type = parsed.Error.Type apiErr.Code = parsed.Error.Code } return apiErr} func parseRetryAfter(header string, now time.Time) time.Duration { header = strings.TrimSpace(header) if header == "" { return 0 } if seconds, err := strconv.Atoi(header); err == nil { if seconds < 0 { return 0 } return time.Duration(seconds) * time.Second } if seconds, err := strconv.ParseFloat(header, 64); err == nil { if seconds < 0 { return 0 } return time.Duration(seconds * float64(time.Second)) } if when, err := http.ParseTime(header); err == nil { delta := when.Sub(now) if delta < 0 { return 0 } return delta } return 0}
package openai
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
crawlembed "github.com/openclaw/crawlkit/embed"
)
type APIError struct {
Status int
Type string
Code string
Message string
RetryAfter time.Duration
}
func (e *APIError) Error() string {
parts := []string{fmt.Sprintf("openai request status=%d", e.Status)}
if e.Type != "" {
parts = append(parts, "type="+e.Type)
}
if e.Code != "" {
parts = append(parts, "code="+e.Code)
}
if e.Message != "" {
parts = append(parts, "message="+e.Message)
}
return strings.Join(parts, " ")
}
func apiErrorFromHTTP(status int, header http.Header, body []byte, now time.Time) *APIError {
apiErr := &APIError{
Status: status,
Message: strings.TrimSpace(string(body)),
RetryAfter: parseRetryAfter(header.Get("Retry-After"), now),
}
var parsed summaryResponse
if err := json.Unmarshal(body, &parsed); err == nil && parsed.Error != nil {
apiErr.Message = parsed.Error.Message
apiErr.Type = parsed.Error.Type
apiErr.Code = parsed.Error.Code
}
return apiErr
}
func (e *APIError) Retryable() bool {
if e == nil {
return false
}
switch e.Status {
case http.StatusRequestTimeout, http.StatusTooManyRequests:
return e.Type != "insufficient_quota" && e.Code != "insufficient_quota"
case http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
return true
default:
return false
}
}
func (e *APIError) IsOverloaded() bool {
return e != nil && (e.Type == "overloaded_error" || (e.Status == http.StatusServiceUnavailable && e.Code == "overloaded"))
}
func AsAPIError(err error) *APIError {
if err == nil {
return nil
}
var apiErr *APIError
if errors.As(err, &apiErr) {
return apiErr
}
return nil
}
func apiErrorFromEmbed(err error, now time.Time) *APIError {
var httpErr *crawlembed.HTTPError
if !errors.As(err, &httpErr) {
return nil
}
apiErr := &APIError{
Status: httpErr.StatusCode,
Message: strings.TrimSpace(httpErr.Body),
RetryAfter: parseRetryAfter(httpErr.Header.Get("Retry-After"), now),
}
var parsed embeddingResponse
if jerr := json.Unmarshal([]byte(httpErr.Body), &parsed); jerr == nil && parsed.Error != nil {
apiErr.Message = parsed.Error.Message
apiErr.Type = parsed.Error.Type
apiErr.Code = parsed.Error.Code
}
return apiErr
}
func parseRetryAfter(header string, now time.Time) time.Duration {
header = strings.TrimSpace(header)
if header == "" {
return 0
}
if seconds, err := strconv.Atoi(header); err == nil {
if seconds < 0 {
return 0
}
return time.Duration(seconds) * time.Second
}
if seconds, err := strconv.ParseFloat(header, 64); err == nil {
if seconds < 0 {
return 0
}
return time.Duration(seconds * float64(time.Second))
}
if when, err := http.ParseTime(header); err == nil {
delta := when.Sub(now)
if delta < 0 {
return 0
}
return delta
}
return 0
}