1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ErrorResponse {
8 pub error: ApiError,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ApiError {
14 pub message: String,
15 #[serde(rename = "type")]
16 pub error_type: String,
17 #[serde(default)]
18 pub param: Option<String>,
19 #[serde(default)]
20 pub code: Option<String>,
21}
22
23#[derive(Debug, thiserror::Error)]
25#[non_exhaustive]
26pub enum LiterLlmError {
27 #[error("authentication failed: {message}")]
28 Authentication { message: String },
29
30 #[error("rate limited: {message}")]
31 RateLimited {
32 message: String,
33 retry_after: Option<Duration>,
34 },
35
36 #[error("bad request: {message}")]
37 BadRequest { message: String },
38
39 #[error("context window exceeded: {message}")]
40 ContextWindowExceeded { message: String },
41
42 #[error("content policy violation: {message}")]
43 ContentPolicy { message: String },
44
45 #[error("not found: {message}")]
46 NotFound { message: String },
47
48 #[error("server error: {message}")]
49 ServerError { message: String },
50
51 #[error("service unavailable: {message}")]
52 ServiceUnavailable { message: String },
53
54 #[error("request timeout")]
55 Timeout,
56
57 #[cfg(any(feature = "native-http", feature = "wasm-http"))]
58 #[error(transparent)]
59 Network(#[from] reqwest::Error),
60
61 #[error("streaming error: {message}")]
68 Streaming { message: String },
69
70 #[error("provider {provider} does not support {endpoint}")]
71 EndpointNotSupported { endpoint: String, provider: String },
72
73 #[error("invalid header {name:?}: {reason}")]
74 InvalidHeader { name: String, reason: String },
75
76 #[error("serialization error: {0}")]
77 Serialization(#[from] serde_json::Error),
78
79 #[error("budget exceeded: {message}")]
80 BudgetExceeded { message: String, model: Option<String> },
81
82 #[error("hook rejected: {message}")]
83 HookRejected { message: String },
84
85 #[error("internal error: {message}")]
90 InternalError { message: String },
91}
92
93impl LiterLlmError {
94 #[must_use]
101 pub fn is_transient(&self) -> bool {
102 match self {
103 Self::RateLimited { .. } | Self::ServiceUnavailable { .. } | Self::Timeout | Self::ServerError { .. } => {
104 true
105 }
106 #[cfg(any(feature = "native-http", feature = "wasm-http"))]
107 Self::Network(_) => true,
108 _ => false,
109 }
110 }
111
112 #[must_use]
117 pub fn error_type(&self) -> &'static str {
118 match self {
119 Self::Authentication { .. } => "Authentication",
120 Self::RateLimited { .. } => "RateLimited",
121 Self::BadRequest { .. } => "BadRequest",
122 Self::ContextWindowExceeded { .. } => "ContextWindowExceeded",
123 Self::ContentPolicy { .. } => "ContentPolicy",
124 Self::NotFound { .. } => "NotFound",
125 Self::ServerError { .. } => "ServerError",
126 Self::ServiceUnavailable { .. } => "ServiceUnavailable",
127 Self::Timeout => "Timeout",
128 #[cfg(any(feature = "native-http", feature = "wasm-http"))]
129 Self::Network(_) => "Network",
130 Self::Streaming { .. } => "Streaming",
131 Self::EndpointNotSupported { .. } => "EndpointNotSupported",
132 Self::InvalidHeader { .. } => "InvalidHeader",
133 Self::Serialization(_) => "Serialization",
134 Self::BudgetExceeded { .. } => "BudgetExceeded",
135 Self::HookRejected { .. } => "HookRejected",
136 Self::InternalError { .. } => "InternalError",
137 }
138 }
139
140 pub fn from_status(status: u16, body: &str, retry_after: Option<Duration>) -> Self {
147 let parsed = serde_json::from_str::<ErrorResponse>(body).ok();
148 let code = parsed.as_ref().and_then(|r| r.error.code.clone());
149 let message = parsed.map(|r| r.error.message).unwrap_or_else(|| body.to_string());
150
151 match status {
152 401 | 403 => Self::Authentication { message },
153 429 => Self::RateLimited { message, retry_after },
154 400 | 422 => {
155 if code.as_deref() == Some("context_length_exceeded") {
158 Self::ContextWindowExceeded { message }
159 } else if code.as_deref() == Some("content_policy_violation")
160 || code.as_deref() == Some("content_filter")
161 {
162 Self::ContentPolicy { message }
163 }
164 else if message.contains("context_length_exceeded")
167 || message.contains("context window")
168 || message.contains("maximum context length")
169 {
170 Self::ContextWindowExceeded { message }
171 } else if message.contains("content_policy") || message.contains("content_filter") {
172 Self::ContentPolicy { message }
173 } else {
174 Self::BadRequest { message }
175 }
176 }
177 404 => Self::NotFound { message },
178 405 | 413 => Self::BadRequest { message },
179 408 => Self::Timeout,
180 500 => Self::ServerError { message },
181 502..=504 => Self::ServiceUnavailable { message },
182 400..=499 => Self::BadRequest { message },
185 _ => Self::ServerError { message },
186 }
187 }
188}
189
190pub type Result<T> = std::result::Result<T, LiterLlmError>;