use std::fmt;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum LellmError {
#[error("LLM error: {0}")]
Llm(#[from] LlmError),
#[error("Tool error: {0}")]
Tool(#[from] ToolError),
#[error("Memory error: {0}")]
Memory(#[from] MemoryError),
#[error("Parse error: {0}")]
Parse(#[from] ParseError),
}
#[derive(Debug, Error, Clone)]
pub enum LlmError {
#[error("invalid request: {message}")]
InvalidRequest { message: String },
#[error("unsupported feature: {feature}")]
UnsupportedFeature { feature: String },
#[error("duplicate system prompt: both config and conversation contain system message")]
DuplicateSystemPrompt,
#[error("network error: {detail}")]
Network { detail: String },
#[error("request timeout: {detail}")]
Timeout { detail: String },
#[error("provider error [{provider}]: {message}")]
Provider {
provider: String,
status: Option<u16>,
code: Option<String>,
message: String,
},
#[error("response parse error: {detail}")]
Parse { detail: String },
#[error("unexpected EOF: stream ended without ResponseComplete")]
UnexpectedEof,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolErrorKind {
NotFound,
Timeout,
Network,
PermissionDenied,
InvalidInput,
RateLimited,
LoopDetected,
Internal,
}
impl ToolErrorKind {
pub fn is_retriable(self) -> bool {
matches!(self, Self::Timeout | Self::Network | Self::RateLimited)
}
}
impl fmt::Display for ToolErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound => write!(f, "NotFound"),
Self::Timeout => write!(f, "Timeout"),
Self::Network => write!(f, "Network"),
Self::PermissionDenied => write!(f, "PermissionDenied"),
Self::InvalidInput => write!(f, "InvalidInput"),
Self::RateLimited => write!(f, "RateLimited"),
Self::LoopDetected => write!(f, "LoopDetected"),
Self::Internal => write!(f, "Internal"),
}
}
}
#[derive(Clone)]
pub struct ToolError {
pub kind: ToolErrorKind,
pub message: String,
}
impl fmt::Display for ToolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.kind, self.message)
}
}
impl fmt::Debug for ToolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ToolError({}: {})", self.kind, self.message)
}
}
impl std::error::Error for ToolError {}
pub type ToolResult = Result<String, ToolError>;
#[derive(Debug, Error)]
pub enum MemoryError {
#[error("memory IO error: {0}")]
IoError(String),
#[error("memory database error: {0}")]
DatabaseError(String),
}
#[derive(Debug, Error)]
#[error("parse error: {detail}")]
pub struct ParseError {
pub detail: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_llm_error_display() {
let err = LlmError::Timeout {
detail: "timed out after 60s".into(),
};
assert!(format!("{}", err).contains("timeout"));
assert!(format!("{}", err).contains("60s"));
}
#[test]
fn test_llm_error_provider_display() {
let err = LlmError::Provider {
provider: "openai".into(),
status: Some(429),
code: Some("rate_limit".into()),
message: "Too many requests".into(),
};
assert!(format!("{}", err).contains("openai"));
assert!(format!("{}", err).contains("Too many requests"));
}
#[test]
fn test_llm_error_invalid_request_display() {
let err = LlmError::InvalidRequest {
message: "Anthropic requires max_tokens".into(),
};
assert!(format!("{}", err).contains("invalid request"));
assert!(format!("{}", err).contains("max_tokens"));
}
#[test]
fn test_tool_error_display() {
let err = ToolError {
kind: ToolErrorKind::NotFound,
message: "read_file".into(),
};
assert!(format!("{}", err).contains("read_file"));
}
#[test]
fn test_lellm_error_from_tool_error() {
let tool_err = ToolError {
kind: ToolErrorKind::Timeout,
message: "timeout".into(),
};
let top_err: LellmError = tool_err.into();
assert!(format!("{}", top_err).contains("Tool error"));
}
#[test]
fn test_tool_error_is_retriable() {
assert!(ToolErrorKind::Timeout.is_retriable());
assert!(ToolErrorKind::Network.is_retriable());
assert!(ToolErrorKind::RateLimited.is_retriable());
assert!(!ToolErrorKind::NotFound.is_retriable());
assert!(!ToolErrorKind::InvalidInput.is_retriable());
}
}