1use thiserror::Error;
2
3#[derive(Debug, Clone, Default)]
5pub struct ErrorDetails {
6 pub message: String,
7 pub status_code: u16,
8 pub error_code: Option<String>,
10 pub request_id: Option<String>,
12 pub details: Option<serde_json::Value>,
15 pub retry_after_seconds: Option<f64>,
17 pub raw_body: String,
18}
19
20impl std::fmt::Display for ErrorDetails {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 write!(f, "{}", self.message)
23 }
24}
25
26#[derive(Debug, Error)]
28#[non_exhaustive]
29pub enum Error {
30 #[error("ValidationException: {0}")]
32 Validation(Box<ErrorDetails>),
33
34 #[error("AccessDeniedException: {0}")]
36 AccessDenied(Box<ErrorDetails>),
37
38 #[error("ServiceQuotaExceededException: {0}")]
40 ServiceQuotaExceeded(Box<ErrorDetails>),
41
42 #[error("ResourceNotFoundException: {0}")]
44 ResourceNotFound(Box<ErrorDetails>),
45
46 #[error("ThrottlingException: {0}")]
48 Throttling(Box<ErrorDetails>),
49
50 #[error("ModelTimeoutException: {0}")]
52 ModelTimeout(Box<ErrorDetails>),
53
54 #[error("ModelErrorException: {0}")]
56 ModelError(Box<ErrorDetails>),
57
58 #[error("ModelStreamErrorException: {0}")]
60 ModelStreamError(Box<ErrorDetails>),
61
62 #[error("ServiceUnavailableException: {0}")]
64 ServiceUnavailable(Box<ErrorDetails>),
65
66 #[error("InternalServer: {0}")]
68 InternalServer(Box<ErrorDetails>),
69
70 #[error("MissingApiKeyError: no API key provided; set AUDACITY_API_KEY or pass api_key to Config::builder()")]
72 MissingApiKey,
73
74 #[error("SdkError: {message}")]
77 Sdk { message: String, retryable: bool },
78}
79
80impl Error {
81 pub(crate) fn sdk_network(message: impl Into<String>) -> Self {
83 Error::Sdk {
84 message: message.into(),
85 retryable: true,
86 }
87 }
88
89 pub(crate) fn sdk(message: impl Into<String>) -> Self {
91 Error::Sdk {
92 message: message.into(),
93 retryable: false,
94 }
95 }
96
97 pub(crate) fn client_validation(message: impl Into<String>) -> Self {
99 Error::Validation(Box::new(ErrorDetails {
100 message: message.into(),
101 ..Default::default()
102 }))
103 }
104
105 pub(crate) fn model_stream_error(message: impl Into<String>) -> Self {
107 Error::ModelStreamError(Box::new(ErrorDetails {
108 message: message.into(),
109 ..Default::default()
110 }))
111 }
112
113 pub fn is_retryable(&self) -> bool {
115 matches!(
116 self,
117 Error::Throttling(_)
118 | Error::ModelTimeout(_)
119 | Error::ServiceUnavailable(_)
120 | Error::InternalServer(_)
121 | Error::Sdk {
122 retryable: true,
123 ..
124 }
125 )
126 }
127
128 pub fn retry_after_seconds(&self) -> Option<f64> {
130 match self {
131 Error::Throttling(d)
132 | Error::ModelTimeout(d)
133 | Error::ServiceUnavailable(d)
134 | Error::InternalServer(d)
135 | Error::ModelError(d)
136 | Error::ModelStreamError(d)
137 | Error::Validation(d)
138 | Error::AccessDenied(d)
139 | Error::ServiceQuotaExceeded(d)
140 | Error::ResourceNotFound(d) => d.retry_after_seconds,
141 _ => None,
142 }
143 }
144}
145
146pub(crate) fn parse_error(status: u16, body: &str, retry_after: Option<f64>) -> Error {
150 let (message, error_code, request_id, error_details) = extract_error_fields(body);
151
152 let details = Box::new(ErrorDetails {
153 message: message.clone(),
154 status_code: status,
155 error_code: error_code.clone(),
156 request_id,
157 details: error_details,
158 retry_after_seconds: retry_after,
159 raw_body: body.to_owned(),
160 });
161
162 classify_error(status, error_code.as_deref(), details)
163}
164
165#[allow(clippy::type_complexity)]
167pub(crate) fn extract_error_fields(
168 body: &str,
169) -> (
170 String,
171 Option<String>,
172 Option<String>,
173 Option<serde_json::Value>,
174) {
175 let Ok(v) = serde_json::from_str::<serde_json::Value>(body) else {
176 return (body.to_owned(), None, None, None);
177 };
178
179 if v.get("success").and_then(|s| s.as_bool()) == Some(false) {
182 if let Some(err) = v.get("error") {
183 let message = err
184 .get("message")
185 .and_then(|m| m.as_str())
186 .unwrap_or("unknown error")
187 .to_owned();
188 let code = err.get("code").and_then(|c| c.as_str()).map(str::to_owned);
189 let request_id = err
190 .get("request_id")
191 .and_then(|r| r.as_str())
192 .map(str::to_owned);
193 let details = err.get("details").filter(|d| !d.is_null()).cloned();
194 return (message, code, request_id, details);
195 }
196 }
197
198 if let Some(err) = v.get("error") {
200 let message = err
201 .get("message")
202 .and_then(|m| m.as_str())
203 .unwrap_or("unknown error")
204 .to_owned();
205 let code = err
207 .get("code")
208 .and_then(|c| c.as_str())
209 .filter(|s| !s.is_empty())
210 .or_else(|| err.get("type").and_then(|t| t.as_str()))
211 .map(str::to_owned);
212 return (message, code, None, None);
213 }
214
215 (body.to_owned(), None, None, None)
216}
217
218pub(crate) fn classify_error(status: u16, code: Option<&str>, details: Box<ErrorDetails>) -> Error {
220 let details = match code {
221 Some(c) => match classify_by_code(c, status, details) {
222 Ok(err) => return err,
223 Err(details) => details, },
225 None => details,
226 };
227 classify_by_status(status, details)
228}
229
230pub(crate) fn classify_by_code(
233 code: &str,
234 status: u16,
235 details: Box<ErrorDetails>,
236) -> Result<Error, Box<ErrorDetails>> {
237 Ok(match code.to_uppercase().as_str() {
238 "INVALID_API_KEY"
239 | "API_KEY_REQUIRED"
240 | "AUTHENTICATION_ERROR"
241 | "AUTHORIZATION_ERROR"
242 | "MODEL_NOT_ALLOWED" => Error::AccessDenied(details),
243
244 "USAGE_CAP_EXCEEDED" | "USAGE_CAP_ERROR" | "BUDGET_EXCEEDED" => {
245 Error::ServiceQuotaExceeded(details)
246 }
247
248 "RATE_LIMIT_EXCEEDED" | "RATE_LIMIT_ERROR" => Error::Throttling(details),
249
250 "INVALID_REQUEST_ERROR" | "VALIDATION_ERROR" => Error::Validation(details),
251
252 "MODEL_NOT_FOUND" => Error::ResourceNotFound(details),
253
254 "TIMEOUT_ERROR" => Error::ModelTimeout(details),
255
256 "STREAM_ERROR" => Error::ModelStreamError(details),
257
258 "UPSTREAM_ERROR" => {
259 if status >= 500 {
260 Error::ServiceUnavailable(details)
261 } else {
262 Error::ModelError(details)
263 }
264 }
265
266 _ => return Err(details),
267 })
268}
269
270fn classify_by_status(status: u16, details: Box<ErrorDetails>) -> Error {
272 match status {
273 400 => Error::Validation(details),
274 401 | 403 => Error::AccessDenied(details),
275 402 => Error::ServiceQuotaExceeded(details),
276 404 => Error::ResourceNotFound(details),
277 408 => Error::ModelTimeout(details),
278 429 => Error::Throttling(details),
279 500 => Error::InternalServer(details),
280 502..=504 => Error::ServiceUnavailable(details),
281 s if (400..500).contains(&s) => Error::Validation(details),
282 s if s >= 500 => Error::InternalServer(details),
283 s => Error::sdk(format!(
285 "unexpected HTTP status {s}: {}",
286 details.message
287 )),
288 }
289}
290
291pub(crate) fn parse_retry_after(value: &str) -> Option<f64> {
293 value.trim().parse::<f64>().ok()
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn shape_a_authentication_error() {
302 let body = r#"{"error":{"message":"Invalid API key","type":"authentication_error","param":null,"code":"invalid_api_key"}}"#;
303 let err = parse_error(401, body, None);
304 assert!(matches!(err, Error::AccessDenied(_)));
305 if let Error::AccessDenied(d) = err {
306 assert_eq!(d.message, "Invalid API key");
307 assert_eq!(d.error_code.as_deref(), Some("invalid_api_key"));
308 assert!(d.request_id.is_none());
309 }
310 }
311
312 #[test]
313 fn shape_b_model_not_allowed() {
314 let body = r#"{"success":false,"error":{"code":"MODEL_NOT_ALLOWED","message":"Model not allowed","request_id":"req-123","details":{"binding_cap":"team-alpha"}}}"#;
315 let err = parse_error(403, body, None);
316 assert!(matches!(err, Error::AccessDenied(_)));
317 if let Error::AccessDenied(d) = err {
318 assert_eq!(d.request_id.as_deref(), Some("req-123"));
319 let details = d.details.expect("details must be captured from shape B");
321 assert_eq!(details["binding_cap"], "team-alpha");
322 }
323 }
324
325 #[test]
326 fn shape_a_has_no_details() {
327 let body = r#"{"error":{"message":"bad request","type":"invalid_request_error"}}"#;
328 let err = parse_error(400, body, None);
329 if let Error::Validation(d) = err {
330 assert!(d.details.is_none());
331 } else {
332 panic!("expected Validation");
333 }
334 }
335
336 #[test]
337 fn shape_b_budget_exceeded() {
338 let body = r#"{"success":false,"error":{"code":"BUDGET_EXCEEDED","message":"over budget","request_id":null}}"#;
339 let err = parse_error(429, body, None);
340 assert!(matches!(err, Error::ServiceQuotaExceeded(_)));
341 assert!(!err.is_retryable());
343 }
344
345 #[test]
346 fn rate_limit_is_retryable() {
347 let body = r#"{"error":{"message":"rate limited","type":"rate_limit_error","code":"rate_limit_exceeded"}}"#;
348 let err = parse_error(429, body, Some(2.0));
349 assert!(matches!(err, Error::Throttling(_)));
350 assert!(err.is_retryable());
351 assert_eq!(err.retry_after_seconds(), Some(2.0));
352 }
353
354 #[test]
355 fn http_status_fallback_503() {
356 let err = parse_error(503, "Service Unavailable", None);
357 assert!(matches!(err, Error::ServiceUnavailable(_)));
358 assert!(err.is_retryable());
359 }
360
361 #[test]
362 fn upstream_error_5xx_is_service_unavailable() {
363 let body = r#"{"error":{"message":"upstream","code":"UPSTREAM_ERROR"}}"#;
364 let err = parse_error(502, body, None);
365 assert!(matches!(err, Error::ServiceUnavailable(_)));
366 }
367
368 #[test]
369 fn upstream_error_4xx_is_model_error() {
370 let body = r#"{"error":{"message":"upstream","code":"UPSTREAM_ERROR"}}"#;
371 let err = parse_error(400, body, None);
372 assert!(matches!(err, Error::ModelError(_)));
373 }
374
375 #[test]
376 fn parse_retry_after_value() {
377 assert_eq!(parse_retry_after("5"), Some(5.0));
378 assert_eq!(parse_retry_after("2.5"), Some(2.5));
379 assert_eq!(parse_retry_after("bad"), None);
380 }
381}