1use 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#[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 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 pub const fn is_retryable(self) -> bool {
53 matches!(
54 self,
55 Self::RateLimited | Self::Timeout | Self::Unavailable | Self::Network
56 )
57 }
58}
59
60pub 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}