litellm-rs 0.6.0

A high-performance AI Gateway written in Rust, providing OpenAI-compatible APIs with intelligent routing, load balancing, and enterprise features
Documentation
use super::RequestValidator;
use crate::core::models::openai::{ChatMessage, ContentPart, MessageContent, MessageRole};
use crate::utils::error::gateway_error::{GatewayError, Result};

impl RequestValidator {
    /// Validate chat completion request
    pub fn validate_chat_completion_request(
        model: &str,
        messages: &[ChatMessage],
        max_tokens: Option<u32>,
        temperature: Option<f32>,
    ) -> Result<()> {
        // Validate model
        Self::validate_model_name(model)?;

        // Validate messages
        if messages.is_empty() {
            return Err(GatewayError::Validation(
                "Messages cannot be empty".to_string(),
            ));
        }

        for (i, message) in messages.iter().enumerate() {
            Self::validate_chat_message(message, i)?;
        }

        // Validate max_tokens
        if let Some(max_tokens) = max_tokens {
            if max_tokens == 0 {
                return Err(GatewayError::Validation(
                    "max_tokens must be greater than 0".to_string(),
                ));
            }
            if max_tokens > 100000 {
                return Err(GatewayError::Validation(
                    "max_tokens cannot exceed 100000".to_string(),
                ));
            }
        }

        // Validate temperature
        if let Some(temperature) = temperature
            && !(0.0..=2.0).contains(&temperature)
        {
            return Err(GatewayError::Validation(
                "temperature must be between 0.0 and 2.0".to_string(),
            ));
        }

        Ok(())
    }

    /// Validate chat message
    pub(super) fn validate_chat_message(message: &ChatMessage, index: usize) -> Result<()> {
        // Validate role
        match message.role {
            MessageRole::System
            | MessageRole::Developer
            | MessageRole::User
            | MessageRole::Assistant => {
                // These roles should have content
                if message.content.is_none() {
                    return Err(GatewayError::Validation(format!(
                        "Message at index {} with role {:?} must have content",
                        index, message.role
                    )));
                }
            }
            MessageRole::Function => {
                // Function messages should have name and content
                if message.name.is_none() {
                    return Err(GatewayError::Validation(format!(
                        "Function message at index {} must have a name",
                        index
                    )));
                }
                if message.content.is_none() {
                    return Err(GatewayError::Validation(format!(
                        "Function message at index {} must have content",
                        index
                    )));
                }
            }
            MessageRole::Tool => {
                // Tool messages should have tool_call_id and content
                if message.tool_call_id.is_none() {
                    return Err(GatewayError::Validation(format!(
                        "Tool message at index {} must have tool_call_id",
                        index
                    )));
                }
                if message.content.is_none() {
                    return Err(GatewayError::Validation(format!(
                        "Tool message at index {} must have content",
                        index
                    )));
                }
            }
        }

        // Validate content if present
        if let Some(content) = &message.content {
            Self::validate_message_content(content, index)?;
        }

        // Validate name if present
        if let Some(name) = &message.name {
            Self::validate_function_name(name)?;
        }

        Ok(())
    }

    /// Validate message content
    pub(super) fn validate_message_content(content: &MessageContent, index: usize) -> Result<()> {
        match content {
            MessageContent::Text(text) => {
                if text.trim().is_empty() {
                    return Err(GatewayError::Validation(format!(
                        "Text content at message index {} cannot be empty",
                        index
                    )));
                }
                if text.len() > 1_000_000 {
                    return Err(GatewayError::Validation(format!(
                        "Text content at message index {} is too long (max 1M characters)",
                        index
                    )));
                }
            }
            MessageContent::Parts(parts) => {
                if parts.is_empty() {
                    return Err(GatewayError::Validation(format!(
                        "Content parts at message index {} cannot be empty",
                        index
                    )));
                }
                for (part_index, part) in parts.iter().enumerate() {
                    Self::validate_content_part(part, index, part_index)?;
                }
            }
        }

        Ok(())
    }

    /// Validate content part
    pub(super) fn validate_content_part(
        part: &ContentPart,
        message_index: usize,
        part_index: usize,
    ) -> Result<()> {
        match part {
            ContentPart::Text { text } => {
                if text.trim().is_empty() {
                    return Err(GatewayError::Validation(format!(
                        "Text part at message {} part {} cannot be empty",
                        message_index, part_index
                    )));
                }
            }
            ContentPart::ImageUrl { image_url } => {
                Self::validate_image_url(&image_url.url)?;
                if let Some(detail) = &image_url.detail
                    && !["low", "high", "auto"].contains(&detail.as_str())
                {
                    return Err(GatewayError::Validation(
                        "Image detail must be 'low', 'high', or 'auto'".to_string(),
                    ));
                }
            }
            ContentPart::Audio { audio } => {
                Self::validate_audio_data(&audio.data)?;
                Self::validate_audio_format(&audio.format)?;
            }
            ContentPart::Image {
                source,
                detail,
                image_url,
            } => {
                if source.media_type.trim().is_empty()
                    || !source.media_type.to_ascii_lowercase().starts_with("image/")
                {
                    return Err(GatewayError::Validation(format!(
                        "Image part at message {} part {} must have image/* media_type",
                        message_index, part_index
                    )));
                }
                Self::validate_base64_payload(&source.data, "image")?;
                if let Some(detail) = detail
                    && !["low", "high", "auto"].contains(&detail.as_str())
                {
                    return Err(GatewayError::Validation(
                        "Image detail must be 'low', 'high', or 'auto'".to_string(),
                    ));
                }
                if let Some(url) = image_url {
                    Self::validate_image_url(&url.url)?;
                }
            }
            ContentPart::Document { source, .. } => {
                if source.media_type.trim().is_empty() {
                    return Err(GatewayError::Validation(format!(
                        "Document part at message {} part {} must have media_type",
                        message_index, part_index
                    )));
                }
                Self::validate_base64_payload(&source.data, "document")?;
            }
            ContentPart::ToolResult { tool_use_id, .. } => {
                if tool_use_id.trim().is_empty() {
                    return Err(GatewayError::Validation(format!(
                        "Tool result at message {} part {} must have non-empty tool_use_id",
                        message_index, part_index
                    )));
                }
            }
            ContentPart::ToolUse { id, name, .. } => {
                if id.trim().is_empty() || name.trim().is_empty() {
                    return Err(GatewayError::Validation(format!(
                        "Tool use at message {} part {} must have non-empty id/name",
                        message_index, part_index
                    )));
                }
            }
        }

        Ok(())
    }
}