2026-08-23 18:54:42 +00:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-23 18:56:15 +00:00
|
|
|
|
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))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-23 18:54:42 +00:00
|
|
|
|
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, "<22>")
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
}
|