Skip to main content

llm_trait/
error.rs

1//! LLM layer unified error type.
2//!
3//! Defined in `llm-trait` so downstream runtimes can convert it into their own
4//! error type via `From<LlmError>`. This keeps `llm-trait` lightweight — it does
5//! not depend on any consumer crate.
6
7/// LLM layer unified error type.
8///
9/// Covers configuration errors, API errors, general LLM errors,
10/// and stream parsing errors. Designed to be converted into a
11/// consumer's own error type by the runtime layer.
12#[derive(Debug, Clone, thiserror::Error)]
13pub enum LlmError {
14    /// Configuration error (missing API key, invalid model name, etc.)
15    #[error("Config error: {0}")]
16    Config(String),
17
18    /// API returned an error (with HTTP status code).
19    #[error("API error {status}: {message}")]
20    LlmApi { status: u16, message: String },
21
22    /// General LLM error (network timeout, connection failure, etc.)
23    #[error("LLM error: {0}")]
24    Llm(String),
25
26    /// Stream parsing error (SSE format issues, etc.)
27    #[error("Stream error: {0}")]
28    Stream(String),
29}
30
31impl LlmError {
32    pub fn config(msg: impl Into<String>) -> Self {
33        Self::Config(msg.into())
34    }
35
36    pub fn llm(msg: impl Into<String>) -> Self {
37        Self::Llm(msg.into())
38    }
39
40    pub fn stream(msg: impl Into<String>) -> Self {
41        Self::Stream(msg.into())
42    }
43
44    pub fn api(status: u16, message: impl Into<String>) -> Self {
45        Self::LlmApi {
46            status,
47            message: message.into(),
48        }
49    }
50
51    /// HTTP status code, if this error came from an API response.
52    ///
53    /// Useful for deciding whether to retry or how to surface the failure.
54    pub fn status(&self) -> Option<u16> {
55        match self {
56            Self::LlmApi { status, .. } => Some(*status),
57            _ => None,
58        }
59    }
60}
61
62// Convenience From impls for common error types.
63
64impl From<serde_json::Error> for LlmError {
65    fn from(e: serde_json::Error) -> Self {
66        LlmError::Llm(format!("JSON error: {e}"))
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn display_config() {
76        let err = LlmError::config("missing key");
77        assert_eq!(err.to_string(), "Config error: missing key");
78    }
79
80    #[test]
81    fn display_api() {
82        let err = LlmError::api(401, "unauthorized");
83        assert_eq!(err.to_string(), "API error 401: unauthorized");
84        assert_eq!(err.status(), Some(401));
85    }
86
87    #[test]
88    fn status_is_none_for_non_api_errors() {
89        assert_eq!(LlmError::config("missing key").status(), None);
90        assert_eq!(LlmError::llm("timeout").status(), None);
91        assert_eq!(LlmError::stream("bad SSE").status(), None);
92    }
93
94    #[test]
95    fn display_llm() {
96        let err = LlmError::llm("timeout");
97        assert_eq!(err.to_string(), "LLM error: timeout");
98    }
99
100    #[test]
101    fn display_stream() {
102        let err = LlmError::stream("bad SSE");
103        assert_eq!(err.to_string(), "Stream error: bad SSE");
104    }
105
106    #[test]
107    fn from_serde_json_error() {
108        let json_err = serde_json::from_str::<serde_json::Value>("bad").unwrap_err();
109        let llm_err: LlmError = json_err.into();
110        assert!(llm_err.to_string().contains("JSON error"));
111    }
112
113    #[test]
114    fn is_clone() {
115        let err = LlmError::llm("test");
116        let cloned = err.clone();
117        assert_eq!(err.to_string(), cloned.to_string());
118    }
119}