litellm/litellm-rust/crates/python-bridge/src/errors.rs
devin-ai-integration[bot] 74cad08997
refactor(rust): remove delivery routing abstraction (#43514)
* wip

* refactor: finish removing delivery routing abstraction

* refactor(rust): remove chat completion decline admission

---------

Co-authored-by: Yujong Lee <yujong@berri.ai>
2026-09-28 03:31:22 +00:00

100 lines
3.5 KiB
Rust

use litellm_core::RouteError;
use litellm_http::transport::Error as TransportError;
use pyo3::{
exceptions::{PyRuntimeError, PyValueError},
prelude::*,
};
pyo3::create_exception!(
_native,
RustBridgeDeclined,
pyo3::exceptions::PyException,
"The route declined before calling the provider, so the host may retry on its own path."
);
pyo3::create_exception!(
_native,
RustUpstreamError,
pyo3::exceptions::PyException,
"The provider call was already issued and failed. Args are (status, message); status is 0 when there was no HTTP response."
);
pub(crate) fn route_error_to_pyerr(error: RouteError) -> PyErr {
match error {
RouteError::Transport(TransportError::Http { status, body }) => {
RustUpstreamError::new_err((status, body))
}
other => by_fault(other.is_request(), other.to_string()),
}
}
/// A request the caller got wrong is a `ValueError`; anything else is a `RuntimeError`.
pub(crate) fn by_fault(is_request: bool, message: String) -> PyErr {
if is_request {
PyValueError::new_err(message)
} else {
PyRuntimeError::new_err(message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[rstest::rstest]
#[case::unsupported(RouteError::Unsupported("test capability"), true)]
#[case::invalid_provider(RouteError::InvalidProvider("unknown".into()), true)]
#[case::invalid_request(RouteError::InvalidRequest("empty messages".into()), true)]
#[case::connection(TransportError::Connect("unreachable".into()).into(), false)]
#[case::network(TransportError::Network("timed out".into()).into(), false)]
#[case::invalid_response(RouteError::InvalidResponse("missing usage".into()), false)]
fn route_failures_are_terminal(#[case] error: RouteError, #[case] is_request: bool) {
Python::initialize();
Python::attach(|py| {
let failure = route_error_to_pyerr(error);
assert!(!failure.is_instance_of::<RustBridgeDeclined>(py));
assert_eq!(failure.is_instance_of::<PyValueError>(py), is_request);
assert_eq!(failure.is_instance_of::<PyRuntimeError>(py), !is_request);
});
}
#[rstest::rstest]
fn transport_status_survives_python_mapping() {
Python::initialize();
Python::attach(|py| {
let upstream = route_error_to_pyerr(
TransportError::Http {
status: 429,
body: "slow down".into(),
}
.into(),
);
assert!(upstream.is_instance_of::<RustUpstreamError>(py));
assert_eq!(
upstream
.value(py)
.getattr("args")
.unwrap()
.extract::<(u16, String)>()
.unwrap(),
(429, "slow down".into())
);
});
}
#[test]
fn missing_api_key_stays_a_runtime_error_while_other_auth_failures_are_value_errors() {
Python::initialize();
Python::attach(|py| {
let missing =
route_error_to_pyerr(RouteError::Auth(litellm_auth::Error::MissingApiKey {
provider: "Anthropic",
environment_variable: "ANTHROPIC_API_KEY",
}));
assert!(missing.is_instance_of::<PyRuntimeError>(py));
let invalid =
route_error_to_pyerr(RouteError::Auth(litellm_auth::Error::InvalidHeader));
assert!(invalid.is_instance_of::<PyValueError>(py));
});
}
}