use crate::inference::registry::ProviderId;
#[derive(Debug, thiserror::Error)]
pub enum InferenceError {
#[error("inference transport error: {0}")]
Transport(String),
#[error("inference API error {status}: {body}")]
Api {
status: u16,
body: String,
},
#[error("inference response deserialisation failed: {message}")]
Deserialise {
message: String,
body: String,
},
#[error("no credential resolved for provider {provider}")]
MissingCredential {
provider: ProviderId,
},
#[error("no adapter registered for provider {provider}")]
NoAdapterRegistered {
provider: ProviderId,
},
#[error("inference provider error: {0}")]
Provider(String),
#[error("unsupported inference capability: {0}")]
Unsupported(String),
}
impl InferenceError {
pub fn is_retryable(&self) -> bool {
match self {
Self::Transport(_) => true,
Self::Api { status, .. } => *status == 429 || (500..=599).contains(status),
_ => false,
}
}
pub fn is_alarm(&self) -> bool {
match self {
Self::MissingCredential { .. }
| Self::NoAdapterRegistered { .. }
| Self::Unsupported(_) => true,
Self::Api { status, .. } => matches!(status, 401 | 403 | 404),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retryable_classification() {
assert!(InferenceError::Transport("timeout".into()).is_retryable());
assert!(
InferenceError::Api {
status: 429,
body: "slow down".into()
}
.is_retryable()
);
assert!(
InferenceError::Api {
status: 503,
body: "unavailable".into()
}
.is_retryable()
);
assert!(
!InferenceError::Api {
status: 401,
body: "bad key".into()
}
.is_retryable()
);
assert!(
!InferenceError::MissingCredential {
provider: ProviderId::OpenRouter
}
.is_retryable()
);
}
#[test]
fn alarm_classification() {
assert!(
InferenceError::MissingCredential {
provider: ProviderId::Anthropic
}
.is_alarm()
);
assert!(
InferenceError::NoAdapterRegistered {
provider: ProviderId::Fireworks
}
.is_alarm()
);
assert!(InferenceError::Unsupported("vision".into()).is_alarm());
assert!(
InferenceError::Api {
status: 403,
body: "denied".into()
}
.is_alarm()
);
assert!(!InferenceError::Transport("blip".into()).is_alarm());
}
#[test]
fn display_includes_context() {
let e = InferenceError::Api {
status: 429,
body: "rate limited".into(),
};
let s = e.to_string();
assert!(s.contains("429"), "{s}");
assert!(s.contains("rate limited"), "{s}");
}
}