use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum UpstreamFailureClass {
Connection,
Timeout,
RetryableStatus,
ContextWindow,
ModelUnavailable,
Authentication,
InvalidRequest,
Other,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UpstreamFailure {
pub status: Option<u16>,
pub body: String,
pub headers: BTreeMap<String, String>,
pub class: UpstreamFailureClass,
}
impl UpstreamFailure {
pub fn is_retryable(&self) -> bool {
matches!(
self.class,
UpstreamFailureClass::Connection
| UpstreamFailureClass::Timeout
| UpstreamFailureClass::RetryableStatus
| UpstreamFailureClass::ContextWindow
| UpstreamFailureClass::ModelUnavailable
)
}
}
impl std::fmt::Display for UpstreamFailure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.status {
Some(status) => write!(
formatter,
"upstream provider returned HTTP {status} ({:?}): {}",
self.class, self.body
),
None => write!(
formatter,
"upstream provider transport failure ({:?}): {}",
self.class, self.body
),
}
}
}
#[derive(Clone, Debug, Error)]
pub enum FlowError {
#[error("already exists: {0}")]
AlreadyExists(String),
#[error("not found: {0}")]
NotFound(String),
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("scope stack empty")]
ScopeStackEmpty,
#[error("guardrail rejected: {0}")]
GuardrailRejected(String),
#[error("{0}")]
Upstream(UpstreamFailure),
#[error("internal error: {0}")]
Internal(String),
#[error("internal error: {message}")]
CallbackException {
message: String,
exception_type: String,
},
}
pub type Result<T> = std::result::Result<T, FlowError>;
impl FlowError {
pub(crate) fn otel_error_type(&self) -> &str {
match self {
Self::AlreadyExists(_) => "already_exists",
Self::NotFound(_) => "not_found",
Self::InvalidArgument(_) => "invalid_argument",
Self::ScopeStackEmpty => "scope_stack_empty",
Self::GuardrailRejected(_) => "guardrail_rejected",
Self::Upstream(failure) => match failure.class {
UpstreamFailureClass::Connection => "connection_error",
UpstreamFailureClass::Timeout => "timeout",
UpstreamFailureClass::RetryableStatus => "retryable_status",
UpstreamFailureClass::ContextWindow => "context_window",
UpstreamFailureClass::ModelUnavailable => "model_unavailable",
UpstreamFailureClass::Authentication => "authentication",
UpstreamFailureClass::InvalidRequest => "invalid_request",
UpstreamFailureClass::Other => "upstream_error",
},
Self::Internal(_) | Self::CallbackException { .. } => "internal_error",
}
}
pub(crate) fn exception_type(&self) -> Option<&str> {
match self {
Self::CallbackException { exception_type, .. } => Some(exception_type),
_ => None,
}
}
}
#[cfg(test)]
#[path = "../tests/coverage/error_tests.rs"]
mod tests;