refactor(rust): standardize messages errors

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Yujong Lee 2026-09-17 02:08:26 +00:00
parent 4e5a9efd9d
commit 03cd00fbb1
6 changed files with 72 additions and 56 deletions

View file

@ -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(_)
)
}
}

View file

@ -46,9 +46,7 @@ pub(super) async fn execute_messages_provider_stream(
) -> Result<reqwest::Response, Error> {
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);

View file

@ -149,9 +149,7 @@ impl AnthropicBatchesConfig for AnthropicBatchesTransformation {
}
fn transform_create_batch_request(&self) -> Result<Value, Error> {
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<LiteLlmMessageBatch, Error> {
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<String>,
) -> Result<String, Error> {
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"))
));
}
}

View file

@ -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"))
));
}

View file

@ -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<AnthropicMessagesStreamEvent, AnthropicStreamDecodeError> {
let data = frame
.data
.ok_or(AnthropicStreamDecodeError::MissingSseData)?;
Ok(serde_json::from_str(&data)?)
pub fn decode_anthropic_sse_frame(frame: SseFrame) -> Result<AnthropicMessagesStreamEvent, Error> {
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<AnthropicMessagesStreamEvent, AnthropicStreamDecodeError> {
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<AnthropicMessagesStreamEvent, Error> {
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<S, B, E>(
input: S,
) -> impl Stream<Item = Result<AnthropicMessagesStreamEvent, AnthropicStreamDecodeError>> + Send
) -> impl Stream<Item = Result<AnthropicMessagesStreamEvent, Error>> + Send
where
S: Stream<Item = Result<B, E>> + 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<S, B, E>(
input: S,
) -> impl Stream<Item = Result<AnthropicMessagesStreamEvent, AnthropicStreamDecodeError>> + Send
) -> impl Stream<Item = Result<AnthropicMessagesStreamEvent, Error>> + Send
where
S: Stream<Item = Result<B, E>> + 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)]

View file

@ -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),