1#![allow(missing_docs)]
4
5use std::time::Duration;
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub(crate) struct ErrorResponse {
12 error: ApiError,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub(crate) struct ApiError {
18 message: String,
19 #[serde(default)]
20 code: Option<String>,
21}
22
23#[derive(Debug, thiserror::Error)]
28#[non_exhaustive]
29pub enum LiterLlmError {
30 #[cfg_attr(alef, alef(error_code = 100))]
32 #[error("authentication failed: {message}")]
33 Authentication { message: String, status: u16 },
34
35 #[cfg_attr(alef, alef(error_code = 101))]
36 #[error("rate limited: {message}")]
37 RateLimited {
38 message: String,
39 retry_after: Option<Duration>,
40 },
41
42 #[cfg_attr(alef, alef(error_code = 102))]
44 #[error("bad request: {message}")]
45 BadRequest { message: String, status: u16 },
46
47 #[cfg_attr(alef, alef(error_code = 103))]
48 #[error("context window exceeded: {message}")]
49 ContextWindowExceeded { message: String },
50
51 #[cfg_attr(alef, alef(error_code = 104))]
52 #[error("content policy violation: {message}")]
53 ContentPolicy { message: String },
54
55 #[cfg_attr(alef, alef(error_code = 105))]
56 #[error("not found: {message}")]
57 NotFound { message: String },
58
59 #[cfg_attr(alef, alef(error_code = 106))]
62 #[error("server error: {message}")]
63 ServerError { message: String, status: u16 },
64
65 #[cfg_attr(alef, alef(error_code = 107))]
67 #[error("service unavailable: {message}")]
68 ServiceUnavailable {
69 message: String,
70 status: u16,
71 retry_after: Option<Duration>,
72 },
73
74 #[cfg_attr(alef, alef(error_code = 108))]
75 #[error("request timeout")]
76 Timeout,
77
78 #[cfg_attr(alef, alef(error_code = 109))]
79 #[error("network error: {message}")]
80 Network { message: String },
81
82 #[cfg_attr(alef, alef(error_code = 110))]
89 #[error("streaming error: {message}")]
90 Streaming { message: String },
91
92 #[cfg_attr(alef, alef(error_code = 111))]
93 #[error("provider {provider} does not support {endpoint}")]
94 EndpointNotSupported { endpoint: String, provider: String },
95
96 #[cfg_attr(alef, alef(error_code = 112))]
97 #[error("invalid header {name:?}: {reason}")]
98 InvalidHeader { name: String, reason: String },
99
100 #[cfg_attr(alef, alef(error_code = 113))]
101 #[error("serialization error: {0}")]
102 Serialization(
103 #[from]
104 #[cfg_attr(alef, alef(skip))]
105 serde_json::Error,
106 ),
107
108 #[cfg_attr(alef, alef(error_code = 114))]
109 #[error("budget exceeded: {message}")]
110 BudgetExceeded { message: String, model: Option<String> },
111
112 #[cfg_attr(alef, alef(error_code = 115))]
113 #[error("hook rejected: {message}")]
114 HookRejected { message: String },
115
116 #[cfg_attr(alef, alef(error_code = 116))]
121 #[error("internal error: {message}")]
122 InternalError { message: String },
123
124 #[cfg_attr(alef, alef(error_code = 117))]
130 #[error("outbound request to {url} forbidden: {reason}")]
131 OutboundForbidden { url: String, reason: String },
132
133 #[cfg_attr(alef, alef(error_code = 118))]
141 #[error("idempotency conflict: key '{key}' was already used with a different request body")]
142 IdempotencyConflict { key: String },
143
144 #[cfg_attr(alef, alef(error_code = 119))]
153 #[error("idempotency key '{key}' is currently in-flight; retry after the first request completes")]
154 IdempotencyInFlight { key: String },
155
156 #[cfg_attr(alef, alef(error_code = 120))]
158 #[error("provider quota exceeded: {message}")]
159 ProviderQuotaExceeded { message: String, status: u16 },
160}
161
162#[cfg(any(feature = "native-http", feature = "wasm-http"))]
163impl From<reqwest::Error> for LiterLlmError {
164 fn from(error: reqwest::Error) -> Self {
165 #[cfg(not(target_arch = "wasm32"))]
166 if let Some(policy_error) = crate::provider::outbound_forbidden_from_reqwest(&error) {
167 return policy_error;
168 }
169 if error.is_timeout() {
170 return Self::Timeout;
171 }
172 Self::Network {
173 message: error.without_url().to_string(),
174 }
175 }
176}
177
178impl LiterLlmError {
179 #[must_use]
185 pub fn status_code(&self) -> u16 {
186 match self {
187 Self::Authentication { status, .. } => *status,
188 Self::RateLimited { .. } => 429,
189 Self::BadRequest { status, .. } => *status,
190 Self::ContextWindowExceeded { .. } => 400,
191 Self::ContentPolicy { .. } => 400,
192 Self::NotFound { .. } => 404,
193 Self::ServerError { status, .. } => *status,
194 Self::ServiceUnavailable { status, .. } => *status,
195 Self::Timeout => 408,
196 Self::Network { .. } => 0,
197 Self::Streaming { .. } => 0,
198 Self::EndpointNotSupported { .. } => 400,
199 Self::InvalidHeader { .. } => 400,
200 Self::Serialization(_) => 0,
201 Self::BudgetExceeded { .. } => 0,
202 Self::HookRejected { .. } => 0,
203 Self::InternalError { .. } => 0,
204 Self::OutboundForbidden { .. } => 0,
205 Self::IdempotencyConflict { .. } => 409,
206 Self::IdempotencyInFlight { .. } => 409,
207 Self::ProviderQuotaExceeded { status, .. } => *status,
208 }
209 }
210
211 #[must_use]
219 pub fn retry_after(&self) -> Option<Duration> {
220 match self {
221 Self::RateLimited { retry_after, .. } | Self::ServiceUnavailable { retry_after, .. } => *retry_after,
222 _ => None,
223 }
224 }
225
226 #[must_use]
233 pub fn is_transient(&self) -> bool {
234 matches!(
235 self,
236 Self::RateLimited { .. }
237 | Self::ServiceUnavailable { .. }
238 | Self::Timeout
239 | Self::ServerError { .. }
240 | Self::Network { .. }
241 )
242 }
243
244 #[must_use]
249 pub fn error_type(&self) -> &'static str {
250 match self {
251 Self::Authentication { .. } => "Authentication",
252 Self::RateLimited { .. } => "RateLimited",
253 Self::BadRequest { .. } => "BadRequest",
254 Self::ContextWindowExceeded { .. } => "ContextWindowExceeded",
255 Self::ContentPolicy { .. } => "ContentPolicy",
256 Self::NotFound { .. } => "NotFound",
257 Self::ServerError { .. } => "ServerError",
258 Self::ServiceUnavailable { .. } => "ServiceUnavailable",
259 Self::Timeout => "Timeout",
260 Self::Network { .. } => "Network",
261 Self::Streaming { .. } => "Streaming",
262 Self::EndpointNotSupported { .. } => "EndpointNotSupported",
263 Self::InvalidHeader { .. } => "InvalidHeader",
264 Self::Serialization(_) => "Serialization",
265 Self::BudgetExceeded { .. } => "BudgetExceeded",
266 Self::HookRejected { .. } => "HookRejected",
267 Self::InternalError { .. } => "InternalError",
268 Self::OutboundForbidden { .. } => "OutboundForbidden",
269 Self::IdempotencyConflict { .. } => "IdempotencyConflict",
270 Self::IdempotencyInFlight { .. } => "IdempotencyInFlight",
271 Self::ProviderQuotaExceeded { .. } => "ProviderQuotaExceeded",
272 }
273 }
274
275 #[cfg(feature = "tower")]
283 pub(crate) fn to_singleflight_error(&self) -> Self {
284 match self {
285 Self::Authentication { message, status } => Self::Authentication {
286 message: message.clone(),
287 status: *status,
288 },
289 Self::RateLimited { message, retry_after } => Self::RateLimited {
290 message: message.clone(),
291 retry_after: *retry_after,
292 },
293 Self::BadRequest { message, status } => Self::BadRequest {
294 message: message.clone(),
295 status: *status,
296 },
297 Self::ContextWindowExceeded { message } => Self::ContextWindowExceeded {
298 message: message.clone(),
299 },
300 Self::ContentPolicy { message } => Self::ContentPolicy {
301 message: message.clone(),
302 },
303 Self::NotFound { message } => Self::NotFound {
304 message: message.clone(),
305 },
306 Self::ServerError { message, status } => Self::ServerError {
307 message: message.clone(),
308 status: *status,
309 },
310 Self::ServiceUnavailable {
311 message,
312 status,
313 retry_after,
314 } => Self::ServiceUnavailable {
315 message: message.clone(),
316 status: *status,
317 retry_after: *retry_after,
318 },
319 Self::Timeout => Self::Timeout,
320 Self::Network { message } => Self::Network {
321 message: message.clone(),
322 },
323 Self::Streaming { message } => Self::Streaming {
324 message: message.clone(),
325 },
326 Self::EndpointNotSupported { endpoint, provider } => Self::EndpointNotSupported {
327 endpoint: endpoint.clone(),
328 provider: provider.clone(),
329 },
330 Self::InvalidHeader { name, reason } => Self::InvalidHeader {
331 name: name.clone(),
332 reason: reason.clone(),
333 },
334 Self::Serialization(e) => Self::InternalError { message: e.to_string() },
335 Self::BudgetExceeded { message, model } => Self::BudgetExceeded {
336 message: message.clone(),
337 model: model.clone(),
338 },
339 Self::HookRejected { message } => Self::HookRejected {
340 message: message.clone(),
341 },
342 Self::InternalError { message } => Self::InternalError {
343 message: message.clone(),
344 },
345 Self::OutboundForbidden { url, reason } => Self::OutboundForbidden {
346 url: url.clone(),
347 reason: reason.clone(),
348 },
349 Self::IdempotencyConflict { key } => Self::IdempotencyConflict { key: key.clone() },
350 Self::IdempotencyInFlight { key } => Self::IdempotencyInFlight { key: key.clone() },
351 Self::ProviderQuotaExceeded { message, status } => Self::ProviderQuotaExceeded {
352 message: message.clone(),
353 status: *status,
354 },
355 }
356 }
357
358 pub(crate) fn from_status(status: u16, body: &str, retry_after: Option<Duration>) -> Self {
365 let parsed = serde_json::from_str::<ErrorResponse>(body).ok();
366 let code = parsed.as_ref().and_then(|r| r.error.code.clone());
367 let message = parsed.map(|r| r.error.message).unwrap_or_else(|| body.to_string());
368 let message_lower = message.to_ascii_lowercase();
369
370 match status {
371 401 | 403 => Self::Authentication { message, status },
372 429 if code.as_deref() == Some("insufficient_quota") => Self::ProviderQuotaExceeded { message, status },
373 429 => Self::RateLimited { message, retry_after },
374 529 => Self::ServiceUnavailable {
375 message,
376 status,
377 retry_after,
378 },
379 400 | 422 => {
380 if code.as_deref() == Some("context_length_exceeded") {
381 Self::ContextWindowExceeded { message }
382 } else if code.as_deref() == Some("content_policy_violation")
383 || code.as_deref() == Some("content_filter")
384 {
385 Self::ContentPolicy { message }
386 }
387 else if message_lower.contains("context_length_exceeded")
389 || message_lower.contains("context window")
390 || message_lower.contains("maximum context length")
391 || message_lower.contains("prompt is too long")
392 || message_lower.contains("input token count exceeds the maximum")
393 {
394 Self::ContextWindowExceeded { message }
395 } else if message_lower.contains("content_policy") || message_lower.contains("content_filter") {
396 Self::ContentPolicy { message }
397 } else {
398 Self::BadRequest { message, status }
399 }
400 }
401 404 => Self::NotFound { message },
402 405 | 413 => Self::BadRequest { message, status },
403 408 => Self::Timeout,
404 500 => Self::ServerError { message, status },
405 502..=504 => Self::ServiceUnavailable {
406 message,
407 status,
408 retry_after,
409 },
410 400..=499 => Self::BadRequest { message, status },
411 _ => Self::ServerError { message, status },
412 }
413 }
414}
415
416#[cfg_attr(alef, alef(skip))]
417pub type Result<T> = std::result::Result<T, LiterLlmError>;