use serde::Deserialize;
use std::fmt;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Api(ApiError),
Http(reqwest::Error),
Json(serde_json::Error),
WebSocket(tokio_tungstenite::tungstenite::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Api(e) => write!(f, "{e}"),
Error::Http(e) => write!(f, "qai: http error: {e}"),
Error::Json(e) => write!(f, "qai: json error: {e}"),
Error::WebSocket(e) => write!(f, "qai: websocket error: {e}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Api(_) => None,
Error::Http(e) => Some(e),
Error::Json(e) => Some(e),
Error::WebSocket(e) => Some(e),
}
}
}
impl From<tokio_tungstenite::tungstenite::Error> for Error {
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
Error::WebSocket(err)
}
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Error::Http(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Error::Json(err)
}
}
#[derive(Debug, Clone)]
pub struct ApiError {
pub status_code: u16,
pub code: String,
pub message: String,
pub request_id: String,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.request_id.is_empty() {
write!(
f,
"qai: {} {}: {}",
self.status_code, self.code, self.message
)
} else {
write!(
f,
"qai: {} {}: {} (request_id={})",
self.status_code, self.code, self.message, self.request_id
)
}
}
}
impl std::error::Error for ApiError {}
impl ApiError {
pub fn is_rate_limit(&self) -> bool {
self.status_code == 429
}
pub fn is_auth(&self) -> bool {
self.status_code == 401 || self.status_code == 403
}
pub fn is_not_found(&self) -> bool {
self.status_code == 404
}
}
pub fn is_rate_limit_error(err: &Error) -> bool {
matches!(err, Error::Api(e) if e.is_rate_limit())
}
pub fn is_auth_error(err: &Error) -> bool {
matches!(err, Error::Api(e) if e.is_auth())
}
pub fn is_not_found_error(err: &Error) -> bool {
matches!(err, Error::Api(e) if e.is_not_found())
}
#[derive(Deserialize)]
pub(crate) struct ApiErrorBody {
pub error: ApiErrorInner,
}
#[derive(Deserialize)]
pub(crate) struct ApiErrorInner {
#[serde(default)]
pub message: String,
#[serde(default)]
pub code: String,
#[serde(rename = "type", default)]
pub error_type: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorCode {
AuthHeaderMissing,
AuthHeaderEmpty,
KeyBearerMalformed,
KeyNotFound,
KeyExpired,
KeyRevokedByAdmin,
KeyRevokedByOwner,
KeyFrozenByBudget,
KeyPartnerRejected,
SessionExpired,
EphemeralExpired,
ScopeEndpointDenied,
AdminRequired,
ServiceAccountRequired,
InsufficientBalance,
TrialExpired,
SubscriptionLapsed,
SpendCapExceeded,
BudgetFrozen,
PaymentNotConfigured,
BillingPortalNoHistory,
RateLimitedPerKey,
RateLimitedPerIP,
QuotaExceeded,
ProviderRateLimited,
ProviderUnavailable,
ProviderAuthFailed,
ProviderInvalidRequest,
ContentRejected,
ModelNotAvailable,
InvalidRequestBody,
MissingRequiredField,
FieldTooLong,
InvalidAttachment,
AttachmentTooLarge,
UnsupportedCapability,
InternalError,
ServiceUnavailable,
StripeApiError,
IdempotencyConflict,
RecipeBoxPaywall,
Unknown,
}
impl ErrorCode {
pub fn from_wire(code: &str) -> Self {
match code {
"AUTH_HEADER_MISSING" => Self::AuthHeaderMissing,
"AUTH_HEADER_EMPTY" => Self::AuthHeaderEmpty,
"KEY_BEARER_MALFORMED" => Self::KeyBearerMalformed,
"KEY_NOT_FOUND" => Self::KeyNotFound,
"KEY_EXPIRED" => Self::KeyExpired,
"KEY_REVOKED_BY_ADMIN" => Self::KeyRevokedByAdmin,
"KEY_REVOKED_BY_OWNER" => Self::KeyRevokedByOwner,
"KEY_FROZEN_BY_BUDGET" => Self::KeyFrozenByBudget,
"KEY_PARTNER_REJECTED" => Self::KeyPartnerRejected,
"SESSION_EXPIRED" => Self::SessionExpired,
"EPHEMERAL_EXPIRED" => Self::EphemeralExpired,
"SCOPE_ENDPOINT_DENIED" => Self::ScopeEndpointDenied,
"ADMIN_REQUIRED" => Self::AdminRequired,
"SERVICE_ACCOUNT_REQUIRED" => Self::ServiceAccountRequired,
"INSUFFICIENT_BALANCE" => Self::InsufficientBalance,
"TRIAL_EXPIRED" => Self::TrialExpired,
"SUBSCRIPTION_LAPSED" => Self::SubscriptionLapsed,
"SPEND_CAP_EXCEEDED" => Self::SpendCapExceeded,
"BUDGET_FROZEN" => Self::BudgetFrozen,
"PAYMENT_NOT_CONFIGURED" => Self::PaymentNotConfigured,
"BILLING_PORTAL_NO_HISTORY" => Self::BillingPortalNoHistory,
"RATE_LIMITED_PER_KEY" => Self::RateLimitedPerKey,
"RATE_LIMITED_PER_IP" => Self::RateLimitedPerIP,
"QUOTA_EXCEEDED" => Self::QuotaExceeded,
"PROVIDER_RATE_LIMITED" => Self::ProviderRateLimited,
"PROVIDER_UNAVAILABLE" => Self::ProviderUnavailable,
"PROVIDER_AUTH_FAILED" => Self::ProviderAuthFailed,
"PROVIDER_INVALID_REQUEST" => Self::ProviderInvalidRequest,
"CONTENT_REJECTED" => Self::ContentRejected,
"MODEL_NOT_AVAILABLE" => Self::ModelNotAvailable,
"INVALID_REQUEST_BODY" => Self::InvalidRequestBody,
"MISSING_REQUIRED_FIELD" => Self::MissingRequiredField,
"FIELD_TOO_LONG" => Self::FieldTooLong,
"INVALID_ATTACHMENT" => Self::InvalidAttachment,
"ATTACHMENT_TOO_LARGE" => Self::AttachmentTooLarge,
"UNSUPPORTED_CAPABILITY" => Self::UnsupportedCapability,
"INTERNAL_ERROR" => Self::InternalError,
"SERVICE_UNAVAILABLE" => Self::ServiceUnavailable,
"STRIPE_API_ERROR" => Self::StripeApiError,
"IDEMPOTENCY_CONFLICT" => Self::IdempotencyConflict,
"RECIPE_BOX_PAYWALL" => Self::RecipeBoxPaywall,
_ => Self::Unknown,
}
}
}
impl ApiError {
pub fn typed_code(&self) -> ErrorCode {
ErrorCode::from_wire(&self.code)
}
}