use thiserror::Error;
#[derive(Debug, Clone)]
pub struct HttpErrorDetail {
pub status: u16,
pub body: String,
pub provider: Option<String>,
pub request_id: Option<String>,
}
impl HttpErrorDetail {
pub fn new(status: u16, body: String) -> Self {
Self {
status,
body,
provider: None,
request_id: None,
}
}
pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
self.provider = Some(provider.into());
self
}
pub fn with_request_id(mut self, request_id: Option<String>) -> Self {
self.request_id = request_id;
self
}
}
impl std::fmt::Display for HttpErrorDetail {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "HTTP error {}: {}", self.status, self.body)?;
if let Some(provider) = &self.provider {
write!(f, " [{provider}]")?;
}
if let Some(id) = &self.request_id {
write!(f, " (request-id: {id})")?;
}
Ok(())
}
}
#[derive(Error, Debug)]
pub enum ProviderError {
#[error("Missing API key")]
MissingApiKey,
#[error("Unknown provider: {0}")]
UnknownProvider(String),
#[error("Provider not implemented: {0}")]
NotImplemented(String),
#[error("{0}")]
HttpError(HttpErrorDetail),
#[error("Request failed: {0}")]
RequestFailed(#[from] reqwest::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Invalid response: {0}")]
InvalidResponse(String),
#[error("Invalid API key format")]
InvalidApiKey,
#[error("JSON parse error: {0}")]
JsonParse(#[from] serde_json::Error),
#[error("Stream error: {0}")]
StreamError(String),
#[error("Network error: {0}")]
NetworkError(String),
#[error("Context overflow")]
ContextOverflow,
#[error("Request timed out")]
Timeout,
#[error("Rate limited")]
RateLimited {
retry_after: Option<std::time::Duration>,
},
}
impl ProviderError {
pub fn is_retryable(&self) -> bool {
match self {
Self::HttpError(detail) => detail.status == 429 || detail.status >= 500,
Self::NetworkError(_) => true,
Self::Timeout => true,
Self::RateLimited { .. } => true,
_ => false,
}
}
pub fn retry_after(&self) -> Option<std::time::Duration> {
match self {
Self::RateLimited { retry_after } => *retry_after,
Self::HttpError(detail) if detail.status == 429 => {
Some(std::time::Duration::from_secs(5))
}
_ => None,
}
}
pub fn http_status(&self) -> Option<u16> {
match self {
Self::HttpError(detail) => Some(detail.status),
_ => None,
}
}
}
#[derive(Error, Debug)]
pub enum ValidationError {
#[error("Invalid JSON: {0}")]
InvalidJson(#[from] serde_json::Error),
#[error("Schema validation failed: {0}")]
SchemaValidation(String),
#[error("Missing required field: {0}")]
MissingRequiredField(String),
}
#[derive(Error, Debug)]
pub enum Error {
#[error("Provider error: {0}")]
Provider(#[from] ProviderError),
#[error("Validation error: {0}")]
Validation(#[from] ValidationError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provider_error_display() {
assert_eq!(ProviderError::MissingApiKey.to_string(), "Missing API key");
assert_eq!(
ProviderError::UnknownProvider("foo".to_string()).to_string(),
"Unknown provider: foo"
);
assert_eq!(
ProviderError::HttpError(HttpErrorDetail::new(429, "rate limited".to_string()))
.to_string(),
"HTTP error 429: rate limited"
);
assert_eq!(
ProviderError::HttpError(
HttpErrorDetail::new(500, "boom".to_string())
.with_provider("anthropic")
.with_request_id(Some("req_123".to_string()))
)
.to_string(),
"HTTP error 500: boom [anthropic] (request-id: req_123)"
);
assert_eq!(
ProviderError::InvalidResponse("bad json".to_string()).to_string(),
"Invalid response: bad json"
);
assert_eq!(
ProviderError::StreamError("disconnected".to_string()).to_string(),
"Stream error: disconnected"
);
assert_eq!(
ProviderError::NotImplemented("x".to_string()).to_string(),
"Provider not implemented: x"
);
}
#[test]
fn error_chain_from_provider_error() {
let inner = ProviderError::MissingApiKey;
let outer: Error = inner.into();
assert!(matches!(
outer,
Error::Provider(ProviderError::MissingApiKey)
));
assert!(outer.to_string().contains("Missing API key"));
}
#[test]
fn validation_error_display() {
let err = ValidationError::MissingRequiredField("model".to_string());
assert_eq!(err.to_string(), "Missing required field: model");
}
#[test]
fn error_chain_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let outer: Error = io_err.into();
assert!(matches!(outer, Error::Io(_)));
}
}