use super::RequestValidator;
use crate::core::models::openai::{ChatMessage, ContentPart, MessageContent, MessageRole};
use crate::core::providers::ChatMessageContinuation;
use crate::utils::error::gateway_error::{GatewayError, Result};
impl RequestValidator {
pub fn validate_chat_completion_request(
model: &str,
messages: &[ChatMessage],
max_tokens: Option<u32>,
temperature: Option<f32>,
) -> Result<()> {
Self::validate_chat_completion_request_inner(model, messages, None, max_tokens, temperature)
}
pub(crate) fn validate_chat_completion_request_with_extensions(
model: &str,
messages: &[ChatMessage],
extensions: &[ChatMessageContinuation],
max_tokens: Option<u32>,
temperature: Option<f32>,
) -> Result<()> {
if messages.len() != extensions.len() {
return Err(GatewayError::Validation(format!(
"Message extension length mismatch: expected {}, got {}",
messages.len(),
extensions.len()
)));
}
Self::validate_chat_completion_request_inner(
model,
messages,
Some(extensions),
max_tokens,
temperature,
)
}
fn validate_chat_completion_request_inner(
model: &str,
messages: &[ChatMessage],
extensions: Option<&[ChatMessageContinuation]>,
max_tokens: Option<u32>,
temperature: Option<f32>,
) -> Result<()> {
Self::validate_model_name(model)?;
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_with_extension(
message,
extensions.and_then(|items| items.get(i)),
i,
)?;
}
if let Some(max_tokens) = max_tokens {
let max_output_tokens = 100_000;
if max_tokens == 0 {
return Err(GatewayError::Validation(
"max_tokens must be greater than 0".to_string(),
));
}
if max_tokens > max_output_tokens {
return Err(GatewayError::Validation(format!(
"max_tokens cannot exceed {max_output_tokens}"
)));
}
}
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(())
}
pub(super) fn validate_chat_message(message: &ChatMessage, index: usize) -> Result<()> {
Self::validate_chat_message_with_extension(message, None, index)
}
fn validate_chat_message_with_extension(
message: &ChatMessage,
extension: Option<&ChatMessageContinuation>,
index: usize,
) -> Result<()> {
match message.role {
MessageRole::System | MessageRole::Developer | MessageRole::User => {
if message.content.is_none() {
return Err(GatewayError::Validation(format!(
"Message at index {} with role {:?} must have content",
index, message.role
)));
}
}
MessageRole::Assistant => {
let has_tools = message
.tool_calls
.as_ref()
.is_some_and(|calls| !calls.is_empty());
let has_extension = extension.is_some_and(|value| !value.is_empty());
if message.content.is_none()
&& !has_tools
&& message.function_call.is_none()
&& !has_extension
{
return Err(GatewayError::Validation(format!(
"Message at index {} with role {:?} must have content, tool calls, function_call, or a typed continuation",
index, message.role
)));
}
}
MessageRole::Function => {
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 => {
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
)));
}
}
}
if extension.is_some_and(|value| !value.is_empty())
&& message.role != MessageRole::Assistant
{
return Err(GatewayError::Validation(format!(
"Typed Anthropic continuation at message index {} requires assistant role",
index
)));
}
if let Some(content) = &message.content {
Self::validate_message_content(content, index)?;
}
if let Some(name) = &message.name {
Self::validate_function_name(name)?;
}
Ok(())
}
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(())
}
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(())
}
}