use std::time::Duration;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("unsupported: {0}")]
Unsupported(String),
#[error("authentication failed: {0}")]
Auth(String),
#[error("not found: {0}")]
NotFound(String),
#[error("invalid request: {0}")]
InvalidRequest(String),
#[error("rate limited{}", match .retry_after {
Some(d) => format!(", retry after {}ms", d.as_millis()),
None => String::new(),
})]
RateLimited {
retry_after: Option<Duration>,
},
#[error("timed out after {}ms", .elapsed.as_millis())]
Timeout {
elapsed: Duration,
},
#[error("the model declined{}", match .category {
Some(c) => format!(": {c}"),
None => String::new(),
})]
Refused {
category: Option<String>,
},
#[error("transient: {0}")]
Transient(String),
#[error("the provider replied and the reply could not be read: {0}")]
Unreadable(String),
}
impl Error {
pub fn is_retryable(&self) -> bool {
matches!(
self,
Error::RateLimited { .. } | Error::Timeout { .. } | Error::Transient(_)
)
}
pub fn retry_after(&self) -> Option<Duration> {
match self {
Error::RateLimited { retry_after } => *retry_after,
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bad_credential_is_never_retried() {
assert!(!Error::Auth("no key".into()).is_retryable());
}
#[test]
fn a_refusal_is_an_answer_rather_than_a_failure_to_retry() {
assert!(!Error::Refused { category: None }.is_retryable());
}
#[test]
fn an_unreadable_reply_is_not_retried_either() {
assert!(!Error::Unreadable("missing content".into()).is_retryable());
}
#[test]
fn a_rate_limit_carries_the_wait_the_provider_asked_for() {
let limited = Error::RateLimited {
retry_after: Some(Duration::from_secs(30)),
};
assert!(limited.is_retryable());
assert_eq!(limited.retry_after(), Some(Duration::from_secs(30)));
assert!(limited.to_string().contains("30000ms"));
}
}