use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error, Clone, Serialize, Deserialize)]
pub enum FerrumError {
#[error("Configuration error: {message}")]
Config { message: String },
#[error("Model error: {message}")]
Model { message: String },
#[error("Tokenizer error: {message}")]
Tokenizer { message: String },
#[error("Backend error: {message}")]
Backend { message: String },
#[error("Device error: {message}")]
Device { message: String },
#[error("Scheduler error: {message}")]
Scheduler { message: String },
#[error("Request validation error: {message}")]
RequestValidation { message: String },
#[error("Resource exhausted: {message}")]
ResourceExhausted { message: String },
#[error("Operation timed out: {message}")]
Timeout { message: String },
#[error("Authentication error: {message}")]
Auth { message: String },
#[error("Rate limit exceeded: {message}")]
RateLimit { message: String },
#[error("I/O error: {message}")]
IO { message: String },
#[error("Serialization error: {message}")]
Serialization { message: String },
#[error("Network error: {message}")]
Network { message: String },
#[error("Internal error: {message}")]
Internal { message: String },
#[error("Request cancelled: {message}")]
Cancelled { message: String },
#[error("Not found: {message}")]
NotFound { message: String },
#[error("Already exists: {message}")]
AlreadyExists { message: String },
#[error("Permission denied: {message}")]
PermissionDenied { message: String },
#[error("Unsupported operation: {message}")]
Unsupported { message: String },
#[error("Invalid format: {message}")]
InvalidFormat { message: String },
#[error("Invalid parameter: {message}")]
InvalidParameter { message: String },
}
impl FerrumError {
pub fn config(message: impl Into<String>) -> Self {
Self::Config {
message: message.into(),
}
}
pub fn model(message: impl Into<String>) -> Self {
Self::Model {
message: message.into(),
}
}
pub fn tokenizer(message: impl Into<String>) -> Self {
Self::Tokenizer {
message: message.into(),
}
}
pub fn backend(message: impl Into<String>) -> Self {
Self::Backend {
message: message.into(),
}
}
pub fn device(message: impl Into<String>) -> Self {
Self::Device {
message: message.into(),
}
}
pub fn scheduler(message: impl Into<String>) -> Self {
Self::Scheduler {
message: message.into(),
}
}
pub fn request_validation(message: impl Into<String>) -> Self {
Self::RequestValidation {
message: message.into(),
}
}
pub fn resource_exhausted(message: impl Into<String>) -> Self {
Self::ResourceExhausted {
message: message.into(),
}
}
pub fn timeout(message: impl Into<String>) -> Self {
Self::Timeout {
message: message.into(),
}
}
pub fn auth(message: impl Into<String>) -> Self {
Self::Auth {
message: message.into(),
}
}
pub fn rate_limit(message: impl Into<String>) -> Self {
Self::RateLimit {
message: message.into(),
}
}
pub fn io(message: impl Into<String>) -> Self {
Self::IO {
message: message.into(),
}
}
pub fn serialization(message: impl Into<String>) -> Self {
Self::Serialization {
message: message.into(),
}
}
pub fn network(message: impl Into<String>) -> Self {
Self::Network {
message: message.into(),
}
}
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal {
message: message.into(),
}
}
pub fn cancelled(message: impl Into<String>) -> Self {
Self::Cancelled {
message: message.into(),
}
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::NotFound {
message: message.into(),
}
}
pub fn already_exists(message: impl Into<String>) -> Self {
Self::AlreadyExists {
message: message.into(),
}
}
pub fn permission_denied(message: impl Into<String>) -> Self {
Self::PermissionDenied {
message: message.into(),
}
}
pub fn unsupported(message: impl Into<String>) -> Self {
Self::Unsupported {
message: message.into(),
}
}
pub fn invalid_format(message: impl Into<String>) -> Self {
Self::InvalidFormat {
message: message.into(),
}
}
pub fn invalid_parameter(message: impl Into<String>) -> Self {
Self::InvalidParameter {
message: message.into(),
}
}
pub fn io_str(message: impl Into<String>) -> Self {
Self::io(message)
}
pub fn configuration(message: impl Into<String>) -> Self {
Self::config(message)
}
pub fn deserialization(message: impl Into<String>) -> Self {
Self::serialization(message)
}
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::request_validation(message)
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::ResourceExhausted { .. } | Self::Timeout { .. } | Self::Network { .. }
)
}
pub fn is_client_error(&self) -> bool {
matches!(
self,
Self::RequestValidation { .. }
| Self::Auth { .. }
| Self::RateLimit { .. }
| Self::NotFound { .. }
| Self::AlreadyExists { .. }
| Self::PermissionDenied { .. }
| Self::Unsupported { .. }
)
}
pub fn is_server_error(&self) -> bool {
matches!(
self,
Self::Model { .. }
| Self::Backend { .. }
| Self::Device { .. }
| Self::Scheduler { .. }
| Self::ResourceExhausted { .. }
| Self::Timeout { .. }
| Self::Internal { .. }
)
}
pub fn observability_failure_kind(&self) -> &'static str {
match self {
Self::ResourceExhausted { .. } => "oom_admission",
Self::Device { message } if looks_like_oom(message) => "oom",
Self::Scheduler { message } if looks_like_admission(message) => "admission",
Self::Backend { message } if looks_like_oom(message) => "oom",
_ => "error",
}
}
pub fn observability_error_kind(&self) -> &'static str {
match self {
Self::Config { .. } => "config",
Self::Model { .. } => "model",
Self::Tokenizer { .. } => "tokenizer",
Self::Backend { .. } => "backend",
Self::Device { .. } => "device",
Self::Scheduler { .. } => "scheduler",
Self::RequestValidation { .. } => "request_validation",
Self::ResourceExhausted { .. } => "resource_exhausted",
Self::Timeout { .. } => "timeout",
Self::Auth { .. } => "auth",
Self::RateLimit { .. } => "rate_limit",
Self::IO { .. } => "io",
Self::Serialization { .. } => "serialization",
Self::Network { .. } => "network",
Self::Internal { .. } => "internal",
Self::Cancelled { .. } => "cancelled",
Self::NotFound { .. } => "not_found",
Self::AlreadyExists { .. } => "already_exists",
Self::PermissionDenied { .. } => "permission_denied",
Self::Unsupported { .. } => "unsupported",
Self::InvalidFormat { .. } => "invalid_format",
Self::InvalidParameter { .. } => "invalid_parameter",
}
}
}
fn looks_like_oom(message: &str) -> bool {
let normalized = message.to_ascii_lowercase();
normalized.contains("out of memory")
|| normalized.contains("oom")
|| normalized.contains("cuda error 2")
|| normalized.contains("cudaerroroutofmemory")
|| normalized.contains("metal out of memory")
}
fn looks_like_admission(message: &str) -> bool {
let normalized = message.to_ascii_lowercase();
normalized.contains("admission")
|| normalized.contains("capacity")
|| normalized.contains("resource")
|| normalized.contains("queue")
}
impl From<std::io::Error> for FerrumError {
fn from(err: std::io::Error) -> Self {
Self::io(format!("{}", err))
}
}
impl From<serde_json::Error> for FerrumError {
fn from(err: serde_json::Error) -> Self {
Self::serialization(format!("{}", err))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn observability_failure_kind_classifies_resource_and_oom() {
assert_eq!(
FerrumError::resource_exhausted("kv capacity exhausted").observability_failure_kind(),
"oom_admission"
);
assert_eq!(
FerrumError::device("CUDA out of memory while allocating KV")
.observability_failure_kind(),
"oom"
);
assert_eq!(
FerrumError::scheduler("admission capacity rejected request")
.observability_failure_kind(),
"admission"
);
assert_eq!(
FerrumError::internal("stub generation failed").observability_failure_kind(),
"error"
);
}
#[test]
fn observability_error_kind_uses_stable_variant_labels() {
assert_eq!(
FerrumError::resource_exhausted("slots exhausted").observability_error_kind(),
"resource_exhausted"
);
assert_eq!(
FerrumError::invalid_parameter("bad").observability_error_kind(),
"invalid_parameter"
);
}
}