Skip to main content

ferrum_types/
errors.rs

1//! Error types for Ferrum inference framework
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6/// Main error type for Ferrum operations
7#[derive(Debug, Error, Clone, Serialize, Deserialize)]
8pub enum FerrumError {
9    /// Configuration errors
10    #[error("Configuration error: {message}")]
11    Config { message: String },
12
13    /// Model loading/initialization errors
14    #[error("Model error: {message}")]
15    Model { message: String },
16
17    /// Tokenizer errors
18    #[error("Tokenizer error: {message}")]
19    Tokenizer { message: String },
20
21    /// Backend/runtime errors
22    #[error("Backend error: {message}")]
23    Backend { message: String },
24
25    /// Device/memory errors
26    #[error("Device error: {message}")]
27    Device { message: String },
28
29    /// Scheduling/queue errors
30    #[error("Scheduler error: {message}")]
31    Scheduler { message: String },
32
33    /// Request validation errors
34    #[error("Request validation error: {message}")]
35    RequestValidation { message: String },
36
37    /// Resource exhaustion errors
38    #[error("Resource exhausted: {message}")]
39    ResourceExhausted { message: String },
40
41    /// Timeout errors
42    #[error("Operation timed out: {message}")]
43    Timeout { message: String },
44
45    /// Authentication/authorization errors
46    #[error("Authentication error: {message}")]
47    Auth { message: String },
48
49    /// Rate limiting errors
50    #[error("Rate limit exceeded: {message}")]
51    RateLimit { message: String },
52
53    /// I/O errors
54    #[error("I/O error: {message}")]
55    IO { message: String },
56
57    /// Serialization/deserialization errors
58    #[error("Serialization error: {message}")]
59    Serialization { message: String },
60
61    /// Network errors
62    #[error("Network error: {message}")]
63    Network { message: String },
64
65    /// Internal errors (should not happen in normal operation)
66    #[error("Internal error: {message}")]
67    Internal { message: String },
68
69    /// Request was cancelled
70    #[error("Request cancelled: {message}")]
71    Cancelled { message: String },
72
73    /// Not found errors
74    #[error("Not found: {message}")]
75    NotFound { message: String },
76
77    /// Already exists errors
78    #[error("Already exists: {message}")]
79    AlreadyExists { message: String },
80
81    /// Permission denied errors
82    #[error("Permission denied: {message}")]
83    PermissionDenied { message: String },
84
85    /// Unsupported operation errors
86    #[error("Unsupported operation: {message}")]
87    Unsupported { message: String },
88
89    /// Invalid format errors (parsing, schema mismatches)
90    #[error("Invalid format: {message}")]
91    InvalidFormat { message: String },
92
93    /// Invalid parameters or configuration values
94    #[error("Invalid parameter: {message}")]
95    InvalidParameter { message: String },
96}
97
98impl FerrumError {
99    /// Create a configuration error
100    pub fn config(message: impl Into<String>) -> Self {
101        Self::Config {
102            message: message.into(),
103        }
104    }
105
106    /// Create a model error
107    pub fn model(message: impl Into<String>) -> Self {
108        Self::Model {
109            message: message.into(),
110        }
111    }
112
113    /// Create a tokenizer error
114    pub fn tokenizer(message: impl Into<String>) -> Self {
115        Self::Tokenizer {
116            message: message.into(),
117        }
118    }
119
120    /// Create a backend error
121    pub fn backend(message: impl Into<String>) -> Self {
122        Self::Backend {
123            message: message.into(),
124        }
125    }
126
127    /// Create a device error
128    pub fn device(message: impl Into<String>) -> Self {
129        Self::Device {
130            message: message.into(),
131        }
132    }
133
134    /// Create a scheduler error
135    pub fn scheduler(message: impl Into<String>) -> Self {
136        Self::Scheduler {
137            message: message.into(),
138        }
139    }
140
141    /// Create a request validation error
142    pub fn request_validation(message: impl Into<String>) -> Self {
143        Self::RequestValidation {
144            message: message.into(),
145        }
146    }
147
148    /// Create a resource exhausted error
149    pub fn resource_exhausted(message: impl Into<String>) -> Self {
150        Self::ResourceExhausted {
151            message: message.into(),
152        }
153    }
154
155    /// Create a timeout error
156    pub fn timeout(message: impl Into<String>) -> Self {
157        Self::Timeout {
158            message: message.into(),
159        }
160    }
161
162    /// Create an auth error
163    pub fn auth(message: impl Into<String>) -> Self {
164        Self::Auth {
165            message: message.into(),
166        }
167    }
168
169    /// Create a rate limit error
170    pub fn rate_limit(message: impl Into<String>) -> Self {
171        Self::RateLimit {
172            message: message.into(),
173        }
174    }
175
176    /// Create an I/O error
177    pub fn io(message: impl Into<String>) -> Self {
178        Self::IO {
179            message: message.into(),
180        }
181    }
182
183    /// Create a serialization error
184    pub fn serialization(message: impl Into<String>) -> Self {
185        Self::Serialization {
186            message: message.into(),
187        }
188    }
189
190    /// Create a network error
191    pub fn network(message: impl Into<String>) -> Self {
192        Self::Network {
193            message: message.into(),
194        }
195    }
196
197    /// Create an internal error
198    pub fn internal(message: impl Into<String>) -> Self {
199        Self::Internal {
200            message: message.into(),
201        }
202    }
203
204    /// Create a cancelled error
205    pub fn cancelled(message: impl Into<String>) -> Self {
206        Self::Cancelled {
207            message: message.into(),
208        }
209    }
210
211    /// Create a not found error
212    pub fn not_found(message: impl Into<String>) -> Self {
213        Self::NotFound {
214            message: message.into(),
215        }
216    }
217
218    /// Create an already exists error
219    pub fn already_exists(message: impl Into<String>) -> Self {
220        Self::AlreadyExists {
221            message: message.into(),
222        }
223    }
224
225    /// Create a permission denied error
226    pub fn permission_denied(message: impl Into<String>) -> Self {
227        Self::PermissionDenied {
228            message: message.into(),
229        }
230    }
231
232    /// Create an unsupported operation error
233    pub fn unsupported(message: impl Into<String>) -> Self {
234        Self::Unsupported {
235            message: message.into(),
236        }
237    }
238
239    /// Create an invalid format error (parsing/schema mismatch)
240    pub fn invalid_format(message: impl Into<String>) -> Self {
241        Self::InvalidFormat {
242            message: message.into(),
243        }
244    }
245
246    /// Create an invalid parameter error
247    pub fn invalid_parameter(message: impl Into<String>) -> Self {
248        Self::InvalidParameter {
249            message: message.into(),
250        }
251    }
252
253    // Alias methods for compatibility
254
255    /// Alias for io() - Create an I/O error from string
256    pub fn io_str(message: impl Into<String>) -> Self {
257        Self::io(message)
258    }
259
260    /// Alias for config() - Create a configuration error
261    pub fn configuration(message: impl Into<String>) -> Self {
262        Self::config(message)
263    }
264
265    /// Alias for serialization() - Create a deserialization error
266    pub fn deserialization(message: impl Into<String>) -> Self {
267        Self::serialization(message)
268    }
269
270    /// Alias for request_validation() - Create an invalid request error
271    pub fn invalid_request(message: impl Into<String>) -> Self {
272        Self::request_validation(message)
273    }
274
275    /// Check if this is a retryable error
276    pub fn is_retryable(&self) -> bool {
277        matches!(
278            self,
279            Self::ResourceExhausted { .. } | Self::Timeout { .. } | Self::Network { .. }
280        )
281    }
282
283    /// Check if this is a client error (4xx equivalent)
284    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    /// Check if this is a server error (5xx equivalent)
298    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    /// Stable failure class used by product observability replay bundles.
312    ///
313    /// This intentionally stays smaller than the full error enum because the
314    /// replay validator has different evidence requirements for resource
315    /// failures and generic panic/error failures.
316    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    /// Stable error-kind label for first-failure-event diagnostics.
327    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
372/// Conversion from std::io::Error
373impl From<std::io::Error> for FerrumError {
374    fn from(err: std::io::Error) -> Self {
375        Self::io(format!("{}", err))
376    }
377}
378
379/// Conversion from serde_json::Error
380impl 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}