use serde::{Deserialize, Serialize};
use std::{fmt, time::Duration};
use thiserror::Error;
use crate::ResponseMetadata;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
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,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum StreamErrorKind {
InvalidEventEncoding,
InvalidEventJson,
InvalidProviderEvent,
ProviderStreamError,
IncompleteEventStream,
InvalidArrayElement {
index: usize,
},
MissingArray,
IncompleteArray {
next_index: usize,
},
InvalidArrayEnvelope,
IncompleteArrayEnvelope,
}
impl fmt::Display for StreamErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidEventEncoding => f.write_str("invalid SSE event encoding"),
Self::InvalidEventJson => f.write_str("invalid SSE event JSON"),
Self::InvalidProviderEvent => f.write_str("invalid provider stream event"),
Self::ProviderStreamError => f.write_str("provider stream error"),
Self::IncompleteEventStream => {
f.write_str("incomplete stream without a provider terminal event")
}
Self::InvalidArrayElement { index } => {
write!(f, "invalid streamed array element at index {index}")
}
Self::MissingArray => f.write_str("missing streamed array"),
Self::IncompleteArray { next_index } => {
write!(
f,
"incomplete streamed array before element index {next_index}"
)
}
Self::InvalidArrayEnvelope => f.write_str("invalid streamed array envelope"),
Self::IncompleteArrayEnvelope => f.write_str("incomplete streamed array envelope"),
}
}
}
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,
response: Option<Box<ResponseMetadata>>,
},
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Schema error: {0}")]
SchemaError(String),
#[error("{provider} cannot represent {context} at {path}: {message}")]
SchemaCompatibilityError {
provider: Box<str>,
context: Box<str>,
path: Box<str>,
message: Box<str>,
},
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Failed to decode structured output at {path}: {message}")]
OutputDecodeError {
path: String,
message: String,
},
#[error("Failed to decode tool arguments at {path}: {message}")]
ToolArgumentDecodeError {
path: String,
message: String,
},
#[error("Streaming error ({kind}): {message}")]
StreamingError {
kind: StreamErrorKind,
message: Box<str>,
},
#[error("Timeout error")]
Timeout,
#[error("Unsupported operation: {0}")]
Unsupported(String),
#[cfg(feature = "_client")]
#[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,
response: None,
}
}
pub fn api_error_with_response(
provider: impl Into<String>,
kind: ApiErrorKind,
response: ResponseMetadata,
) -> Self {
RStructorError::ApiError {
provider: provider.into(),
kind,
response: Some(Box::new(response)),
}
}
pub fn api_error_kind(&self) -> Option<&ApiErrorKind> {
match self {
RStructorError::ApiError { kind, .. } => Some(kind),
_ => None,
}
}
#[must_use]
pub fn response_metadata(&self) -> Option<&ResponseMetadata> {
match self {
RStructorError::ApiError { response, .. } => response.as_deref(),
_ => None,
}
}
#[must_use]
pub fn status_code(&self) -> Option<u16> {
self.response_metadata().map(|response| response.status)
}
#[must_use]
pub fn request_id(&self) -> Option<&str> {
self.response_metadata()
.and_then(ResponseMetadata::request_id)
}
pub fn is_retryable(&self) -> bool {
match self {
RStructorError::ApiError { kind, .. } => kind.is_retryable(),
RStructorError::Timeout => true,
#[cfg(feature = "_client")]
RStructorError::HttpError(e) => e.is_connect() || e.is_timeout(),
_ => false,
}
}
pub fn retry_delay(&self) -> Option<Duration> {
match self {
RStructorError::ApiError { kind, .. } => kind.retry_delay(),
RStructorError::Timeout => Some(Duration::from_secs(1)),
#[cfg(feature = "_client")]
RStructorError::HttpError(e) if e.is_connect() || e.is_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,
response: r1,
},
Self::ApiError {
provider: p2,
kind: k2,
response: r2,
},
) => p1 == p2 && k1 == k2 && r1 == r2,
(Self::ValidationError(a), Self::ValidationError(b)) => a == b,
(Self::SchemaError(a), Self::SchemaError(b)) => a == b,
(
Self::SchemaCompatibilityError {
provider: provider_a,
context: context_a,
path: path_a,
message: message_a,
},
Self::SchemaCompatibilityError {
provider: provider_b,
context: context_b,
path: path_b,
message: message_b,
},
) => {
provider_a == provider_b
&& context_a == context_b
&& path_a == path_b
&& message_a == message_b
}
(Self::SerializationError(a), Self::SerializationError(b)) => a == b,
(
Self::OutputDecodeError {
path: path_a,
message: message_a,
},
Self::OutputDecodeError {
path: path_b,
message: message_b,
},
) => path_a == path_b && message_a == message_b,
(
Self::ToolArgumentDecodeError {
path: path_a,
message: message_a,
},
Self::ToolArgumentDecodeError {
path: path_b,
message: message_b,
},
) => path_a == path_b && message_a == message_b,
(
Self::StreamingError {
kind: kind_a,
message: message_a,
},
Self::StreamingError {
kind: kind_b,
message: message_b,
},
) => kind_a == kind_b && message_a == message_b,
(Self::Unsupported(a), Self::Unsupported(b)) => a == b,
(Self::Timeout, Self::Timeout) => true,
#[cfg(feature = "_client")]
(Self::HttpError(_), Self::HttpError(_)) => false,
(Self::JsonError(_), Self::JsonError(_)) => false,
_ => false,
}
}
}
pub type Result<T> = std::result::Result<T, RStructorError>;