package llm import ( "encoding/json" "errors" "fmt" "io" "strings" "unicode" ) const ( maxProviderErrorResponseBytes int64 = 64 << 10 maxProviderErrorIdentifierRunes = 256 maxProviderErrorMessageRunes = 4096 ) // ProviderHTTPError describes a non-success response from an LLM provider. type ProviderHTTPError struct { statusCode int providerCode string providerType string providerMessage string } func (e *ProviderHTTPError) StatusCode() int { if e == nil { return 0 } return e.statusCode } func (e *ProviderHTTPError) ProviderCode() string { if e == nil { return "" } return e.providerCode } func (e *ProviderHTTPError) ProviderType() string { if e == nil { return "" } return e.providerType } func (e *ProviderHTTPError) ProviderMessage() string { if e == nil { return "" } return e.providerMessage } func (e *ProviderHTTPError) Error() string { if e == nil || e.statusCode == 0 { return ErrUnexpectedStatus.Error() } return fmt.Sprintf("%s: status=%d", ErrUnexpectedStatus, e.statusCode) } func (e *ProviderHTTPError) GoString() string { return e.Error() } func (e *ProviderHTTPError) Unwrap() error { return ErrUnexpectedStatus } type providerErrorDetails struct { providerCode string providerType string providerMessage string } func newProviderHTTPError(statusCode int, details providerErrorDetails) *ProviderHTTPError { return &ProviderHTTPError{ statusCode: statusCode, providerCode: details.providerCode, providerType: details.providerType, providerMessage: details.providerMessage, } } func providerHTTPErrorFromBody(statusCode int, contentLength int64, body io.Reader) *ProviderHTTPError { if contentLength > maxProviderErrorResponseBytes { return newProviderHTTPError(statusCode, providerErrorDetails{}) } limited := &io.LimitedReader{ R: body, N: maxProviderErrorResponseBytes + 1, } contents, err := io.ReadAll(limited) if err != nil || limited.N == 0 { return newProviderHTTPError(statusCode, providerErrorDetails{}) } return newProviderHTTPError(statusCode, parseProviderErrorEnvelope(contents)) } func parseProviderErrorEnvelope(body []byte) providerErrorDetails { decoder := json.NewDecoder(strings.NewReader(string(body))) decoder.UseNumber() var envelope map[string]json.RawMessage if err := decoder.Decode(&envelope); err != nil { return providerErrorDetails{} } var trailing any if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { return providerErrorDetails{} } rawError, ok := envelope["error"] if !ok { return providerErrorDetails{} } var providerError map[string]json.RawMessage if err := json.Unmarshal(rawError, &providerError); err != nil || providerError == nil { return providerErrorDetails{} } var details providerErrorDetails if raw, ok := providerError["message"]; ok { var value string if json.Unmarshal(raw, &value) == nil { details.providerMessage = normalizeProviderErrorMessage(value) } } if raw, ok := providerError["type"]; ok { var value string if json.Unmarshal(raw, &value) == nil { details.providerType = normalizeProviderErrorIdentifier(value) } } if raw, ok := providerError["code"]; ok { var value any fieldDecoder := json.NewDecoder(strings.NewReader(string(raw))) fieldDecoder.UseNumber() if fieldDecoder.Decode(&value) == nil { switch value := value.(type) { case string: details.providerCode = normalizeProviderErrorIdentifier(value) case json.Number: details.providerCode = normalizeProviderErrorIdentifier(value.String()) } } } return details } func normalizeProviderErrorIdentifier(value string) string { normalized := normalizeProviderErrorText(value) if len([]rune(normalized)) > maxProviderErrorIdentifierRunes { return "" } return normalized } func normalizeProviderErrorMessage(value string) string { normalized := normalizeProviderErrorText(value) runes := []rune(normalized) if len(runes) <= maxProviderErrorMessageRunes { return normalized } return string(runes[:maxProviderErrorMessageRunes-1]) + "…" } func normalizeProviderErrorText(value string) string { value = strings.ToValidUTF8(value, "�") var result strings.Builder result.Grow(len(value)) separatorPending := false for _, r := range value { if unicode.IsSpace(r) || unicode.IsControl(r) || unicode.In(r, unicode.Cf) { if result.Len() > 0 { separatorPending = true } continue } if separatorPending { result.WriteByte(' ') separatorPending = false } result.WriteRune(r) } return result.String() }