camel_component_llm/
error.rs1use std::time::Duration;
2
3use camel_api::CamelError;
4
5#[derive(Debug, Clone, thiserror::Error)]
11#[non_exhaustive]
12pub enum LlmError {
13 #[error("rate limited by provider")]
15 RateLimit {
16 retry_after: Option<Duration>,
18 },
19
20 #[error("quota exceeded: {detail}")]
22 QuotaExceeded {
23 detail: String,
25 },
26
27 #[error("context window exceeded: {max_tokens} tokens max")]
29 ContextLengthExceeded {
30 max_tokens: u32,
32 },
33
34 #[error("authentication failed: {detail}")]
36 AuthFailed {
37 detail: String,
39 },
40
41 #[error("model not found: {0}")]
43 ModelNotFound(String),
44
45 #[error("model unavailable: {0}")]
47 ModelUnavailable(String),
48
49 #[error("provider unavailable: {0}")]
51 ProviderUnavailable(String),
52
53 #[error("content filtered by safety policy: {detail}")]
55 ContentFiltered {
56 detail: String,
58 },
59
60 #[error("network error: {0}")]
62 Network(String),
63
64 #[error("timeout after {0:?}")]
66 Timeout(Duration),
67
68 #[error("invalid request: {0}")]
70 InvalidRequest(String),
71
72 #[error("capability not supported: {0}")]
74 UnsupportedCapability(String),
75
76 #[error("malformed provider response: {0}")]
78 Protocol(String),
79
80 #[error("stream interrupted: {0}")]
82 StreamInterrupted(String),
83
84 #[error("provider error: {0}")]
87 Provider(String),
88}
89
90const MAX_PROVIDER_ERROR_BYTES: usize = 200;
92
93fn truncate_for_display(msg: &str) -> String {
96 if msg.len() <= MAX_PROVIDER_ERROR_BYTES {
97 msg.to_string()
98 } else {
99 let cut = msg.floor_char_boundary(MAX_PROVIDER_ERROR_BYTES);
100 format!("{}...[truncated]", &msg[..cut])
101 }
102}
103
104impl LlmError {
105 pub fn provider(msg: impl Into<String>) -> Self {
110 LlmError::Provider(truncate_for_display(&msg.into()))
111 }
112}
113
114pub fn is_retryable(err: &LlmError) -> bool {
124 matches!(
125 err,
126 LlmError::RateLimit { .. }
127 | LlmError::Network(_)
128 | LlmError::ProviderUnavailable(_)
129 | LlmError::ModelUnavailable(_)
130 | LlmError::Timeout(_)
131 )
132}
133
134impl From<LlmError> for CamelError {
135 fn from(e: LlmError) -> Self {
136 match &e {
137 LlmError::AuthFailed { .. } => CamelError::Unauthenticated(e.to_string()),
138 LlmError::Network(_) | LlmError::ProviderUnavailable(_) => {
139 CamelError::Io(e.to_string())
140 }
141 _ => CamelError::ProcessorError(e.to_string()),
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use std::time::Duration;
150
151 #[test]
152 fn retryable_errors() {
153 assert!(is_retryable(&LlmError::Network("conn reset".into())));
154 assert!(is_retryable(&LlmError::Timeout(Duration::from_secs(30))));
155 assert!(is_retryable(&LlmError::RateLimit { retry_after: None }));
156 assert!(is_retryable(&LlmError::ProviderUnavailable("503".into())));
157 assert!(is_retryable(&LlmError::ModelUnavailable(
158 "overloaded".into()
159 )));
160 }
161
162 #[test]
163 fn non_retryable_errors() {
164 assert!(!is_retryable(&LlmError::AuthFailed {
165 detail: "bad key".into()
166 }));
167 assert!(!is_retryable(&LlmError::QuotaExceeded {
168 detail: "billing".into()
169 }));
170 assert!(!is_retryable(&LlmError::ContextLengthExceeded {
171 max_tokens: 4096
172 }));
173 assert!(!is_retryable(&LlmError::ModelNotFound("gpt-99".into())));
174 assert!(!is_retryable(&LlmError::ContentFiltered {
175 detail: "safety".into()
176 }));
177 assert!(!is_retryable(&LlmError::InvalidRequest("bad json".into())));
178 assert!(!is_retryable(&LlmError::Protocol("decode".into())));
179 assert!(!is_retryable(&LlmError::StreamInterrupted(
180 "dropped".into()
181 )));
182 assert!(!is_retryable(&LlmError::UnsupportedCapability(
183 "embed".into()
184 )));
185 assert!(!is_retryable(&LlmError::Provider("generic error".into())));
186 }
187
188 #[test]
189 fn converts_to_camel_error() {
190 let err: camel_api::CamelError = LlmError::Timeout(Duration::from_secs(5)).into();
191 assert!(err.to_string().contains("timeout"));
192
193 assert!(matches!(
195 CamelError::from(LlmError::Network("conn".into())),
196 CamelError::Io(_)
197 ));
198 assert!(matches!(
199 CamelError::from(LlmError::ProviderUnavailable("503".into())),
200 CamelError::Io(_)
201 ));
202 assert!(matches!(
203 CamelError::from(LlmError::AuthFailed {
204 detail: "bad key".into()
205 }),
206 CamelError::Unauthenticated(_)
207 ));
208 assert!(matches!(
210 CamelError::from(LlmError::InvalidRequest("bad json".into())),
211 CamelError::ProcessorError(_)
212 ));
213 }
214
215 #[test]
216 fn provider_constructor_truncates_long_messages() {
217 let long = "x".repeat(300);
218 let err = LlmError::provider(long);
219 let display = err.to_string();
220 assert!(display.contains("provider error"));
221 assert!(display.contains("[truncated]"));
222 assert!(
224 display.len() <= 200 + 40,
225 "display too long: {} bytes",
226 display.len()
227 );
228 }
229}