use thiserror::Error;
#[derive(Debug, Error)]
pub enum LlmError {
#[error("model not found: {0}")]
ModelNotFound(String),
#[error("model not ready: {0}")]
ModelNotReady(String),
#[error("validation error: {0}")]
Validation(String),
#[error("access denied: {0}")]
AccessDenied(String),
#[error("transport error: {0}")]
Transport(String),
#[error("rate limited")]
RateLimited,
#[error("upstream error (HTTP {status}): {body}")]
Upstream {
status: u16,
body: String,
},
}
impl LlmError {
pub fn is_alarm(&self) -> bool {
matches!(
self,
LlmError::ModelNotFound(_)
| LlmError::ModelNotReady(_)
| LlmError::Validation(_)
| LlmError::AccessDenied(_)
)
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
LlmError::Transport(_) | LlmError::RateLimited | LlmError::Upstream { .. }
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lm_error_classification() {
let alarm_variants: Vec<LlmError> = vec![
LlmError::ModelNotFound("no-such-model".into()),
LlmError::ModelNotReady("in-creating-state".into()),
LlmError::Validation("missing us. prefix".into()),
LlmError::AccessDenied("invalid api key".into()),
];
for err in &alarm_variants {
assert!(err.is_alarm(), "{err:?} should be alarm");
assert!(!err.is_retryable(), "{err:?} should not be retryable");
}
let retryable_variants: Vec<LlmError> = vec![
LlmError::Transport("connection refused".into()),
LlmError::RateLimited,
LlmError::Upstream {
status: 503,
body: "service unavailable".into(),
},
];
for err in &retryable_variants {
assert!(!err.is_alarm(), "{err:?} should not be alarm");
assert!(err.is_retryable(), "{err:?} should be retryable");
}
}
#[test]
fn error_messages_are_informative() {
assert_eq!(
LlmError::ModelNotFound("openai/gpt-99".into()).to_string(),
"model not found: openai/gpt-99"
);
assert_eq!(LlmError::RateLimited.to_string(), "rate limited");
assert_eq!(
LlmError::Upstream {
status: 503,
body: "overloaded".into()
}
.to_string(),
"upstream error (HTTP 503): overloaded"
);
}
}