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    /// Input plus requested generation exceeds the effective model/KV context.
38    /// Retrying the same request cannot succeed; the caller must change it.
39    #[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    /// Resource exhaustion errors
47    #[error("Resource exhausted: {message}")]
48    ResourceExhausted { message: String },
49
50    /// Timeout errors
51    #[error("Operation timed out: {message}")]
52    Timeout { message: String },
53
54    /// Authentication/authorization errors
55    #[error("Authentication error: {message}")]
56    Auth { message: String },
57
58    /// Rate limiting errors
59    #[error("Rate limit exceeded: {message}")]
60    RateLimit { message: String },
61
62    /// I/O errors
63    #[error("I/O error: {message}")]
64    IO { message: String },
65
66    /// Serialization/deserialization errors
67    #[error("Serialization error: {message}")]
68    Serialization { message: String },
69
70    /// Network errors
71    #[error("Network error: {message}")]
72    Network { message: String },
73
74    /// Internal errors (should not happen in normal operation)
75    #[error("Internal error: {message}")]
76    Internal { message: String },
77
78    /// Request was cancelled
79    #[error("Request cancelled: {message}")]
80    Cancelled { message: String },
81
82    /// Not found errors
83    #[error("Not found: {message}")]
84    NotFound { message: String },
85
86    /// Already exists errors
87    #[error("Already exists: {message}")]
88    AlreadyExists { message: String },
89
90    /// Permission denied errors
91    #[error("Permission denied: {message}")]
92    PermissionDenied { message: String },
93
94    /// Unsupported operation errors
95    #[error("Unsupported operation: {message}")]
96    Unsupported { message: String },
97
98    /// Invalid format errors (parsing, schema mismatches)
99    #[error("Invalid format: {message}")]
100    InvalidFormat { message: String },
101
102    /// Invalid parameters or configuration values
103    #[error("Invalid parameter: {message}")]
104    InvalidParameter { message: String },
105}
106
107impl FerrumError {
108    /// Create a configuration error
109    pub fn config(message: impl Into<String>) -> Self {
110        Self::Config {
111            message: message.into(),
112        }
113    }
114
115    /// Create a model error
116    pub fn model(message: impl Into<String>) -> Self {
117        Self::Model {
118            message: message.into(),
119        }
120    }
121
122    /// Create a tokenizer error
123    pub fn tokenizer(message: impl Into<String>) -> Self {
124        Self::Tokenizer {
125            message: message.into(),
126        }
127    }
128
129    /// Create a backend error
130    pub fn backend(message: impl Into<String>) -> Self {
131        Self::Backend {
132            message: message.into(),
133        }
134    }
135
136    /// Create a device error
137    pub fn device(message: impl Into<String>) -> Self {
138        Self::Device {
139            message: message.into(),
140        }
141    }
142
143    /// Create a scheduler error
144    pub fn scheduler(message: impl Into<String>) -> Self {
145        Self::Scheduler {
146            message: message.into(),
147        }
148    }
149
150    /// Create a request validation error
151    pub fn request_validation(message: impl Into<String>) -> Self {
152        Self::RequestValidation {
153            message: message.into(),
154        }
155    }
156
157    /// Create a resource exhausted error
158    pub fn resource_exhausted(message: impl Into<String>) -> Self {
159        Self::ResourceExhausted {
160            message: message.into(),
161        }
162    }
163
164    /// Create a timeout error
165    pub fn timeout(message: impl Into<String>) -> Self {
166        Self::Timeout {
167            message: message.into(),
168        }
169    }
170
171    /// Create an auth error
172    pub fn auth(message: impl Into<String>) -> Self {
173        Self::Auth {
174            message: message.into(),
175        }
176    }
177
178    /// Create a rate limit error
179    pub fn rate_limit(message: impl Into<String>) -> Self {
180        Self::RateLimit {
181            message: message.into(),
182        }
183    }
184
185    /// Create an I/O error
186    pub fn io(message: impl Into<String>) -> Self {
187        Self::IO {
188            message: message.into(),
189        }
190    }
191
192    /// Create a serialization error
193    pub fn serialization(message: impl Into<String>) -> Self {
194        Self::Serialization {
195            message: message.into(),
196        }
197    }
198
199    /// Create a network error
200    pub fn network(message: impl Into<String>) -> Self {
201        Self::Network {
202            message: message.into(),
203        }
204    }
205
206    /// Create an internal error
207    pub fn internal(message: impl Into<String>) -> Self {
208        Self::Internal {
209            message: message.into(),
210        }
211    }
212
213    /// Create a cancelled error
214    pub fn cancelled(message: impl Into<String>) -> Self {
215        Self::Cancelled {
216            message: message.into(),
217        }
218    }
219
220    /// Create a not found error
221    pub fn not_found(message: impl Into<String>) -> Self {
222        Self::NotFound {
223            message: message.into(),
224        }
225    }
226
227    /// Create an already exists error
228    pub fn already_exists(message: impl Into<String>) -> Self {
229        Self::AlreadyExists {
230            message: message.into(),
231        }
232    }
233
234    /// Create a permission denied error
235    pub fn permission_denied(message: impl Into<String>) -> Self {
236        Self::PermissionDenied {
237            message: message.into(),
238        }
239    }
240
241    /// Create an unsupported operation error
242    pub fn unsupported(message: impl Into<String>) -> Self {
243        Self::Unsupported {
244            message: message.into(),
245        }
246    }
247
248    /// Create an invalid format error (parsing/schema mismatch)
249    pub fn invalid_format(message: impl Into<String>) -> Self {
250        Self::InvalidFormat {
251            message: message.into(),
252        }
253    }
254
255    /// Create an invalid parameter error
256    pub fn invalid_parameter(message: impl Into<String>) -> Self {
257        Self::InvalidParameter {
258            message: message.into(),
259        }
260    }
261
262    // Alias methods for compatibility
263
264    /// Alias for io() - Create an I/O error from string
265    pub fn io_str(message: impl Into<String>) -> Self {
266        Self::io(message)
267    }
268
269    /// Alias for config() - Create a configuration error
270    pub fn configuration(message: impl Into<String>) -> Self {
271        Self::config(message)
272    }
273
274    /// Alias for serialization() - Create a deserialization error
275    pub fn deserialization(message: impl Into<String>) -> Self {
276        Self::serialization(message)
277    }
278
279    /// Alias for request_validation() - Create an invalid request error
280    pub fn invalid_request(message: impl Into<String>) -> Self {
281        Self::request_validation(message)
282    }
283
284    /// Check if this is a retryable error
285    pub fn is_retryable(&self) -> bool {
286        matches!(
287            self,
288            Self::ResourceExhausted { .. } | Self::Timeout { .. } | Self::Network { .. }
289        )
290    }
291
292    /// Check if this is a client error (4xx equivalent)
293    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    /// Check if this is a server error (5xx equivalent)
308    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    /// Stable failure class used by product observability replay bundles.
322    ///
323    /// This intentionally stays smaller than the full error enum because the
324    /// replay validator has different evidence requirements for resource
325    /// failures and generic panic/error failures.
326    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    /// Stable error-kind label for first-failure-event diagnostics.
337    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
383/// Conversion from std::io::Error
384impl From<std::io::Error> for FerrumError {
385    fn from(err: std::io::Error) -> Self {
386        Self::io(format!("{}", err))
387    }
388}
389
390/// Conversion from serde_json::Error
391impl 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}