lellm-core 0.4.8

Core types for LeLLM — Message, ChatRequest, LlmError
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
//! 错误类型定义。

use std::fmt;
use thiserror::Error;

/// lellm 顶层错误类型 — 门面层统一错误出口。
///
/// **架构归属:** `lellm` facade crate(聚合各子层错误)
/// **代码位置:** `lellm-core`(暂留,便于 `#[from]` 跨 crate 转换)
///
/// **铁律:Core 公共 API 禁止返回 `LellmError`。**
/// 各层必须返回各自的领域错误:
/// - Provider API → `Result<T, LlmError>`
/// - Tool 执行 → `Result<T, ToolError>`
/// - 记忆操作 → `Result<T, MemoryError>`
/// - 解析操作 → `Result<T, ParseError>`
///
/// **迁移计划:** 等 facade 承担业务逻辑时,移至 `lellm/src/error.rs`。
#[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),
}

/// LLM API 错误。
///
/// 错误分类:
/// - **InvalidRequest** — 调用方构造了非法请求(发请求前本地可发现)
/// - **UnsupportedFeature** — SDK 不支持的功能(能力边界)
/// - **DuplicateSystemPrompt** — 系统提示冲突
/// - **Provider** — 请求已发出,对端返回错误(401/429/500/…)
/// - **Parse** — 响应体 JSON 解析失败
/// - **Network** — 网络层错误
/// - **Timeout** — 请求超时
/// - **UnexpectedEof** — 流式输出意外结束
#[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,
}

/// 工具执行错误的分类。
///
/// `Copy` 约束保留——所有变体均为 `Copy` 类型。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolErrorKind {
    /// 工具未找到(静态目录中从未存在)
    NotFound,
    /// 工具不可用(动态目录中曾存在但当前刷新后消失)
    ToolUnavailable,
    /// 工具执行超时
    Timeout,
    /// 网络相关错误
    Network,
    /// 权限不足
    PermissionDenied,
    /// 输入参数无效
    InvalidInput,
    /// 被限流
    RateLimited,
    /// 检测到循环调用
    LoopDetected,
    /// 内部错误(兜底)
    Internal,
    /// 外部业务错误(由用户代码抛出,自动桥接)
    ///
    /// `source` 为原始错误类型的 `type_name`,用于可观测性。
    External { source: &'static str },
}

impl ToolErrorKind {
    /// 判断该错误是否属于基础设施层面的瞬态故障(Transient Failure)。
    ///
    /// **可重试(原地静默重试):**
    /// - `Timeout` / `Network` / `RateLimited` — 网络抖动、服务端过载
    /// - `ToolUnavailable` — 动态目录瞬态不可用(MCP 重启等)
    ///
    /// **不可重试(立即弹回 LLM 修复层):**
    /// - `InvalidInput` — 参数错了就是错了
    /// - `NotFound` — 工具不存在,重试也没用
    /// - `PermissionDenied` — 权限不会自动恢复
    /// - `External` — 用户业务错误,框架不应猜测
    /// - `LoopDetected` — 循环检测,重试无意义
    /// - `Internal` — 内部错误,重试无意义
    pub fn is_retryable(self) -> bool {
        matches!(
            self,
            Self::Timeout | Self::Network | Self::RateLimited | Self::ToolUnavailable
        )
    }
}

impl fmt::Display for ToolErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NotFound => write!(f, "NotFound"),
            Self::ToolUnavailable => write!(f, "ToolUnavailable"),
            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"),
            Self::External { source } => write!(f, "External({})", source),
        }
    }
}

/// 工具执行错误 — 携带错误分类与详细描述。
#[derive(Clone)]
pub struct ToolError {
    pub kind: ToolErrorKind,
    pub message: String,
}

impl ToolError {
    /// 构造 `InvalidInput` 错误。
    pub fn invalid_input(msg: impl Into<String>) -> Self {
        Self {
            kind: ToolErrorKind::InvalidInput,
            message: msg.into(),
        }
    }

    /// 构造 `NotFound` 错误。
    pub fn not_found(msg: impl Into<String>) -> Self {
        Self {
            kind: ToolErrorKind::NotFound,
            message: msg.into(),
        }
    }

    /// 构造 `External` 错误,自动记录原始错误类型名。
    pub fn external<E: std::fmt::Display>(source: E) -> Self {
        Self {
            kind: ToolErrorKind::External {
                source: std::any::type_name::<E>(),
            },
            message: source.to_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 {}

/// 工具执行结果 — `serde_json::Value` 支持结构化数据。
///
/// 通过 `IntoToolResult` trait,用户可返回 `String`、`Value`、
/// `Option<T>`、`Result<T, E>` 等类型,框架自动转换。
pub type ToolResult = Result<serde_json::Value, ToolError>;

// ─── IntoToolError ───────────────────────────────────────────────

/// 将已知错误类型转换为 `ToolError`。
///
/// **设计原则:** 不使用 blanket impl(`impl<E: Display>`),避免吞掉 `ToolError` 原始分类。
/// 只为核心错误类型提供显式实现。
///
/// **已有实现:**
/// - `ToolError` → 直接透传
/// - `std::io::Error` → `External`
/// - `serde_json::Error` → `Internal`
/// - `anyhow::Error` → `External`(需 `anyhow` feature)
pub trait IntoToolError {
    fn into_tool_error(self) -> ToolError;
}

/// `ToolError` → 直接透传,不包装
impl IntoToolError for ToolError {
    fn into_tool_error(self) -> ToolError {
        self
    }
}

/// `std::io::Error` → `External`
impl IntoToolError for std::io::Error {
    fn into_tool_error(self) -> ToolError {
        ToolError::external(self)
    }
}

/// `serde_json::Error` → `Internal`
impl IntoToolError for serde_json::Error {
    fn into_tool_error(self) -> ToolError {
        ToolError {
            kind: ToolErrorKind::Internal,
            message: self.to_string(),
        }
    }
}

/// `anyhow::Error` → `External`
#[cfg(feature = "anyhow")]
impl IntoToolError for anyhow::Error {
    fn into_tool_error(self) -> ToolError {
        ToolError::external(self)
    }
}

// ─── IntoToolResult ──────────────────────────────────────────────

/// 将工具函数返回值统一转换为 `ToolResult`。
///
/// 由 `#[tool]` 宏在闭包中调用,用户无需手动实现。
///
/// **支持的返回类型:**
/// - `String` → `Ok(Value::String(s))`
/// - `serde_json::Value` → `Ok(v)`
/// - `T: Serialize` → `Ok(serde_json::to_value(t)?)`
/// - `Option<T>` → `Some` 转 Value,`None` → `Ok(Value::Null)`
/// - `Result<T, ToolError>` → 直接透传
/// - `Result<T, E: Display>` → `Ok` 转 Value,`Err` → `External`
pub trait IntoToolResult: Sized {
    fn into_tool(self) -> ToolResult;
}

/// `String` → `Ok(Value::String(s))`
impl IntoToolResult for String {
    fn into_tool(self) -> ToolResult {
        Ok(serde_json::Value::String(self))
    }
}

/// `serde_json::Value` → 直接透传
impl IntoToolResult for serde_json::Value {
    fn into_tool(self) -> ToolResult {
        Ok(self)
    }
}

/// `Option<T>` → `Some` 序列化,`None` → `Value::Null`
impl<T> IntoToolResult for Option<T>
where
    T: serde::Serialize,
{
    fn into_tool(self) -> ToolResult {
        match self {
            Some(v) => serde_json::to_value(v).map_err(|e| ToolError {
                kind: ToolErrorKind::Internal,
                message: format!("failed to serialize tool result: {}", e),
            }),
            None => Ok(serde_json::Value::Null),
        }
    }
}

/// `Result<T, E>` (T: Serialize, E: IntoToolError) → 自动桥接
///
/// `E: IntoToolError` 约束确保只有显式实现的错误类型才能转换。
/// `ToolError` → 直接透传,`std::io::Error` → External,`serde_json::Error` → Internal。
impl<T, E> IntoToolResult for Result<T, E>
where
    T: serde::Serialize,
    E: IntoToolError,
{
    fn into_tool(self) -> ToolResult {
        match self {
            Ok(v) => serde_json::to_value(v).map_err(|e| ToolError {
                kind: ToolErrorKind::Internal,
                message: format!("failed to serialize tool result: {}", e),
            }),
            Err(e) => Err(e.into_tool_error()),
        }
    }
}

// ─── 其他错误类型 ────────────────────────────────────────────────

/// 记忆操作错误。
#[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_retryable() {
        // 可重试
        assert!(ToolErrorKind::Timeout.is_retryable());
        assert!(ToolErrorKind::Network.is_retryable());
        assert!(ToolErrorKind::RateLimited.is_retryable());
        assert!(ToolErrorKind::ToolUnavailable.is_retryable());

        // 不可重试
        assert!(!ToolErrorKind::NotFound.is_retryable());
        assert!(!ToolErrorKind::InvalidInput.is_retryable());
        assert!(!ToolErrorKind::PermissionDenied.is_retryable());
        assert!(!ToolErrorKind::Internal.is_retryable());
        assert!(!ToolErrorKind::LoopDetected.is_retryable());
        assert!(!ToolErrorKind::External { source: "test" }.is_retryable());
    }

    #[test]
    fn test_into_tool_result_string() {
        let result: ToolResult = "hello".to_string().into_tool();
        assert_eq!(result.unwrap(), serde_json::json!("hello"));
    }

    #[test]
    fn test_into_tool_result_option() {
        let some: Option<String> = Some("hello".to_string());
        assert_eq!(some.into_tool().unwrap(), serde_json::json!("hello"));

        let none: Option<String> = None;
        assert_eq!(none.into_tool().unwrap(), serde_json::json!(null));
    }

    #[test]
    fn test_into_tool_result_external_error() {
        #[derive(Debug)]
        struct MyError;
        impl fmt::Display for MyError {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "my error")
            }
        }
        // 自定义错误需显式实现 IntoToolError
        impl IntoToolError for MyError {
            fn into_tool_error(self) -> ToolError {
                ToolError::external(self)
            }
        }

        let result: ToolResult = Err::<(), MyError>(MyError).into_tool();
        let err = result.unwrap_err();
        assert_eq!(
            err.kind,
            ToolErrorKind::External {
                source: std::any::type_name::<MyError>()
            }
        );
        assert_eq!(err.message, "my error");
    }

    #[test]
    fn test_into_tool_result_tool_error_passthrough() {
        // ToolError 应直接透传,不被包装成 External
        let err = ToolError::invalid_input("bad param");
        let result: ToolResult = Err::<serde_json::Value, ToolError>(err).into_tool();
        let out_err = result.unwrap_err();
        assert_eq!(out_err.kind, ToolErrorKind::InvalidInput);
        assert_eq!(out_err.message, "bad param");
    }

    #[test]
    fn test_tool_error_factories() {
        let err = ToolError::invalid_input("bad input");
        assert_eq!(err.kind, ToolErrorKind::InvalidInput);
        assert_eq!(err.message, "bad input");

        let err = ToolError::not_found("search");
        assert_eq!(err.kind, ToolErrorKind::NotFound);
        assert_eq!(err.message, "search");
    }
}