ferrous_llm_core/
error.rs

1//! Error handling for LLM providers.
2//!
3//! This module defines common error patterns and traits that all providers
4//! should implement, allowing for consistent error handling across the ecosystem.
5
6use std::error::Error;
7use std::time::Duration;
8use thiserror::Error;
9
10/// Common trait for all provider errors.
11///
12/// This trait provides a consistent interface for error handling across
13/// different providers, allowing clients to handle errors generically
14/// while still preserving provider-specific error information.
15pub trait ProviderError: Error + Send + Sync + 'static {
16    /// Get the provider-specific error code if available.
17    fn error_code(&self) -> Option<&str>;
18
19    /// Check if this error is retryable.
20    ///
21    /// Returns true if the operation that caused this error can be safely retried.
22    fn is_retryable(&self) -> bool;
23
24    /// Check if this error is due to rate limiting.
25    ///
26    /// Returns true if the error was caused by hitting rate limits.
27    fn is_rate_limited(&self) -> bool;
28
29    /// Check if this error is due to authentication issues.
30    ///
31    /// Returns true if the error was caused by invalid or missing credentials.
32    fn is_auth_error(&self) -> bool;
33
34    /// Get the suggested retry delay if this is a rate limit error.
35    ///
36    /// Returns the duration to wait before retrying, if specified by the provider.
37    fn retry_after(&self) -> Option<Duration>;
38
39    /// Check if this error is due to invalid input.
40    ///
41    /// Returns true if the error was caused by invalid request parameters.
42    fn is_invalid_input(&self) -> bool {
43        false
44    }
45
46    /// Check if this error is due to service unavailability.
47    ///
48    /// Returns true if the error was caused by the service being temporarily unavailable.
49    fn is_service_unavailable(&self) -> bool {
50        false
51    }
52
53    /// Check if this error is due to content filtering.
54    ///
55    /// Returns true if the error was caused by content being filtered or blocked.
56    fn is_content_filtered(&self) -> bool {
57        false
58    }
59}
60
61/// Common configuration errors.
62#[derive(Debug, Error)]
63pub enum ConfigError {
64    /// Missing required configuration field
65    #[error("Missing required configuration: {field}")]
66    MissingField { field: String },
67
68    /// Invalid configuration value
69    #[error("Invalid configuration value for {field}: {message}")]
70    InvalidValue { field: String, message: String },
71
72    /// Invalid URL format
73    #[error("Invalid URL: {url}")]
74    InvalidUrl { url: String },
75
76    /// Invalid API key format
77    #[error("Invalid API key format")]
78    InvalidApiKey,
79
80    /// Configuration validation failed
81    #[error("Configuration validation failed: {message}")]
82    ValidationFailed { message: String },
83}
84
85/// Common request errors.
86#[derive(Debug, Error)]
87pub enum RequestError {
88    /// Invalid request parameters
89    #[error("Invalid request: {message}")]
90    InvalidRequest { message: String },
91
92    /// Request too large
93    #[error("Request too large: {size} bytes exceeds limit of {limit} bytes")]
94    RequestTooLarge { size: usize, limit: usize },
95
96    /// Unsupported feature
97    #[error("Unsupported feature: {feature}")]
98    UnsupportedFeature { feature: String },
99
100    /// Invalid message format
101    #[error("Invalid message format: {message}")]
102    InvalidMessage { message: String },
103
104    /// Invalid tool definition
105    #[error("Invalid tool definition: {message}")]
106    InvalidTool { message: String },
107}
108
109/// Common response errors.
110#[derive(Debug, Error)]
111pub enum ResponseError {
112    /// Failed to parse response
113    #[error("Failed to parse response: {message}")]
114    ParseError { message: String },
115
116    /// Unexpected response format
117    #[error("Unexpected response format: expected {expected}, got {actual}")]
118    UnexpectedFormat { expected: String, actual: String },
119
120    /// Missing required response field
121    #[error("Missing required response field: {field}")]
122    MissingField { field: String },
123
124    /// Invalid response data
125    #[error("Invalid response data: {message}")]
126    InvalidData { message: String },
127}
128
129/// Common network errors.
130#[derive(Debug, Error)]
131pub enum NetworkError {
132    /// HTTP request failed
133    #[error("HTTP request failed: {status}")]
134    HttpError { status: u16, message: String },
135
136    /// Connection timeout
137    #[error("Connection timeout after {timeout:?}")]
138    Timeout { timeout: Duration },
139
140    /// Connection failed
141    #[error("Connection failed: {message}")]
142    ConnectionFailed { message: String },
143
144    /// DNS resolution failed
145    #[error("DNS resolution failed: {host}")]
146    DnsError { host: String },
147
148    /// TLS/SSL error
149    #[error("TLS error: {message}")]
150    TlsError { message: String },
151}
152
153/// A generic error type that can wrap any provider error.
154#[derive(Debug, Error)]
155pub enum LlmError<E: ProviderError> {
156    /// Provider-specific error
157    #[error("Provider error: {0}")]
158    Provider(E),
159
160    /// Configuration error
161    #[error("Configuration error: {0}")]
162    Config(#[from] ConfigError),
163
164    /// Request error
165    #[error("Request error: {0}")]
166    Request(#[from] RequestError),
167
168    /// Response error
169    #[error("Response error: {0}")]
170    Response(#[from] ResponseError),
171
172    /// Network error
173    #[error("Network error: {0}")]
174    Network(#[from] NetworkError),
175
176    /// Memory error (for memory-enabled providers)
177    #[error("Memory error: {message}")]
178    Memory { message: String },
179
180    /// Tool execution error
181    #[error("Tool execution error: {message}")]
182    ToolExecution { message: String },
183
184    /// Generic error for cases not covered above
185    #[error("Error: {message}")]
186    Other { message: String },
187}
188
189impl<E: ProviderError> ProviderError for LlmError<E> {
190    fn error_code(&self) -> Option<&str> {
191        match self {
192            Self::Provider(e) => e.error_code(),
193            Self::Config(_) => Some("config_error"),
194            Self::Request(_) => Some("request_error"),
195            Self::Response(_) => Some("response_error"),
196            Self::Network(_) => Some("network_error"),
197            Self::Memory { .. } => Some("memory_error"),
198            Self::ToolExecution { .. } => Some("tool_error"),
199            Self::Other { .. } => Some("other_error"),
200        }
201    }
202
203    fn is_retryable(&self) -> bool {
204        match self {
205            Self::Provider(e) => e.is_retryable(),
206            Self::Network(NetworkError::Timeout { .. }) => true,
207            Self::Network(NetworkError::ConnectionFailed { .. }) => true,
208            Self::Network(NetworkError::HttpError { status, .. }) => {
209                // Retry on 5xx errors and some 4xx errors
210                *status >= 500 || *status == 429 || *status == 408
211            }
212            _ => false,
213        }
214    }
215
216    fn is_rate_limited(&self) -> bool {
217        match self {
218            Self::Provider(e) => e.is_rate_limited(),
219            Self::Network(NetworkError::HttpError { status, .. }) => *status == 429,
220            _ => false,
221        }
222    }
223
224    fn is_auth_error(&self) -> bool {
225        match self {
226            Self::Provider(e) => e.is_auth_error(),
227            Self::Config(ConfigError::InvalidApiKey) => true,
228            Self::Network(NetworkError::HttpError { status, .. }) => {
229                *status == 401 || *status == 403
230            }
231            _ => false,
232        }
233    }
234
235    fn retry_after(&self) -> Option<Duration> {
236        match self {
237            Self::Provider(e) => e.retry_after(),
238            Self::Network(NetworkError::HttpError { status, .. }) if *status == 429 => {
239                // Default retry after for rate limits
240                Some(Duration::from_secs(60))
241            }
242            _ => None,
243        }
244    }
245
246    fn is_invalid_input(&self) -> bool {
247        match self {
248            Self::Provider(e) => e.is_invalid_input(),
249            Self::Request(_) => true,
250            Self::Config(_) => true,
251            Self::Network(NetworkError::HttpError { status, .. }) => *status == 400,
252            _ => false,
253        }
254    }
255
256    fn is_service_unavailable(&self) -> bool {
257        match self {
258            Self::Provider(e) => e.is_service_unavailable(),
259            Self::Network(NetworkError::HttpError { status, .. }) => {
260                *status == 503 || *status == 502 || *status == 504
261            }
262            Self::Network(NetworkError::ConnectionFailed { .. }) => true,
263            _ => false,
264        }
265    }
266
267    fn is_content_filtered(&self) -> bool {
268        match self {
269            Self::Provider(e) => e.is_content_filtered(),
270            _ => false,
271        }
272    }
273}
274
275/// Result type alias for provider operations.
276pub type ProviderResult<T, E> = Result<T, E>;
277
278/// Result type alias for LLM operations with generic error.
279pub type LlmResult<T, E> = Result<T, LlmError<E>>;
280
281// Utility functions for creating common errors
282impl ConfigError {
283    /// Create a missing field error
284    pub fn missing_field(field: impl Into<String>) -> Self {
285        Self::MissingField {
286            field: field.into(),
287        }
288    }
289
290    /// Create an invalid value error
291    pub fn invalid_value(field: impl Into<String>, message: impl Into<String>) -> Self {
292        Self::InvalidValue {
293            field: field.into(),
294            message: message.into(),
295        }
296    }
297
298    /// Create an invalid URL error
299    pub fn invalid_url(url: impl Into<String>) -> Self {
300        Self::InvalidUrl { url: url.into() }
301    }
302
303    /// Create a validation failed error
304    pub fn validation_failed(message: impl Into<String>) -> Self {
305        Self::ValidationFailed {
306            message: message.into(),
307        }
308    }
309}
310
311impl RequestError {
312    /// Create an invalid request error
313    pub fn invalid_request(message: impl Into<String>) -> Self {
314        Self::InvalidRequest {
315            message: message.into(),
316        }
317    }
318
319    /// Create a request too large error
320    pub fn request_too_large(size: usize, limit: usize) -> Self {
321        Self::RequestTooLarge { size, limit }
322    }
323
324    /// Create an unsupported feature error
325    pub fn unsupported_feature(feature: impl Into<String>) -> Self {
326        Self::UnsupportedFeature {
327            feature: feature.into(),
328        }
329    }
330
331    /// Create an invalid message error
332    pub fn invalid_message(message: impl Into<String>) -> Self {
333        Self::InvalidMessage {
334            message: message.into(),
335        }
336    }
337
338    /// Create an invalid tool error
339    pub fn invalid_tool(message: impl Into<String>) -> Self {
340        Self::InvalidTool {
341            message: message.into(),
342        }
343    }
344}
345
346impl ResponseError {
347    /// Create a parse error
348    pub fn parse_error(message: impl Into<String>) -> Self {
349        Self::ParseError {
350            message: message.into(),
351        }
352    }
353
354    /// Create an unexpected format error
355    pub fn unexpected_format(expected: impl Into<String>, actual: impl Into<String>) -> Self {
356        Self::UnexpectedFormat {
357            expected: expected.into(),
358            actual: actual.into(),
359        }
360    }
361
362    /// Create a missing field error
363    pub fn missing_field(field: impl Into<String>) -> Self {
364        Self::MissingField {
365            field: field.into(),
366        }
367    }
368
369    /// Create an invalid data error
370    pub fn invalid_data(message: impl Into<String>) -> Self {
371        Self::InvalidData {
372            message: message.into(),
373        }
374    }
375}
376
377impl NetworkError {
378    /// Create an HTTP error
379    pub fn http_error(status: u16, message: impl Into<String>) -> Self {
380        Self::HttpError {
381            status,
382            message: message.into(),
383        }
384    }
385
386    /// Create a timeout error
387    pub fn timeout(timeout: Duration) -> Self {
388        Self::Timeout { timeout }
389    }
390
391    /// Create a connection failed error
392    pub fn connection_failed(message: impl Into<String>) -> Self {
393        Self::ConnectionFailed {
394            message: message.into(),
395        }
396    }
397
398    /// Create a DNS error
399    pub fn dns_error(host: impl Into<String>) -> Self {
400        Self::DnsError { host: host.into() }
401    }
402
403    /// Create a TLS error
404    pub fn tls_error(message: impl Into<String>) -> Self {
405        Self::TlsError {
406            message: message.into(),
407        }
408    }
409}