pub trait LlmError: std::error::Error + Send + Sync + 'static {
fn kind(&self) -> LlmErrorKind {
LlmErrorKind::Other
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LlmErrorKind {
RateLimit,
Timeout,
Unavailable,
Auth,
BadRequest,
#[default]
Other,
}
#[must_use]
pub const fn kind_from_http_status(status: u16) -> LlmErrorKind {
match status {
429 => LlmErrorKind::RateLimit,
408 | 504 => LlmErrorKind::Timeout,
401 | 403 => LlmErrorKind::Auth,
400 | 404 | 422 => LlmErrorKind::BadRequest,
500..=599 => LlmErrorKind::Unavailable,
_ => LlmErrorKind::Other,
}
}
#[derive(Debug, thiserror::Error)]
pub enum DummyError {
#[error("transport: {0}")]
Transport(String),
#[error("provider returned status {status}: {body}")]
Provider {
status: u16,
body: String,
},
#[error("stream interrupted: {0}")]
StreamInterrupted(String),
#[error("other: {0}")]
Other(String),
}
impl LlmError for DummyError {
fn kind(&self) -> LlmErrorKind {
match self {
Self::Transport(_) | Self::StreamInterrupted(_) => LlmErrorKind::Unavailable,
Self::Provider { status, .. } => kind_from_http_status(*status),
Self::Other(_) => LlmErrorKind::Other,
}
}
}
#[cfg(test)]
mod tests {
use super::{DummyError, LlmError};
fn require_llm_error<E: LlmError>() {}
fn assert_send_sync<T: Send + Sync + 'static>() {}
#[test]
fn display_transport() {
let e = DummyError::Transport("DNS failure".to_owned());
assert_eq!(format!("{e}"), "transport: DNS failure");
}
#[test]
fn display_provider() {
let e = DummyError::Provider {
status: 404,
body: "not found".to_owned(),
};
assert_eq!(format!("{e}"), "provider returned status 404: not found");
}
#[test]
fn display_stream_interrupted() {
let e = DummyError::StreamInterrupted("EOF".to_owned());
assert_eq!(format!("{e}"), "stream interrupted: EOF");
}
#[test]
fn display_other() {
let e = DummyError::Other("unexpected".to_owned());
assert_eq!(format!("{e}"), "other: unexpected");
}
#[test]
fn debug_is_derived() {
let e = DummyError::Transport("t".to_owned());
assert!(format!("{e:?}").contains("Transport"));
}
#[test]
fn dummy_error_satisfies_llm_error() {
require_llm_error::<DummyError>();
}
#[test]
fn dummy_error_is_send_sync_static() {
assert_send_sync::<DummyError>();
}
#[test]
fn dummy_error_boxes_as_std_error() {
let _: Box<dyn std::error::Error + Send + Sync + 'static> =
Box::new(DummyError::Other("boxed".to_owned()));
}
#[test]
fn http_status_maps_to_kind() {
use super::{LlmErrorKind, kind_from_http_status};
assert_eq!(kind_from_http_status(429), LlmErrorKind::RateLimit);
assert_eq!(kind_from_http_status(504), LlmErrorKind::Timeout);
assert_eq!(kind_from_http_status(408), LlmErrorKind::Timeout);
assert_eq!(kind_from_http_status(401), LlmErrorKind::Auth);
assert_eq!(kind_from_http_status(403), LlmErrorKind::Auth);
assert_eq!(kind_from_http_status(400), LlmErrorKind::BadRequest);
assert_eq!(kind_from_http_status(404), LlmErrorKind::BadRequest);
assert_eq!(kind_from_http_status(503), LlmErrorKind::Unavailable);
assert_eq!(kind_from_http_status(200), LlmErrorKind::Other);
}
#[test]
fn dummy_error_classifies() {
use super::{LlmError, LlmErrorKind};
assert_eq!(
DummyError::Provider {
status: 429,
body: String::new()
}
.kind(),
LlmErrorKind::RateLimit
);
assert_eq!(
DummyError::Transport("reset".to_owned()).kind(),
LlmErrorKind::Unavailable
);
assert_eq!(
DummyError::Other("x".to_owned()).kind(),
LlmErrorKind::Other
);
}
}