Skip to main content

openai_protocol/
interactions.rs

1// Gemini Interactions API types
2// https://ai.google.dev/gemini-api/docs/interactions
3
4use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use serde_with::skip_serializing_none;
9use validator::{Validate, ValidationError};
10
11use super::common::{default_model, default_true, Function, GenerationRequest};
12
13// ============================================================================
14// Request Type
15// ============================================================================
16
17#[skip_serializing_none]
18#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
19#[validate(schema(function = "validate_interactions_request"))]
20pub struct InteractionsRequest {
21    /// Model identifier (e.g., "gemini-2.5-flash")
22    /// Required if agent is not provided
23    pub model: Option<String>,
24
25    /// Agent name (e.g., "deep-research-pro-preview-12-2025")
26    /// Required if model is not provided
27    pub agent: Option<String>,
28
29    /// Input content - can be string or array of Content objects
30    #[validate(custom(function = "validate_input"))]
31    pub input: InteractionsInput,
32
33    /// System instruction for the model
34    pub system_instruction: Option<String>,
35
36    /// Available tools
37    #[validate(custom(function = "validate_tools"))]
38    pub tools: Option<Vec<InteractionsTool>>,
39
40    /// Response format for structured outputs
41    pub response_format: Option<Value>,
42
43    /// MIME type for the response (required if response_format is set)
44    pub response_mime_type: Option<String>,
45
46    /// Whether to stream the response
47    #[serde(default)]
48    pub stream: bool,
49
50    /// Whether to store the interaction (default: true)
51    #[serde(default = "default_true")]
52    pub store: bool,
53
54    /// Run request in background (agents only)
55    #[serde(default)]
56    pub background: bool,
57
58    /// Generation configuration
59    pub generation_config: Option<GenerationConfig>,
60
61    /// Agent configuration (only applicable when agent is specified)
62    pub agent_config: Option<AgentConfig>,
63
64    /// Response modalities (text, image, audio)
65    pub response_modalities: Option<Vec<ResponseModality>>,
66
67    /// Link to prior interaction for stateful conversations
68    pub previous_interaction_id: Option<String>,
69}
70
71impl Default for InteractionsRequest {
72    fn default() -> Self {
73        Self {
74            model: Some(default_model()),
75            agent: None,
76            agent_config: None,
77            input: InteractionsInput::Text(String::new()),
78            system_instruction: None,
79            previous_interaction_id: None,
80            tools: None,
81            generation_config: None,
82            response_format: None,
83            response_mime_type: None,
84            response_modalities: None,
85            stream: false,
86            background: false,
87            store: true,
88        }
89    }
90}
91
92impl GenerationRequest for InteractionsRequest {
93    fn is_stream(&self) -> bool {
94        self.stream
95    }
96
97    fn get_model(&self) -> Option<&str> {
98        self.model.as_deref()
99    }
100
101    fn extract_text_for_routing(&self) -> String {
102        fn extract_from_content(content: &Content) -> Option<String> {
103            match content {
104                Content::Text { text, .. } => text.clone(),
105                _ => None,
106            }
107        }
108
109        fn extract_from_turn(turn: &Turn) -> String {
110            match &turn.content {
111                Some(TurnContent::Text(text)) => text.clone(),
112                Some(TurnContent::Contents(contents)) => contents
113                    .iter()
114                    .filter_map(extract_from_content)
115                    .collect::<Vec<String>>()
116                    .join(" "),
117                None => String::new(),
118            }
119        }
120
121        match &self.input {
122            InteractionsInput::Text(text) => text.clone(),
123            InteractionsInput::Content(content) => {
124                extract_from_content(content).unwrap_or_default()
125            }
126            InteractionsInput::Contents(contents) => contents
127                .iter()
128                .filter_map(extract_from_content)
129                .collect::<Vec<String>>()
130                .join(" "),
131            InteractionsInput::Turns(turns) => turns
132                .iter()
133                .map(extract_from_turn)
134                .collect::<Vec<String>>()
135                .join(" "),
136        }
137    }
138}
139
140// ============================================================================
141// Response Type
142// ============================================================================
143
144#[skip_serializing_none]
145#[derive(Debug, Clone, Default, Deserialize, Serialize)]
146pub struct Interaction {
147    /// Object type, always "interaction"
148    pub object: Option<String>,
149
150    /// Model used
151    pub model: Option<String>,
152
153    /// Agent used
154    pub agent: Option<String>,
155
156    /// Interaction ID
157    pub id: String,
158
159    /// Interaction status
160    pub status: InteractionsStatus,
161
162    /// Creation timestamp (ISO 8601)
163    pub created: Option<String>,
164
165    /// Last update timestamp (ISO 8601)
166    pub updated: Option<String>,
167
168    /// Role of the interaction
169    pub role: Option<String>,
170
171    /// Output content
172    pub outputs: Option<Vec<Content>>,
173
174    /// Usage information
175    pub usage: Option<InteractionsUsage>,
176
177    /// Previous interaction ID for conversation threading
178    pub previous_interaction_id: Option<String>,
179}
180
181impl Interaction {
182    /// Check if the interaction is complete
183    pub fn is_complete(&self) -> bool {
184        matches!(self.status, InteractionsStatus::Completed)
185    }
186
187    /// Check if the interaction is in progress
188    pub fn is_in_progress(&self) -> bool {
189        matches!(self.status, InteractionsStatus::InProgress)
190    }
191
192    /// Check if the interaction failed
193    pub fn is_failed(&self) -> bool {
194        matches!(self.status, InteractionsStatus::Failed)
195    }
196
197    /// Check if the interaction requires action (tool execution)
198    pub fn requires_action(&self) -> bool {
199        matches!(self.status, InteractionsStatus::RequiresAction)
200    }
201}
202
203// ============================================================================
204// Streaming Event Types (SSE)
205// ============================================================================
206
207/// Server-Sent Event for Interactions API streaming
208/// See: https://ai.google.dev/api/interactions-api#streaming
209#[skip_serializing_none]
210#[derive(Debug, Clone, Deserialize, Serialize)]
211#[serde(tag = "event_type")]
212pub enum InteractionStreamEvent {
213    /// Emitted when an interaction begins processing
214    #[serde(rename = "interaction.start")]
215    InteractionStart {
216        /// The interaction object
217        interaction: Option<Interaction>,
218        /// Event ID for resuming streams
219        event_id: Option<String>,
220    },
221
222    /// Emitted when an interaction completes
223    #[serde(rename = "interaction.complete")]
224    InteractionComplete {
225        /// The interaction object
226        interaction: Option<Interaction>,
227        /// Event ID for resuming streams
228        event_id: Option<String>,
229    },
230
231    /// Emitted when interaction status changes
232    #[serde(rename = "interaction.status_update")]
233    InteractionStatusUpdate {
234        /// The interaction ID
235        interaction_id: Option<String>,
236        /// The new status
237        status: Option<InteractionsStatus>,
238        /// Event ID for resuming streams
239        event_id: Option<String>,
240    },
241
242    /// Signals the beginning of a new content block
243    #[serde(rename = "content.start")]
244    ContentStart {
245        /// Content block index in outputs array
246        index: Option<u32>,
247        /// The content object
248        content: Option<Content>,
249        /// Event ID for resuming streams
250        event_id: Option<String>,
251    },
252
253    /// Streams incremental content updates
254    #[serde(rename = "content.delta")]
255    ContentDelta {
256        /// Content block index in outputs array
257        index: Option<u32>,
258        /// Event ID for resuming streams
259        event_id: Option<String>,
260        /// The delta content
261        delta: Option<Delta>,
262    },
263
264    /// Marks the end of a content block
265    #[serde(rename = "content.stop")]
266    ContentStop {
267        /// Content block index in outputs array
268        index: Option<u32>,
269        /// Event ID for resuming streams
270        event_id: Option<String>,
271    },
272
273    /// Error event
274    #[serde(rename = "error")]
275    Error {
276        /// Error information
277        error: Option<InteractionsError>,
278        /// Event ID for resuming streams
279        event_id: Option<String>,
280    },
281}
282
283/// Delta content for streaming updates
284/// See: https://ai.google.dev/api/interactions-api#ContentDelta
285#[skip_serializing_none]
286#[derive(Debug, Clone, Deserialize, Serialize)]
287#[serde(tag = "type", rename_all = "snake_case")]
288pub enum Delta {
289    /// Text delta
290    Text {
291        text: Option<String>,
292        annotations: Option<Vec<Annotation>>,
293    },
294    /// Image delta
295    Image {
296        data: Option<String>,
297        uri: Option<String>,
298        mime_type: Option<ImageMimeType>,
299        resolution: Option<MediaResolution>,
300    },
301    /// Audio delta
302    Audio {
303        data: Option<String>,
304        uri: Option<String>,
305        mime_type: Option<AudioMimeType>,
306    },
307    /// Document delta
308    Document {
309        data: Option<String>,
310        uri: Option<String>,
311        mime_type: Option<DocumentMimeType>,
312    },
313    /// Video delta
314    Video {
315        data: Option<String>,
316        uri: Option<String>,
317        mime_type: Option<VideoMimeType>,
318        resolution: Option<MediaResolution>,
319    },
320    /// Thought summary delta
321    ThoughtSummary {
322        content: Option<ThoughtSummaryContent>,
323    },
324    /// Thought signature delta
325    ThoughtSignature { signature: Option<String> },
326    /// Function call delta
327    FunctionCall {
328        name: Option<String>,
329        arguments: Option<String>,
330        id: Option<String>,
331    },
332    /// Function result delta
333    FunctionResult {
334        name: Option<String>,
335        is_error: Option<bool>,
336        result: Option<Value>,
337        call_id: Option<String>,
338    },
339    /// Code execution call delta
340    CodeExecutionCall {
341        arguments: Option<CodeExecutionArguments>,
342        id: Option<String>,
343    },
344    /// Code execution result delta
345    CodeExecutionResult {
346        result: Option<String>,
347        is_error: Option<bool>,
348        signature: Option<String>,
349        call_id: Option<String>,
350    },
351    /// URL context call delta
352    UrlContextCall {
353        arguments: Option<UrlContextArguments>,
354        id: Option<String>,
355    },
356    /// URL context result delta
357    UrlContextResult {
358        signature: Option<String>,
359        result: Option<Vec<UrlContextResultData>>,
360        is_error: Option<bool>,
361        call_id: Option<String>,
362    },
363    /// Google search call delta
364    GoogleSearchCall {
365        arguments: Option<GoogleSearchArguments>,
366        id: Option<String>,
367    },
368    /// Google search result delta
369    GoogleSearchResult {
370        signature: Option<String>,
371        result: Option<Vec<GoogleSearchResultData>>,
372        is_error: Option<bool>,
373        call_id: Option<String>,
374    },
375    /// File search call delta
376    FileSearchCall { id: Option<String> },
377    /// File search result delta
378    FileSearchResult {
379        result: Option<Vec<FileSearchResultData>>,
380    },
381    /// MCP server tool call delta
382    McpServerToolCall {
383        name: Option<String>,
384        server_name: Option<String>,
385        arguments: Option<Value>,
386        id: Option<String>,
387    },
388    /// MCP server tool result delta
389    McpServerToolResult {
390        name: Option<String>,
391        server_name: Option<String>,
392        result: Option<Value>,
393        call_id: Option<String>,
394    },
395}
396
397/// Error information in streaming events
398#[skip_serializing_none]
399#[derive(Debug, Clone, Deserialize, Serialize)]
400pub struct InteractionsError {
401    /// Error code
402    pub code: Option<String>,
403    /// Error message
404    pub message: Option<String>,
405}
406
407// ============================================================================
408// Query Parameters
409// ============================================================================
410
411/// Query parameters for GET /interactions/{id}
412#[skip_serializing_none]
413#[derive(Debug, Clone, Default, Deserialize, Serialize)]
414pub struct InteractionsGetParams {
415    /// Whether to stream the response
416    pub stream: Option<bool>,
417    /// Last event ID for resuming a stream
418    pub last_event_id: Option<String>,
419    /// API version
420    pub api_version: Option<String>,
421}
422
423/// Query parameters for DELETE /interactions/{id}
424#[skip_serializing_none]
425#[derive(Debug, Clone, Default, Deserialize, Serialize)]
426pub struct InteractionsDeleteParams {
427    /// API version
428    pub api_version: Option<String>,
429}
430
431/// Query parameters for POST /interactions/{id}/cancel
432#[skip_serializing_none]
433#[derive(Debug, Clone, Default, Deserialize, Serialize)]
434pub struct InteractionsCancelParams {
435    /// API version
436    pub api_version: Option<String>,
437}
438
439// ============================================================================
440// Interaction Tools
441// ============================================================================
442
443/// Interaction tool types
444/// See: https://ai.google.dev/api/interactions-api#Resource:Tool
445#[skip_serializing_none]
446#[derive(Debug, Clone, Deserialize, Serialize)]
447#[serde(tag = "type", rename_all = "snake_case")]
448pub enum InteractionsTool {
449    /// Function tool with function declaration
450    Function(Function),
451    /// Google Search built-in tool
452    GoogleSearch {},
453    /// Code Execution built-in tool
454    CodeExecution {},
455    /// URL Context built-in tool
456    UrlContext {},
457    /// MCP Server tool
458    McpServer {
459        name: Option<String>,
460        url: Option<String>,
461        headers: Option<HashMap<String, String>>,
462        allowed_tools: Option<AllowedTools>,
463    },
464    /// File Search built-in tool
465    FileSearch {
466        /// Names of file search stores to search
467        file_search_store_names: Option<Vec<String>>,
468        /// Maximum number of results to return
469        top_k: Option<u32>,
470        /// Metadata filter for search
471        metadata_filter: Option<String>,
472    },
473}
474
475/// Allowed tools configuration for MCP server
476#[skip_serializing_none]
477#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
478pub struct AllowedTools {
479    /// Tool choice mode: auto, any, none, or validated
480    pub mode: Option<ToolChoiceType>,
481    /// List of allowed tool names
482    pub tools: Option<Vec<String>>,
483}
484
485#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
486#[serde(rename_all = "snake_case")]
487pub enum ToolChoiceType {
488    Auto,
489    Any,
490    None,
491    Validated,
492}
493
494// ============================================================================
495// Generation Config (Gemini-specific)
496// ============================================================================
497
498#[skip_serializing_none]
499#[derive(Debug, Clone, Deserialize, Serialize)]
500pub struct GenerationConfig {
501    pub temperature: Option<f32>,
502
503    pub top_p: Option<f32>,
504
505    pub seed: Option<i64>,
506
507    pub stop_sequences: Option<Vec<String>>,
508
509    pub tool_choice: Option<ToolChoice>,
510
511    pub thinking_level: Option<ThinkingLevel>,
512
513    pub thinking_summaries: Option<ThinkingSummaries>,
514
515    pub max_output_tokens: Option<u32>,
516
517    pub speech_config: Option<Vec<SpeechConfig>>,
518
519    pub image_config: Option<ImageConfig>,
520}
521
522#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
523#[serde(rename_all = "snake_case")]
524pub enum ThinkingLevel {
525    Minimal,
526    Low,
527    Medium,
528    High,
529}
530
531#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
532#[serde(rename_all = "snake_case")]
533pub enum ThinkingSummaries {
534    Auto,
535    None,
536}
537
538/// Tool choice can be a simple mode or a detailed config
539#[derive(Debug, Clone, Deserialize, Serialize)]
540#[serde(untagged)]
541pub enum ToolChoice {
542    Type(ToolChoiceType),
543    Config(ToolChoiceConfig),
544}
545
546#[skip_serializing_none]
547#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
548pub struct ToolChoiceConfig {
549    pub allowed_tools: Option<AllowedTools>,
550}
551
552#[skip_serializing_none]
553#[derive(Debug, Clone, Deserialize, Serialize)]
554pub struct SpeechConfig {
555    pub voice: Option<String>,
556    pub language: Option<String>,
557    pub speaker: Option<String>,
558}
559
560#[skip_serializing_none]
561#[derive(Debug, Clone, Deserialize, Serialize)]
562pub struct ImageConfig {
563    pub aspect_ratio: Option<AspectRatio>,
564    pub image_size: Option<ImageSize>,
565}
566
567#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
568pub enum AspectRatio {
569    #[serde(rename = "1:1")]
570    Square,
571    #[serde(rename = "2:3")]
572    Portrait2x3,
573    #[serde(rename = "3:2")]
574    Landscape3x2,
575    #[serde(rename = "3:4")]
576    Portrait3x4,
577    #[serde(rename = "4:3")]
578    Landscape4x3,
579    #[serde(rename = "4:5")]
580    Portrait4x5,
581    #[serde(rename = "5:4")]
582    Landscape5x4,
583    #[serde(rename = "9:16")]
584    Portrait9x16,
585    #[serde(rename = "16:9")]
586    Landscape16x9,
587    #[serde(rename = "21:9")]
588    UltraWide,
589}
590
591#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
592pub enum ImageSize {
593    #[serde(rename = "1K")]
594    OneK,
595    #[serde(rename = "2K")]
596    TwoK,
597    #[serde(rename = "4K")]
598    FourK,
599}
600
601/// Agent configuration
602/// See: https://ai.google.dev/api/interactions-api#CreateInteraction-deep_research
603#[derive(Debug, Clone, Deserialize, Serialize)]
604#[serde(tag = "type", rename_all = "snake_case")]
605pub enum AgentConfig {
606    /// Dynamic agent configuration
607    Dynamic {},
608    /// Deep Research agent configuration
609    #[serde(rename = "deep-research")]
610    DeepResearch {
611        /// Whether to include thought summaries ("auto" or "none")
612        #[serde(skip_serializing_if = "Option::is_none")]
613        thinking_summaries: Option<ThinkingSummaries>,
614    },
615}
616
617// ============================================================================
618// Input/Output Types
619// ============================================================================
620
621/// Input can be Content, array of Content, array of Turn, or string
622/// See: https://ai.google.dev/api/interactions-api#request-body
623#[derive(Debug, Clone, Deserialize, Serialize)]
624#[serde(untagged)]
625pub enum InteractionsInput {
626    /// Simple text input
627    Text(String),
628    /// Single content block
629    Content(Content),
630    /// Array of content blocks
631    Contents(Vec<Content>),
632    /// Array of turns (conversation history)
633    Turns(Vec<Turn>),
634}
635
636/// A turn in a conversation with role and content
637/// See: https://ai.google.dev/api/interactions-api#Resource:Turn
638#[skip_serializing_none]
639#[derive(Debug, Clone, Deserialize, Serialize)]
640pub struct Turn {
641    /// Role: "user" or "model"
642    pub role: Option<String>,
643    /// Content can be array of Content or string
644    pub content: Option<TurnContent>,
645}
646
647/// Turn content can be array of Content or a simple string
648#[derive(Debug, Clone, Deserialize, Serialize)]
649#[serde(untagged)]
650pub enum TurnContent {
651    Contents(Vec<Content>),
652    Text(String),
653}
654
655/// Content is a polymorphic type representing different content types
656/// See: https://ai.google.dev/api/interactions-api#Resource:Content
657#[skip_serializing_none]
658#[derive(Debug, Clone, Deserialize, Serialize)]
659#[serde(tag = "type", rename_all = "snake_case")]
660pub enum Content {
661    /// Text content
662    Text {
663        text: Option<String>,
664        annotations: Option<Vec<Annotation>>,
665    },
666
667    /// Image content
668    Image {
669        data: Option<String>,
670        uri: Option<String>,
671        mime_type: Option<ImageMimeType>,
672        resolution: Option<MediaResolution>,
673    },
674
675    /// Audio content
676    Audio {
677        data: Option<String>,
678        uri: Option<String>,
679        mime_type: Option<AudioMimeType>,
680    },
681
682    /// Document content (PDF)
683    Document {
684        data: Option<String>,
685        uri: Option<String>,
686        mime_type: Option<DocumentMimeType>,
687    },
688
689    /// Video content
690    Video {
691        data: Option<String>,
692        uri: Option<String>,
693        mime_type: Option<VideoMimeType>,
694        resolution: Option<MediaResolution>,
695    },
696
697    /// Thought content
698    Thought {
699        signature: Option<String>,
700        summary: Option<Vec<ThoughtSummaryContent>>,
701    },
702
703    /// Function call content
704    FunctionCall {
705        name: String,
706        arguments: Value,
707        id: String,
708    },
709
710    /// Function result content
711    FunctionResult {
712        name: Option<String>,
713        is_error: Option<bool>,
714        result: Value,
715        call_id: String,
716    },
717
718    /// Code execution call content
719    CodeExecutionCall {
720        arguments: Option<CodeExecutionArguments>,
721        id: Option<String>,
722    },
723
724    /// Code execution result content
725    CodeExecutionResult {
726        result: Option<String>,
727        is_error: Option<bool>,
728        signature: Option<String>,
729        call_id: Option<String>,
730    },
731
732    /// URL context call content
733    UrlContextCall {
734        arguments: Option<UrlContextArguments>,
735        id: Option<String>,
736    },
737
738    /// URL context result content
739    UrlContextResult {
740        signature: Option<String>,
741        result: Option<Vec<UrlContextResultData>>,
742        is_error: Option<bool>,
743        call_id: Option<String>,
744    },
745
746    /// Google search call content
747    GoogleSearchCall {
748        arguments: Option<GoogleSearchArguments>,
749        id: Option<String>,
750    },
751
752    /// Google search result content
753    GoogleSearchResult {
754        signature: Option<String>,
755        result: Option<Vec<GoogleSearchResultData>>,
756        is_error: Option<bool>,
757        call_id: Option<String>,
758    },
759
760    /// File search call content
761    FileSearchCall { id: Option<String> },
762
763    /// File search result content
764    FileSearchResult {
765        result: Option<Vec<FileSearchResultData>>,
766    },
767
768    /// MCP server tool call content
769    McpServerToolCall {
770        name: String,
771        server_name: String,
772        arguments: Value,
773        id: String,
774    },
775
776    /// MCP server tool result content
777    McpServerToolResult {
778        name: Option<String>,
779        server_name: Option<String>,
780        result: Value,
781        call_id: String,
782    },
783}
784
785/// Content types allowed in thought summary (text or image only)
786#[skip_serializing_none]
787#[derive(Debug, Clone, Deserialize, Serialize)]
788#[serde(tag = "type", rename_all = "snake_case")]
789pub enum ThoughtSummaryContent {
790    /// Text content in thought summary
791    Text {
792        text: Option<String>,
793        annotations: Option<Vec<Annotation>>,
794    },
795    /// Image content in thought summary
796    Image {
797        data: Option<String>,
798        uri: Option<String>,
799        mime_type: Option<ImageMimeType>,
800        resolution: Option<MediaResolution>,
801    },
802}
803
804/// Annotation for text content (citations)
805#[skip_serializing_none]
806#[derive(Debug, Clone, Deserialize, Serialize)]
807pub struct Annotation {
808    /// Start of the attributed segment, measured in bytes
809    pub start_index: Option<u32>,
810    /// End of the attributed segment, exclusive
811    pub end_index: Option<u32>,
812    /// Source attributed for a portion of the text (URL, title, or other identifier)
813    pub source: Option<String>,
814}
815
816/// Arguments for URL context call
817#[skip_serializing_none]
818#[derive(Debug, Clone, Deserialize, Serialize)]
819pub struct UrlContextArguments {
820    /// The URLs to fetch
821    pub urls: Option<Vec<String>>,
822}
823
824/// Result data for URL context result
825#[skip_serializing_none]
826#[derive(Debug, Clone, Deserialize, Serialize)]
827pub struct UrlContextResultData {
828    /// The URL that was fetched
829    pub url: Option<String>,
830    /// The status of the URL retrieval
831    pub status: Option<UrlContextStatus>,
832}
833
834/// Status of URL context retrieval
835#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
836#[serde(rename_all = "snake_case")]
837pub enum UrlContextStatus {
838    Success,
839    Error,
840    Paywall,
841    Unsafe,
842}
843
844/// Arguments for Google search call
845#[skip_serializing_none]
846#[derive(Debug, Clone, Deserialize, Serialize)]
847pub struct GoogleSearchArguments {
848    /// Web search queries
849    pub queries: Option<Vec<String>>,
850}
851
852/// Result data for Google search result
853#[skip_serializing_none]
854#[derive(Debug, Clone, Deserialize, Serialize)]
855pub struct GoogleSearchResultData {
856    /// URI reference of the search result
857    pub url: Option<String>,
858    /// Title of the search result
859    pub title: Option<String>,
860    /// Web content snippet
861    pub rendered_content: Option<String>,
862}
863
864/// Result data for file search result
865#[skip_serializing_none]
866#[derive(Debug, Clone, Deserialize, Serialize)]
867pub struct FileSearchResultData {
868    /// Search result title
869    pub title: Option<String>,
870    /// Search result text
871    pub text: Option<String>,
872    /// Name of the file search store
873    pub file_search_store: Option<String>,
874}
875
876/// Arguments for code execution call
877#[skip_serializing_none]
878#[derive(Debug, Clone, Deserialize, Serialize)]
879pub struct CodeExecutionArguments {
880    /// Programming language (currently only Python is supported)
881    pub language: Option<CodeExecutionLanguage>,
882    /// The code to be executed
883    pub code: Option<String>,
884}
885
886/// Supported languages for code execution
887#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
888#[serde(rename_all = "snake_case")]
889pub enum CodeExecutionLanguage {
890    Python,
891}
892
893/// Image/video resolution options
894#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
895#[serde(rename_all = "snake_case")]
896pub enum MediaResolution {
897    Low,
898    Medium,
899    High,
900    UltraHigh,
901}
902
903/// Supported image MIME types
904#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
905pub enum ImageMimeType {
906    #[serde(rename = "image/png")]
907    Png,
908    #[serde(rename = "image/jpeg")]
909    Jpeg,
910    #[serde(rename = "image/webp")]
911    Webp,
912    #[serde(rename = "image/heic")]
913    Heic,
914    #[serde(rename = "image/heif")]
915    Heif,
916}
917
918impl ImageMimeType {
919    pub fn as_str(&self) -> &'static str {
920        match self {
921            ImageMimeType::Png => "image/png",
922            ImageMimeType::Jpeg => "image/jpeg",
923            ImageMimeType::Webp => "image/webp",
924            ImageMimeType::Heic => "image/heic",
925            ImageMimeType::Heif => "image/heif",
926        }
927    }
928}
929
930/// Supported audio MIME types
931#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
932pub enum AudioMimeType {
933    #[serde(rename = "audio/wav")]
934    Wav,
935    #[serde(rename = "audio/mp3")]
936    Mp3,
937    #[serde(rename = "audio/aiff")]
938    Aiff,
939    #[serde(rename = "audio/aac")]
940    Aac,
941    #[serde(rename = "audio/ogg")]
942    Ogg,
943    #[serde(rename = "audio/flac")]
944    Flac,
945}
946
947impl AudioMimeType {
948    pub fn as_str(&self) -> &'static str {
949        match self {
950            AudioMimeType::Wav => "audio/wav",
951            AudioMimeType::Mp3 => "audio/mp3",
952            AudioMimeType::Aiff => "audio/aiff",
953            AudioMimeType::Aac => "audio/aac",
954            AudioMimeType::Ogg => "audio/ogg",
955            AudioMimeType::Flac => "audio/flac",
956        }
957    }
958}
959
960/// Supported document MIME types
961#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
962pub enum DocumentMimeType {
963    #[serde(rename = "application/pdf")]
964    Pdf,
965}
966
967impl DocumentMimeType {
968    pub fn as_str(&self) -> &'static str {
969        match self {
970            DocumentMimeType::Pdf => "application/pdf",
971        }
972    }
973}
974
975/// Supported video MIME types
976#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
977pub enum VideoMimeType {
978    #[serde(rename = "video/mp4")]
979    Mp4,
980    #[serde(rename = "video/mpeg")]
981    Mpeg,
982    #[serde(rename = "video/mov")]
983    Mov,
984    #[serde(rename = "video/avi")]
985    Avi,
986    #[serde(rename = "video/x-flv")]
987    Flv,
988    #[serde(rename = "video/mpg")]
989    Mpg,
990    #[serde(rename = "video/webm")]
991    Webm,
992    #[serde(rename = "video/wmv")]
993    Wmv,
994    #[serde(rename = "video/3gpp")]
995    ThreeGpp,
996}
997
998impl VideoMimeType {
999    pub fn as_str(&self) -> &'static str {
1000        match self {
1001            VideoMimeType::Mp4 => "video/mp4",
1002            VideoMimeType::Mpeg => "video/mpeg",
1003            VideoMimeType::Mov => "video/mov",
1004            VideoMimeType::Avi => "video/avi",
1005            VideoMimeType::Flv => "video/x-flv",
1006            VideoMimeType::Mpg => "video/mpg",
1007            VideoMimeType::Webm => "video/webm",
1008            VideoMimeType::Wmv => "video/wmv",
1009            VideoMimeType::ThreeGpp => "video/3gpp",
1010        }
1011    }
1012}
1013
1014// ============================================================================
1015// Status Types
1016// ============================================================================
1017
1018#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
1019#[serde(rename_all = "snake_case")]
1020pub enum InteractionsStatus {
1021    #[default]
1022    InProgress,
1023    RequiresAction,
1024    Completed,
1025    Failed,
1026    Cancelled,
1027}
1028
1029// ============================================================================
1030// Usage Types
1031// ============================================================================
1032
1033/// Token count by modality
1034#[skip_serializing_none]
1035#[derive(Debug, Clone, Deserialize, Serialize)]
1036pub struct ModalityTokens {
1037    pub modality: Option<ResponseModality>,
1038    pub tokens: Option<u32>,
1039}
1040
1041#[skip_serializing_none]
1042#[derive(Debug, Clone, Deserialize, Serialize)]
1043pub struct InteractionsUsage {
1044    pub total_input_tokens: Option<u32>,
1045    pub input_tokens_by_modality: Option<Vec<ModalityTokens>>,
1046    pub total_cached_tokens: Option<u32>,
1047    pub cached_tokens_by_modality: Option<Vec<ModalityTokens>>,
1048    pub total_output_tokens: Option<u32>,
1049    pub output_tokens_by_modality: Option<Vec<ModalityTokens>>,
1050    pub total_tool_use_tokens: Option<u32>,
1051    pub tool_use_tokens_by_modality: Option<Vec<ModalityTokens>>,
1052    pub total_thought_tokens: Option<u32>,
1053    pub total_tokens: Option<u32>,
1054}
1055
1056#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
1057#[serde(rename_all = "snake_case")]
1058pub enum ResponseModality {
1059    Text,
1060    Image,
1061    Audio,
1062}
1063
1064fn validate_interactions_request(req: &InteractionsRequest) -> Result<(), ValidationError> {
1065    let is_blank = |v: &Option<String>| v.as_deref().map(|s| s.trim().is_empty()).unwrap_or(true);
1066    // Either model or agent must be provided
1067    if is_blank(&req.model) && is_blank(&req.agent) {
1068        return Err(ValidationError::new("model_or_agent_required"));
1069    }
1070    // response_mime_type is required when response_format is set
1071    if req.response_format.is_some() && is_blank(&req.response_mime_type) {
1072        return Err(ValidationError::new("response_mime_type_required"));
1073    }
1074    Ok(())
1075}
1076
1077fn validate_tools(tools: &[InteractionsTool]) -> Result<(), ValidationError> {
1078    // FileSearch tool is not supported yet
1079    if tools
1080        .iter()
1081        .any(|t| matches!(t, InteractionsTool::FileSearch { .. }))
1082    {
1083        return Err(ValidationError::new("file_search_tool_not_supported"));
1084    }
1085    Ok(())
1086}
1087
1088fn validate_input(input: &InteractionsInput) -> Result<(), ValidationError> {
1089    fn has_file_search_content(content: &Content) -> bool {
1090        matches!(
1091            content,
1092            Content::FileSearchCall { .. } | Content::FileSearchResult { .. }
1093        )
1094    }
1095
1096    fn check_turn(turn: &Turn) -> bool {
1097        if let Some(content) = &turn.content {
1098            match content {
1099                TurnContent::Contents(contents) => contents.iter().any(has_file_search_content),
1100                TurnContent::Text(_) => false,
1101            }
1102        } else {
1103            false
1104        }
1105    }
1106
1107    let has_file_search = match input {
1108        InteractionsInput::Text(_) => false,
1109        InteractionsInput::Content(content) => has_file_search_content(content),
1110        InteractionsInput::Contents(contents) => contents.iter().any(has_file_search_content),
1111        InteractionsInput::Turns(turns) => turns.iter().any(check_turn),
1112    };
1113
1114    if has_file_search {
1115        return Err(ValidationError::new("file_search_content_not_supported"));
1116    }
1117    Ok(())
1118}