diff --git a/litellm-rust/crates/auth-types/src/error.rs b/litellm-rust/crates/auth-types/src/error.rs index 8a6f4115e3a..b0fc43a91b7 100644 --- a/litellm-rust/crates/auth-types/src/error.rs +++ b/litellm-rust/crates/auth-types/src/error.rs @@ -68,11 +68,9 @@ impl Error { pub fn is_request(&self) -> bool { match self { Self::InvalidConfiguration(_) - | Self::CredentialAcquisition(_) | Self::EmptyCallerCredential { .. } - | Self::Provider(_) - | Self::CredentialChain(_) | Self::InvalidHeader => true, + Self::CredentialAcquisition(_) | Self::Provider(_) | Self::CredentialChain(_) => false, Self::MissingParam { spec, .. } => match spec.kind { ParamKind::Setting => true, ParamKind::Credential => false, @@ -273,6 +271,11 @@ mod tests { #[case::api_key(Error::MissingApiKey { provider: "Example", environment_variable: "EXAMPLE_API_KEY" }, false)] #[case::invalid_configuration(Error::InvalidConfiguration(ErrorDetail::PlanNotAllowed), true)] #[case::invalid_header(Error::InvalidHeader, true)] + #[case::identity_provider_outage( + Error::CredentialAcquisition(ErrorDetail::MissingResolver), + false + )] + #[case::provider_sdk_failure(Error::provider(denied()), false)] fn a_setting_the_caller_can_pass_is_theirs_to_fix_but_credentials_and_api_bases_are_not( #[case] error: Error, #[case] expected: bool, diff --git a/litellm-rust/crates/gateway-inference/src/error.rs b/litellm-rust/crates/gateway-inference/src/error.rs index 75fe23a7035..117c5f5eda7 100644 --- a/litellm-rust/crates/gateway-inference/src/error.rs +++ b/litellm-rust/crates/gateway-inference/src/error.rs @@ -320,7 +320,7 @@ mod tests { StatusCode::INTERNAL_SERVER_ERROR )] #[case::ocr_provider_answer( - Error::Ocr(OcrError::Provider { status: 429, body: "slow".into(), headers: vec![] }), + Error::Ocr(OcrError::Provider { status: 429, body: "slow".into(), headers: Default::default() }), StatusCode::TOO_MANY_REQUESTS )] #[case::ocr_missing_credential( diff --git a/litellm-rust/crates/gateway-ui/Cargo.toml b/litellm-rust/crates/gateway-ui/Cargo.toml index 8a8c6328712..1c65665be72 100644 --- a/litellm-rust/crates/gateway-ui/Cargo.toml +++ b/litellm-rust/crates/gateway-ui/Cargo.toml @@ -6,12 +6,12 @@ license.workspace = true repository.workspace = true [dependencies] +litellm-error.workspace = true axum = { workspace = true, features = ["json", "original-uri", "query"] } axum-login.workspace = true base64.workspace = true governor = { version = "0.10.4", default-features = false, features = ["std"] } jsonwebtoken = { workspace = true, features = ["rust_crypto"] } -litellm-error.workspace = true litellm-gateway-auth.workspace = true rand.workspace = true serde.workspace = true diff --git a/litellm-rust/crates/gateway-ui/src/error.rs b/litellm-rust/crates/gateway-ui/src/error.rs index 99940e03cd7..cce01ba76ce 100644 --- a/litellm-rust/crates/gateway-ui/src/error.rs +++ b/litellm-rust/crates/gateway-ui/src/error.rs @@ -48,12 +48,26 @@ impl Error { } } +impl Error { + fn message(&self) -> String { + match self { + Self::Body(_) | Self::Query(_) => litellm_error::Report::new(self).to_string(), + Self::InvalidCredentials + | Self::RateLimited + | Self::Login(_) + | Self::Session(_) + | Self::Token(_) + | Self::Auth(_) => self.to_string(), + } + } +} + impl IntoResponse for Error { fn into_response(self) -> Response { let status = self.status(); let body = Json(ErrorBody { error: ErrorMessage { - message: litellm_error::Report::new(&self).to_string(), + message: self.message(), }, }); match status { @@ -99,4 +113,16 @@ mod tests { retry_after ); } + + #[tokio::test] + async fn a_body_never_shows_what_the_backend_said() { + let backend = std::io::Error::other("vault token rejected"); + let response = Error::Auth(AuthError::Unavailable(litellm_error::Source::new(backend))) + .into_response(); + let body = axum::body::to_bytes(response.into_body(), 4096) + .await + .unwrap(); + let body = String::from_utf8(body.to_vec()).unwrap(); + assert!(!body.contains("vault token rejected"), "{body}"); + } } diff --git a/litellm-rust/crates/inference-chat/src/handler.rs b/litellm-rust/crates/inference-chat/src/handler.rs index 73df38df6b7..c34d60e1b8a 100644 --- a/litellm-rust/crates/inference-chat/src/handler.rs +++ b/litellm-rust/crates/inference-chat/src/handler.rs @@ -144,7 +144,7 @@ pub(super) async fn execute( /// implementation must not retry those, so collapse them to one variant that /// can only mean the provider was already called. pub(super) fn as_response_error(err: Error) -> Error { - if err.is_response() { + if err.is_response() || err.upstream_response().is_some() { return err; } Error::invalid_response(litellm_llms::ErrorDetail::invalid( @@ -340,4 +340,13 @@ mod tests { let report = litellm_error::Report::new(&as_response_error(original)).to_string(); assert!(!report.contains("chat completions response"), "{report}"); } + + #[test] + fn a_provider_answer_keeps_its_status_and_body() { + let upstream = Error::from(litellm_http::Error::upstream(429, "slow down".into())); + assert_eq!( + as_response_error(upstream).upstream_response(), + Some((429, "slow down")) + ); + } } diff --git a/litellm-rust/crates/inference-ocr/tests/ocr/mistral.rs b/litellm-rust/crates/inference-ocr/tests/ocr/mistral.rs index a5d40496ab9..21cf4ce455d 100644 --- a/litellm-rust/crates/inference-ocr/tests/ocr/mistral.rs +++ b/litellm-rust/crates/inference-ocr/tests/ocr/mistral.rs @@ -95,7 +95,7 @@ async fn an_upstream_error_keeps_its_status_whole_body_and_headers( let (status, body, headers) = error.upstream_response().expect("the provider answered"); assert_eq!(status, 422); assert_eq!(body, expected_body); - let header = |name: &str| litellm_http::request::header_value(headers, name); + let header = |name: &str| headers.get(name).and_then(|value| value.to_str().ok()); assert_eq!(header("retry-after"), Some("17")); assert_eq!(header("x-request-id"), Some("request-123")); assert_eq!(header("x-future-header"), Some("retained")); diff --git a/litellm-rust/crates/llms/src/aws_textract/ocr/analyze_transformation.rs b/litellm-rust/crates/llms/src/aws_textract/ocr/analyze_transformation.rs index e813d4ac8cc..82119cecfa5 100644 --- a/litellm-rust/crates/llms/src/aws_textract/ocr/analyze_transformation.rs +++ b/litellm-rust/crates/llms/src/aws_textract/ocr/analyze_transformation.rs @@ -123,7 +123,7 @@ impl BaseOcrConfig for TextractAnalyzeDocumentConfig { &self, error_message: String, status_code: u16, - headers: Vec<(String, String)>, + headers: reqwest::header::HeaderMap, ) -> Error { error_class(error_message, status_code, headers) } diff --git a/litellm-rust/crates/llms/src/aws_textract/ocr/common_utils.rs b/litellm-rust/crates/llms/src/aws_textract/ocr/common_utils.rs index bc199f40938..112e9cf299b 100644 --- a/litellm-rust/crates/llms/src/aws_textract/ocr/common_utils.rs +++ b/litellm-rust/crates/llms/src/aws_textract/ocr/common_utils.rs @@ -321,7 +321,7 @@ struct AwsError { /// Textract answers both an unsupported format and a multi-page PDF or TIFF /// with a bare "unsupported document format", which reads like a corrupt file. /// Say what the synchronous API accepts. -pub(super) fn error_class(body: String, status: u16, headers: Vec<(String, String)>) -> Error { +pub(super) fn error_class(body: String, status: u16, headers: reqwest::header::HeaderMap) -> Error { let unsupported = serde_json::from_str::(&body) .ok() .filter(|error| error.kind.ends_with(UNSUPPORTED_DOCUMENT)); @@ -559,7 +559,10 @@ mod tests { #[case] body: &str, #[case] hinted_message: Option<&str>, ) { - let response_headers = vec![("x-amzn-requestid".to_string(), "abc".to_string())]; + let response_headers = reqwest::header::HeaderMap::from_iter([( + reqwest::header::HeaderName::from_static("x-amzn-requestid"), + reqwest::header::HeaderValue::from_static("abc"), + )]); let Error::Provider { status, diff --git a/litellm-rust/crates/llms/src/aws_textract/ocr/transformation.rs b/litellm-rust/crates/llms/src/aws_textract/ocr/transformation.rs index 4b27a191734..34213926942 100644 --- a/litellm-rust/crates/llms/src/aws_textract/ocr/transformation.rs +++ b/litellm-rust/crates/llms/src/aws_textract/ocr/transformation.rs @@ -104,7 +104,7 @@ impl BaseOcrConfig for TextractDetectTextConfig { &self, error_message: String, status_code: u16, - headers: Vec<(String, String)>, + headers: reqwest::header::HeaderMap, ) -> Error { error_class(error_message, status_code, headers) } @@ -237,7 +237,7 @@ mod tests { let error = TextractDetectTextConfig.get_error_class( r#"{"__type":"UnsupportedDocumentException","Message":"Request has unsupported document format"}"#.into(), 400, - Vec::new(), + reqwest::header::HeaderMap::new(), ); assert!( diff --git a/litellm-rust/crates/llms/src/base_llm/ocr/error.rs b/litellm-rust/crates/llms/src/base_llm/ocr/error.rs index 682e3045a7a..40302251320 100644 --- a/litellm-rust/crates/llms/src/base_llm/ocr/error.rs +++ b/litellm-rust/crates/llms/src/base_llm/ocr/error.rs @@ -2,8 +2,9 @@ use std::{io, path::PathBuf, sync::Arc}; use litellm_core_utils::call_arguments::ArgumentError; use litellm_error::Source; +use reqwest::header::HeaderMap; -pub type Headers = [(String, String)]; +static NO_HEADERS: std::sync::LazyLock = std::sync::LazyLock::new(HeaderMap::new); #[derive(Clone, Debug, thiserror::Error)] pub enum Error { @@ -11,7 +12,7 @@ pub enum Error { Provider { status: u16, body: String, - headers: Vec<(String, String)>, + headers: HeaderMap, }, #[error("File is empty or could not be read")] EmptyFile, @@ -146,7 +147,7 @@ impl Error { } } - pub fn upstream_response(&self) -> Option<(u16, &str, &Headers)> { + pub fn upstream_response(&self) -> Option<(u16, &str, &HeaderMap)> { match self { Self::Provider { status, @@ -155,7 +156,7 @@ impl Error { } => Some((*status, body, headers)), Self::Http(error) => error .upstream_response() - .map(|(status, body)| (status, body, &[][..])), + .map(|(status, body)| (status, body, &*NO_HEADERS)), Self::EmptyFile | Self::FileRead { .. } | Self::DocumentTask(_) diff --git a/litellm-rust/crates/llms/src/base_llm/ocr/handler.rs b/litellm-rust/crates/llms/src/base_llm/ocr/handler.rs index 036c3c44ccb..4ea6f07e446 100644 --- a/litellm-rust/crates/llms/src/base_llm/ocr/handler.rs +++ b/litellm-rust/crates/llms/src/base_llm/ocr/handler.rs @@ -136,7 +136,7 @@ pub async fn ocr( .map_err(litellm_http::Error::from)?; if !response.status().is_success() { let status = response.status().as_u16(); - let headers = header_pairs(response.headers()); + let headers = response.headers().clone(); let body = read_error_body(response, request.connection.max_response_bytes).await?; return Err(config.get_error_class(body, status, headers)); } @@ -168,7 +168,7 @@ pub async fn read_response_bytes( ) -> Result { if !response.status().is_success() { let status = response.status().as_u16(); - let headers = header_pairs(response.headers()); + let headers = response.headers().clone(); let body = read_error_body(response, limit).await?; return Err(Error::Provider { status, diff --git a/litellm-rust/crates/llms/src/base_llm/ocr/transformation.rs b/litellm-rust/crates/llms/src/base_llm/ocr/transformation.rs index 42332ab8bc5..f04998434b9 100644 --- a/litellm-rust/crates/llms/src/base_llm/ocr/transformation.rs +++ b/litellm-rust/crates/llms/src/base_llm/ocr/transformation.rs @@ -361,7 +361,7 @@ pub trait BaseOcrConfig: Send + Sync + Sized + 'static { &self, error_message: String, status_code: u16, - headers: Vec<(String, String)>, + headers: reqwest::header::HeaderMap, ) -> Error { Error::Provider { status: status_code, diff --git a/litellm-rust/crates/python-bridge/src/errors.rs b/litellm-rust/crates/python-bridge/src/errors.rs index 2d3125049a1..7d325266178 100644 --- a/litellm-rust/crates/python-bridge/src/errors.rs +++ b/litellm-rust/crates/python-bridge/src/errors.rs @@ -6,6 +6,7 @@ use pyo3::{ exceptions::{PyRuntimeError, PyValueError}, prelude::*, }; +use reqwest::header::HeaderMap; pyo3::create_exception!( _native, @@ -27,10 +28,19 @@ pub(crate) fn upstream_error( py: Python<'_>, status: u16, body: &str, - headers: &[(String, String)], + headers: &HeaderMap, ) -> PyResult { let error = RustUpstreamError::new_err((status, body.to_owned())); - error.value(py).setattr("headers", headers.to_vec())?; + let pairs: Vec<(&str, String)> = headers + .iter() + .map(|(name, value)| { + ( + name.as_str(), + String::from_utf8_lossy(value.as_bytes()).into_owned(), + ) + }) + .collect(); + error.value(py).setattr("headers", pairs)?; Ok(error) } @@ -64,7 +74,7 @@ pub(crate) fn route_failure( return Ok(raised); } if let Some((status, body)) = error.upstream_response() { - return public(upstream_error(py, status, body, &[])?); + return public(upstream_error(py, status, body, &HeaderMap::new())?); } let native = match (marker, error.rejected_request()) { (RequestMarker::Messages, Some(rejection)) => { @@ -87,6 +97,8 @@ pub(crate) fn route_error_to_pyerr(error: Error) -> PyErr { #[cfg(test)] mod tests { + use reqwest::header::{HeaderValue, RETRY_AFTER}; + use super::*; fn upstream_payload(py: Python<'_>, failure: &PyErr) -> ((u16, String), Vec<(String, String)>) { @@ -155,11 +167,14 @@ mod tests { fn upstream_error_keeps_the_provider_headers() { Python::initialize(); Python::attach(|py| { - let headers = [("Retry-After".to_string(), "17".to_string())]; + let headers = HeaderMap::from_iter([(RETRY_AFTER, HeaderValue::from_static("17"))]); let upstream = upstream_error(py, 503, "busy", &headers).unwrap(); assert_eq!( upstream_payload(py, &upstream), - ((503, "busy".into()), headers.to_vec()) + ( + (503, "busy".into()), + vec![("retry-after".into(), "17".into())] + ) ); }); } diff --git a/litellm-rust/crates/python-bridge/src/routes/ocr/errors.rs b/litellm-rust/crates/python-bridge/src/routes/ocr/errors.rs index e889aa5d43a..76601dcd706 100644 --- a/litellm-rust/crates/python-bridge/src/routes/ocr/errors.rs +++ b/litellm-rust/crates/python-bridge/src/routes/ocr/errors.rs @@ -80,9 +80,12 @@ mod tests { Error::Provider { status: 429, body: r#"{"message":"rate limited"}"#.into(), - headers: vec![("Retry-After".into(), "17".into())], + headers: reqwest::header::HeaderMap::from_iter([( + reqwest::header::RETRY_AFTER, + reqwest::header::HeaderValue::from_static("17"), + )]), }, - vec![("Retry-After".into(), "17".into())] + vec![("retry-after".into(), "17".into())] )] #[case::transport(litellm_http::Error::upstream(429, r#"{"message":"rate limited"}"#.into()).into(), vec![])] fn a_provider_answer_keeps_its_status_body_and_headers(