#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum LlmError {
#[error("[{provider}] {message}")]
Auth {
provider: String,
message: String,
},
#[error("[{provider}] Rate limit exceeded. Please retry after some time.")]
RateLimited {
provider: String,
},
#[error("Context length exceeded: used {used}, max {max}")]
ContextExceeded {
used: usize,
max: usize,
},
#[error("Expected {expected}, got {got}")]
ResponseFormat {
expected: String,
got: String,
},
#[error("{0}")]
Network(String),
#[error("{0}")]
Stream(String),
#[error("HTTP {status}: {body}")]
HttpStatus {
status: u16,
body: String,
},
#[error("[{provider}] {message}")]
Provider {
provider: String,
message: String,
code: Option<String>,
},
#[error("{0}")]
Internal(String),
#[error("Feature not supported: {0}")]
NotSupported(String),
}
impl LlmError {
#[must_use]
pub fn auth(provider: impl Into<String>, message: impl Into<String>) -> Self {
Self::Auth {
provider: provider.into(),
message: message.into(),
}
}
#[must_use]
pub fn rate_limited(provider: impl Into<String>) -> Self {
Self::RateLimited {
provider: provider.into(),
}
}
#[must_use]
pub const fn context_exceeded(used: usize, max: usize) -> Self {
Self::ContextExceeded { used, max }
}
#[must_use]
pub fn response_format(expected: impl Into<String>, got: impl Into<String>) -> Self {
Self::ResponseFormat {
expected: expected.into(),
got: got.into(),
}
}
#[must_use]
pub fn network(message: impl Into<String>) -> Self {
Self::Network(message.into())
}
#[must_use]
pub fn stream(message: impl Into<String>) -> Self {
Self::Stream(message.into())
}
#[must_use]
pub fn http_status(status: u16, body: impl Into<String>) -> Self {
Self::HttpStatus {
status,
body: body.into(),
}
}
#[must_use]
pub fn provider(provider: impl Into<String>, message: impl Into<String>) -> Self {
Self::Provider {
provider: provider.into(),
message: message.into(),
code: None,
}
}
#[must_use]
pub fn provider_code(
provider: impl Into<String>,
code: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::Provider {
provider: provider.into(),
message: message.into(),
code: Some(code.into()),
}
}
#[must_use]
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal(message.into())
}
#[must_use]
pub fn not_supported(feature: impl Into<String>) -> Self {
Self::NotSupported(feature.into())
}
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(self, Self::RateLimited { .. } | Self::Network(_))
}
}
impl From<reqwest::Error> for LlmError {
fn from(err: reqwest::Error) -> Self {
if err.is_timeout() {
Self::network("Request timed out")
} else if err.is_connect() {
Self::network(format!("Connection failed: {err}"))
} else {
Self::network(err.to_string())
}
}
}