Skip to main content

litellm_rs/utils/error/
canonical.rs

1//! Canonical cross-protocol error classification.
2//!
3//! This provides a single error code taxonomy and retryable semantics that can
4//! be reused by HTTP/OpenAI-compatible, A2A, and MCP layers.
5
6use super::gateway_error::GatewayError;
7use crate::core::a2a::error::A2AError;
8use crate::core::mcp::error::McpError;
9use crate::core::providers::unified_provider::ProviderError;
10
11/// Canonical error code shared across protocol boundaries.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ErrorCode {
14    Authentication,
15    Authorization,
16    RateLimited,
17    QuotaExceeded,
18    InvalidRequest,
19    NotFound,
20    Conflict,
21    Timeout,
22    Unavailable,
23    Network,
24    Configuration,
25    Parsing,
26    NotImplemented,
27    Internal,
28}
29
30impl ErrorCode {
31    /// Stable machine-readable canonical string.
32    pub const fn as_str(self) -> &'static str {
33        match self {
34            Self::Authentication => "AUTHENTICATION",
35            Self::Authorization => "AUTHORIZATION",
36            Self::RateLimited => "RATE_LIMITED",
37            Self::QuotaExceeded => "QUOTA_EXCEEDED",
38            Self::InvalidRequest => "INVALID_REQUEST",
39            Self::NotFound => "NOT_FOUND",
40            Self::Conflict => "CONFLICT",
41            Self::Timeout => "TIMEOUT",
42            Self::Unavailable => "UNAVAILABLE",
43            Self::Network => "NETWORK",
44            Self::Configuration => "CONFIGURATION",
45            Self::Parsing => "PARSING",
46            Self::NotImplemented => "NOT_IMPLEMENTED",
47            Self::Internal => "INTERNAL",
48        }
49    }
50
51    /// Default retryability for canonical classes.
52    pub const fn is_retryable(self) -> bool {
53        matches!(
54            self,
55            Self::RateLimited | Self::Timeout | Self::Unavailable | Self::Network
56        )
57    }
58}
59
60/// Canonical code and retryability mapping.
61pub trait CanonicalError {
62    fn canonical_code(&self) -> ErrorCode;
63
64    fn canonical_retryable(&self) -> bool {
65        self.canonical_code().is_retryable()
66    }
67}
68
69impl CanonicalError for ProviderError {
70    fn canonical_code(&self) -> ErrorCode {
71        match self {
72            ProviderError::Authentication { .. } => ErrorCode::Authentication,
73            ProviderError::RateLimit { .. } => ErrorCode::RateLimited,
74            ProviderError::QuotaExceeded { .. } => ErrorCode::QuotaExceeded,
75            ProviderError::ModelNotFound { .. } | ProviderError::DeploymentError { .. } => {
76                ErrorCode::NotFound
77            }
78            ProviderError::InvalidRequest { .. }
79            | ProviderError::ContextLengthExceeded { .. }
80            | ProviderError::ContentFiltered { .. }
81            | ProviderError::TokenLimitExceeded { .. }
82            | ProviderError::FeatureDisabled { .. }
83            | ProviderError::Cancelled { .. } => ErrorCode::InvalidRequest,
84            ProviderError::Network { .. } => ErrorCode::Network,
85            ProviderError::ProviderUnavailable { .. } | ProviderError::RoutingError { .. } => {
86                ErrorCode::Unavailable
87            }
88            ProviderError::NotSupported { .. } | ProviderError::NotImplemented { .. } => {
89                ErrorCode::NotImplemented
90            }
91            ProviderError::Configuration { .. } => ErrorCode::Configuration,
92            ProviderError::Serialization { .. }
93            | ProviderError::ResponseParsing { .. }
94            | ProviderError::TransformationError { .. } => ErrorCode::Parsing,
95            ProviderError::Timeout { .. } => ErrorCode::Timeout,
96            ProviderError::ApiError { status, .. } => match *status {
97                401 => ErrorCode::Authentication,
98                403 => ErrorCode::Authorization,
99                404 => ErrorCode::NotFound,
100                408 | 504 => ErrorCode::Timeout,
101                409 => ErrorCode::Conflict,
102                429 => ErrorCode::RateLimited,
103                400..=499 => ErrorCode::InvalidRequest,
104                500..=599 => ErrorCode::Unavailable,
105                _ => ErrorCode::Internal,
106            },
107            ProviderError::Streaming { .. } | ProviderError::Other { .. } => ErrorCode::Internal,
108        }
109    }
110
111    fn canonical_retryable(&self) -> bool {
112        self.is_retryable()
113    }
114}
115
116impl CanonicalError for GatewayError {
117    fn canonical_code(&self) -> ErrorCode {
118        match self {
119            GatewayError::Config(_) => ErrorCode::Configuration,
120            GatewayError::Auth(_) => ErrorCode::Authentication,
121            GatewayError::Forbidden(_) => ErrorCode::Authorization,
122            GatewayError::Provider(provider_error) => provider_error.canonical_code(),
123            GatewayError::RateLimit { .. } => ErrorCode::RateLimited,
124            GatewayError::Validation(_) | GatewayError::BadRequest(_) => ErrorCode::InvalidRequest,
125            GatewayError::NotFound(_) => ErrorCode::NotFound,
126            GatewayError::Conflict(_) => ErrorCode::Conflict,
127            GatewayError::Timeout(_) => ErrorCode::Timeout,
128            GatewayError::Unavailable(_) => ErrorCode::Unavailable,
129            GatewayError::Network(_) => ErrorCode::Network,
130            GatewayError::NotImplemented(_) => ErrorCode::NotImplemented,
131            GatewayError::Storage(_)
132            | GatewayError::HttpClient(_)
133            | GatewayError::Serialization(_)
134            | GatewayError::Io(_)
135            | GatewayError::Internal(_) => ErrorCode::Internal,
136        }
137    }
138
139    fn canonical_retryable(&self) -> bool {
140        match self {
141            GatewayError::Provider(provider_error) => provider_error.canonical_retryable(),
142            _ => self.canonical_code().is_retryable(),
143        }
144    }
145}
146
147impl CanonicalError for A2AError {
148    fn canonical_code(&self) -> ErrorCode {
149        match self {
150            A2AError::AgentNotFound { .. } | A2AError::TaskNotFound { .. } => ErrorCode::NotFound,
151            A2AError::AgentAlreadyExists { .. } => ErrorCode::Conflict,
152            A2AError::ConnectionError { .. } => ErrorCode::Network,
153            A2AError::AuthenticationError { .. } => ErrorCode::Authentication,
154            A2AError::ProtocolError { .. }
155            | A2AError::InvalidRequest { .. }
156            | A2AError::ContentBlocked { .. } => ErrorCode::InvalidRequest,
157            A2AError::Timeout { .. } => ErrorCode::Timeout,
158            A2AError::ConfigurationError { .. } => ErrorCode::Configuration,
159            A2AError::SerializationError { .. } => ErrorCode::Parsing,
160            A2AError::UnsupportedProvider { .. } => ErrorCode::NotImplemented,
161            A2AError::RateLimitExceeded { .. } => ErrorCode::RateLimited,
162            A2AError::AgentBusy { .. } => ErrorCode::Unavailable,
163            A2AError::TaskFailed { .. } => ErrorCode::Internal,
164        }
165    }
166
167    fn canonical_retryable(&self) -> bool {
168        matches!(
169            self,
170            A2AError::ConnectionError { .. }
171                | A2AError::Timeout { .. }
172                | A2AError::RateLimitExceeded { .. }
173                | A2AError::AgentBusy { .. }
174        )
175    }
176}
177
178impl CanonicalError for McpError {
179    fn canonical_code(&self) -> ErrorCode {
180        match self {
181            McpError::ServerNotFound { .. } | McpError::ToolNotFound { .. } => ErrorCode::NotFound,
182            McpError::ConnectionError { .. } | McpError::TransportError { .. } => {
183                ErrorCode::Network
184            }
185            McpError::AuthenticationError { .. } => ErrorCode::Authentication,
186            McpError::AuthorizationError { .. } => ErrorCode::Authorization,
187            McpError::ProtocolError { .. } | McpError::InvalidUrl { .. } => {
188                ErrorCode::InvalidRequest
189            }
190            McpError::ToolExecutionError { .. } => ErrorCode::Internal,
191            McpError::Timeout { .. } => ErrorCode::Timeout,
192            McpError::ConfigurationError { .. } => ErrorCode::Configuration,
193            McpError::SerializationError { .. } => ErrorCode::Parsing,
194            McpError::ServerAlreadyExists { .. } => ErrorCode::Conflict,
195            McpError::RateLimitExceeded { .. } => ErrorCode::RateLimited,
196            McpError::ValidationError { .. } => ErrorCode::InvalidRequest,
197        }
198    }
199
200    fn canonical_retryable(&self) -> bool {
201        matches!(
202            self,
203            McpError::ConnectionError { .. }
204                | McpError::TransportError { .. }
205                | McpError::Timeout { .. }
206                | McpError::RateLimitExceeded { .. }
207        )
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_provider_rate_limit_mapping() {
217        let err = ProviderError::rate_limit("openai", Some(10));
218        assert_eq!(err.canonical_code(), ErrorCode::RateLimited);
219        assert!(err.canonical_retryable());
220    }
221
222    #[test]
223    fn test_provider_auth_mapping() {
224        let err = ProviderError::authentication("openai", "bad key");
225        assert_eq!(err.canonical_code(), ErrorCode::Authentication);
226        assert!(!err.canonical_retryable());
227    }
228
229    #[test]
230    fn test_gateway_provider_delegates_retryable() {
231        let err = GatewayError::Provider(ProviderError::timeout("openai", "timeout"));
232        assert_eq!(err.canonical_code(), ErrorCode::Timeout);
233        assert!(err.canonical_retryable());
234    }
235
236    #[test]
237    fn test_gateway_not_found_mapping() {
238        let err = GatewayError::NotFound("missing".to_string());
239        assert_eq!(err.canonical_code(), ErrorCode::NotFound);
240        assert!(!err.canonical_retryable());
241    }
242
243    #[cfg(feature = "s3")]
244    #[test]
245    fn test_gateway_s3_mapping() {
246        let err = GatewayError::Storage("bucket error".to_string());
247        assert_eq!(err.canonical_code(), ErrorCode::Internal);
248        assert!(!err.canonical_retryable());
249    }
250
251    #[cfg(feature = "vector-db")]
252    #[test]
253    fn test_gateway_qdrant_mapping() {
254        let err = GatewayError::Storage("connection failed".to_string());
255        assert_eq!(err.canonical_code(), ErrorCode::Internal);
256        assert!(!err.canonical_retryable());
257    }
258
259    #[cfg(feature = "websockets")]
260    #[test]
261    fn test_gateway_websocket_mapping() {
262        let err = GatewayError::Network("connection closed".to_string());
263        assert_eq!(err.canonical_code(), ErrorCode::Network);
264        assert!(err.canonical_retryable());
265    }
266
267    #[test]
268    fn test_a2a_busy_mapping() {
269        let err = A2AError::AgentBusy {
270            agent_name: "agent-1".to_string(),
271            message: "overloaded".to_string(),
272        };
273        assert_eq!(err.canonical_code(), ErrorCode::Unavailable);
274        assert!(err.canonical_retryable());
275    }
276
277    #[test]
278    fn test_mcp_auth_mapping() {
279        let err = McpError::AuthenticationError {
280            server_name: "s1".to_string(),
281            message: "bad token".to_string(),
282        };
283        assert_eq!(err.canonical_code(), ErrorCode::Authentication);
284        assert!(!err.canonical_retryable());
285    }
286
287    #[test]
288    fn test_error_code_str_values() {
289        assert_eq!(ErrorCode::Authentication.as_str(), "AUTHENTICATION");
290        assert_eq!(ErrorCode::RateLimited.as_str(), "RATE_LIMITED");
291    }
292}