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("Resource exhausted: {message}")]
39 ResourceExhausted { message: String },
40
41 #[error("Operation timed out: {message}")]
43 Timeout { message: String },
44
45 #[error("Authentication error: {message}")]
47 Auth { message: String },
48
49 #[error("Rate limit exceeded: {message}")]
51 RateLimit { message: String },
52
53 #[error("I/O error: {message}")]
55 IO { message: String },
56
57 #[error("Serialization error: {message}")]
59 Serialization { message: String },
60
61 #[error("Network error: {message}")]
63 Network { message: String },
64
65 #[error("Internal error: {message}")]
67 Internal { message: String },
68
69 #[error("Request cancelled: {message}")]
71 Cancelled { message: String },
72
73 #[error("Not found: {message}")]
75 NotFound { message: String },
76
77 #[error("Already exists: {message}")]
79 AlreadyExists { message: String },
80
81 #[error("Permission denied: {message}")]
83 PermissionDenied { message: String },
84
85 #[error("Unsupported operation: {message}")]
87 Unsupported { message: String },
88
89 #[error("Invalid format: {message}")]
91 InvalidFormat { message: String },
92
93 #[error("Invalid parameter: {message}")]
95 InvalidParameter { message: String },
96}
97
98impl FerrumError {
99 pub fn config(message: impl Into<String>) -> Self {
101 Self::Config {
102 message: message.into(),
103 }
104 }
105
106 pub fn model(message: impl Into<String>) -> Self {
108 Self::Model {
109 message: message.into(),
110 }
111 }
112
113 pub fn tokenizer(message: impl Into<String>) -> Self {
115 Self::Tokenizer {
116 message: message.into(),
117 }
118 }
119
120 pub fn backend(message: impl Into<String>) -> Self {
122 Self::Backend {
123 message: message.into(),
124 }
125 }
126
127 pub fn device(message: impl Into<String>) -> Self {
129 Self::Device {
130 message: message.into(),
131 }
132 }
133
134 pub fn scheduler(message: impl Into<String>) -> Self {
136 Self::Scheduler {
137 message: message.into(),
138 }
139 }
140
141 pub fn request_validation(message: impl Into<String>) -> Self {
143 Self::RequestValidation {
144 message: message.into(),
145 }
146 }
147
148 pub fn resource_exhausted(message: impl Into<String>) -> Self {
150 Self::ResourceExhausted {
151 message: message.into(),
152 }
153 }
154
155 pub fn timeout(message: impl Into<String>) -> Self {
157 Self::Timeout {
158 message: message.into(),
159 }
160 }
161
162 pub fn auth(message: impl Into<String>) -> Self {
164 Self::Auth {
165 message: message.into(),
166 }
167 }
168
169 pub fn rate_limit(message: impl Into<String>) -> Self {
171 Self::RateLimit {
172 message: message.into(),
173 }
174 }
175
176 pub fn io(message: impl Into<String>) -> Self {
178 Self::IO {
179 message: message.into(),
180 }
181 }
182
183 pub fn serialization(message: impl Into<String>) -> Self {
185 Self::Serialization {
186 message: message.into(),
187 }
188 }
189
190 pub fn network(message: impl Into<String>) -> Self {
192 Self::Network {
193 message: message.into(),
194 }
195 }
196
197 pub fn internal(message: impl Into<String>) -> Self {
199 Self::Internal {
200 message: message.into(),
201 }
202 }
203
204 pub fn cancelled(message: impl Into<String>) -> Self {
206 Self::Cancelled {
207 message: message.into(),
208 }
209 }
210
211 pub fn not_found(message: impl Into<String>) -> Self {
213 Self::NotFound {
214 message: message.into(),
215 }
216 }
217
218 pub fn already_exists(message: impl Into<String>) -> Self {
220 Self::AlreadyExists {
221 message: message.into(),
222 }
223 }
224
225 pub fn permission_denied(message: impl Into<String>) -> Self {
227 Self::PermissionDenied {
228 message: message.into(),
229 }
230 }
231
232 pub fn unsupported(message: impl Into<String>) -> Self {
234 Self::Unsupported {
235 message: message.into(),
236 }
237 }
238
239 pub fn invalid_format(message: impl Into<String>) -> Self {
241 Self::InvalidFormat {
242 message: message.into(),
243 }
244 }
245
246 pub fn invalid_parameter(message: impl Into<String>) -> Self {
248 Self::InvalidParameter {
249 message: message.into(),
250 }
251 }
252
253 pub fn io_str(message: impl Into<String>) -> Self {
257 Self::io(message)
258 }
259
260 pub fn configuration(message: impl Into<String>) -> Self {
262 Self::config(message)
263 }
264
265 pub fn deserialization(message: impl Into<String>) -> Self {
267 Self::serialization(message)
268 }
269
270 pub fn invalid_request(message: impl Into<String>) -> Self {
272 Self::request_validation(message)
273 }
274
275 pub fn is_retryable(&self) -> bool {
277 matches!(
278 self,
279 Self::ResourceExhausted { .. } | Self::Timeout { .. } | Self::Network { .. }
280 )
281 }
282
283 pub fn is_client_error(&self) -> bool {
285 matches!(
286 self,
287 Self::RequestValidation { .. }
288 | Self::Auth { .. }
289 | Self::RateLimit { .. }
290 | Self::NotFound { .. }
291 | Self::AlreadyExists { .. }
292 | Self::PermissionDenied { .. }
293 | Self::Unsupported { .. }
294 )
295 }
296
297 pub fn is_server_error(&self) -> bool {
299 matches!(
300 self,
301 Self::Model { .. }
302 | Self::Backend { .. }
303 | Self::Device { .. }
304 | Self::Scheduler { .. }
305 | Self::ResourceExhausted { .. }
306 | Self::Timeout { .. }
307 | Self::Internal { .. }
308 )
309 }
310
311 pub fn observability_failure_kind(&self) -> &'static str {
317 match self {
318 Self::ResourceExhausted { .. } => "oom_admission",
319 Self::Device { message } if looks_like_oom(message) => "oom",
320 Self::Scheduler { message } if looks_like_admission(message) => "admission",
321 Self::Backend { message } if looks_like_oom(message) => "oom",
322 _ => "error",
323 }
324 }
325
326 pub fn observability_error_kind(&self) -> &'static str {
328 match self {
329 Self::Config { .. } => "config",
330 Self::Model { .. } => "model",
331 Self::Tokenizer { .. } => "tokenizer",
332 Self::Backend { .. } => "backend",
333 Self::Device { .. } => "device",
334 Self::Scheduler { .. } => "scheduler",
335 Self::RequestValidation { .. } => "request_validation",
336 Self::ResourceExhausted { .. } => "resource_exhausted",
337 Self::Timeout { .. } => "timeout",
338 Self::Auth { .. } => "auth",
339 Self::RateLimit { .. } => "rate_limit",
340 Self::IO { .. } => "io",
341 Self::Serialization { .. } => "serialization",
342 Self::Network { .. } => "network",
343 Self::Internal { .. } => "internal",
344 Self::Cancelled { .. } => "cancelled",
345 Self::NotFound { .. } => "not_found",
346 Self::AlreadyExists { .. } => "already_exists",
347 Self::PermissionDenied { .. } => "permission_denied",
348 Self::Unsupported { .. } => "unsupported",
349 Self::InvalidFormat { .. } => "invalid_format",
350 Self::InvalidParameter { .. } => "invalid_parameter",
351 }
352 }
353}
354
355fn looks_like_oom(message: &str) -> bool {
356 let normalized = message.to_ascii_lowercase();
357 normalized.contains("out of memory")
358 || normalized.contains("oom")
359 || normalized.contains("cuda error 2")
360 || normalized.contains("cudaerroroutofmemory")
361 || normalized.contains("metal out of memory")
362}
363
364fn looks_like_admission(message: &str) -> bool {
365 let normalized = message.to_ascii_lowercase();
366 normalized.contains("admission")
367 || normalized.contains("capacity")
368 || normalized.contains("resource")
369 || normalized.contains("queue")
370}
371
372impl From<std::io::Error> for FerrumError {
374 fn from(err: std::io::Error) -> Self {
375 Self::io(format!("{}", err))
376 }
377}
378
379impl From<serde_json::Error> for FerrumError {
381 fn from(err: serde_json::Error) -> Self {
382 Self::serialization(format!("{}", err))
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn observability_failure_kind_classifies_resource_and_oom() {
392 assert_eq!(
393 FerrumError::resource_exhausted("kv capacity exhausted").observability_failure_kind(),
394 "oom_admission"
395 );
396 assert_eq!(
397 FerrumError::device("CUDA out of memory while allocating KV")
398 .observability_failure_kind(),
399 "oom"
400 );
401 assert_eq!(
402 FerrumError::scheduler("admission capacity rejected request")
403 .observability_failure_kind(),
404 "admission"
405 );
406 assert_eq!(
407 FerrumError::internal("stub generation failed").observability_failure_kind(),
408 "error"
409 );
410 }
411
412 #[test]
413 fn observability_error_kind_uses_stable_variant_labels() {
414 assert_eq!(
415 FerrumError::resource_exhausted("slots exhausted").observability_error_kind(),
416 "resource_exhausted"
417 );
418 assert_eq!(
419 FerrumError::invalid_parameter("bad").observability_error_kind(),
420 "invalid_parameter"
421 );
422 }
423}