use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::skip_serializing_none;
use validator::{Validate, ValidationError};
use super::{
common::{default_true, deserialize_null_as_false, Function, GenerationRequest},
sampling_params::validate_top_p_value,
validated::Normalizable,
};
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
#[validate(schema(function = "validate_interactions_request"))]
pub struct InteractionsRequest {
pub model: Option<String>,
pub agent: Option<String>,
#[validate(custom(function = "validate_input"))]
pub input: InteractionsInput,
pub system_instruction: Option<String>,
#[validate(custom(function = "validate_tools"))]
pub tools: Option<Vec<InteractionsTool>>,
pub response_format: Option<Value>,
pub response_mime_type: Option<String>,
#[serde(default, deserialize_with = "deserialize_null_as_false")]
pub stream: bool,
#[serde(default = "default_true")]
pub store: bool,
#[serde(default)]
pub background: bool,
#[validate(nested)]
pub generation_config: Option<GenerationConfig>,
pub agent_config: Option<AgentConfig>,
pub response_modalities: Option<Vec<ResponseModality>>,
pub previous_interaction_id: Option<String>,
}
impl Default for InteractionsRequest {
fn default() -> Self {
Self {
model: None,
agent: None,
agent_config: None,
input: InteractionsInput::Text(String::new()),
system_instruction: None,
previous_interaction_id: None,
tools: None,
generation_config: None,
response_format: None,
response_mime_type: None,
response_modalities: None,
stream: false,
background: false,
store: true,
}
}
}
impl Normalizable for InteractionsRequest {
}
impl GenerationRequest for InteractionsRequest {
fn is_stream(&self) -> bool {
self.stream
}
fn get_model(&self) -> Option<&str> {
self.model.as_deref()
}
fn extract_text_for_routing(&self) -> String {
fn extract_from_content(content: &Content) -> Option<String> {
match content {
Content::Text { text, .. } => text.clone(),
_ => None,
}
}
fn extract_from_turn(turn: &Turn) -> String {
match &turn.content {
Some(TurnContent::Text(text)) => text.clone(),
Some(TurnContent::Contents(contents)) => contents
.iter()
.filter_map(extract_from_content)
.collect::<Vec<String>>()
.join(" "),
None => String::new(),
}
}
match &self.input {
InteractionsInput::Text(text) => text.clone(),
InteractionsInput::Content(content) => {
extract_from_content(content).unwrap_or_default()
}
InteractionsInput::Contents(contents) => contents
.iter()
.filter_map(extract_from_content)
.collect::<Vec<String>>()
.join(" "),
InteractionsInput::Turns(turns) => turns
.iter()
.map(extract_from_turn)
.collect::<Vec<String>>()
.join(" "),
}
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Interaction {
pub object: Option<String>,
pub model: Option<String>,
pub agent: Option<String>,
pub id: String,
pub status: InteractionsStatus,
pub created: Option<String>,
pub updated: Option<String>,
pub role: Option<String>,
pub outputs: Option<Vec<Content>>,
pub usage: Option<InteractionsUsage>,
pub previous_interaction_id: Option<String>,
}
impl Interaction {
pub fn is_complete(&self) -> bool {
matches!(self.status, InteractionsStatus::Completed)
}
pub fn is_in_progress(&self) -> bool {
matches!(self.status, InteractionsStatus::InProgress)
}
pub fn is_failed(&self) -> bool {
matches!(self.status, InteractionsStatus::Failed)
}
pub fn requires_action(&self) -> bool {
matches!(self.status, InteractionsStatus::RequiresAction)
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "event_type")]
pub enum InteractionStreamEvent {
#[serde(rename = "interaction.start")]
InteractionStart {
interaction: Option<Interaction>,
event_id: Option<String>,
},
#[serde(rename = "interaction.complete")]
InteractionComplete {
interaction: Option<Interaction>,
event_id: Option<String>,
},
#[serde(rename = "interaction.status_update")]
InteractionStatusUpdate {
interaction_id: Option<String>,
status: Option<InteractionsStatus>,
event_id: Option<String>,
},
#[serde(rename = "content.start")]
ContentStart {
index: Option<u32>,
content: Option<Content>,
event_id: Option<String>,
},
#[serde(rename = "content.delta")]
ContentDelta {
index: Option<u32>,
event_id: Option<String>,
delta: Option<Delta>,
},
#[serde(rename = "content.stop")]
ContentStop {
index: Option<u32>,
event_id: Option<String>,
},
#[serde(rename = "error")]
Error {
error: Option<InteractionsError>,
event_id: Option<String>,
},
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Delta {
Text {
text: Option<String>,
annotations: Option<Vec<Annotation>>,
},
Image {
data: Option<String>,
uri: Option<String>,
mime_type: Option<ImageMimeType>,
resolution: Option<MediaResolution>,
},
Audio {
data: Option<String>,
uri: Option<String>,
mime_type: Option<AudioMimeType>,
},
Document {
data: Option<String>,
uri: Option<String>,
mime_type: Option<DocumentMimeType>,
},
Video {
data: Option<String>,
uri: Option<String>,
mime_type: Option<VideoMimeType>,
resolution: Option<MediaResolution>,
},
ThoughtSummary {
content: Option<ThoughtSummaryContent>,
},
ThoughtSignature { signature: Option<String> },
FunctionCall {
name: Option<String>,
arguments: Option<String>,
id: Option<String>,
},
FunctionResult {
name: Option<String>,
is_error: Option<bool>,
result: Option<Value>,
call_id: Option<String>,
},
CodeExecutionCall {
arguments: Option<CodeExecutionArguments>,
id: Option<String>,
},
CodeExecutionResult {
result: Option<String>,
is_error: Option<bool>,
signature: Option<String>,
call_id: Option<String>,
},
UrlContextCall {
arguments: Option<UrlContextArguments>,
id: Option<String>,
},
UrlContextResult {
signature: Option<String>,
result: Option<Vec<UrlContextResultData>>,
is_error: Option<bool>,
call_id: Option<String>,
},
GoogleSearchCall {
arguments: Option<GoogleSearchArguments>,
id: Option<String>,
},
GoogleSearchResult {
signature: Option<String>,
result: Option<Vec<GoogleSearchResultData>>,
is_error: Option<bool>,
call_id: Option<String>,
},
FileSearchCall { id: Option<String> },
FileSearchResult {
result: Option<Vec<FileSearchResultData>>,
},
McpServerToolCall {
name: Option<String>,
server_name: Option<String>,
arguments: Option<Value>,
id: Option<String>,
},
McpServerToolResult {
name: Option<String>,
server_name: Option<String>,
result: Option<Value>,
call_id: Option<String>,
},
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct InteractionsError {
pub code: Option<String>,
pub message: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct InteractionsGetParams {
pub stream: Option<bool>,
pub last_event_id: Option<String>,
pub api_version: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct InteractionsDeleteParams {
pub api_version: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct InteractionsCancelParams {
pub api_version: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InteractionsTool {
Function(Function),
GoogleSearch {},
CodeExecution {},
UrlContext {},
McpServer {
name: Option<String>,
url: Option<String>,
headers: Option<HashMap<String, String>>,
allowed_tools: Option<AllowedTools>,
},
FileSearch {
file_search_store_names: Option<Vec<String>>,
top_k: Option<u32>,
metadata_filter: Option<String>,
},
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct AllowedTools {
pub mode: Option<ToolChoiceType>,
pub tools: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ToolChoiceType {
Auto,
Any,
None,
Validated,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
pub struct GenerationConfig {
#[validate(range(min = 0.0, max = 2.0))]
pub temperature: Option<f32>,
#[validate(custom(function = "validate_top_p_value"))]
pub top_p: Option<f32>,
pub seed: Option<i64>,
#[validate(custom(function = "validate_stop_sequences"))]
pub stop_sequences: Option<Vec<String>>,
pub tool_choice: Option<ToolChoice>,
pub thinking_level: Option<ThinkingLevel>,
pub thinking_summaries: Option<ThinkingSummaries>,
#[validate(range(min = 1))]
pub max_output_tokens: Option<u32>,
pub speech_config: Option<Vec<SpeechConfig>>,
pub image_config: Option<ImageConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ThinkingLevel {
Minimal,
Low,
Medium,
High,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ThinkingSummaries {
Auto,
None,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ToolChoice {
Type(ToolChoiceType),
Config(ToolChoiceConfig),
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ToolChoiceConfig {
pub allowed_tools: Option<AllowedTools>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SpeechConfig {
pub voice: Option<String>,
pub language: Option<String>,
pub speaker: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ImageConfig {
pub aspect_ratio: Option<AspectRatio>,
pub image_size: Option<ImageSize>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum AspectRatio {
#[serde(rename = "1:1")]
Square,
#[serde(rename = "2:3")]
Portrait2x3,
#[serde(rename = "3:2")]
Landscape3x2,
#[serde(rename = "3:4")]
Portrait3x4,
#[serde(rename = "4:3")]
Landscape4x3,
#[serde(rename = "4:5")]
Portrait4x5,
#[serde(rename = "5:4")]
Landscape5x4,
#[serde(rename = "9:16")]
Portrait9x16,
#[serde(rename = "16:9")]
Landscape16x9,
#[serde(rename = "21:9")]
UltraWide,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum ImageSize {
#[serde(rename = "1K")]
OneK,
#[serde(rename = "2K")]
TwoK,
#[serde(rename = "4K")]
FourK,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentConfig {
Dynamic {},
#[serde(rename = "deep-research")]
DeepResearch {
#[serde(skip_serializing_if = "Option::is_none")]
thinking_summaries: Option<ThinkingSummaries>,
},
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum InteractionsInput {
Text(String),
Content(Content),
Contents(Vec<Content>),
Turns(Vec<Turn>),
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Turn {
pub role: Option<String>,
pub content: Option<TurnContent>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum TurnContent {
Contents(Vec<Content>),
Text(String),
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Content {
Text {
text: Option<String>,
annotations: Option<Vec<Annotation>>,
},
Image {
data: Option<String>,
uri: Option<String>,
mime_type: Option<ImageMimeType>,
resolution: Option<MediaResolution>,
},
Audio {
data: Option<String>,
uri: Option<String>,
mime_type: Option<AudioMimeType>,
},
Document {
data: Option<String>,
uri: Option<String>,
mime_type: Option<DocumentMimeType>,
},
Video {
data: Option<String>,
uri: Option<String>,
mime_type: Option<VideoMimeType>,
resolution: Option<MediaResolution>,
},
Thought {
signature: Option<String>,
summary: Option<Vec<ThoughtSummaryContent>>,
},
FunctionCall {
name: String,
arguments: Value,
id: String,
},
FunctionResult {
name: Option<String>,
is_error: Option<bool>,
result: Value,
call_id: String,
},
CodeExecutionCall {
arguments: Option<CodeExecutionArguments>,
id: Option<String>,
},
CodeExecutionResult {
result: Option<String>,
is_error: Option<bool>,
signature: Option<String>,
call_id: Option<String>,
},
UrlContextCall {
arguments: Option<UrlContextArguments>,
id: Option<String>,
},
UrlContextResult {
signature: Option<String>,
result: Option<Vec<UrlContextResultData>>,
is_error: Option<bool>,
call_id: Option<String>,
},
GoogleSearchCall {
arguments: Option<GoogleSearchArguments>,
id: Option<String>,
},
GoogleSearchResult {
signature: Option<String>,
result: Option<Vec<GoogleSearchResultData>>,
is_error: Option<bool>,
call_id: Option<String>,
},
FileSearchCall { id: Option<String> },
FileSearchResult {
result: Option<Vec<FileSearchResultData>>,
},
McpServerToolCall {
name: String,
server_name: String,
arguments: Value,
id: String,
},
McpServerToolResult {
name: Option<String>,
server_name: Option<String>,
result: Value,
call_id: String,
},
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ThoughtSummaryContent {
Text {
text: Option<String>,
annotations: Option<Vec<Annotation>>,
},
Image {
data: Option<String>,
uri: Option<String>,
mime_type: Option<ImageMimeType>,
resolution: Option<MediaResolution>,
},
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Annotation {
pub start_index: Option<u32>,
pub end_index: Option<u32>,
pub source: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UrlContextArguments {
pub urls: Option<Vec<String>>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UrlContextResultData {
pub url: Option<String>,
pub status: Option<UrlContextStatus>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum UrlContextStatus {
Success,
Error,
Paywall,
Unsafe,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GoogleSearchArguments {
pub queries: Option<Vec<String>>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GoogleSearchResultData {
pub url: Option<String>,
pub title: Option<String>,
pub rendered_content: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FileSearchResultData {
pub title: Option<String>,
pub text: Option<String>,
pub file_search_store: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CodeExecutionArguments {
pub language: Option<CodeExecutionLanguage>,
pub code: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CodeExecutionLanguage {
Python,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum MediaResolution {
Low,
Medium,
High,
UltraHigh,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum ImageMimeType {
#[serde(rename = "image/png")]
Png,
#[serde(rename = "image/jpeg")]
Jpeg,
#[serde(rename = "image/webp")]
Webp,
#[serde(rename = "image/heic")]
Heic,
#[serde(rename = "image/heif")]
Heif,
}
impl ImageMimeType {
pub fn as_str(&self) -> &'static str {
match self {
ImageMimeType::Png => "image/png",
ImageMimeType::Jpeg => "image/jpeg",
ImageMimeType::Webp => "image/webp",
ImageMimeType::Heic => "image/heic",
ImageMimeType::Heif => "image/heif",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum AudioMimeType {
#[serde(rename = "audio/wav")]
Wav,
#[serde(rename = "audio/mp3")]
Mp3,
#[serde(rename = "audio/aiff")]
Aiff,
#[serde(rename = "audio/aac")]
Aac,
#[serde(rename = "audio/ogg")]
Ogg,
#[serde(rename = "audio/flac")]
Flac,
}
impl AudioMimeType {
pub fn as_str(&self) -> &'static str {
match self {
AudioMimeType::Wav => "audio/wav",
AudioMimeType::Mp3 => "audio/mp3",
AudioMimeType::Aiff => "audio/aiff",
AudioMimeType::Aac => "audio/aac",
AudioMimeType::Ogg => "audio/ogg",
AudioMimeType::Flac => "audio/flac",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum DocumentMimeType {
#[serde(rename = "application/pdf")]
Pdf,
}
impl DocumentMimeType {
pub fn as_str(&self) -> &'static str {
match self {
DocumentMimeType::Pdf => "application/pdf",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum VideoMimeType {
#[serde(rename = "video/mp4")]
Mp4,
#[serde(rename = "video/mpeg")]
Mpeg,
#[serde(rename = "video/mov")]
Mov,
#[serde(rename = "video/avi")]
Avi,
#[serde(rename = "video/x-flv")]
Flv,
#[serde(rename = "video/mpg")]
Mpg,
#[serde(rename = "video/webm")]
Webm,
#[serde(rename = "video/wmv")]
Wmv,
#[serde(rename = "video/3gpp")]
ThreeGpp,
}
impl VideoMimeType {
pub fn as_str(&self) -> &'static str {
match self {
VideoMimeType::Mp4 => "video/mp4",
VideoMimeType::Mpeg => "video/mpeg",
VideoMimeType::Mov => "video/mov",
VideoMimeType::Avi => "video/avi",
VideoMimeType::Flv => "video/x-flv",
VideoMimeType::Mpg => "video/mpg",
VideoMimeType::Webm => "video/webm",
VideoMimeType::Wmv => "video/wmv",
VideoMimeType::ThreeGpp => "video/3gpp",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InteractionsStatus {
#[default]
InProgress,
RequiresAction,
Completed,
Failed,
Cancelled,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ModalityTokens {
pub modality: Option<ResponseModality>,
pub tokens: Option<u32>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct InteractionsUsage {
pub total_input_tokens: Option<u32>,
pub input_tokens_by_modality: Option<Vec<ModalityTokens>>,
pub total_cached_tokens: Option<u32>,
pub cached_tokens_by_modality: Option<Vec<ModalityTokens>>,
pub total_output_tokens: Option<u32>,
pub output_tokens_by_modality: Option<Vec<ModalityTokens>>,
pub total_tool_use_tokens: Option<u32>,
pub tool_use_tokens_by_modality: Option<Vec<ModalityTokens>>,
pub total_thought_tokens: Option<u32>,
pub total_tokens: Option<u32>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ResponseModality {
Text,
Image,
Audio,
}
fn is_option_blank(v: Option<&String>) -> bool {
v.map(|s| s.trim().is_empty()).unwrap_or(true)
}
fn validate_interactions_request(req: &InteractionsRequest) -> Result<(), ValidationError> {
if is_option_blank(req.model.as_ref()) && is_option_blank(req.agent.as_ref()) {
return Err(ValidationError::new("model_or_agent_required"));
}
if !is_option_blank(req.model.as_ref()) && !is_option_blank(req.agent.as_ref()) {
let mut e = ValidationError::new("model_and_agent_mutually_exclusive");
e.message = Some("Cannot set both model and agent. Provide exactly one.".into());
return Err(e);
}
if req.response_format.is_some() && is_option_blank(req.response_mime_type.as_ref()) {
return Err(ValidationError::new("response_mime_type_required"));
}
if !is_option_blank(req.agent.as_ref()) && !req.background {
let mut e = ValidationError::new("agent_requires_background");
e.message = Some("Agent interactions require background mode to be enabled.".into());
return Err(e);
}
if !is_option_blank(req.model.as_ref()) && req.background {
let mut e = ValidationError::new("background_requires_agent");
e.message = Some("Background mode is only supported for agent interactions.".into());
return Err(e);
}
if req.background && req.stream {
let mut e = ValidationError::new("background_conflicts_with_stream");
e.message = Some("Cannot set both background and stream to true.".into());
return Err(e);
}
Ok(())
}
fn validate_tools(tools: &[InteractionsTool]) -> Result<(), ValidationError> {
if tools
.iter()
.any(|t| matches!(t, InteractionsTool::FileSearch { .. }))
{
return Err(ValidationError::new("file_search_tool_not_supported"));
}
Ok(())
}
fn validate_input(input: &InteractionsInput) -> Result<(), ValidationError> {
let empty_msg = match input {
InteractionsInput::Text(s) if s.trim().is_empty() => Some("Input text cannot be empty"),
InteractionsInput::Content(content) if is_content_empty(content) => {
Some("Input content cannot be empty")
}
InteractionsInput::Contents(contents) if contents.is_empty() => {
Some("Input content array cannot be empty")
}
InteractionsInput::Contents(contents) if contents.iter().any(is_content_empty) => {
Some("Input content array contains empty content items")
}
InteractionsInput::Turns(turns) if turns.is_empty() => {
Some("Input turns array cannot be empty")
}
InteractionsInput::Turns(turns) if turns.iter().any(is_turn_empty) => {
Some("Input turns array contains empty turn items")
}
_ => None,
};
if let Some(msg) = empty_msg {
let mut e = ValidationError::new("input_cannot_be_empty");
e.message = Some(msg.into());
return Err(e);
}
fn has_file_search_content(content: &Content) -> bool {
matches!(
content,
Content::FileSearchCall { .. } | Content::FileSearchResult { .. }
)
}
fn check_turn(turn: &Turn) -> bool {
if let Some(content) = &turn.content {
match content {
TurnContent::Contents(contents) => contents.iter().any(has_file_search_content),
TurnContent::Text(_) => false,
}
} else {
false
}
}
let has_file_search = match input {
InteractionsInput::Text(_) => false,
InteractionsInput::Content(content) => has_file_search_content(content),
InteractionsInput::Contents(contents) => contents.iter().any(has_file_search_content),
InteractionsInput::Turns(turns) => turns.iter().any(check_turn),
};
if has_file_search {
return Err(ValidationError::new("file_search_content_not_supported"));
}
Ok(())
}
fn is_content_empty(content: &Content) -> bool {
match content {
Content::Text { text, .. } => is_option_blank(text.as_ref()),
Content::Image { data, uri, .. }
| Content::Audio { data, uri, .. }
| Content::Document { data, uri, .. }
| Content::Video { data, uri, .. } => {
is_option_blank(data.as_ref()) && is_option_blank(uri.as_ref())
}
Content::CodeExecutionCall { id, .. }
| Content::UrlContextCall { id, .. }
| Content::GoogleSearchCall { id, .. } => is_option_blank(id.as_ref()),
Content::CodeExecutionResult { call_id, .. }
| Content::UrlContextResult { call_id, .. }
| Content::GoogleSearchResult { call_id, .. } => is_option_blank(call_id.as_ref()),
_ => false,
}
}
fn is_turn_empty(turn: &Turn) -> bool {
match &turn.content {
None => true,
Some(TurnContent::Text(s)) => s.trim().is_empty(),
Some(TurnContent::Contents(contents)) => {
contents.is_empty() || contents.iter().any(is_content_empty)
}
}
}
fn validate_stop_sequences(seqs: &[String]) -> Result<(), ValidationError> {
if seqs.len() > 5 {
let mut e = ValidationError::new("too_many_stop_sequences");
e.message = Some("Maximum 5 stop sequences allowed".into());
return Err(e);
}
if seqs.iter().any(|s| s.trim().is_empty()) {
let mut e = ValidationError::new("stop_sequences_cannot_be_empty");
e.message = Some("Stop sequences cannot contain empty strings".into());
return Err(e);
}
Ok(())
}