diff --git a/litellm-rust/crates/core/src/messages/error.rs b/litellm-rust/crates/core/src/messages/error.rs index 8bea035f0b0..f5e86c4850e 100644 --- a/litellm-rust/crates/core/src/messages/error.rs +++ b/litellm-rust/crates/core/src/messages/error.rs @@ -2,16 +2,54 @@ pub enum Error { #[error("invalid provider: {0}")] InvalidProvider(String), + #[error("missing required field: {0}")] + MissingField(&'static str), #[error("invalid request: {0}")] InvalidRequest(String), #[error("invalid response: {0}")] InvalidResponse(String), - #[error("routing error: {0}")] - Routing(String), + #[error("unsupported by the Rust messages route: {0}")] + Unsupported(&'static str), #[error(transparent)] Auth(#[from] litellm_auth::Error), #[error(transparent)] Transport(#[from] crate::transport::Error), #[error(transparent)] Headers(#[from] crate::http_utils::HeaderError), + #[error("stream framing failed: {0}")] + StreamFraming(String), + #[error("Anthropic SSE frame has no data")] + MissingStreamData, + #[error("Anthropic stream event is invalid: {0}")] + InvalidStreamEvent(String), + #[error("Bedrock event payload is invalid: {0}")] + InvalidBedrockPayload(String), + #[error("Bedrock event payload has invalid base64: {0}")] + InvalidBedrockBase64(String), +} + +impl Error { + pub fn is_request(&self) -> bool { + match self { + Self::InvalidProvider(_) + | Self::MissingField(_) + | Self::InvalidRequest(_) + | Self::Unsupported(_) + | Self::Headers(_) => true, + Self::Auth(error) => !matches!(error, litellm_auth::Error::MissingApiKey { .. }), + _ => false, + } + } + + pub fn is_response(&self) -> bool { + matches!( + self, + Self::InvalidResponse(_) + | Self::StreamFraming(_) + | Self::MissingStreamData + | Self::InvalidStreamEvent(_) + | Self::InvalidBedrockPayload(_) + | Self::InvalidBedrockBase64(_) + ) + } } diff --git a/litellm-rust/crates/core/src/messages/handler.rs b/litellm-rust/crates/core/src/messages/handler.rs index d7d593f2d57..aaf51e8647e 100644 --- a/litellm-rust/crates/core/src/messages/handler.rs +++ b/litellm-rust/crates/core/src/messages/handler.rs @@ -46,9 +46,7 @@ pub(super) async fn execute_messages_provider_stream( ) -> Result { let request = prepare_provider_request(request)?; if request.provider != ANTHROPIC_MESSAGES_PROVIDER { - return Err(Error::InvalidRequest( - "streaming messages is not supported for this provider".to_string(), - )); + return Err(Error::Unsupported("streaming messages for this provider")); } let mut request_builder = http_client().post(&request.url).json(&request.body); diff --git a/litellm-rust/crates/core/src/providers/anthropic/messages/batches.rs b/litellm-rust/crates/core/src/providers/anthropic/messages/batches.rs index cf9bb0964be..fcd4a3445c2 100644 --- a/litellm-rust/crates/core/src/providers/anthropic/messages/batches.rs +++ b/litellm-rust/crates/core/src/providers/anthropic/messages/batches.rs @@ -149,9 +149,7 @@ impl AnthropicBatchesConfig for AnthropicBatchesTransformation { } fn transform_create_batch_request(&self) -> Result { - Err(Error::InvalidRequest( - "Batch creation not yet implemented for Anthropic".into(), - )) + Err(Error::Unsupported("Anthropic message batch creation")) } fn transform_create_batch_response( @@ -159,9 +157,7 @@ impl AnthropicBatchesConfig for AnthropicBatchesTransformation { _response: AnthropicMessageBatch, _now: i64, ) -> Result { - Err(Error::InvalidResponse( - "Batch creation not yet implemented for Anthropic".into(), - )) + Err(Error::Unsupported("Anthropic message batch creation")) } fn retrieve_batch_url( @@ -171,7 +167,7 @@ impl AnthropicBatchesConfig for AnthropicBatchesTransformation { env_lookup: &dyn Fn(&str) -> Option, ) -> Result { if batch_id.is_empty() { - return Err(Error::InvalidRequest("batch_id is required".into())); + return Err(Error::MissingField("batch_id")); } let mut url = batches_base_url(api_base, env_lookup)?; url.path_segments_mut() @@ -331,14 +327,12 @@ mod tests { fn preserves_python_placeholder_for_batch_creation() { assert!(matches!( ANTHROPIC_BATCHES_TRANSFORMATION.transform_create_batch_request(), - Err(Error::InvalidRequest(message)) - if message == "Batch creation not yet implemented for Anthropic" + Err(Error::Unsupported("Anthropic message batch creation")) )); let response: AnthropicMessageBatch = serde_json::from_value(json!({})).unwrap(); assert!(matches!( ANTHROPIC_BATCHES_TRANSFORMATION.transform_create_batch_response(response, 0), - Err(Error::InvalidResponse(message)) - if message == "Batch creation not yet implemented for Anthropic" + Err(Error::Unsupported("Anthropic message batch creation")) )); } } diff --git a/litellm-rust/crates/core/src/providers/anthropic/messages/count_tokens.rs b/litellm-rust/crates/core/src/providers/anthropic/messages/count_tokens.rs index 34c3dfdde56..8ad96e2ead5 100644 --- a/litellm-rust/crates/core/src/providers/anthropic/messages/count_tokens.rs +++ b/litellm-rust/crates/core/src/providers/anthropic/messages/count_tokens.rs @@ -68,12 +68,10 @@ impl AnthropicCountTokensConfig for AnthropicCountTokensTransformation { fn validate_request(&self, model: &str, messages: &[AnthropicMessage]) -> Result<(), Error> { if model.is_empty() { - return Err(Error::InvalidRequest("model parameter is required".into())); + return Err(Error::MissingField("model")); } if messages.is_empty() { - return Err(Error::InvalidRequest( - "messages parameter is required".into(), - )); + return Err(Error::MissingField("messages")); } Ok(()) } @@ -143,7 +141,7 @@ mod tests { None, None ), - Err(Error::InvalidRequest(message)) if message == "model parameter is required" + Err(Error::MissingField("model")) )); assert!(matches!( ANTHROPIC_COUNT_TOKENS_TRANSFORMATION.transform_request( @@ -152,7 +150,7 @@ mod tests { None, None ), - Err(Error::InvalidRequest(message)) if message == "messages parameter is required" + Err(Error::MissingField("messages")) )); } diff --git a/litellm-rust/crates/core/src/providers/anthropic/messages/streaming.rs b/litellm-rust/crates/core/src/providers/anthropic/messages/streaming.rs index 8b98ea3645b..3dabf58c7af 100644 --- a/litellm-rust/crates/core/src/providers/anthropic/messages/streaming.rs +++ b/litellm-rust/crates/core/src/providers/anthropic/messages/streaming.rs @@ -7,17 +7,7 @@ use litellm_framing::sse::{SseFrame, SseFramer}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -#[derive(Debug, thiserror::Error)] -pub enum AnthropicStreamDecodeError { - #[error("stream framing failed: {0}")] - Framing(#[from] litellm_framing::Error), - #[error("Anthropic SSE frame has no data")] - MissingSseData, - #[error("Anthropic stream event is invalid: {0}")] - InvalidEvent(#[from] serde_json::Error), - #[error("Bedrock event payload has invalid base64: {0}")] - InvalidBedrockPayload(#[from] base64::DecodeError), -} +use crate::messages::Error; #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct AnthropicStreamUsage { @@ -148,47 +138,48 @@ struct BedrockChunkPayload { bytes: String, } -pub fn decode_anthropic_sse_frame( - frame: SseFrame, -) -> Result { - let data = frame - .data - .ok_or(AnthropicStreamDecodeError::MissingSseData)?; - Ok(serde_json::from_str(&data)?) +pub fn decode_anthropic_sse_frame(frame: SseFrame) -> Result { + let data = frame.data.ok_or(Error::MissingStreamData)?; + serde_json::from_str(&data).map_err(|error| Error::InvalidStreamEvent(error.to_string())) } pub fn decode_bedrock_anthropic_frame( frame: AwsEventStreamFrame, -) -> Result { - let payload: BedrockChunkPayload = serde_json::from_slice(&frame.payload)?; - let event = base64::engine::general_purpose::STANDARD.decode(payload.bytes)?; - Ok(serde_json::from_slice(&event)?) +) -> Result { + let payload: BedrockChunkPayload = serde_json::from_slice(&frame.payload) + .map_err(|error| Error::InvalidBedrockPayload(error.to_string()))?; + let event = base64::engine::general_purpose::STANDARD + .decode(payload.bytes) + .map_err(|error| Error::InvalidBedrockBase64(error.to_string()))?; + serde_json::from_slice(&event).map_err(|error| Error::InvalidStreamEvent(error.to_string())) } pub fn direct_anthropic_event_stream( input: S, -) -> impl Stream> + Send +) -> impl Stream> + Send where S: Stream> + Send, B: Buf + Send, E: std::error::Error + Send + Sync + 'static, { - SseFramer - .frame(input) - .map(|frame| decode_anthropic_sse_frame(frame?)) + SseFramer.frame(input).map(|frame| { + let frame = frame.map_err(|error| Error::StreamFraming(error.to_string()))?; + decode_anthropic_sse_frame(frame) + }) } pub fn bedrock_anthropic_event_stream( input: S, -) -> impl Stream> + Send +) -> impl Stream> + Send where S: Stream> + Send, B: Buf + Send, E: std::error::Error + Send + Sync + 'static, { - AwsEventStreamFramer - .frame(input) - .map(|frame| decode_bedrock_anthropic_frame(frame?)) + AwsEventStreamFramer.frame(input).map(|frame| { + let frame = frame.map_err(|error| Error::StreamFraming(error.to_string()))?; + decode_bedrock_anthropic_frame(frame) + }) } #[cfg(test)] diff --git a/litellm-rust/crates/python-bridge/src/errors.rs b/litellm-rust/crates/python-bridge/src/errors.rs index 7ca86b3ccfa..3b67280ae46 100644 --- a/litellm-rust/crates/python-bridge/src/errors.rs +++ b/litellm-rust/crates/python-bridge/src/errors.rs @@ -46,10 +46,7 @@ pub(crate) fn core_error_to_pyerr(error: Error) -> PyErr { ), Error::Messages(error) => match error { messages::Error::Auth(source) => auth_is_value_error(source), - messages::Error::InvalidProvider(_) - | messages::Error::InvalidRequest(_) - | messages::Error::Headers(_) => true, - _ => false, + _ => error.is_request(), }, Error::AudioTranscription(error) => match error { audio_transcription::Error::Auth(source) => auth_is_value_error(source),