use std::fmt;
pub type LlmResult<T> = Result<T, LlmError>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LlmError {
#[error("invalid request: {0}")]
InvalidRequest(String),
#[error("authentication failed: {0}")]
Auth(String),
#[error("rate limited{}", .retry_after.map(|d| format!(" (retry after {}s)", d.as_secs())).unwrap_or_default())]
RateLimited {
retry_after: Option<std::time::Duration>,
message: String,
},
#[error("provider error (status {status}): {message}")]
Provider {
status: u16,
message: String,
},
#[error("transport error: {0}")]
Transport(String),
#[error("request timed out")]
Timeout,
#[error("serialization error: {0}")]
Serialization(String),
#[error("stream error: {0}")]
Stream(String),
#[error("budget exceeded: {0}")]
BudgetExceeded(String),
#[error("tool `{tool}` failed: {message}")]
Tool {
tool: String,
message: String,
},
#[error("unsupported: {0}")]
Unsupported(String),
#[error("all providers failed ({} attempted)", .0.len())]
AllProvidersFailed(Vec<LlmError>),
#[error("{0}")]
Other(String),
}
impl LlmError {
pub fn is_retryable(&self) -> bool {
match self {
LlmError::Timeout
| LlmError::Transport(_)
| LlmError::Stream(_)
| LlmError::RateLimited { .. } => true,
LlmError::Provider { status, .. } => *status >= 500 || *status == 408,
_ => false,
}
}
pub fn invalid(msg: impl fmt::Display) -> Self {
LlmError::InvalidRequest(msg.to_string())
}
pub fn serde(msg: impl fmt::Display) -> Self {
LlmError::Serialization(msg.to_string())
}
}
impl From<serde_json::Error> for LlmError {
fn from(e: serde_json::Error) -> Self {
LlmError::Serialization(e.to_string())
}
}