use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum LlmError {
#[error("LLM API error: {0}")]
Api(String),
#[error("Request error: {0}")]
Request(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Failed to parse response: {0}")]
Parse(String),
#[error("Rate limit exceeded: {0}")]
RateLimit(String),
#[error("Request timeout: {0}")]
Timeout(String),
#[error("LLM returned no content")]
NoContent,
#[error("Retry exhausted after {attempts} attempts: {last_error}")]
RetryExhausted {
attempts: usize,
last_error: String,
},
}
impl LlmError {
pub fn is_retryable(&self) -> bool {
match self {
LlmError::Api(msg) => {
let msg_lower = msg.to_lowercase();
msg_lower.contains("rate limit")
|| msg_lower.contains("429")
|| msg_lower.contains("503")
|| msg_lower.contains("502")
|| msg_lower.contains("timeout")
|| msg_lower.contains("overloaded")
}
LlmError::Timeout(_) => true,
LlmError::RateLimit(_) => true,
_ => false,
}
}
pub fn from_api_message(msg: &str) -> Self {
let msg_lower = msg.to_lowercase();
if msg_lower.contains("rate limit") || msg_lower.contains("429") {
LlmError::RateLimit(msg.to_string())
} else if msg_lower.contains("timeout") {
LlmError::Timeout(msg.to_string())
} else {
LlmError::Api(msg.to_string())
}
}
}
impl From<async_openai::error::OpenAIError> for LlmError {
fn from(e: async_openai::error::OpenAIError) -> Self {
let msg = e.to_string();
LlmError::from_api_message(&msg)
}
}
impl From<serde_json::Error> for LlmError {
fn from(e: serde_json::Error) -> Self {
LlmError::Parse(e.to_string())
}
}
impl From<LlmError> for crate::Error {
fn from(e: LlmError) -> Self {
crate::Error::Llm(e.to_string())
}
}
impl From<LlmError> for String {
fn from(e: LlmError) -> Self {
e.to_string()
}
}
pub type LlmResult<T> = std::result::Result<T, LlmError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_retryable() {
assert!(LlmError::RateLimit("test".to_string()).is_retryable());
assert!(LlmError::Timeout("test".to_string()).is_retryable());
assert!(LlmError::Api("rate limit exceeded".to_string()).is_retryable());
assert!(!LlmError::Config("test".to_string()).is_retryable());
assert!(!LlmError::Parse("test".to_string()).is_retryable());
}
#[test]
fn test_from_api_message() {
let err = LlmError::from_api_message("Rate limit exceeded");
assert!(matches!(err, LlmError::RateLimit(_)));
let err = LlmError::from_api_message("Request timeout");
assert!(matches!(err, LlmError::Timeout(_)));
let err = LlmError::from_api_message("Internal server error");
assert!(matches!(err, LlmError::Api(_)));
}
}