1use thiserror::Error;
2
3#[derive(Debug, Clone)]
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 retry_after_seconds: Option<f64>,
14 pub raw_body: String,
15}
16
17impl std::fmt::Display for ErrorDetails {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 write!(f, "{}", self.message)
20 }
21}
22
23#[derive(Debug, Error)]
25pub enum Error {
26 #[error("ValidationException: {0}")]
28 Validation(ErrorDetails),
29
30 #[error("AccessDeniedException: {0}")]
32 AccessDenied(ErrorDetails),
33
34 #[error("ServiceQuotaExceededException: {0}")]
36 ServiceQuotaExceeded(ErrorDetails),
37
38 #[error("ResourceNotFoundException: {0}")]
40 ResourceNotFound(ErrorDetails),
41
42 #[error("ThrottlingException: {0}")]
44 Throttling(ErrorDetails),
45
46 #[error("ModelTimeoutException: {0}")]
48 ModelTimeout(ErrorDetails),
49
50 #[error("ModelErrorException: {0}")]
52 ModelError(ErrorDetails),
53
54 #[error("ModelStreamErrorException: {0}")]
56 ModelStreamError(ErrorDetails),
57
58 #[error("ServiceUnavailableException: {0}")]
60 ServiceUnavailable(ErrorDetails),
61
62 #[error("InternalServer: {0}")]
64 InternalServer(ErrorDetails),
65
66 #[error("MissingApiKeyError: no API key provided; set AUDACITY_API_KEY or pass api_key to Config::builder()")]
68 MissingApiKey,
69
70 #[error("SdkError: {0}")]
72 Sdk(String),
73}
74
75impl Error {
76 pub fn is_retryable(&self) -> bool {
78 matches!(
79 self,
80 Error::Throttling(_)
81 | Error::ModelTimeout(_)
82 | Error::ServiceUnavailable(_)
83 | Error::InternalServer(_)
84 | Error::Sdk(_)
85 )
86 }
87
88 pub fn retry_after_seconds(&self) -> Option<f64> {
90 match self {
91 Error::Throttling(d)
92 | Error::ModelTimeout(d)
93 | Error::ServiceUnavailable(d)
94 | Error::InternalServer(d)
95 | Error::ModelError(d)
96 | Error::ModelStreamError(d)
97 | Error::Validation(d)
98 | Error::AccessDenied(d)
99 | Error::ServiceQuotaExceeded(d)
100 | Error::ResourceNotFound(d) => d.retry_after_seconds,
101 _ => None,
102 }
103 }
104}
105
106pub(crate) fn parse_error(status: u16, body: &str, retry_after: Option<f64>) -> Error {
110 let (message, error_code, request_id) = extract_error_fields(body);
111
112 let details = ErrorDetails {
113 message: message.clone(),
114 status_code: status,
115 error_code: error_code.clone(),
116 request_id,
117 retry_after_seconds: retry_after,
118 raw_body: body.to_owned(),
119 };
120
121 classify_error(status, error_code.as_deref(), details)
122}
123
124pub(crate) fn extract_error_fields(body: &str) -> (String, Option<String>, Option<String>) {
126 let Ok(v) = serde_json::from_str::<serde_json::Value>(body) else {
127 return (body.to_owned(), None, None);
128 };
129
130 if v.get("success").and_then(|s| s.as_bool()) == Some(false) {
132 if let Some(err) = v.get("error") {
133 let message = err
134 .get("message")
135 .and_then(|m| m.as_str())
136 .unwrap_or("unknown error")
137 .to_owned();
138 let code = err.get("code").and_then(|c| c.as_str()).map(str::to_owned);
139 let request_id = err
140 .get("request_id")
141 .and_then(|r| r.as_str())
142 .map(str::to_owned);
143 return (message, code, request_id);
144 }
145 }
146
147 if let Some(err) = v.get("error") {
149 let message = err
150 .get("message")
151 .and_then(|m| m.as_str())
152 .unwrap_or("unknown error")
153 .to_owned();
154 let code = err
156 .get("code")
157 .and_then(|c| c.as_str())
158 .filter(|s| !s.is_empty())
159 .or_else(|| err.get("type").and_then(|t| t.as_str()))
160 .map(str::to_owned);
161 return (message, code, None);
162 }
163
164 (body.to_owned(), None, None)
165}
166
167pub(crate) fn classify_error(status: u16, code: Option<&str>, details: ErrorDetails) -> Error {
169 let details = match code {
170 Some(c) => match classify_by_code(c, status, details) {
171 Ok(err) => return err,
172 Err(details) => details, },
174 None => details,
175 };
176 classify_by_status(status, details)
177}
178
179pub(crate) fn classify_by_code(
182 code: &str,
183 status: u16,
184 details: ErrorDetails,
185) -> Result<Error, ErrorDetails> {
186 Ok(match code.to_uppercase().as_str() {
187 "INVALID_API_KEY"
188 | "API_KEY_REQUIRED"
189 | "AUTHENTICATION_ERROR"
190 | "AUTHORIZATION_ERROR"
191 | "MODEL_NOT_ALLOWED" => Error::AccessDenied(details),
192
193 "USAGE_CAP_EXCEEDED" | "USAGE_CAP_ERROR" | "BUDGET_EXCEEDED" => {
194 Error::ServiceQuotaExceeded(details)
195 }
196
197 "RATE_LIMIT_EXCEEDED" | "RATE_LIMIT_ERROR" => Error::Throttling(details),
198
199 "INVALID_REQUEST_ERROR" | "VALIDATION_ERROR" => Error::Validation(details),
200
201 "MODEL_NOT_FOUND" => Error::ResourceNotFound(details),
202
203 "TIMEOUT_ERROR" => Error::ModelTimeout(details),
204
205 "STREAM_ERROR" => Error::ModelStreamError(details),
206
207 "UPSTREAM_ERROR" => {
208 if status >= 500 {
209 Error::ServiceUnavailable(details)
210 } else {
211 Error::ModelError(details)
212 }
213 }
214
215 _ => return Err(details),
216 })
217}
218
219fn classify_by_status(status: u16, details: ErrorDetails) -> Error {
221 match status {
222 400 => Error::Validation(details),
223 401 | 403 => Error::AccessDenied(details),
224 402 => Error::ServiceQuotaExceeded(details),
225 404 => Error::ResourceNotFound(details),
226 408 => Error::ModelTimeout(details),
227 429 => Error::Throttling(details),
228 500 => Error::InternalServer(details),
229 502..=504 => Error::ServiceUnavailable(details),
230 s if (400..500).contains(&s) => Error::Validation(details),
231 s if s >= 500 => Error::InternalServer(details),
232 _ => Error::Sdk(details.message),
233 }
234}
235
236pub(crate) fn parse_retry_after(value: &str) -> Option<f64> {
238 value.trim().parse::<f64>().ok()
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn shape_a_authentication_error() {
247 let body = r#"{"error":{"message":"Invalid API key","type":"authentication_error","param":null,"code":"invalid_api_key"}}"#;
248 let err = parse_error(401, body, None);
249 assert!(matches!(err, Error::AccessDenied(_)));
250 if let Error::AccessDenied(d) = err {
251 assert_eq!(d.message, "Invalid API key");
252 assert_eq!(d.error_code.as_deref(), Some("invalid_api_key"));
253 assert!(d.request_id.is_none());
254 }
255 }
256
257 #[test]
258 fn shape_b_model_not_allowed() {
259 let body = r#"{"success":false,"error":{"code":"MODEL_NOT_ALLOWED","message":"Model not allowed","request_id":"req-123","details":{}}}"#;
260 let err = parse_error(403, body, None);
261 assert!(matches!(err, Error::AccessDenied(_)));
262 if let Error::AccessDenied(d) = err {
263 assert_eq!(d.request_id.as_deref(), Some("req-123"));
264 }
265 }
266
267 #[test]
268 fn shape_b_budget_exceeded() {
269 let body = r#"{"success":false,"error":{"code":"BUDGET_EXCEEDED","message":"over budget","request_id":null}}"#;
270 let err = parse_error(429, body, None);
271 assert!(matches!(err, Error::ServiceQuotaExceeded(_)));
272 assert!(!err.is_retryable());
274 }
275
276 #[test]
277 fn rate_limit_is_retryable() {
278 let body = r#"{"error":{"message":"rate limited","type":"rate_limit_error","code":"rate_limit_exceeded"}}"#;
279 let err = parse_error(429, body, Some(2.0));
280 assert!(matches!(err, Error::Throttling(_)));
281 assert!(err.is_retryable());
282 assert_eq!(err.retry_after_seconds(), Some(2.0));
283 }
284
285 #[test]
286 fn http_status_fallback_503() {
287 let err = parse_error(503, "Service Unavailable", None);
288 assert!(matches!(err, Error::ServiceUnavailable(_)));
289 assert!(err.is_retryable());
290 }
291
292 #[test]
293 fn upstream_error_5xx_is_service_unavailable() {
294 let body = r#"{"error":{"message":"upstream","code":"UPSTREAM_ERROR"}}"#;
295 let err = parse_error(502, body, None);
296 assert!(matches!(err, Error::ServiceUnavailable(_)));
297 }
298
299 #[test]
300 fn upstream_error_4xx_is_model_error() {
301 let body = r#"{"error":{"message":"upstream","code":"UPSTREAM_ERROR"}}"#;
302 let err = parse_error(400, body, None);
303 assert!(matches!(err, Error::ModelError(_)));
304 }
305
306 #[test]
307 fn parse_retry_after_value() {
308 assert_eq!(parse_retry_after("5"), Some(5.0));
309 assert_eq!(parse_retry_after("2.5"), Some(2.5));
310 assert_eq!(parse_retry_after("bad"), None);
311 }
312}