1use std::fmt;
2
3use thiserror::Error;
4
5#[doc = include_str!("docs/llm_error.md")]
6#[derive(Debug, Error, Clone)]
7pub enum LlmError {
8 #[error(transparent)]
9 ReasoningValidation(#[from] crate::catalog::ReasoningEffortError),
10 #[error("Disabling reasoning is not implemented for model '{model}' on this transport")]
11 UnsupportedDisableTransport { model: String },
12 #[error("{0} environment variable not set")]
14 MissingApiKey(String),
15 #[error("{0}")]
18 Provider(#[from] ProviderError),
19 #[error("JSON parsing error: {0}")]
21 JsonParsing(String),
22 #[error("Failed to parse tool parameters for {tool_name}: {error}")]
24 ToolParameterParsing { tool_name: String, error: String },
25 #[error("OAuth error: {0}")]
27 OAuthError(String),
28
29 #[error("Invalid JWT: {0}")]
31 InvalidJwt(String),
32 #[error("Unsupported content: {0}")]
34 UnsupportedContent(String),
35 #[error("Provider '{provider}' requires a URL configured via providers.{provider}.url")]
37 MissingProviderUrl { provider: String },
38 #[error("Unknown provider: {provider}")]
40 UnknownProvider { provider: String },
41 #[error("No models provided")]
43 EmptyModelSpec,
44 #[error("providers.{provider}.{field} cannot be used with multiple {provider} models in one alloy spec")]
48 DuplicateProvider { provider: String, field: String },
49 #[error("Invalid model spec: {0}")]
51 InvalidModelSpec(String),
52 #[error("Failed to build provider request: {0}")]
55 ProviderRequest(String),
56 #[error("Invalid argument: {0}")]
58 InvalidArgument(String),
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum ProviderErrorKind {
63 Authentication,
64 Api,
65 RateLimit,
66 Server,
67 Timeout,
68 Network,
69 StreamInterrupted,
70 Unknown,
71}
72
73impl ProviderErrorKind {
74 pub fn is_retryable(&self) -> bool {
75 matches!(
76 self,
77 Self::RateLimit | Self::Server | Self::Timeout | Self::Network | Self::StreamInterrupted | Self::Unknown
78 )
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct ProviderError {
84 pub kind: ProviderErrorKind,
85 pub message: String,
86 pub http_status: Option<u16>,
87 pub request_id: Option<String>,
88 pub code: Option<String>,
89}
90
91impl ProviderError {
92 pub fn new(kind: ProviderErrorKind, message: impl Into<String>) -> Self {
93 Self { kind, message: message.into(), http_status: None, request_id: None, code: None }
94 }
95
96 pub fn authentication(message: impl Into<String>) -> Self {
97 Self::new(ProviderErrorKind::Authentication, message)
98 }
99
100 pub fn api(message: impl Into<String>) -> Self {
101 Self::new(ProviderErrorKind::Api, message)
102 }
103
104 pub fn rate_limit(message: impl Into<String>) -> Self {
105 Self::new(ProviderErrorKind::RateLimit, message)
106 }
107
108 pub fn server(message: impl Into<String>) -> Self {
109 Self::new(ProviderErrorKind::Server, message)
110 }
111
112 pub fn timeout(message: impl Into<String>) -> Self {
113 Self::new(ProviderErrorKind::Timeout, message)
114 }
115
116 pub fn network(message: impl Into<String>) -> Self {
117 Self::new(ProviderErrorKind::Network, message)
118 }
119
120 pub fn stream_interrupted(message: impl Into<String>) -> Self {
121 Self::new(ProviderErrorKind::StreamInterrupted, message)
122 }
123
124 pub fn from_http_status(status: u16, message: impl Into<String>) -> Self {
125 match status {
126 401 | 403 => Self::authentication(message),
127 408 | 504 => Self::timeout(message),
128 429 => Self::rate_limit(message),
129 s if (500..600).contains(&s) => Self::server(message),
130 _ => Self::api(message),
131 }
132 .with_http_status(status)
133 }
134
135 pub fn with_http_status(mut self, status: u16) -> Self {
136 self.http_status = Some(status);
137 self
138 }
139
140 pub fn with_http_metadata(mut self, status: Option<u16>, request_id: Option<String>) -> Self {
141 self.http_status = status;
142 self.request_id = request_id;
143 self
144 }
145
146 pub fn with_code(mut self, code: Option<String>) -> Self {
147 self.code = code;
148 self
149 }
150
151 pub fn with_request_id(mut self, request_id: Option<String>) -> Self {
152 self.request_id = request_id;
153 self
154 }
155
156 pub fn is_retryable(&self) -> bool {
157 self.kind.is_retryable()
158 }
159}
160
161impl fmt::Display for ProviderError {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 let prefix = match self.kind {
164 ProviderErrorKind::Authentication => "Authentication error",
165 ProviderErrorKind::Api => "API error",
166 ProviderErrorKind::RateLimit => "Rate limited",
167 ProviderErrorKind::Server => "Server error",
168 ProviderErrorKind::Timeout => "Request timed out",
169 ProviderErrorKind::Network => "Network error",
170 ProviderErrorKind::StreamInterrupted => "Stream interrupted",
171 ProviderErrorKind::Unknown => "Provider error",
172 };
173 write!(f, "{prefix}: {}", self.message)?;
174 let mut diagnostics = Vec::new();
175 if let Some(status) = self.http_status {
176 diagnostics.push(format!("status {status}"));
177 }
178 if let Some(code) = &self.code {
179 diagnostics.push(format!("code {code}"));
180 }
181 if let Some(request_id) = &self.request_id {
182 diagnostics.push(format!("request_id {request_id}"));
183 }
184 if !diagnostics.is_empty() {
185 write!(f, " ({})", diagnostics.join(", "))?;
186 }
187 Ok(())
188 }
189}
190
191impl std::error::Error for ProviderError {}
192
193impl LlmError {
194 pub fn is_retryable(&self) -> bool {
195 self.provider().is_some_and(ProviderError::is_retryable)
196 }
197
198 pub fn provider(&self) -> Option<&ProviderError> {
199 match self {
200 Self::Provider(error) => Some(error),
201 _ => None,
202 }
203 }
204}
205
206impl From<reqwest::Error> for LlmError {
207 fn from(error: reqwest::Error) -> Self {
208 if error.is_timeout() {
209 return ProviderError::timeout(error.to_string()).into();
210 }
211 if error.is_connect() || error.is_request() {
212 return ProviderError::network(error.to_string()).into();
213 }
214 match error.status().map(|s| s.as_u16()) {
215 Some(status) => ProviderError::from_http_status(status, error.to_string()).into(),
216 None => ProviderError::network(error.to_string()).into(),
217 }
218 }
219}
220
221impl From<serde_json::Error> for LlmError {
222 fn from(error: serde_json::Error) -> Self {
223 LlmError::JsonParsing(error.to_string())
224 }
225}
226
227impl From<reqwest::header::InvalidHeaderValue> for LlmError {
228 fn from(error: reqwest::header::InvalidHeaderValue) -> Self {
229 LlmError::ProviderRequest(error.to_string())
230 }
231}
232
233#[cfg(feature = "codex")]
234impl From<aether_auth::OAuthError> for LlmError {
235 fn from(error: aether_auth::OAuthError) -> Self {
236 LlmError::OAuthError(error.to_string())
237 }
238}
239
240pub type Result<T> = std::result::Result<T, LlmError>;
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn retryable_kinds_cover_transient_failures() {
248 assert!(!ProviderErrorKind::Authentication.is_retryable());
249 assert!(!ProviderErrorKind::Api.is_retryable());
250 assert!(ProviderErrorKind::RateLimit.is_retryable());
251 assert!(ProviderErrorKind::Server.is_retryable());
252 assert!(ProviderErrorKind::Timeout.is_retryable());
253 assert!(ProviderErrorKind::Network.is_retryable());
254 assert!(ProviderErrorKind::StreamInterrupted.is_retryable());
255 assert!(ProviderErrorKind::Unknown.is_retryable());
256 }
257
258 #[test]
259 fn http_status_classification_covers_known_statuses() {
260 assert_eq!(ProviderError::from_http_status(401, "d").kind, ProviderErrorKind::Authentication);
261 assert_eq!(ProviderError::from_http_status(403, "d").kind, ProviderErrorKind::Authentication);
262 assert_eq!(ProviderError::from_http_status(408, "d").kind, ProviderErrorKind::Timeout);
263 assert!(ProviderError::from_http_status(408, "d").is_retryable());
264 assert_eq!(ProviderError::from_http_status(429, "d").kind, ProviderErrorKind::RateLimit);
265 assert_eq!(ProviderError::from_http_status(500, "d").kind, ProviderErrorKind::Server);
266 assert_eq!(ProviderError::from_http_status(503, "d").kind, ProviderErrorKind::Server);
267 assert_eq!(ProviderError::from_http_status(400, "d").kind, ProviderErrorKind::Api);
268 assert!(!ProviderError::from_http_status(400, "d").is_retryable());
269 assert_eq!(ProviderError::from_http_status(404, "d").kind, ProviderErrorKind::Api);
270 }
271
272 #[test]
273 fn display_includes_diagnostics_when_present() {
274 let error = ProviderError::server("boom").with_http_status(200).with_code(Some("server_error".into()));
275 let text = error.to_string();
276 assert!(text.contains("boom"));
277 assert!(text.contains("status 200"));
278 assert!(text.contains("code server_error"));
279 let error = error.with_request_id(Some("req-1".into()));
280 assert!(error.to_string().contains("request_id req-1"));
281 }
282
283 #[test]
284 fn display_omits_suffix_when_no_metadata() {
285 let error = ProviderError::api("bad");
286 assert_eq!(error.to_string(), "API error: bad");
287 }
288
289 #[test]
290 fn is_retryable() {
291 assert!(LlmError::from(ProviderError::rate_limit("rl")).is_retryable());
292 assert!(LlmError::from(ProviderError::server("x").with_http_status(503)).is_retryable());
293 assert!(LlmError::from(ProviderError::server("stream-level")).is_retryable());
294 assert!(LlmError::from(ProviderError::timeout("t")).is_retryable());
295 assert!(LlmError::from(ProviderError::network("n")).is_retryable());
296 assert!(LlmError::from(ProviderError::stream_interrupted("s")).is_retryable());
297
298 assert!(!LlmError::from(ProviderError::api("x")).is_retryable());
299 assert!(!LlmError::from(ProviderError::authentication("x")).is_retryable());
300 assert!(!LlmError::MissingApiKey("x".into()).is_retryable());
301 assert!(!LlmError::JsonParsing("x".into()).is_retryable());
302 assert!(!LlmError::ToolParameterParsing { tool_name: "t".into(), error: "e".into() }.is_retryable());
303 assert!(!LlmError::OAuthError("x".into()).is_retryable());
304 assert!(!LlmError::UnsupportedContent("x".into()).is_retryable());
305 assert!(!LlmError::MissingProviderUrl { provider: "azure-foundry".into() }.is_retryable());
306 assert!(!LlmError::UnknownProvider { provider: "foo".into() }.is_retryable());
307 assert!(!LlmError::EmptyModelSpec.is_retryable());
308 assert!(
309 !LlmError::DuplicateProvider { provider: "bedrock".into(), field: "inferenceProfileArn".into() }
310 .is_retryable()
311 );
312 assert!(!LlmError::InvalidModelSpec("x".into()).is_retryable());
313 assert!(!LlmError::ProviderRequest("x".into()).is_retryable());
314 assert!(!LlmError::InvalidArgument("x".into()).is_retryable());
315 }
316}