Skip to main content

lellm_core/
error.rs

1//! 错误类型定义。
2
3use std::fmt;
4use thiserror::Error;
5
6/// lellm 顶层错误类型 — 门面层统一错误出口。
7///
8/// **架构归属:** `lellm` facade crate(聚合各子层错误)
9/// **代码位置:** `lellm-core`(暂留,便于 `#[from]` 跨 crate 转换)
10///
11/// **铁律:Core 公共 API 禁止返回 `LellmError`。**
12/// 各层必须返回各自的领域错误:
13/// - Provider API → `Result<T, LlmError>`
14/// - Tool 执行 → `Result<String, ToolError>`
15/// - 记忆操作 → `Result<T, MemoryError>`
16/// - 解析操作 → `Result<T, ParseError>`
17///
18/// **迁移计划:** 等 facade 承担业务逻辑时,移至 `lellm/src/error.rs`。
19#[derive(Debug, Error)]
20pub enum LellmError {
21    #[error("LLM error: {0}")]
22    Llm(#[from] LlmError),
23
24    #[error("Tool error: {0}")]
25    Tool(#[from] ToolError),
26
27    #[error("Memory error: {0}")]
28    Memory(#[from] MemoryError),
29
30    #[error("Parse error: {0}")]
31    Parse(#[from] ParseError),
32}
33
34/// LLM API 错误。
35///
36/// 错误分类:
37/// - **InvalidRequest** — 调用方构造了非法请求(发请求前本地可发现)
38/// - **UnsupportedFeature** — SDK 不支持的功能(能力边界)
39/// - **DuplicateSystemPrompt** — 系统提示冲突
40/// - **Provider** — 请求已发出,对端返回错误(401/429/500/…)
41/// - **Parse** — 响应体 JSON 解析失败
42/// - **Network** — 网络层错误
43/// - **Timeout** — 请求超时
44/// - **UnexpectedEof** — 流式输出意外结束
45#[derive(Debug, Error, Clone)]
46pub enum LlmError {
47    #[error("invalid request: {message}")]
48    InvalidRequest { message: String },
49
50    #[error("unsupported feature: {feature}")]
51    UnsupportedFeature { feature: String },
52
53    #[error("duplicate system prompt: both config and conversation contain system message")]
54    DuplicateSystemPrompt,
55
56    #[error("network error: {detail}")]
57    Network { detail: String },
58
59    #[error("request timeout: {detail}")]
60    Timeout { detail: String },
61
62    #[error("provider error [{provider}]: {message}")]
63    Provider {
64        provider: String,
65        status: Option<u16>,
66        code: Option<String>,
67        message: String,
68    },
69
70    #[error("response parse error: {detail}")]
71    Parse { detail: String },
72
73    #[error("unexpected EOF: stream ended without ResponseComplete")]
74    UnexpectedEof,
75}
76
77/// 工具执行错误的分类。
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ToolErrorKind {
80    /// 工具未找到
81    NotFound,
82    /// 工具执行超时
83    Timeout,
84    /// 网络相关错误
85    Network,
86    /// 权限不足
87    PermissionDenied,
88    /// 输入参数无效
89    InvalidInput,
90    /// 被限流
91    RateLimited,
92    /// 检测到循环调用
93    LoopDetected,
94    /// 内部错误(兜底)
95    Internal,
96}
97
98impl ToolErrorKind {
99    /// 该错误类型是否值得重试
100    pub fn is_retriable(self) -> bool {
101        matches!(self, Self::Timeout | Self::Network | Self::RateLimited)
102    }
103}
104
105impl fmt::Display for ToolErrorKind {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            Self::NotFound => write!(f, "NotFound"),
109            Self::Timeout => write!(f, "Timeout"),
110            Self::Network => write!(f, "Network"),
111            Self::PermissionDenied => write!(f, "PermissionDenied"),
112            Self::InvalidInput => write!(f, "InvalidInput"),
113            Self::RateLimited => write!(f, "RateLimited"),
114            Self::LoopDetected => write!(f, "LoopDetected"),
115            Self::Internal => write!(f, "Internal"),
116        }
117    }
118}
119
120/// 工具执行错误 — 携带错误分类与详细描述。
121#[derive(Clone)]
122pub struct ToolError {
123    pub kind: ToolErrorKind,
124    pub message: String,
125}
126
127impl fmt::Display for ToolError {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(f, "[{}] {}", self.kind, self.message)
130    }
131}
132
133impl fmt::Debug for ToolError {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        write!(f, "ToolError({}: {})", self.kind, self.message)
136    }
137}
138
139impl std::error::Error for ToolError {}
140
141/// 工具执行结果 — Rust 原生 Result,不包装枚举。
142pub type ToolResult = Result<String, ToolError>;
143
144/// 记忆操作错误。
145#[derive(Debug, Error)]
146pub enum MemoryError {
147    #[error("memory IO error: {0}")]
148    IoError(String),
149
150    #[error("memory database error: {0}")]
151    DatabaseError(String),
152}
153
154/// 解析错误。
155#[derive(Debug, Error)]
156#[error("parse error: {detail}")]
157pub struct ParseError {
158    pub detail: String,
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_llm_error_display() {
167        let err = LlmError::Timeout {
168            detail: "timed out after 60s".into(),
169        };
170        assert!(format!("{}", err).contains("timeout"));
171        assert!(format!("{}", err).contains("60s"));
172    }
173
174    #[test]
175    fn test_llm_error_provider_display() {
176        let err = LlmError::Provider {
177            provider: "openai".into(),
178            status: Some(429),
179            code: Some("rate_limit".into()),
180            message: "Too many requests".into(),
181        };
182        assert!(format!("{}", err).contains("openai"));
183        assert!(format!("{}", err).contains("Too many requests"));
184    }
185
186    #[test]
187    fn test_llm_error_invalid_request_display() {
188        let err = LlmError::InvalidRequest {
189            message: "Anthropic requires max_tokens".into(),
190        };
191        assert!(format!("{}", err).contains("invalid request"));
192        assert!(format!("{}", err).contains("max_tokens"));
193    }
194
195    #[test]
196    fn test_tool_error_display() {
197        let err = ToolError {
198            kind: ToolErrorKind::NotFound,
199            message: "read_file".into(),
200        };
201        assert!(format!("{}", err).contains("read_file"));
202    }
203
204    #[test]
205    fn test_lellm_error_from_tool_error() {
206        let tool_err = ToolError {
207            kind: ToolErrorKind::Timeout,
208            message: "timeout".into(),
209        };
210        let top_err: LellmError = tool_err.into();
211        assert!(format!("{}", top_err).contains("Tool error"));
212    }
213
214    #[test]
215    fn test_tool_error_is_retriable() {
216        assert!(ToolErrorKind::Timeout.is_retriable());
217        assert!(ToolErrorKind::Network.is_retriable());
218        assert!(ToolErrorKind::RateLimited.is_retriable());
219        assert!(!ToolErrorKind::NotFound.is_retriable());
220        assert!(!ToolErrorKind::InvalidInput.is_retriable());
221    }
222}