use thiserror::Error;
#[derive(Debug, Clone, Default)]
pub struct ErrorDetails {
pub message: String,
pub status_code: u16,
pub error_code: Option<String>,
pub request_id: Option<String>,
pub details: Option<serde_json::Value>,
pub retry_after_seconds: Option<f64>,
pub raw_body: String,
}
impl std::fmt::Display for ErrorDetails {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("ValidationException: {0}")]
Validation(Box<ErrorDetails>),
#[error("AccessDeniedException: {0}")]
AccessDenied(Box<ErrorDetails>),
#[error("ServiceQuotaExceededException: {0}")]
ServiceQuotaExceeded(Box<ErrorDetails>),
#[error("ResourceNotFoundException: {0}")]
ResourceNotFound(Box<ErrorDetails>),
#[error("ThrottlingException: {0}")]
Throttling(Box<ErrorDetails>),
#[error("ModelTimeoutException: {0}")]
ModelTimeout(Box<ErrorDetails>),
#[error("ModelErrorException: {0}")]
ModelError(Box<ErrorDetails>),
#[error("ModelStreamErrorException: {0}")]
ModelStreamError(Box<ErrorDetails>),
#[error("ServiceUnavailableException: {0}")]
ServiceUnavailable(Box<ErrorDetails>),
#[error("InternalServer: {0}")]
InternalServer(Box<ErrorDetails>),
#[error("MissingApiKeyError: no API key provided; set AUDACITY_API_KEY or pass api_key to Config::builder()")]
MissingApiKey,
#[error("SdkError: {message}")]
Sdk { message: String, retryable: bool },
}
impl Error {
pub(crate) fn sdk_network(message: impl Into<String>) -> Self {
Error::Sdk {
message: message.into(),
retryable: true,
}
}
pub(crate) fn sdk(message: impl Into<String>) -> Self {
Error::Sdk {
message: message.into(),
retryable: false,
}
}
pub(crate) fn client_validation(message: impl Into<String>) -> Self {
Error::Validation(Box::new(ErrorDetails {
message: message.into(),
..Default::default()
}))
}
pub(crate) fn model_stream_error(message: impl Into<String>) -> Self {
Error::ModelStreamError(Box::new(ErrorDetails {
message: message.into(),
..Default::default()
}))
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
Error::Throttling(_)
| Error::ModelTimeout(_)
| Error::ServiceUnavailable(_)
| Error::InternalServer(_)
| Error::Sdk {
retryable: true,
..
}
)
}
pub fn retry_after_seconds(&self) -> Option<f64> {
match self {
Error::Throttling(d)
| Error::ModelTimeout(d)
| Error::ServiceUnavailable(d)
| Error::InternalServer(d)
| Error::ModelError(d)
| Error::ModelStreamError(d)
| Error::Validation(d)
| Error::AccessDenied(d)
| Error::ServiceQuotaExceeded(d)
| Error::ResourceNotFound(d) => d.retry_after_seconds,
_ => None,
}
}
}
pub(crate) fn parse_error(status: u16, body: &str, retry_after: Option<f64>) -> Error {
let (message, error_code, request_id, error_details) = extract_error_fields(body);
let details = Box::new(ErrorDetails {
message: message.clone(),
status_code: status,
error_code: error_code.clone(),
request_id,
details: error_details,
retry_after_seconds: retry_after,
raw_body: body.to_owned(),
});
classify_error(status, error_code.as_deref(), details)
}
#[allow(clippy::type_complexity)]
pub(crate) fn extract_error_fields(
body: &str,
) -> (
String,
Option<String>,
Option<String>,
Option<serde_json::Value>,
) {
let Ok(v) = serde_json::from_str::<serde_json::Value>(body) else {
return (body.to_owned(), None, None, None);
};
if v.get("success").and_then(|s| s.as_bool()) == Some(false) {
if let Some(err) = v.get("error") {
let message = err
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error")
.to_owned();
let code = err.get("code").and_then(|c| c.as_str()).map(str::to_owned);
let request_id = err
.get("request_id")
.and_then(|r| r.as_str())
.map(str::to_owned);
let details = err.get("details").filter(|d| !d.is_null()).cloned();
return (message, code, request_id, details);
}
}
if let Some(err) = v.get("error") {
let message = err
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error")
.to_owned();
let code = err
.get("code")
.and_then(|c| c.as_str())
.filter(|s| !s.is_empty())
.or_else(|| err.get("type").and_then(|t| t.as_str()))
.map(str::to_owned);
return (message, code, None, None);
}
(body.to_owned(), None, None, None)
}
pub(crate) fn classify_error(status: u16, code: Option<&str>, details: Box<ErrorDetails>) -> Error {
let details = match code {
Some(c) => match classify_by_code(c, status, details) {
Ok(err) => return err,
Err(details) => details, },
None => details,
};
classify_by_status(status, details)
}
pub(crate) fn classify_by_code(
code: &str,
status: u16,
details: Box<ErrorDetails>,
) -> Result<Error, Box<ErrorDetails>> {
Ok(match code.to_uppercase().as_str() {
"INVALID_API_KEY"
| "API_KEY_REQUIRED"
| "AUTHENTICATION_ERROR"
| "AUTHORIZATION_ERROR"
| "MODEL_NOT_ALLOWED" => Error::AccessDenied(details),
"USAGE_CAP_EXCEEDED" | "USAGE_CAP_ERROR" | "BUDGET_EXCEEDED" => {
Error::ServiceQuotaExceeded(details)
}
"RATE_LIMIT_EXCEEDED" | "RATE_LIMIT_ERROR" => Error::Throttling(details),
"INVALID_REQUEST_ERROR" | "VALIDATION_ERROR" => Error::Validation(details),
"MODEL_NOT_FOUND" => Error::ResourceNotFound(details),
"TIMEOUT_ERROR" => Error::ModelTimeout(details),
"STREAM_ERROR" => Error::ModelStreamError(details),
"UPSTREAM_ERROR" => {
if status >= 500 {
Error::ServiceUnavailable(details)
} else {
Error::ModelError(details)
}
}
_ => return Err(details),
})
}
fn classify_by_status(status: u16, details: Box<ErrorDetails>) -> Error {
match status {
400 => Error::Validation(details),
401 | 403 => Error::AccessDenied(details),
402 => Error::ServiceQuotaExceeded(details),
404 => Error::ResourceNotFound(details),
408 => Error::ModelTimeout(details),
429 => Error::Throttling(details),
500 => Error::InternalServer(details),
502..=504 => Error::ServiceUnavailable(details),
s if (400..500).contains(&s) => Error::Validation(details),
s if s >= 500 => Error::InternalServer(details),
s => Error::sdk(format!(
"unexpected HTTP status {s}: {}",
details.message
)),
}
}
pub(crate) fn parse_retry_after(value: &str) -> Option<f64> {
value.trim().parse::<f64>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_a_authentication_error() {
let body = r#"{"error":{"message":"Invalid API key","type":"authentication_error","param":null,"code":"invalid_api_key"}}"#;
let err = parse_error(401, body, None);
assert!(matches!(err, Error::AccessDenied(_)));
if let Error::AccessDenied(d) = err {
assert_eq!(d.message, "Invalid API key");
assert_eq!(d.error_code.as_deref(), Some("invalid_api_key"));
assert!(d.request_id.is_none());
}
}
#[test]
fn shape_b_model_not_allowed() {
let body = r#"{"success":false,"error":{"code":"MODEL_NOT_ALLOWED","message":"Model not allowed","request_id":"req-123","details":{"binding_cap":"team-alpha"}}}"#;
let err = parse_error(403, body, None);
assert!(matches!(err, Error::AccessDenied(_)));
if let Error::AccessDenied(d) = err {
assert_eq!(d.request_id.as_deref(), Some("req-123"));
let details = d.details.expect("details must be captured from shape B");
assert_eq!(details["binding_cap"], "team-alpha");
}
}
#[test]
fn shape_a_has_no_details() {
let body = r#"{"error":{"message":"bad request","type":"invalid_request_error"}}"#;
let err = parse_error(400, body, None);
if let Error::Validation(d) = err {
assert!(d.details.is_none());
} else {
panic!("expected Validation");
}
}
#[test]
fn shape_b_budget_exceeded() {
let body = r#"{"success":false,"error":{"code":"BUDGET_EXCEEDED","message":"over budget","request_id":null}}"#;
let err = parse_error(429, body, None);
assert!(matches!(err, Error::ServiceQuotaExceeded(_)));
assert!(!err.is_retryable());
}
#[test]
fn rate_limit_is_retryable() {
let body = r#"{"error":{"message":"rate limited","type":"rate_limit_error","code":"rate_limit_exceeded"}}"#;
let err = parse_error(429, body, Some(2.0));
assert!(matches!(err, Error::Throttling(_)));
assert!(err.is_retryable());
assert_eq!(err.retry_after_seconds(), Some(2.0));
}
#[test]
fn http_status_fallback_503() {
let err = parse_error(503, "Service Unavailable", None);
assert!(matches!(err, Error::ServiceUnavailable(_)));
assert!(err.is_retryable());
}
#[test]
fn upstream_error_5xx_is_service_unavailable() {
let body = r#"{"error":{"message":"upstream","code":"UPSTREAM_ERROR"}}"#;
let err = parse_error(502, body, None);
assert!(matches!(err, Error::ServiceUnavailable(_)));
}
#[test]
fn upstream_error_4xx_is_model_error() {
let body = r#"{"error":{"message":"upstream","code":"UPSTREAM_ERROR"}}"#;
let err = parse_error(400, body, None);
assert!(matches!(err, Error::ModelError(_)));
}
#[test]
fn parse_retry_after_value() {
assert_eq!(parse_retry_after("5"), Some(5.0));
assert_eq!(parse_retry_after("2.5"), Some(2.5));
assert_eq!(parse_retry_after("bad"), None);
}
}