use thiserror::Error;
#[derive(Debug, Error)]
pub enum SmLlmError {
#[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,
},
#[error("degraded: no inference provider configured ({0})")]
Degraded(String),
}
impl SmLlmError {
pub fn is_alarm(&self) -> bool {
matches!(
self,
SmLlmError::ModelNotFound(_)
| SmLlmError::ModelNotReady(_)
| SmLlmError::Validation(_)
| SmLlmError::AccessDenied(_)
)
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
SmLlmError::Transport(_) | SmLlmError::RateLimited | SmLlmError::Upstream { .. }
)
}
pub fn is_degraded(&self) -> bool {
matches!(self, SmLlmError::Degraded(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sm_llm_error_classification() {
let alarm: Vec<SmLlmError> = vec![
SmLlmError::ModelNotFound("x".into()),
SmLlmError::ModelNotReady("x".into()),
SmLlmError::Validation("x".into()),
SmLlmError::AccessDenied("x".into()),
];
for e in &alarm {
assert!(e.is_alarm(), "{e:?} should alarm");
assert!(!e.is_retryable(), "{e:?} should not retry");
assert!(!e.is_degraded(), "{e:?} is not degraded");
}
let transient: Vec<SmLlmError> = vec![
SmLlmError::Transport("x".into()),
SmLlmError::RateLimited,
SmLlmError::Upstream {
status: 503,
body: "x".into(),
},
];
for e in &transient {
assert!(!e.is_alarm(), "{e:?} should not alarm");
assert!(e.is_retryable(), "{e:?} should retry");
assert!(!e.is_degraded(), "{e:?} is not degraded");
}
let degraded = SmLlmError::Degraded("no keys".into());
assert!(!degraded.is_alarm());
assert!(!degraded.is_retryable());
assert!(degraded.is_degraded());
}
#[test]
fn sm_llm_error_messages() {
assert_eq!(
SmLlmError::ModelNotFound("anthropic/claude-x".into()).to_string(),
"model not found: anthropic/claude-x"
);
assert_eq!(SmLlmError::RateLimited.to_string(), "rate limited");
assert_eq!(
SmLlmError::Upstream {
status: 503,
body: "overloaded".into()
}
.to_string(),
"upstream error (HTTP 503): overloaded"
);
assert_eq!(
SmLlmError::Degraded("no ANTHROPIC_API_KEY / AWS / OPENROUTER_API_KEY".into())
.to_string(),
"degraded: no inference provider configured (no ANTHROPIC_API_KEY / AWS / OPENROUTER_API_KEY)"
);
}
}