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
165pub(crate) fn parse_stream_error(raw: &str) -> Error {
170 let (message, code, request_id, error_details) = extract_error_fields(raw);
171 let details = Box::new(ErrorDetails {
172 message,
173 status_code: 0,
174 error_code: code.clone(),
175 request_id,
176 details: error_details,
177 retry_after_seconds: None,
178 raw_body: raw.to_owned(),
179 });
180 match code.as_deref() {
181 Some(c) => classify_by_code(c, 0, details).unwrap_or_else(Error::ModelStreamError),
182 None => Error::ModelStreamError(details),
183 }
184}
185
186#[allow(clippy::type_complexity)]
188pub(crate) fn extract_error_fields(
189 body: &str,
190) -> (
191 String,
192 Option<String>,
193 Option<String>,
194 Option<serde_json::Value>,
195) {
196 let Ok(v) = serde_json::from_str::<serde_json::Value>(body) else {
197 return (body.to_owned(), None, None, None);
198 };
199
200 if v.get("success").and_then(|s| s.as_bool()) == Some(false) {
203 if let Some(err) = v.get("error") {
204 let message = err
205 .get("message")
206 .and_then(|m| m.as_str())
207 .unwrap_or("unknown error")
208 .to_owned();
209 let code = err.get("code").and_then(|c| c.as_str()).map(str::to_owned);
210 let request_id = err
211 .get("request_id")
212 .and_then(|r| r.as_str())
213 .map(str::to_owned);
214 let details = err.get("details").filter(|d| !d.is_null()).cloned();
215 return (message, code, request_id, details);
216 }
217 }
218
219 if let Some(err) = v.get("error") {
221 let message = err
222 .get("message")
223 .and_then(|m| m.as_str())
224 .unwrap_or("unknown error")
225 .to_owned();
226 let code = err
228 .get("code")
229 .and_then(|c| c.as_str())
230 .filter(|s| !s.is_empty())
231 .or_else(|| err.get("type").and_then(|t| t.as_str()))
232 .map(str::to_owned);
233 return (message, code, None, None);
234 }
235
236 (body.to_owned(), None, None, None)
237}
238
239pub(crate) fn classify_error(status: u16, code: Option<&str>, details: Box<ErrorDetails>) -> Error {
241 let details = match code {
242 Some(c) => match classify_by_code(c, status, details) {
243 Ok(err) => return err,
244 Err(details) => details, },
246 None => details,
247 };
248 classify_by_status(status, details)
249}
250
251pub(crate) fn classify_by_code(
254 code: &str,
255 status: u16,
256 details: Box<ErrorDetails>,
257) -> Result<Error, Box<ErrorDetails>> {
258 Ok(match code.to_uppercase().as_str() {
259 "INVALID_API_KEY"
260 | "API_KEY_REQUIRED"
261 | "AUTHENTICATION_ERROR"
262 | "AUTHORIZATION_ERROR"
263 | "MODEL_NOT_ALLOWED" => Error::AccessDenied(details),
264
265 "USAGE_CAP_EXCEEDED" | "USAGE_CAP_ERROR" | "BUDGET_EXCEEDED" => {
266 Error::ServiceQuotaExceeded(details)
267 }
268
269 "RATE_LIMIT_EXCEEDED" | "RATE_LIMIT_ERROR" => Error::Throttling(details),
270
271 "INVALID_REQUEST_ERROR" | "VALIDATION_ERROR" => Error::Validation(details),
272
273 "MODEL_NOT_FOUND" => Error::ResourceNotFound(details),
274
275 "TIMEOUT_ERROR" => Error::ModelTimeout(details),
276
277 "STREAM_ERROR" => Error::ModelStreamError(details),
278
279 "UPSTREAM_ERROR" => {
280 if status >= 500 {
281 Error::ServiceUnavailable(details)
282 } else {
283 Error::ModelError(details)
284 }
285 }
286
287 _ => return Err(details),
288 })
289}
290
291pub(crate) fn status_error(status: u16, body: &str) -> Error {
295 let details = Box::new(ErrorDetails {
296 message: if body.is_empty() {
297 format!("HTTP {status}")
298 } else {
299 body.to_owned()
300 },
301 status_code: status,
302 raw_body: body.to_owned(),
303 ..Default::default()
304 });
305 classify_by_status(status, details)
306}
307
308fn classify_by_status(status: u16, details: Box<ErrorDetails>) -> Error {
310 match status {
311 400 => Error::Validation(details),
312 401 | 403 => Error::AccessDenied(details),
313 402 => Error::ServiceQuotaExceeded(details),
314 404 => Error::ResourceNotFound(details),
315 408 => Error::ModelTimeout(details),
316 429 => Error::Throttling(details),
317 500 => Error::InternalServer(details),
318 502..=504 => Error::ServiceUnavailable(details),
319 s if (400..500).contains(&s) => Error::Validation(details),
320 s if s >= 500 => Error::InternalServer(details),
321 s => Error::sdk(format!("unexpected HTTP status {s}: {}", details.message)),
323 }
324}
325
326pub(crate) fn parse_retry_after(value: &str) -> Option<f64> {
328 value.trim().parse::<f64>().ok()
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn shape_a_authentication_error() {
337 let body = r#"{"error":{"message":"Invalid API key","type":"authentication_error","param":null,"code":"invalid_api_key"}}"#;
338 let err = parse_error(401, body, None);
339 assert!(matches!(err, Error::AccessDenied(_)));
340 if let Error::AccessDenied(d) = err {
341 assert_eq!(d.message, "Invalid API key");
342 assert_eq!(d.error_code.as_deref(), Some("invalid_api_key"));
343 assert!(d.request_id.is_none());
344 }
345 }
346
347 #[test]
348 fn shape_b_model_not_allowed() {
349 let body = r#"{"success":false,"error":{"code":"MODEL_NOT_ALLOWED","message":"Model not allowed","request_id":"req-123","details":{"binding_cap":"team-alpha"}}}"#;
350 let err = parse_error(403, body, None);
351 assert!(matches!(err, Error::AccessDenied(_)));
352 if let Error::AccessDenied(d) = err {
353 assert_eq!(d.request_id.as_deref(), Some("req-123"));
354 let details = d.details.expect("details must be captured from shape B");
356 assert_eq!(details["binding_cap"], "team-alpha");
357 }
358 }
359
360 #[test]
361 fn shape_a_has_no_details() {
362 let body = r#"{"error":{"message":"bad request","type":"invalid_request_error"}}"#;
363 let err = parse_error(400, body, None);
364 if let Error::Validation(d) = err {
365 assert!(d.details.is_none());
366 } else {
367 panic!("expected Validation");
368 }
369 }
370
371 #[test]
372 fn shape_b_budget_exceeded() {
373 let body = r#"{"success":false,"error":{"code":"BUDGET_EXCEEDED","message":"over budget","request_id":null}}"#;
374 let err = parse_error(429, body, None);
375 assert!(matches!(err, Error::ServiceQuotaExceeded(_)));
376 assert!(!err.is_retryable());
378 }
379
380 #[test]
381 fn rate_limit_is_retryable() {
382 let body = r#"{"error":{"message":"rate limited","type":"rate_limit_error","code":"rate_limit_exceeded"}}"#;
383 let err = parse_error(429, body, Some(2.0));
384 assert!(matches!(err, Error::Throttling(_)));
385 assert!(err.is_retryable());
386 assert_eq!(err.retry_after_seconds(), Some(2.0));
387 }
388
389 #[test]
390 fn http_status_fallback_503() {
391 let err = parse_error(503, "Service Unavailable", None);
392 assert!(matches!(err, Error::ServiceUnavailable(_)));
393 assert!(err.is_retryable());
394 }
395
396 #[test]
397 fn upstream_error_5xx_is_service_unavailable() {
398 let body = r#"{"error":{"message":"upstream","code":"UPSTREAM_ERROR"}}"#;
399 let err = parse_error(502, body, None);
400 assert!(matches!(err, Error::ServiceUnavailable(_)));
401 }
402
403 #[test]
404 fn upstream_error_4xx_is_model_error() {
405 let body = r#"{"error":{"message":"upstream","code":"UPSTREAM_ERROR"}}"#;
406 let err = parse_error(400, body, None);
407 assert!(matches!(err, Error::ModelError(_)));
408 }
409
410 #[test]
411 fn parse_retry_after_value() {
412 assert_eq!(parse_retry_after("5"), Some(5.0));
413 assert_eq!(parse_retry_after("2.5"), Some(2.5));
414 assert_eq!(parse_retry_after("bad"), None);
415 }
416}