use thiserror::Error;
static_assertions::assert_impl_all!(LlmError: Send, Sync, Clone);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorCategory {
Network,
Authentication,
RateLimit,
Client,
Server,
Parsing,
Validation,
Configuration,
Unsupported,
Stream,
Provider,
Unknown,
}
#[derive(Error, Debug, Clone)]
pub enum LlmError {
#[error("HTTP request failed: {0}")]
HttpError(String),
#[error("JSON error: {0}")]
JsonError(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("IO error: {0}")]
IoError(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Missing API key: {0}")]
MissingApiKey(String),
#[error("Invalid parameter: {0}")]
InvalidParameter(String),
#[error("API error: {code} - {message}")]
ApiError {
code: u16,
message: String,
details: Option<serde_json::Value>,
},
#[error("Authentication failed: {0}")]
AuthenticationError(String),
#[error("Rate limit exceeded: {0}")]
RateLimitError(String),
#[error("Quota exceeded: {0}")]
QuotaExceededError(String),
#[error("Model not supported: {0}")]
ModelNotSupported(String),
#[error("Stream error: {0}")]
StreamError(String),
#[error("Request timeout: {0}")]
TimeoutError(String),
#[error("Connection error: {0}")]
ConnectionError(String),
#[error("Provider error ({provider}): {message}")]
ProviderError {
provider: String,
message: String,
error_code: Option<String>,
},
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Internal error: {0}")]
InternalError(String),
#[error("Unsupported operation: {0}")]
UnsupportedOperation(String),
#[error("Processing error: {0}")]
ProcessingError(String),
#[error("Tool call error: {0}")]
ToolCallError(String),
#[error("Tool validation error: {0}")]
ToolValidationError(String),
#[error("Unsupported tool type: {0}")]
UnsupportedToolType(String),
#[error("Error in {context}: {message}")]
ContextualError {
context: String,
message: String,
source_error: Option<Box<LlmError>>,
metadata: std::collections::HashMap<String, String>,
},
#[error("Other error: {0}")]
Other(String),
}
impl LlmError {
pub fn api_error(code: u16, message: impl Into<String>) -> Self {
Self::ApiError {
code,
message: message.into(),
details: None,
}
}
pub fn api_error_with_details(
code: u16,
message: impl Into<String>,
details: serde_json::Value,
) -> Self {
Self::ApiError {
code,
message: message.into(),
details: Some(details),
}
}
pub fn provider_error(provider: impl Into<String>, message: impl Into<String>) -> Self {
Self::ProviderError {
provider: provider.into(),
message: message.into(),
error_code: None,
}
}
pub fn provider_error_with_code(
provider: impl Into<String>,
message: impl Into<String>,
error_code: impl Into<String>,
) -> Self {
Self::ProviderError {
provider: provider.into(),
message: message.into(),
error_code: Some(error_code.into()),
}
}
pub fn contextual_error(context: impl Into<String>, message: impl Into<String>) -> Self {
Self::ContextualError {
context: context.into(),
message: message.into(),
source_error: None,
metadata: std::collections::HashMap::new(),
}
}
pub fn contextual_error_with_source(
context: impl Into<String>,
message: impl Into<String>,
source: LlmError,
) -> Self {
Self::ContextualError {
context: context.into(),
message: message.into(),
source_error: Some(Box::new(source)),
metadata: std::collections::HashMap::new(),
}
}
pub fn contextual_error_with_metadata(
context: impl Into<String>,
message: impl Into<String>,
metadata: std::collections::HashMap<String, String>,
) -> Self {
Self::ContextualError {
context: context.into(),
message: message.into(),
source_error: None,
metadata,
}
}
pub fn is_retryable(&self) -> bool {
match self {
Self::HttpError(e) => {
let retryable_keywords = [
"timeout",
"connect",
"network",
"dns",
"socket",
"connection reset",
"connection refused",
"temporary failure",
];
retryable_keywords
.iter()
.any(|keyword| e.to_lowercase().contains(keyword))
}
Self::ApiError { code, .. } => {
match *code {
408 | 429 => true, 500..=599 => true,
400..=499 => false,
_ => false,
}
}
Self::RateLimitError(_) => true,
Self::TimeoutError(_) => true,
Self::ConnectionError(_) => true,
Self::QuotaExceededError(_) => false, Self::AuthenticationError(_) => false, Self::ModelNotSupported(_) => false, Self::InvalidInput(_) | Self::InvalidParameter(_) => false, Self::ContextualError {
source_error: Some(source),
..
} => source.is_retryable(),
Self::ContextualError { .. } => false, _ => false,
}
}
pub const fn is_auth_error(&self) -> bool {
match self {
Self::AuthenticationError(_) => true,
Self::ApiError { code, .. } => *code == 401 || *code == 403,
_ => false,
}
}
pub const fn is_rate_limit_error(&self) -> bool {
match self {
Self::RateLimitError(_) => true,
Self::ApiError { code, .. } => *code == 429,
_ => false,
}
}
pub const fn status_code(&self) -> Option<u16> {
match self {
Self::ApiError { code, .. } => Some(*code),
Self::HttpError(_) => None, _ => None,
}
}
pub fn category(&self) -> ErrorCategory {
match self {
Self::HttpError(_) | Self::ConnectionError(_) | Self::TimeoutError(_) => {
ErrorCategory::Network
}
Self::AuthenticationError(_) | Self::MissingApiKey(_) => ErrorCategory::Authentication,
Self::RateLimitError(_) | Self::QuotaExceededError(_) => ErrorCategory::RateLimit,
Self::ApiError { code, .. } => match *code {
429 => ErrorCategory::RateLimit,
400..=499 => ErrorCategory::Client,
500..=599 => ErrorCategory::Server,
_ => ErrorCategory::Unknown,
},
Self::JsonError(_) | Self::ParseError(_) => ErrorCategory::Parsing,
Self::InvalidInput(_) | Self::InvalidParameter(_) | Self::ToolValidationError(_) => {
ErrorCategory::Validation
}
Self::ConfigurationError(_) => ErrorCategory::Configuration,
Self::ModelNotSupported(_)
| Self::UnsupportedOperation(_)
| Self::UnsupportedToolType(_) => ErrorCategory::Unsupported,
Self::StreamError(_) => ErrorCategory::Stream,
Self::ProviderError { .. } | Self::ToolCallError(_) => ErrorCategory::Provider,
Self::ContextualError {
source_error: Some(source),
..
} => source.category(),
Self::ContextualError { .. } => ErrorCategory::Unknown,
_ => ErrorCategory::Unknown,
}
}
pub fn user_message(&self) -> String {
match self {
Self::AuthenticationError(_) | Self::MissingApiKey(_) => {
"Authentication failed. Please check your API key.".to_string()
}
Self::RateLimitError(_) => {
"Rate limit exceeded. Please wait before making more requests.".to_string()
}
Self::QuotaExceededError(_) => {
"API quota exceeded. Please check your usage limits.".to_string()
}
Self::ModelNotSupported(model) => {
format!("The model '{model}' is not supported by this provider.")
}
Self::ConnectionError(_) | Self::TimeoutError(_) => {
"Network connection failed. Please check your internet connection and try again."
.to_string()
}
Self::ApiError {
code: 500..=599, ..
} => "The service is temporarily unavailable. Please try again later.".to_string(),
_ => self.to_string(),
}
}
pub fn recovery_suggestions(&self) -> Vec<String> {
match self {
Self::AuthenticationError(_) | Self::MissingApiKey(_) => {
vec![
"Verify your API key is correct and properly formatted".to_string(),
"Check if your API key has the required permissions for this operation"
.to_string(),
"Ensure your API key is not expired or revoked".to_string(),
"Verify you're using the correct API endpoint".to_string(),
]
}
Self::RateLimitError(_) => {
vec![
"Implement exponential backoff with jitter".to_string(),
"Reduce request frequency".to_string(),
"Consider upgrading your API plan for higher limits".to_string(),
"Use request batching if supported".to_string(),
]
}
Self::QuotaExceededError(_) => {
vec![
"Check your usage dashboard for current consumption".to_string(),
"Upgrade your API plan for higher quotas".to_string(),
"Wait for quota reset (usually monthly)".to_string(),
"Optimize your requests to use fewer tokens".to_string(),
]
}
Self::ConnectionError(_) | Self::TimeoutError(_) => {
vec![
"Check your internet connection stability".to_string(),
"Retry the request with exponential backoff".to_string(),
"Increase timeout settings for large requests".to_string(),
"Check if the service is experiencing outages".to_string(),
]
}
Self::ModelNotSupported(_) => {
vec![
"Use a supported model from the provider's model list".to_string(),
"Check the provider's documentation for available models".to_string(),
"Verify the model name is spelled correctly".to_string(),
]
}
Self::ApiError { code: 400, .. } => {
vec![
"Check your request parameters for validity".to_string(),
"Ensure all required fields are provided".to_string(),
"Verify parameter types and formats".to_string(),
]
}
Self::ApiError {
code: 500..=599, ..
} => {
vec![
"Retry the request after a delay (server error)".to_string(),
"Check the service status page for outages".to_string(),
"Contact support if the issue persists".to_string(),
]
}
Self::StreamError(_) => {
vec![
"Retry the streaming request".to_string(),
"Check network stability for streaming".to_string(),
"Consider using non-streaming mode as fallback".to_string(),
]
}
Self::InvalidInput(_) | Self::InvalidParameter(_) => {
vec![
"Validate your input parameters".to_string(),
"Check parameter constraints and limits".to_string(),
"Refer to the API documentation for valid formats".to_string(),
]
}
_ => vec![
"Check the error details and documentation".to_string(),
"Contact support if the issue persists".to_string(),
],
}
}
pub const fn recommended_retry_delay(&self) -> Option<u64> {
match self {
Self::RateLimitError(_) => Some(60), Self::ApiError { code: 429, .. } => Some(30), Self::ApiError {
code: 500..=599, ..
} => Some(5), Self::TimeoutError(_) => Some(10), Self::ConnectionError(_) => Some(5), _ => None, }
}
pub const fn max_retry_attempts(&self) -> u32 {
match self {
Self::RateLimitError(_) => 3,
Self::ApiError { code: 429, .. } => 3,
Self::ApiError {
code: 500..=599, ..
} => 5,
Self::TimeoutError(_) => 3,
Self::ConnectionError(_) => 3,
_ => 0, }
}
}
pub type Result<T> = std::result::Result<T, LlmError>;
impl From<reqwest::Error> for LlmError {
fn from(err: reqwest::Error) -> Self {
Self::HttpError(err.to_string())
}
}
impl From<serde_json::Error> for LlmError {
fn from(err: serde_json::Error) -> Self {
Self::JsonError(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let error = LlmError::api_error(404, "Not found");
assert!(matches!(error, LlmError::ApiError { code: 404, .. }));
}
#[test]
fn test_retryable_errors() {
let rate_limit = LlmError::RateLimitError("Too many requests".to_string());
assert!(rate_limit.is_retryable());
let server_error = LlmError::api_error(500, "Internal server error");
assert!(server_error.is_retryable());
let auth_error = LlmError::AuthenticationError("Invalid key".to_string());
assert!(!auth_error.is_retryable());
}
#[test]
fn test_auth_errors() {
let auth_error = LlmError::AuthenticationError("Invalid key".to_string());
assert!(auth_error.is_auth_error());
let api_401 = LlmError::api_error(401, "Unauthorized");
assert!(api_401.is_auth_error());
}
#[test]
fn test_error_categories() {
let auth_error = LlmError::AuthenticationError("Invalid key".to_string());
assert_eq!(auth_error.category(), ErrorCategory::Authentication);
let rate_limit = LlmError::RateLimitError("Too many requests".to_string());
assert_eq!(rate_limit.category(), ErrorCategory::RateLimit);
let server_error = LlmError::api_error(500, "Internal server error");
assert_eq!(server_error.category(), ErrorCategory::Server);
let client_error = LlmError::api_error(400, "Bad request");
assert_eq!(client_error.category(), ErrorCategory::Client);
let parse_error = LlmError::JsonError("Invalid JSON".to_string());
assert_eq!(parse_error.category(), ErrorCategory::Parsing);
}
#[test]
fn test_user_messages() {
let auth_error = LlmError::AuthenticationError("Invalid key".to_string());
let user_msg = auth_error.user_message();
assert!(user_msg.contains("Authentication failed"));
let rate_limit = LlmError::RateLimitError("Too many requests".to_string());
let user_msg = rate_limit.user_message();
assert!(user_msg.contains("Rate limit exceeded"));
}
#[test]
fn test_recovery_suggestions() {
let auth_error = LlmError::AuthenticationError("Invalid key".to_string());
let suggestions = auth_error.recovery_suggestions();
assert!(!suggestions.is_empty());
assert!(suggestions.iter().any(|s| s.contains("API key")));
let rate_limit = LlmError::RateLimitError("Too many requests".to_string());
let suggestions = rate_limit.recovery_suggestions();
assert!(suggestions.iter().any(|s| s.contains("backoff")));
}
}