1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Debug, Error, Clone, Serialize, Deserialize)]
8pub enum FerrumError {
9 #[error("Configuration error: {message}")]
11 Config { message: String },
12
13 #[error("Model error: {message}")]
15 Model { message: String },
16
17 #[error("Tokenizer error: {message}")]
19 Tokenizer { message: String },
20
21 #[error("Backend error: {message}")]
23 Backend { message: String },
24
25 #[error("Device error: {message}")]
27 Device { message: String },
28
29 #[error("Scheduler error: {message}")]
31 Scheduler { message: String },
32
33 #[error("Request validation error: {message}")]
35 RequestValidation { message: String },
36
37 #[error("This model context is limited to {capacity} tokens, but this request needs {input_tokens} input tokens + {output_tokens} output tokens. Reduce max_tokens or shorten the messages.")]
40 ContextLengthExceeded {
41 capacity: usize,
42 input_tokens: usize,
43 output_tokens: usize,
44 },
45
46 #[error("Resource exhausted: {message}")]
48 ResourceExhausted { message: String },
49
50 #[error("Operation timed out: {message}")]
52 Timeout { message: String },
53
54 #[error("Authentication error: {message}")]
56 Auth { message: String },
57
58 #[error("Rate limit exceeded: {message}")]
60 RateLimit { message: String },
61
62 #[error("I/O error: {message}")]
64 IO { message: String },
65
66 #[error("Serialization error: {message}")]
68 Serialization { message: String },
69
70 #[error("Network error: {message}")]
72 Network { message: String },
73
74 #[error("Internal error: {message}")]
76 Internal { message: String },
77
78 #[error("Request cancelled: {message}")]
80 Cancelled { message: String },
81
82 #[error("Not found: {message}")]
84 NotFound { message: String },
85
86 #[error("Already exists: {message}")]
88 AlreadyExists { message: String },
89
90 #[error("Permission denied: {message}")]
92 PermissionDenied { message: String },
93
94 #[error("Unsupported operation: {message}")]
96 Unsupported { message: String },
97
98 #[error("Invalid format: {message}")]
100 InvalidFormat { message: String },
101
102 #[error("Invalid parameter: {message}")]
104 InvalidParameter { message: String },
105}
106
107impl FerrumError {
108 pub fn config(message: impl Into<String>) -> Self {
110 Self::Config {
111 message: message.into(),
112 }
113 }
114
115 pub fn model(message: impl Into<String>) -> Self {
117 Self::Model {
118 message: message.into(),
119 }
120 }
121
122 pub fn tokenizer(message: impl Into<String>) -> Self {
124 Self::Tokenizer {
125 message: message.into(),
126 }
127 }
128
129 pub fn backend(message: impl Into<String>) -> Self {
131 Self::Backend {
132 message: message.into(),
133 }
134 }
135
136 pub fn device(message: impl Into<String>) -> Self {
138 Self::Device {
139 message: message.into(),
140 }
141 }
142
143 pub fn scheduler(message: impl Into<String>) -> Self {
145 Self::Scheduler {
146 message: message.into(),
147 }
148 }
149
150 pub fn request_validation(message: impl Into<String>) -> Self {
152 Self::RequestValidation {
153 message: message.into(),
154 }
155 }
156
157 pub fn resource_exhausted(message: impl Into<String>) -> Self {
159 Self::ResourceExhausted {
160 message: message.into(),
161 }
162 }
163
164 pub fn timeout(message: impl Into<String>) -> Self {
166 Self::Timeout {
167 message: message.into(),
168 }
169 }
170
171 pub fn auth(message: impl Into<String>) -> Self {
173 Self::Auth {
174 message: message.into(),
175 }
176 }
177
178 pub fn rate_limit(message: impl Into<String>) -> Self {
180 Self::RateLimit {
181 message: message.into(),
182 }
183 }
184
185 pub fn io(message: impl Into<String>) -> Self {
187 Self::IO {
188 message: message.into(),
189 }
190 }
191
192 pub fn serialization(message: impl Into<String>) -> Self {
194 Self::Serialization {
195 message: message.into(),
196 }
197 }
198
199 pub fn network(message: impl Into<String>) -> Self {
201 Self::Network {
202 message: message.into(),
203 }
204 }
205
206 pub fn internal(message: impl Into<String>) -> Self {
208 Self::Internal {
209 message: message.into(),
210 }
211 }
212
213 pub fn cancelled(message: impl Into<String>) -> Self {
215 Self::Cancelled {
216 message: message.into(),
217 }
218 }
219
220 pub fn not_found(message: impl Into<String>) -> Self {
222 Self::NotFound {
223 message: message.into(),
224 }
225 }
226
227 pub fn already_exists(message: impl Into<String>) -> Self {
229 Self::AlreadyExists {
230 message: message.into(),
231 }
232 }
233
234 pub fn permission_denied(message: impl Into<String>) -> Self {
236 Self::PermissionDenied {
237 message: message.into(),
238 }
239 }
240
241 pub fn unsupported(message: impl Into<String>) -> Self {
243 Self::Unsupported {
244 message: message.into(),
245 }
246 }
247
248 pub fn invalid_format(message: impl Into<String>) -> Self {
250 Self::InvalidFormat {
251 message: message.into(),
252 }
253 }
254
255 pub fn invalid_parameter(message: impl Into<String>) -> Self {
257 Self::InvalidParameter {
258 message: message.into(),
259 }
260 }
261
262 pub fn io_str(message: impl Into<String>) -> Self {
266 Self::io(message)
267 }
268
269 pub fn configuration(message: impl Into<String>) -> Self {
271 Self::config(message)
272 }
273
274 pub fn deserialization(message: impl Into<String>) -> Self {
276 Self::serialization(message)
277 }
278
279 pub fn invalid_request(message: impl Into<String>) -> Self {
281 Self::request_validation(message)
282 }
283
284 pub fn is_retryable(&self) -> bool {
286 matches!(
287 self,
288 Self::ResourceExhausted { .. } | Self::Timeout { .. } | Self::Network { .. }
289 )
290 }
291
292 pub fn is_client_error(&self) -> bool {
294 matches!(
295 self,
296 Self::RequestValidation { .. }
297 | Self::ContextLengthExceeded { .. }
298 | Self::Auth { .. }
299 | Self::RateLimit { .. }
300 | Self::NotFound { .. }
301 | Self::AlreadyExists { .. }
302 | Self::PermissionDenied { .. }
303 | Self::Unsupported { .. }
304 )
305 }
306
307 pub fn is_server_error(&self) -> bool {
309 matches!(
310 self,
311 Self::Model { .. }
312 | Self::Backend { .. }
313 | Self::Device { .. }
314 | Self::Scheduler { .. }
315 | Self::ResourceExhausted { .. }
316 | Self::Timeout { .. }
317 | Self::Internal { .. }
318 )
319 }
320
321 pub fn observability_failure_kind(&self) -> &'static str {
327 match self {
328 Self::ResourceExhausted { .. } => "oom_admission",
329 Self::Device { message } if looks_like_oom(message) => "oom",
330 Self::Scheduler { message } if looks_like_admission(message) => "admission",
331 Self::Backend { message } if looks_like_oom(message) => "oom",
332 _ => "error",
333 }
334 }
335
336 pub fn observability_error_kind(&self) -> &'static str {
338 match self {
339 Self::Config { .. } => "config",
340 Self::Model { .. } => "model",
341 Self::Tokenizer { .. } => "tokenizer",
342 Self::Backend { .. } => "backend",
343 Self::Device { .. } => "device",
344 Self::Scheduler { .. } => "scheduler",
345 Self::RequestValidation { .. } => "request_validation",
346 Self::ContextLengthExceeded { .. } => "context_length_exceeded",
347 Self::ResourceExhausted { .. } => "resource_exhausted",
348 Self::Timeout { .. } => "timeout",
349 Self::Auth { .. } => "auth",
350 Self::RateLimit { .. } => "rate_limit",
351 Self::IO { .. } => "io",
352 Self::Serialization { .. } => "serialization",
353 Self::Network { .. } => "network",
354 Self::Internal { .. } => "internal",
355 Self::Cancelled { .. } => "cancelled",
356 Self::NotFound { .. } => "not_found",
357 Self::AlreadyExists { .. } => "already_exists",
358 Self::PermissionDenied { .. } => "permission_denied",
359 Self::Unsupported { .. } => "unsupported",
360 Self::InvalidFormat { .. } => "invalid_format",
361 Self::InvalidParameter { .. } => "invalid_parameter",
362 }
363 }
364}
365
366fn looks_like_oom(message: &str) -> bool {
367 let normalized = message.to_ascii_lowercase();
368 normalized.contains("out of memory")
369 || normalized.contains("oom")
370 || normalized.contains("cuda error 2")
371 || normalized.contains("cudaerroroutofmemory")
372 || normalized.contains("metal out of memory")
373}
374
375fn looks_like_admission(message: &str) -> bool {
376 let normalized = message.to_ascii_lowercase();
377 normalized.contains("admission")
378 || normalized.contains("capacity")
379 || normalized.contains("resource")
380 || normalized.contains("queue")
381}
382
383impl From<std::io::Error> for FerrumError {
385 fn from(err: std::io::Error) -> Self {
386 Self::io(format!("{}", err))
387 }
388}
389
390impl From<serde_json::Error> for FerrumError {
392 fn from(err: serde_json::Error) -> Self {
393 Self::serialization(format!("{}", err))
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn observability_failure_kind_classifies_resource_and_oom() {
403 assert_eq!(
404 FerrumError::resource_exhausted("kv capacity exhausted").observability_failure_kind(),
405 "oom_admission"
406 );
407 assert_eq!(
408 FerrumError::device("CUDA out of memory while allocating KV")
409 .observability_failure_kind(),
410 "oom"
411 );
412 assert_eq!(
413 FerrumError::scheduler("admission capacity rejected request")
414 .observability_failure_kind(),
415 "admission"
416 );
417 assert_eq!(
418 FerrumError::internal("stub generation failed").observability_failure_kind(),
419 "error"
420 );
421 }
422
423 #[test]
424 fn observability_error_kind_uses_stable_variant_labels() {
425 assert_eq!(
426 FerrumError::resource_exhausted("slots exhausted").observability_error_kind(),
427 "resource_exhausted"
428 );
429 assert_eq!(
430 FerrumError::invalid_parameter("bad").observability_error_kind(),
431 "invalid_parameter"
432 );
433 }
434}