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<T, 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///
79/// `Copy` 约束保留——所有变体均为 `Copy` 类型。
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum ToolErrorKind {
82    /// 工具未找到(静态目录中从未存在)
83    NotFound,
84    /// 工具不可用(动态目录中曾存在但当前刷新后消失)
85    ToolUnavailable,
86    /// 工具执行超时
87    Timeout,
88    /// 网络相关错误
89    Network,
90    /// 权限不足
91    PermissionDenied,
92    /// 输入参数无效
93    InvalidInput,
94    /// 被限流
95    RateLimited,
96    /// 检测到循环调用
97    LoopDetected,
98    /// 内部错误(兜底)
99    Internal,
100    /// 外部业务错误(由用户代码抛出,自动桥接)
101    ///
102    /// `source` 为原始错误类型的 `type_name`,用于可观测性。
103    External { source: &'static str },
104}
105
106impl ToolErrorKind {
107    /// 判断该错误是否属于基础设施层面的瞬态故障(Transient Failure)。
108    ///
109    /// **可重试(原地静默重试):**
110    /// - `Timeout` / `Network` / `RateLimited` — 网络抖动、服务端过载
111    /// - `ToolUnavailable` — 动态目录瞬态不可用(MCP 重启等)
112    ///
113    /// **不可重试(立即弹回 LLM 修复层):**
114    /// - `InvalidInput` — 参数错了就是错了
115    /// - `NotFound` — 工具不存在,重试也没用
116    /// - `PermissionDenied` — 权限不会自动恢复
117    /// - `External` — 用户业务错误,框架不应猜测
118    /// - `LoopDetected` — 循环检测,重试无意义
119    /// - `Internal` — 内部错误,重试无意义
120    pub fn is_retryable(self) -> bool {
121        matches!(
122            self,
123            Self::Timeout | Self::Network | Self::RateLimited | Self::ToolUnavailable
124        )
125    }
126}
127
128impl fmt::Display for ToolErrorKind {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            Self::NotFound => write!(f, "NotFound"),
132            Self::ToolUnavailable => write!(f, "ToolUnavailable"),
133            Self::Timeout => write!(f, "Timeout"),
134            Self::Network => write!(f, "Network"),
135            Self::PermissionDenied => write!(f, "PermissionDenied"),
136            Self::InvalidInput => write!(f, "InvalidInput"),
137            Self::RateLimited => write!(f, "RateLimited"),
138            Self::LoopDetected => write!(f, "LoopDetected"),
139            Self::Internal => write!(f, "Internal"),
140            Self::External { source } => write!(f, "External({})", source),
141        }
142    }
143}
144
145/// 工具执行错误 — 携带错误分类与详细描述。
146#[derive(Clone)]
147pub struct ToolError {
148    pub kind: ToolErrorKind,
149    pub message: String,
150}
151
152impl ToolError {
153    /// 构造 `InvalidInput` 错误。
154    pub fn invalid_input(msg: impl Into<String>) -> Self {
155        Self {
156            kind: ToolErrorKind::InvalidInput,
157            message: msg.into(),
158        }
159    }
160
161    /// 构造 `NotFound` 错误。
162    pub fn not_found(msg: impl Into<String>) -> Self {
163        Self {
164            kind: ToolErrorKind::NotFound,
165            message: msg.into(),
166        }
167    }
168
169    /// 构造 `External` 错误,自动记录原始错误类型名。
170    pub fn external<E: std::fmt::Display>(source: E) -> Self {
171        Self {
172            kind: ToolErrorKind::External {
173                source: std::any::type_name::<E>(),
174            },
175            message: source.to_string(),
176        }
177    }
178}
179
180impl fmt::Display for ToolError {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        write!(f, "[{}] {}", self.kind, self.message)
183    }
184}
185
186impl fmt::Debug for ToolError {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        write!(f, "ToolError({}: {})", self.kind, self.message)
189    }
190}
191
192impl std::error::Error for ToolError {}
193
194/// 工具执行结果 — `serde_json::Value` 支持结构化数据。
195///
196/// 通过 `IntoToolResult` trait,用户可返回 `String`、`Value`、
197/// `Option<T>`、`Result<T, E>` 等类型,框架自动转换。
198pub type ToolResult = Result<serde_json::Value, ToolError>;
199
200// ─── IntoToolError ───────────────────────────────────────────────
201
202/// 将已知错误类型转换为 `ToolError`。
203///
204/// **设计原则:** 不使用 blanket impl(`impl<E: Display>`),避免吞掉 `ToolError` 原始分类。
205/// 只为核心错误类型提供显式实现。
206///
207/// **已有实现:**
208/// - `ToolError` → 直接透传
209/// - `std::io::Error` → `External`
210/// - `serde_json::Error` → `Internal`
211/// - `anyhow::Error` → `External`(需 `anyhow` feature)
212pub trait IntoToolError {
213    fn into_tool_error(self) -> ToolError;
214}
215
216/// `ToolError` → 直接透传,不包装
217impl IntoToolError for ToolError {
218    fn into_tool_error(self) -> ToolError {
219        self
220    }
221}
222
223/// `std::io::Error` → `External`
224impl IntoToolError for std::io::Error {
225    fn into_tool_error(self) -> ToolError {
226        ToolError::external(self)
227    }
228}
229
230/// `serde_json::Error` → `Internal`
231impl IntoToolError for serde_json::Error {
232    fn into_tool_error(self) -> ToolError {
233        ToolError {
234            kind: ToolErrorKind::Internal,
235            message: self.to_string(),
236        }
237    }
238}
239
240/// `anyhow::Error` → `External`
241#[cfg(feature = "anyhow")]
242impl IntoToolError for anyhow::Error {
243    fn into_tool_error(self) -> ToolError {
244        ToolError::external(self)
245    }
246}
247
248// ─── IntoToolResult ──────────────────────────────────────────────
249
250/// 将工具函数返回值统一转换为 `ToolResult`。
251///
252/// 由 `#[tool]` 宏在闭包中调用,用户无需手动实现。
253///
254/// **支持的返回类型:**
255/// - `String` → `Ok(Value::String(s))`
256/// - `serde_json::Value` → `Ok(v)`
257/// - `T: Serialize` → `Ok(serde_json::to_value(t)?)`
258/// - `Option<T>` → `Some` 转 Value,`None` → `Ok(Value::Null)`
259/// - `Result<T, ToolError>` → 直接透传
260/// - `Result<T, E: Display>` → `Ok` 转 Value,`Err` → `External`
261pub trait IntoToolResult: Sized {
262    fn into_tool(self) -> ToolResult;
263}
264
265/// `String` → `Ok(Value::String(s))`
266impl IntoToolResult for String {
267    fn into_tool(self) -> ToolResult {
268        Ok(serde_json::Value::String(self))
269    }
270}
271
272/// `serde_json::Value` → 直接透传
273impl IntoToolResult for serde_json::Value {
274    fn into_tool(self) -> ToolResult {
275        Ok(self)
276    }
277}
278
279/// `Option<T>` → `Some` 序列化,`None` → `Value::Null`
280impl<T> IntoToolResult for Option<T>
281where
282    T: serde::Serialize,
283{
284    fn into_tool(self) -> ToolResult {
285        match self {
286            Some(v) => serde_json::to_value(v).map_err(|e| ToolError {
287                kind: ToolErrorKind::Internal,
288                message: format!("failed to serialize tool result: {}", e),
289            }),
290            None => Ok(serde_json::Value::Null),
291        }
292    }
293}
294
295/// `Result<T, E>` (T: Serialize, E: IntoToolError) → 自动桥接
296///
297/// `E: IntoToolError` 约束确保只有显式实现的错误类型才能转换。
298/// `ToolError` → 直接透传,`std::io::Error` → External,`serde_json::Error` → Internal。
299impl<T, E> IntoToolResult for Result<T, E>
300where
301    T: serde::Serialize,
302    E: IntoToolError,
303{
304    fn into_tool(self) -> ToolResult {
305        match self {
306            Ok(v) => serde_json::to_value(v).map_err(|e| ToolError {
307                kind: ToolErrorKind::Internal,
308                message: format!("failed to serialize tool result: {}", e),
309            }),
310            Err(e) => Err(e.into_tool_error()),
311        }
312    }
313}
314
315// ─── 其他错误类型 ────────────────────────────────────────────────
316
317/// 记忆操作错误。
318#[derive(Debug, Error)]
319pub enum MemoryError {
320    #[error("memory IO error: {0}")]
321    IoError(String),
322
323    #[error("memory database error: {0}")]
324    DatabaseError(String),
325}
326
327/// 解析错误。
328#[derive(Debug, Error)]
329#[error("parse error: {detail}")]
330pub struct ParseError {
331    pub detail: String,
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_llm_error_display() {
340        let err = LlmError::Timeout {
341            detail: "timed out after 60s".into(),
342        };
343        assert!(format!("{}", err).contains("timeout"));
344        assert!(format!("{}", err).contains("60s"));
345    }
346
347    #[test]
348    fn test_llm_error_provider_display() {
349        let err = LlmError::Provider {
350            provider: "openai".into(),
351            status: Some(429),
352            code: Some("rate_limit".into()),
353            message: "Too many requests".into(),
354        };
355        assert!(format!("{}", err).contains("openai"));
356        assert!(format!("{}", err).contains("Too many requests"));
357    }
358
359    #[test]
360    fn test_llm_error_invalid_request_display() {
361        let err = LlmError::InvalidRequest {
362            message: "Anthropic requires max_tokens".into(),
363        };
364        assert!(format!("{}", err).contains("invalid request"));
365        assert!(format!("{}", err).contains("max_tokens"));
366    }
367
368    #[test]
369    fn test_tool_error_display() {
370        let err = ToolError {
371            kind: ToolErrorKind::NotFound,
372            message: "read_file".into(),
373        };
374        assert!(format!("{}", err).contains("read_file"));
375    }
376
377    #[test]
378    fn test_lellm_error_from_tool_error() {
379        let tool_err = ToolError {
380            kind: ToolErrorKind::Timeout,
381            message: "timeout".into(),
382        };
383        let top_err: LellmError = tool_err.into();
384        assert!(format!("{}", top_err).contains("Tool error"));
385    }
386
387    #[test]
388    fn test_tool_error_is_retryable() {
389        // 可重试
390        assert!(ToolErrorKind::Timeout.is_retryable());
391        assert!(ToolErrorKind::Network.is_retryable());
392        assert!(ToolErrorKind::RateLimited.is_retryable());
393        assert!(ToolErrorKind::ToolUnavailable.is_retryable());
394
395        // 不可重试
396        assert!(!ToolErrorKind::NotFound.is_retryable());
397        assert!(!ToolErrorKind::InvalidInput.is_retryable());
398        assert!(!ToolErrorKind::PermissionDenied.is_retryable());
399        assert!(!ToolErrorKind::Internal.is_retryable());
400        assert!(!ToolErrorKind::LoopDetected.is_retryable());
401        assert!(!ToolErrorKind::External { source: "test" }.is_retryable());
402    }
403
404    #[test]
405    fn test_into_tool_result_string() {
406        let result: ToolResult = "hello".to_string().into_tool();
407        assert_eq!(result.unwrap(), serde_json::json!("hello"));
408    }
409
410    #[test]
411    fn test_into_tool_result_option() {
412        let some: Option<String> = Some("hello".to_string());
413        assert_eq!(some.into_tool().unwrap(), serde_json::json!("hello"));
414
415        let none: Option<String> = None;
416        assert_eq!(none.into_tool().unwrap(), serde_json::json!(null));
417    }
418
419    #[test]
420    fn test_into_tool_result_external_error() {
421        #[derive(Debug)]
422        struct MyError;
423        impl fmt::Display for MyError {
424            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425                write!(f, "my error")
426            }
427        }
428        // 自定义错误需显式实现 IntoToolError
429        impl IntoToolError for MyError {
430            fn into_tool_error(self) -> ToolError {
431                ToolError::external(self)
432            }
433        }
434
435        let result: ToolResult = Err::<(), MyError>(MyError).into_tool();
436        let err = result.unwrap_err();
437        assert_eq!(
438            err.kind,
439            ToolErrorKind::External {
440                source: std::any::type_name::<MyError>()
441            }
442        );
443        assert_eq!(err.message, "my error");
444    }
445
446    #[test]
447    fn test_into_tool_result_tool_error_passthrough() {
448        // ToolError 应直接透传,不被包装成 External
449        let err = ToolError::invalid_input("bad param");
450        let result: ToolResult = Err::<serde_json::Value, ToolError>(err).into_tool();
451        let out_err = result.unwrap_err();
452        assert_eq!(out_err.kind, ToolErrorKind::InvalidInput);
453        assert_eq!(out_err.message, "bad param");
454    }
455
456    #[test]
457    fn test_tool_error_factories() {
458        let err = ToolError::invalid_input("bad input");
459        assert_eq!(err.kind, ToolErrorKind::InvalidInput);
460        assert_eq!(err.message, "bad input");
461
462        let err = ToolError::not_found("search");
463        assert_eq!(err.kind, ToolErrorKind::NotFound);
464        assert_eq!(err.message, "search");
465    }
466}