ai_lib/types/
error.rs

1//! 错误处理模块,提供统一的错误类型和处理机制
2//!
3//! Error handling module providing unified error types and handling mechanisms.
4//!
5//! This module defines `AiLibError` as the primary error type throughout ai-lib,
6//! with proper error classification for retry logic and observability.
7
8use crate::transport::TransportError;
9use thiserror::Error;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ErrorSeverity {
13    /// Transient errors - safe to retry
14    Transient,
15    /// Client-side issues - fix configuration/request
16    Client,
17    /// Provider/server-side issues
18    Server,
19    /// Fatal issues - do not retry automatically
20    Fatal,
21}
22
23#[derive(Error, Debug, Clone)]
24pub enum AiLibError {
25    #[error("Provider error: {0}")]
26    ProviderError(String),
27
28    #[error("Transport error: {0}")]
29    TransportError(#[from] TransportError),
30
31    #[error("Invalid request: {0}")]
32    InvalidRequest(String),
33
34    #[error("Rate limit exceeded: {0}")]
35    RateLimitExceeded(String),
36
37    #[error("Authentication failed: {0}")]
38    AuthenticationError(String),
39
40    #[error("Configuration error: {0}")]
41    ConfigurationError(String),
42
43    #[error("Network error: {0}")]
44    NetworkError(String),
45
46    #[error("Timeout error: {0}")]
47    TimeoutError(String),
48
49    #[error("Retry exhausted: {0}")]
50    RetryExhausted(String),
51
52    #[error("Serialization error: {0}")]
53    SerializationError(String),
54
55    #[error("Deserialization error: {0}")]
56    DeserializationError(String),
57
58    #[error("File operation error: {0}")]
59    FileError(String),
60
61    #[error("Unsupported feature: {0}")]
62    UnsupportedFeature(String),
63
64    #[error("Model not found: {0}")]
65    ModelNotFound(String),
66
67    #[error("Invalid model response: {0}")]
68    InvalidModelResponse(String),
69
70    #[error("Context length exceeded: {0}")]
71    ContextLengthExceeded(String),
72}
73
74impl AiLibError {
75    /// Classify the error by severity for observability/failover logic
76    pub fn severity(&self) -> ErrorSeverity {
77        match self {
78            AiLibError::NetworkError(_)
79            | AiLibError::TimeoutError(_)
80            | AiLibError::RateLimitExceeded(_) => ErrorSeverity::Transient,
81            AiLibError::InvalidRequest(_)
82            | AiLibError::ConfigurationError(_)
83            | AiLibError::ContextLengthExceeded(_)
84            | AiLibError::SerializationError(_)
85            | AiLibError::DeserializationError(_)
86            | AiLibError::FileError(_)
87            | AiLibError::ModelNotFound(_) => ErrorSeverity::Client,
88            AiLibError::AuthenticationError(_)
89            | AiLibError::UnsupportedFeature(_)
90            | AiLibError::RetryExhausted(_) => ErrorSeverity::Fatal,
91            AiLibError::ProviderError(_) | AiLibError::InvalidModelResponse(_) => {
92                ErrorSeverity::Server
93            }
94            AiLibError::TransportError(err) => match err {
95                TransportError::Timeout(_) | TransportError::RateLimitExceeded => {
96                    ErrorSeverity::Transient
97                }
98                TransportError::InvalidUrl(_)
99                | TransportError::JsonError(_)
100                | TransportError::ClientError { .. } => ErrorSeverity::Client,
101                TransportError::AuthenticationError(_) => ErrorSeverity::Fatal,
102                TransportError::ServerError { .. } | TransportError::HttpError(_) => {
103                    ErrorSeverity::Server
104                }
105            },
106        }
107    }
108
109    /// Structured error code for logs/metrics
110    pub fn error_code(&self) -> &'static str {
111        match self {
112            AiLibError::ProviderError(_) => "PROVIDER_ERROR",
113            AiLibError::TransportError(_) => "TRANSPORT_ERROR",
114            AiLibError::InvalidRequest(_) => "INVALID_REQUEST",
115            AiLibError::RateLimitExceeded(_) => "RATE_LIMIT",
116            AiLibError::AuthenticationError(_) => "AUTH_FAILED",
117            AiLibError::ConfigurationError(_) => "CONFIG_ERROR",
118            AiLibError::NetworkError(_) => "NETWORK_ERROR",
119            AiLibError::TimeoutError(_) => "TIMEOUT",
120            AiLibError::RetryExhausted(_) => "RETRY_EXHAUSTED",
121            AiLibError::SerializationError(_) => "SERIALIZATION_ERROR",
122            AiLibError::DeserializationError(_) => "DESERIALIZATION_ERROR",
123            AiLibError::FileError(_) => "FILE_ERROR",
124            AiLibError::UnsupportedFeature(_) => "UNSUPPORTED_FEATURE",
125            AiLibError::ModelNotFound(_) => "MODEL_NOT_FOUND",
126            AiLibError::InvalidModelResponse(_) => "INVALID_RESPONSE",
127            AiLibError::ContextLengthExceeded(_) => "CONTEXT_TOO_LONG",
128        }
129    }
130
131    /// Returns an uppercase code that embeds severity, e.g. `TRANSIENT_TIMEOUT`
132    pub fn error_code_with_severity(&self) -> String {
133        format!("{:?}_{}", self.severity(), self.error_code()).to_uppercase()
134    }
135
136    /// Attach contextual information to the error message.
137    pub fn with_context(self, context: impl Into<String>) -> Self {
138        let ctx = context.into();
139        match self {
140            AiLibError::ProviderError(msg) => AiLibError::ProviderError(prepend_context(&ctx, msg)),
141            AiLibError::InvalidRequest(msg) => {
142                AiLibError::InvalidRequest(prepend_context(&ctx, msg))
143            }
144            AiLibError::RateLimitExceeded(msg) => {
145                AiLibError::RateLimitExceeded(prepend_context(&ctx, msg))
146            }
147            AiLibError::AuthenticationError(msg) => {
148                AiLibError::AuthenticationError(prepend_context(&ctx, msg))
149            }
150            AiLibError::ConfigurationError(msg) => {
151                AiLibError::ConfigurationError(prepend_context(&ctx, msg))
152            }
153            AiLibError::NetworkError(msg) => AiLibError::NetworkError(prepend_context(&ctx, msg)),
154            AiLibError::TimeoutError(msg) => AiLibError::TimeoutError(prepend_context(&ctx, msg)),
155            AiLibError::RetryExhausted(msg) => {
156                AiLibError::RetryExhausted(prepend_context(&ctx, msg))
157            }
158            AiLibError::SerializationError(msg) => {
159                AiLibError::SerializationError(prepend_context(&ctx, msg))
160            }
161            AiLibError::DeserializationError(msg) => {
162                AiLibError::DeserializationError(prepend_context(&ctx, msg))
163            }
164            AiLibError::FileError(msg) => AiLibError::FileError(prepend_context(&ctx, msg)),
165            AiLibError::UnsupportedFeature(msg) => {
166                AiLibError::UnsupportedFeature(prepend_context(&ctx, msg))
167            }
168            AiLibError::ModelNotFound(msg) => AiLibError::ModelNotFound(prepend_context(&ctx, msg)),
169            AiLibError::InvalidModelResponse(msg) => {
170                AiLibError::InvalidModelResponse(prepend_context(&ctx, msg))
171            }
172            AiLibError::ContextLengthExceeded(msg) => {
173                AiLibError::ContextLengthExceeded(prepend_context(&ctx, msg))
174            }
175            AiLibError::TransportError(err) => {
176                AiLibError::TransportError(transport_with_context(err, &ctx))
177            }
178        }
179    }
180
181    /// Determine if the error is retryable
182    pub fn is_retryable(&self) -> bool {
183        match self {
184            AiLibError::NetworkError(_) => true,
185            AiLibError::TimeoutError(_) => true,
186            AiLibError::RateLimitExceeded(_) => true,
187            AiLibError::TransportError(transport_err) => {
188                // Check if it's a temporary network error
189                transport_err.to_string().contains("timeout")
190                    || transport_err.to_string().contains("connection")
191                    || transport_err.to_string().contains("temporary")
192            }
193            _ => false,
194        }
195    }
196
197    /// Get suggested retry delay (milliseconds)
198    pub fn retry_delay_ms(&self) -> u64 {
199        match self {
200            AiLibError::RateLimitExceeded(_) => 60000, // 1 minute
201            AiLibError::NetworkError(_) => 1000,       // 1 second
202            AiLibError::TimeoutError(_) => 2000,       // 2 seconds
203            _ => 1000,
204        }
205    }
206
207    /// Get error context for debugging
208    pub fn context(&self) -> &str {
209        match self {
210            AiLibError::ProviderError(_) => "Provider API call failed",
211            AiLibError::TransportError(_) => "Network transport layer error",
212            AiLibError::InvalidRequest(_) => "Invalid request parameters",
213            AiLibError::RateLimitExceeded(_) => "API rate limit exceeded",
214            AiLibError::AuthenticationError(_) => "Authentication failed",
215            AiLibError::ConfigurationError(_) => "Configuration validation failed",
216            AiLibError::NetworkError(_) => "Network connectivity issue",
217            AiLibError::TimeoutError(_) => "Request timed out",
218            AiLibError::RetryExhausted(_) => "All retry attempts failed",
219            AiLibError::SerializationError(_) => "Request serialization failed",
220            AiLibError::DeserializationError(_) => "Response deserialization failed",
221            AiLibError::FileError(_) => "File operation failed",
222            AiLibError::UnsupportedFeature(_) => "Feature not supported by provider",
223            AiLibError::ModelNotFound(_) => "Specified model not found",
224            AiLibError::InvalidModelResponse(_) => "Invalid response from model",
225            AiLibError::ContextLengthExceeded(_) => "Context length limit exceeded",
226        }
227    }
228
229    /// Check if error is related to authentication
230    pub fn is_auth_error(&self) -> bool {
231        match self {
232            AiLibError::AuthenticationError(_) => true,
233            AiLibError::TransportError(TransportError::AuthenticationError(_)) => true,
234            AiLibError::TransportError(TransportError::ClientError { status, .. }) => {
235                *status == 401 || *status == 403
236            }
237            _ => false,
238        }
239    }
240
241    /// Check if error is related to configuration
242    pub fn is_config_error(&self) -> bool {
243        matches!(self, AiLibError::ConfigurationError(_))
244    }
245
246    /// Check if error is related to request validation
247    pub fn is_request_error(&self) -> bool {
248        matches!(
249            self,
250            AiLibError::InvalidRequest(_)
251                | AiLibError::ContextLengthExceeded(_)
252                | AiLibError::UnsupportedFeature(_)
253        )
254    }
255}
256
257fn prepend_context(context: &str, message: String) -> String {
258    if message.is_empty() {
259        context.to_string()
260    } else {
261        format!("{context}: {message}")
262    }
263}
264
265fn transport_with_context(err: TransportError, context: &str) -> TransportError {
266    match err {
267        TransportError::HttpError(msg) => TransportError::HttpError(prepend_context(context, msg)),
268        TransportError::JsonError(msg) => TransportError::JsonError(prepend_context(context, msg)),
269        TransportError::InvalidUrl(msg) => {
270            TransportError::InvalidUrl(prepend_context(context, msg))
271        }
272        TransportError::AuthenticationError(msg) => {
273            TransportError::AuthenticationError(prepend_context(context, msg))
274        }
275        TransportError::RateLimitExceeded => TransportError::RateLimitExceeded,
276        TransportError::ServerError { status, message } => TransportError::ServerError {
277            status,
278            message: prepend_context(context, message),
279        },
280        TransportError::ClientError { status, message } => TransportError::ClientError {
281            status,
282            message: prepend_context(context, message),
283        },
284        TransportError::Timeout(msg) => TransportError::Timeout(prepend_context(context, msg)),
285    }
286}