use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq)]
pub enum ApiErrorKind {
RateLimited {
retry_after: Option<Duration>,
},
InvalidModel {
model: String,
suggestion: Option<String>,
},
ServiceUnavailable,
GatewayError {
code: u16,
},
AuthenticationFailed,
PermissionDenied,
RequestTooLarge,
BadRequest {
details: String,
},
ServerError {
code: u16,
},
Other {
code: u16,
message: String,
},
UnexpectedResponse {
details: String,
},
}
impl ApiErrorKind {
pub fn is_retryable(&self) -> bool {
matches!(
self,
ApiErrorKind::RateLimited { .. }
| ApiErrorKind::ServiceUnavailable
| ApiErrorKind::GatewayError { .. }
| ApiErrorKind::ServerError { .. }
)
}
pub fn retry_delay(&self) -> Option<Duration> {
match self {
ApiErrorKind::RateLimited { retry_after } => {
Some(retry_after.unwrap_or(Duration::from_secs(5)))
}
ApiErrorKind::ServiceUnavailable => Some(Duration::from_secs(2)),
ApiErrorKind::GatewayError { .. } => Some(Duration::from_secs(1)),
ApiErrorKind::ServerError { .. } => Some(Duration::from_secs(2)),
_ => None,
}
}
pub fn user_message(&self, provider_name: &str) -> String {
match self {
ApiErrorKind::RateLimited { retry_after } => {
if let Some(duration) = retry_after {
format!(
"Rate limit exceeded. Please wait {} seconds and try again.",
duration.as_secs()
)
} else {
"Rate limit exceeded. Please wait a moment and try again.".to_string()
}
}
ApiErrorKind::InvalidModel { model, suggestion } => {
let mut msg = format!("Model '{}' not found.", model);
if let Some(s) = suggestion {
msg.push_str(&format!(" Try using '{}'.", s));
}
msg
}
ApiErrorKind::ServiceUnavailable => {
format!(
"{} service is temporarily unavailable. Please try again.",
provider_name
)
}
ApiErrorKind::GatewayError { code } => {
format!(
"Gateway error ({}). This is usually transient - please retry.",
code
)
}
ApiErrorKind::AuthenticationFailed => {
format!(
"Authentication failed. Check your {}_API_KEY environment variable.",
provider_name.to_uppercase()
)
}
ApiErrorKind::PermissionDenied => {
"Permission denied. Your API key may not have access to this model or feature."
.to_string()
}
ApiErrorKind::RequestTooLarge => {
"Request too large. Try reducing the prompt length or max_tokens.".to_string()
}
ApiErrorKind::BadRequest { details } => {
format!("Invalid request: {}", details)
}
ApiErrorKind::ServerError { code } => {
format!(
"{} server error ({}). This may be transient - please retry.",
provider_name, code
)
}
ApiErrorKind::Other { code, message } => {
format!("{} API error ({}): {}", provider_name, code, message)
}
ApiErrorKind::UnexpectedResponse { details } => {
format!(
"{} returned an unexpected response: {}",
provider_name, details
)
}
}
}
}
impl std::fmt::Display for ApiErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiErrorKind::RateLimited { retry_after } => {
write!(f, "Rate limited")?;
if let Some(d) = retry_after {
write!(f, " (retry after {}s)", d.as_secs())?;
}
Ok(())
}
ApiErrorKind::InvalidModel { model, .. } => write!(f, "Invalid model: {}", model),
ApiErrorKind::ServiceUnavailable => write!(f, "Service unavailable"),
ApiErrorKind::GatewayError { code } => write!(f, "Gateway error ({})", code),
ApiErrorKind::AuthenticationFailed => write!(f, "Authentication failed"),
ApiErrorKind::PermissionDenied => write!(f, "Permission denied"),
ApiErrorKind::RequestTooLarge => write!(f, "Request too large"),
ApiErrorKind::BadRequest { details } => write!(f, "Bad request: {}", details),
ApiErrorKind::ServerError { code } => write!(f, "Server error ({})", code),
ApiErrorKind::Other { code, message } => write!(f, "API error ({}): {}", code, message),
ApiErrorKind::UnexpectedResponse { details } => {
write!(f, "Unexpected response: {}", details)
}
}
}
}
#[derive(Error, Debug)]
pub enum RStructorError {
#[error("{}", .kind.user_message(.provider))]
ApiError {
provider: String,
kind: ApiErrorKind,
},
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Schema error: {0}")]
SchemaError(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Timeout error")]
Timeout,
#[error("HTTP client error: {0}")]
HttpError(#[from] reqwest::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
}
impl RStructorError {
pub fn api_error(provider: impl Into<String>, kind: ApiErrorKind) -> Self {
RStructorError::ApiError {
provider: provider.into(),
kind,
}
}
pub fn api_error_kind(&self) -> Option<&ApiErrorKind> {
match self {
RStructorError::ApiError { kind, .. } => Some(kind),
_ => None,
}
}
pub fn is_retryable(&self) -> bool {
match self {
RStructorError::ApiError { kind, .. } => kind.is_retryable(),
RStructorError::Timeout => true,
_ => false,
}
}
pub fn retry_delay(&self) -> Option<Duration> {
match self {
RStructorError::ApiError { kind, .. } => kind.retry_delay(),
RStructorError::Timeout => Some(Duration::from_secs(1)),
_ => None,
}
}
}
impl PartialEq for RStructorError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Self::ApiError {
provider: p1,
kind: k1,
},
Self::ApiError {
provider: p2,
kind: k2,
},
) => p1 == p2 && k1 == k2,
(Self::ValidationError(a), Self::ValidationError(b)) => a == b,
(Self::SchemaError(a), Self::SchemaError(b)) => a == b,
(Self::SerializationError(a), Self::SerializationError(b)) => a == b,
(Self::Timeout, Self::Timeout) => true,
(Self::HttpError(_), Self::HttpError(_)) => false,
(Self::JsonError(_), Self::JsonError(_)) => false,
_ => false,
}
}
}
pub type Result<T> = std::result::Result<T, RStructorError>;