1use thiserror::Error;
4
5#[derive(Debug, Clone)]
12pub struct HttpErrorDetail {
13 pub status: u16,
15 pub body: String,
17 pub provider: Option<String>,
19 pub request_id: Option<String>,
21}
22
23impl HttpErrorDetail {
24 pub fn new(status: u16, body: String) -> Self {
26 Self {
27 status,
28 body,
29 provider: None,
30 request_id: None,
31 }
32 }
33
34 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
36 self.provider = Some(provider.into());
37 self
38 }
39
40 pub fn with_request_id(mut self, request_id: Option<String>) -> Self {
42 self.request_id = request_id;
43 self
44 }
45}
46
47impl std::fmt::Display for HttpErrorDetail {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 write!(f, "HTTP error {}: {}", self.status, self.body)?;
50 if let Some(provider) = &self.provider {
51 write!(f, " [{provider}]")?;
52 }
53 if let Some(id) = &self.request_id {
54 write!(f, " (request-id: {id})")?;
55 }
56 Ok(())
57 }
58}
59
60#[derive(Error, Debug)]
65#[non_exhaustive]
66pub enum ProviderError {
67 #[error("Missing API key")]
69 MissingApiKey,
70
71 #[error("Unknown provider: {0}")]
73 UnknownProvider(String),
74
75 #[error("Provider not implemented: {0}")]
77 NotImplemented(String),
78
79 #[error("{0}")]
81 HttpError(HttpErrorDetail),
82
83 #[error("Request failed: {0}")]
85 RequestFailed(#[from] reqwest::Error),
86
87 #[error("IO error: {0}")]
89 IoError(#[from] std::io::Error),
90
91 #[error("Invalid response: {0}")]
93 InvalidResponse(String),
94
95 #[error("Invalid API key format")]
97 InvalidApiKey,
98
99 #[error("JSON parse error: {0}")]
101 JsonParse(#[from] serde_json::Error),
102
103 #[error("Stream error: {0}")]
105 StreamError(String),
106
107 #[error("Network error: {0}")]
109 NetworkError(String),
110
111 #[error("Context overflow")]
113 ContextOverflow,
114
115 #[error("Request timed out")]
117 Timeout,
118
119 #[error("Rate limited")]
121 RateLimited {
122 retry_after: Option<std::time::Duration>,
124 },
125}
126
127impl ProviderError {
128 pub fn is_retryable(&self) -> bool {
130 match self {
131 Self::HttpError(detail) => detail.status == 429 || detail.status >= 500,
132 Self::NetworkError(_) => true,
133 Self::Timeout => true,
134 Self::RateLimited { .. } => true,
135 _ => false,
136 }
137 }
138
139 pub fn retry_after(&self) -> Option<std::time::Duration> {
141 match self {
142 Self::RateLimited { retry_after } => *retry_after,
143 Self::HttpError(detail) if detail.status == 429 => {
144 Some(std::time::Duration::from_secs(5))
145 }
146 _ => None,
147 }
148 }
149
150 pub fn http_status(&self) -> Option<u16> {
156 match self {
157 Self::HttpError(detail) => Some(detail.status),
158 _ => None,
159 }
160 }
161}
162
163#[derive(Error, Debug)]
165pub enum ValidationError {
166 #[error("Invalid JSON: {0}")]
167 InvalidJson(#[from] serde_json::Error),
168
169 #[error("Schema validation failed: {0}")]
170 SchemaValidation(String),
171
172 #[error("Missing required field: {0}")]
173 MissingRequiredField(String),
174}
175
176#[derive(Error, Debug)]
178pub enum Error {
179 #[error("Provider error: {0}")]
181 Provider(#[from] ProviderError),
182
183 #[error("Validation error: {0}")]
185 Validation(#[from] ValidationError),
186
187 #[error("IO error: {0}")]
189 Io(#[from] std::io::Error),
190}
191
192pub type Result<T> = std::result::Result<T, Error>;
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn provider_error_display() {
201 assert_eq!(ProviderError::MissingApiKey.to_string(), "Missing API key");
202 assert_eq!(
203 ProviderError::UnknownProvider("foo".to_string()).to_string(),
204 "Unknown provider: foo"
205 );
206 assert_eq!(
207 ProviderError::HttpError(HttpErrorDetail::new(429, "rate limited".to_string()))
208 .to_string(),
209 "HTTP error 429: rate limited"
210 );
211 assert_eq!(
213 ProviderError::HttpError(
214 HttpErrorDetail::new(500, "boom".to_string())
215 .with_provider("anthropic")
216 .with_request_id(Some("req_123".to_string()))
217 )
218 .to_string(),
219 "HTTP error 500: boom [anthropic] (request-id: req_123)"
220 );
221 assert_eq!(
222 ProviderError::InvalidResponse("bad json".to_string()).to_string(),
223 "Invalid response: bad json"
224 );
225 assert_eq!(
226 ProviderError::StreamError("disconnected".to_string()).to_string(),
227 "Stream error: disconnected"
228 );
229 assert_eq!(
230 ProviderError::NotImplemented("x".to_string()).to_string(),
231 "Provider not implemented: x"
232 );
233 }
234
235 #[test]
236 fn error_chain_from_provider_error() {
237 let inner = ProviderError::MissingApiKey;
238 let outer: Error = inner.into();
239 assert!(matches!(
240 outer,
241 Error::Provider(ProviderError::MissingApiKey)
242 ));
243 assert!(outer.to_string().contains("Missing API key"));
244 }
245
246 #[test]
247 fn validation_error_display() {
248 let err = ValidationError::MissingRequiredField("model".to_string());
249 assert_eq!(err.to_string(), "Missing required field: model");
250 }
251
252 #[test]
253 fn error_chain_from_io() {
254 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
255 let outer: Error = io_err.into();
256 assert!(matches!(outer, Error::Io(_)));
257 }
258}