// Package clienterror classifies upstream failures caused by the client request. package clienterror import ( "encoding/json" "errors" "net/http" "strings" "github.com/tidwall/gjson" ) var requestFaultCodes = map[string]struct{}{ "cyber_policy": {}, "context_length_exceeded": {}, "message_too_big": {}, "string_above_max_length": {}, "invalid_prompt": {}, "invalid_value": {}, "unsupported_value": {}, "invalid_request_error": {}, "previous_response_not_found": {}, } var requestFaultTypes = map[string]struct{}{ "invalid_request": {}, "invalid_request_error": {}, "bad_request_error": {}, "invalid_prompt": {}, } // IsRequestFault reports whether an upstream failure is caused by the request // and therefore must not rotate or penalize credentials. func IsRequestFault(status int, err error) bool { if status <= 0 && err != nil { type statusCoder interface { StatusCode() int } var statusErr statusCoder if errors.As(err, &statusErr) && statusErr != nil { status = statusErr.StatusCode() } } if hasRequestFaultBody(err) { return true } if err != nil && IsItemNotPersisted(err.Error()) { return true } switch status { case http.StatusBadRequest, http.StatusConflict, http.StatusRequestEntityTooLarge, http.StatusUnprocessableEntity: return true default: return false } } // IsItemNotPersisted matches the upstream 404 raised when a request references a // response item the upstream never stored because `store` was false. The upstream // sends this as a plain-text message rather than a JSON body, so it cannot be // recognized through the structured identifiers above. // // The request can only succeed once the client rebuilds it without the stale // reference, so it is a request fault: rotating credentials cannot help, and the // client must be told rather than left to retry the same broken input. func IsItemNotPersisted(message string) bool { lower := strings.ToLower(message) return strings.Contains(lower, "item with id") && strings.Contains(lower, "not found") && strings.Contains(lower, "items are not persisted when `store` is set to false") } func hasRequestFaultBody(err error) bool { if err == nil { return false } body := strings.TrimSpace(err.Error()) if body == "" || !json.Valid([]byte(body)) { return false } for _, path := range []string{"error.code", "code", "response.error.code", "body.error.code"} { code := strings.ToLower(strings.TrimSpace(gjson.Get(body, path).String())) if _, ok := requestFaultCodes[code]; ok { return true } } for _, path := range []string{"error.type", "type", "response.error.type", "body.error.type"} { errType := strings.ToLower(strings.TrimSpace(gjson.Get(body, path).String())) if _, ok := requestFaultTypes[errType]; ok { return true } } return false }