1use std::time::Duration;
8
9use crate::types::RequestMetadata;
10
11pub type Result<T> = std::result::Result<T, Error>;
13
14#[allow(missing_docs)]
16pub mod codes {
17 pub const LORA_LOADING: &str = "LORA_LOADING";
18 pub const MODEL_LOADING: &str = "MODEL_LOADING";
19 pub const PROVISIONING: &str = "PROVISIONING";
20 pub const MODEL_LOAD_FAILED: &str = "MODEL_LOAD_FAILED";
21 pub const INPUT_TOO_LONG: &str = "INPUT_TOO_LONG";
22 pub const RESOURCE_EXHAUSTED: &str = "RESOURCE_EXHAUSTED";
23 pub const QUEUE_UNAVAILABLE: &str = "QUEUE_UNAVAILABLE";
24 pub const INTERNAL_ERROR: &str = "INTERNAL_ERROR";
25 pub const ENCODE_RESULT_COUNT_MISMATCH: &str = "ENCODE_RESULT_COUNT_MISMATCH";
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ModelLoadErrorClass {
31 Gated,
33 Oom,
35 Dependency,
37 NotFound,
39 Network,
41 Unknown,
43}
44
45impl ModelLoadErrorClass {
46 fn from_wire(value: &str) -> Self {
47 match value {
48 "GATED" => Self::Gated,
49 "OOM" => Self::Oom,
50 "DEPENDENCY" => Self::Dependency,
51 "NOT_FOUND" => Self::NotFound,
52 "NETWORK" => Self::Network,
53 _ => Self::Unknown,
54 }
55 }
56
57 pub(crate) fn parse(value: Option<&str>) -> Self {
58 value.map_or(Self::Unknown, Self::from_wire)
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum TransportErrorKind {
65 Connect,
67 MidFlight,
69 Timeout,
71}
72
73#[derive(Debug, thiserror::Error)]
79#[non_exhaustive]
80#[allow(missing_docs)]
81pub enum Error {
82 #[error("{message}")]
84 Connection {
85 message: String,
86 kind: TransportErrorKind,
87 #[source]
88 source: Option<Box<dyn std::error::Error + Send + Sync>>,
89 },
90
91 #[error("{message}")]
93 Request {
94 message: String,
95 code: Option<String>,
96 status: u16,
97 request: Option<Box<RequestMetadata>>,
98 },
99
100 #[error("{message}")]
102 Server {
103 message: String,
104 code: Option<String>,
105 status: u16,
106 request: Option<Box<RequestMetadata>>,
107 },
108
109 #[error("{message}")]
111 InputTooLong {
112 message: String,
113 model: Option<String>,
114 request: Option<Box<RequestMetadata>>,
115 },
116
117 #[error("{message}")]
119 ModelLoadFailed {
120 message: String,
121 model: Option<String>,
122 error_class: ModelLoadErrorClass,
123 permanent: bool,
125 attempts: u32,
126 request: Option<Box<RequestMetadata>>,
127 },
128
129 #[error("{message}")]
131 ResourceExhausted {
132 message: String,
133 model: Option<String>,
134 retries: u32,
135 request: Option<Box<RequestMetadata>>,
136 },
137
138 #[error("{message}")]
140 EstimateUnroutable {
141 message: String,
142 code: Option<String>,
143 request: Option<Box<RequestMetadata>>,
144 },
145
146 #[error("{message}")]
148 Provisioning {
149 message: String,
150 gpu: Option<String>,
151 retry_after: Option<Duration>,
152 },
153
154 #[error("{message}")]
156 ModelLoading {
157 message: String,
158 model: Option<String>,
159 },
160
161 #[error("{message}")]
163 LoraLoading {
164 message: String,
165 lora: Option<String>,
166 model: Option<String>,
167 },
168
169 #[error("{message}")]
171 Pool {
172 message: String,
173 pool_name: Option<String>,
174 state: Option<String>,
175 },
176
177 #[error("{0}")]
179 Decode(String),
180
181 #[error("{0}")]
183 InvalidRequest(String),
184
185 #[error(transparent)]
187 Io(#[from] std::io::Error),
188}
189
190impl Error {
191 pub(crate) fn decode(message: impl Into<String>) -> Self {
192 Self::Decode(message.into())
193 }
194
195 pub(crate) fn invalid(message: impl Into<String>) -> Self {
196 Self::InvalidRequest(message.into())
197 }
198
199 pub(crate) fn connection(
200 kind: TransportErrorKind,
201 message: impl Into<String>,
202 source: impl std::error::Error + Send + Sync + 'static,
203 ) -> Self {
204 Self::Connection {
205 message: message.into(),
206 kind,
207 source: Some(Box::new(source)),
208 }
209 }
210
211 pub fn status(&self) -> Option<u16> {
213 match self {
214 Self::Request { status, .. } | Self::Server { status, .. } => Some(*status),
215 Self::InputTooLong { .. } => Some(400),
216 Self::ModelLoadFailed { .. } => Some(502),
217 Self::ResourceExhausted { .. }
218 | Self::EstimateUnroutable { .. }
219 | Self::Provisioning { .. }
220 | Self::ModelLoading { .. } => Some(503),
221 _ => None,
222 }
223 }
224
225 pub fn code(&self) -> Option<&str> {
227 match self {
228 Self::Request { code, .. }
229 | Self::Server { code, .. }
230 | Self::EstimateUnroutable { code, .. } => code.as_deref(),
231 Self::InputTooLong { .. } => Some(codes::INPUT_TOO_LONG),
232 Self::ModelLoadFailed { .. } => Some(codes::MODEL_LOAD_FAILED),
233 Self::ResourceExhausted { .. } => Some(codes::RESOURCE_EXHAUSTED),
234 Self::Provisioning { .. } => Some(codes::PROVISIONING),
235 Self::ModelLoading { .. } => Some(codes::MODEL_LOADING),
236 Self::LoraLoading { .. } => Some(codes::LORA_LOADING),
237 _ => None,
238 }
239 }
240
241 pub fn request_metadata(&self) -> Option<&RequestMetadata> {
243 #[allow(clippy::borrowed_box)]
244 match self {
245 Self::Request { request, .. }
246 | Self::Server { request, .. }
247 | Self::InputTooLong { request, .. }
248 | Self::ModelLoadFailed { request, .. }
249 | Self::ResourceExhausted { request, .. }
250 | Self::EstimateUnroutable { request, .. } => request.as_deref(),
251 _ => None,
252 }
253 }
254
255 pub fn retry_after(&self) -> Option<Duration> {
257 match self {
258 Self::Provisioning { retry_after, .. } => *retry_after,
259 _ => None,
260 }
261 }
262
263 pub fn is_server_error(&self) -> bool {
265 matches!(
266 self,
267 Self::Server { .. }
268 | Self::ModelLoadFailed { .. }
269 | Self::ResourceExhausted { .. }
270 | Self::EstimateUnroutable { .. }
271 )
272 }
273
274 pub fn is_request_error(&self) -> bool {
276 matches!(self, Self::Request { .. } | Self::InputTooLong { .. })
277 }
278
279 pub fn is_capacity_error(&self) -> bool {
281 matches!(
282 self,
283 Self::Provisioning { .. }
284 | Self::ModelLoading { .. }
285 | Self::LoraLoading { .. }
286 | Self::ResourceExhausted { .. }
287 )
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn predicates_follow_the_python_hierarchy() {
297 let exhausted = Error::ResourceExhausted {
298 message: "boom".into(),
299 model: None,
300 retries: 3,
301 request: None,
302 };
303 assert!(exhausted.is_server_error());
304 assert!(exhausted.is_capacity_error());
305 assert_eq!(exhausted.status(), Some(503));
306 assert_eq!(exhausted.code(), Some(codes::RESOURCE_EXHAUSTED));
307
308 let too_long = Error::InputTooLong {
309 message: "too long".into(),
310 model: Some("m".into()),
311 request: None,
312 };
313 assert!(too_long.is_request_error());
314 assert!(!too_long.is_server_error());
315 assert_eq!(too_long.status(), Some(400));
316 }
317
318 #[test]
319 fn model_load_error_class_defaults_to_unknown() {
320 assert_eq!(
321 ModelLoadErrorClass::parse(Some("GATED")),
322 ModelLoadErrorClass::Gated
323 );
324 assert_eq!(
325 ModelLoadErrorClass::parse(Some("nonsense")),
326 ModelLoadErrorClass::Unknown
327 );
328 assert_eq!(
329 ModelLoadErrorClass::parse(None),
330 ModelLoadErrorClass::Unknown
331 );
332 }
333}