use std::time::Duration;
use crate::retry::Retryable;
#[derive(Debug, thiserror::Error)]
pub enum LlmError {
#[error("HTTP {status}: {message}")]
Status {
status: u16,
message: String,
retry_after: Option<Duration>,
},
#[error("request failed")]
Transport(#[source] reqwest::Error),
#[error("invalid response: {0}")]
InvalidResponse(String),
#[error("{0} are not supported by this protocol")]
Unsupported(&'static str),
#[error("the API key is not a valid HTTP header value")]
InvalidApiKey,
#[error("cannot build the HTTP client")]
Client(#[source] reqwest::Error),
}
impl LlmError {
#[must_use]
pub fn is_fatal_for_stage(&self) -> bool {
match self {
Self::Status { status, .. } => (401..=404).contains(status),
Self::Unsupported(_) | Self::InvalidApiKey | Self::Client(_) => true,
Self::Transport(_) | Self::InvalidResponse(_) => false,
}
}
}
impl Retryable for LlmError {
fn is_retryable(&self) -> bool {
match self {
Self::Status { status, .. } => *status == 408 || *status == 429 || *status >= 500,
Self::Transport(_) => true,
_ => false,
}
}
fn retry_after(&self) -> Option<Duration> {
match self {
Self::Status { retry_after, .. } => *retry_after,
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn status(status: u16) -> LlmError {
LlmError::Status {
status,
message: String::new(),
retry_after: None,
}
}
#[test]
fn classification_of_errors() {
for code in [408, 429, 500, 503, 529] {
assert!(status(code).is_retryable(), "{code}");
}
for code in [400, 401, 403, 404] {
assert!(!status(code).is_retryable(), "{code}");
}
assert!(status(401).is_fatal_for_stage());
assert!(!status(400).is_fatal_for_stage());
assert!(!status(429).is_fatal_for_stage());
}
}