Skip to main content

llm_core/
error.rs

1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum LlmError {
5    #[error("model error: {0}")]
6    Model(String),
7
8    #[error("no key found: {0}")]
9    NeedsKey(String),
10
11    #[error("provider error: {0}")]
12    Provider(String),
13
14    #[error("HTTP error {status}: {message}")]
15    HttpError { status: u16, message: String },
16
17    #[error("config error: {0}")]
18    Config(String),
19
20    #[error("io error: {0}")]
21    Io(#[from] std::io::Error),
22
23    #[error("store error: {0}")]
24    Store(String),
25}
26
27impl LlmError {
28    /// Returns `true` if this error is transient and worth retrying.
29    /// Only HTTP 429 (rate limit) and 5xx (server errors) are retryable.
30    pub fn is_retryable(&self) -> bool {
31        matches!(self, LlmError::HttpError { status, .. }
32            if *status == 429 || (500..=599).contains(status))
33    }
34}
35
36pub type Result<T> = std::result::Result<T, LlmError>;
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn error_display_model() {
44        let err = LlmError::Model("rate limited".into());
45        assert_eq!(err.to_string(), "model error: rate limited");
46    }
47
48    #[test]
49    fn error_display_needs_key() {
50        let err = LlmError::NeedsKey(
51            "No key found - set one with 'llm keys set openai'".into(),
52        );
53        assert!(err.to_string().contains("llm keys set openai"));
54    }
55
56    #[test]
57    fn error_display_provider() {
58        let err = LlmError::Provider("connection timeout".into());
59        assert_eq!(err.to_string(), "provider error: connection timeout");
60    }
61
62    #[test]
63    fn error_display_config() {
64        let err = LlmError::Config("invalid TOML".into());
65        assert_eq!(err.to_string(), "config error: invalid TOML");
66    }
67
68    #[test]
69    fn error_display_io() {
70        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
71        let err: LlmError = io_err.into();
72        assert!(err.to_string().contains("file not found"));
73    }
74
75    #[test]
76    fn error_display_store() {
77        let err = LlmError::Store("conversation not found".into());
78        assert_eq!(err.to_string(), "store error: conversation not found");
79    }
80
81    #[test]
82    fn error_io_from_conversion() {
83        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
84        let llm_err: LlmError = io_err.into();
85        assert!(matches!(llm_err, LlmError::Io(_)));
86    }
87
88    #[test]
89    fn error_is_send_sync() {
90        fn assert_send_sync<T: Send + Sync>() {}
91        assert_send_sync::<LlmError>();
92    }
93
94    #[test]
95    fn result_type_works() {
96        let ok: Result<i32> = Ok(42);
97        assert_eq!(ok.unwrap(), 42);
98
99        let err: Result<i32> = Err(LlmError::Config("bad".into()));
100        assert!(err.is_err());
101    }
102
103    #[test]
104    fn error_display_http() {
105        let err = LlmError::HttpError { status: 429, message: "rate limited".into() };
106        assert_eq!(err.to_string(), "HTTP error 429: rate limited");
107    }
108
109    #[test]
110    fn http_error_retryable_429() {
111        let err = LlmError::HttpError { status: 429, message: "rate limited".into() };
112        assert!(err.is_retryable());
113    }
114
115    #[test]
116    fn http_error_retryable_5xx() {
117        for status in [500, 502, 503, 504] {
118            let err = LlmError::HttpError { status, message: "server error".into() };
119            assert!(err.is_retryable(), "status {status} should be retryable");
120        }
121    }
122
123    #[test]
124    fn http_error_not_retryable_4xx() {
125        for status in [400, 401, 403, 404, 422] {
126            let err = LlmError::HttpError { status, message: "client error".into() };
127            assert!(!err.is_retryable(), "status {status} should not be retryable");
128        }
129    }
130
131    #[test]
132    fn non_http_errors_not_retryable() {
133        assert!(!LlmError::Provider("fail".into()).is_retryable());
134        assert!(!LlmError::Model("bad".into()).is_retryable());
135        assert!(!LlmError::NeedsKey("key".into()).is_retryable());
136        assert!(!LlmError::Config("cfg".into()).is_retryable());
137        assert!(!LlmError::Store("store".into()).is_retryable());
138        let io_err = std::io::Error::new(std::io::ErrorKind::Other, "io");
139        assert!(!LlmError::Io(io_err).is_retryable());
140    }
141}