1use 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::{
12 common::{default_model, default_true, Function, GenerationRequest},
13 sampling_params::validate_top_p_value,
14 validated::Normalizable,
15};
16
17#[skip_serializing_none]
22#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
23#[validate(schema(function = "validate_interactions_request"))]
24pub struct InteractionsRequest {
25 pub model: Option<String>,
28
29 pub agent: Option<String>,
32
33 #[validate(custom(function = "validate_input"))]
35 pub input: InteractionsInput,
36
37 pub system_instruction: Option<String>,
39
40 #[validate(custom(function = "validate_tools"))]
42 pub tools: Option<Vec<InteractionsTool>>,
43
44 pub response_format: Option<Value>,
46
47 pub response_mime_type: Option<String>,
49
50 #[serde(default)]
52 pub stream: bool,
53
54 #[serde(default = "default_true")]
56 pub store: bool,
57
58 #[serde(default)]
60 pub background: bool,
61
62 #[validate(nested)]
64 pub generation_config: Option<GenerationConfig>,
65
66 pub agent_config: Option<AgentConfig>,
68
69 pub response_modalities: Option<Vec<ResponseModality>>,
71
72 pub previous_interaction_id: Option<String>,
74}
75
76impl Default for InteractionsRequest {
77 fn default() -> Self {
78 Self {
79 model: Some(default_model()),
80 agent: None,
81 agent_config: None,
82 input: InteractionsInput::Text(String::new()),
83 system_instruction: None,
84 previous_interaction_id: None,
85 tools: None,
86 generation_config: None,
87 response_format: None,
88 response_mime_type: None,
89 response_modalities: None,
90 stream: false,
91 background: false,
92 store: true,
93 }
94 }
95}
96
97impl Normalizable for InteractionsRequest {
98 }
100
101impl GenerationRequest for InteractionsRequest {
102 fn is_stream(&self) -> bool {
103 self.stream
104 }
105
106 fn get_model(&self) -> Option<&str> {
107 self.model.as_deref()
108 }
109
110 fn extract_text_for_routing(&self) -> String {
111 fn extract_from_content(content: &Content) -> Option<String> {
112 match content {
113 Content::Text { text, .. } => text.clone(),
114 _ => None,
115 }
116 }
117
118 fn extract_from_turn(turn: &Turn) -> String {
119 match &turn.content {
120 Some(TurnContent::Text(text)) => text.clone(),
121 Some(TurnContent::Contents(contents)) => contents
122 .iter()
123 .filter_map(extract_from_content)
124 .collect::<Vec<String>>()
125 .join(" "),
126 None => String::new(),
127 }
128 }
129
130 match &self.input {
131 InteractionsInput::Text(text) => text.clone(),
132 InteractionsInput::Content(content) => {
133 extract_from_content(content).unwrap_or_default()
134 }
135 InteractionsInput::Contents(contents) => contents
136 .iter()
137 .filter_map(extract_from_content)
138 .collect::<Vec<String>>()
139 .join(" "),
140 InteractionsInput::Turns(turns) => turns
141 .iter()
142 .map(extract_from_turn)
143 .collect::<Vec<String>>()
144 .join(" "),
145 }
146 }
147}
148
149#[skip_serializing_none]
154#[derive(Debug, Clone, Default, Deserialize, Serialize)]
155pub struct Interaction {
156 pub object: Option<String>,
158
159 pub model: Option<String>,
161
162 pub agent: Option<String>,
164
165 pub id: String,
167
168 pub status: InteractionsStatus,
170
171 pub created: Option<String>,
173
174 pub updated: Option<String>,
176
177 pub role: Option<String>,
179
180 pub outputs: Option<Vec<Content>>,
182
183 pub usage: Option<InteractionsUsage>,
185
186 pub previous_interaction_id: Option<String>,
188}
189
190impl Interaction {
191 pub fn is_complete(&self) -> bool {
193 matches!(self.status, InteractionsStatus::Completed)
194 }
195
196 pub fn is_in_progress(&self) -> bool {
198 matches!(self.status, InteractionsStatus::InProgress)
199 }
200
201 pub fn is_failed(&self) -> bool {
203 matches!(self.status, InteractionsStatus::Failed)
204 }
205
206 pub fn requires_action(&self) -> bool {
208 matches!(self.status, InteractionsStatus::RequiresAction)
209 }
210}
211
212#[skip_serializing_none]
219#[derive(Debug, Clone, Deserialize, Serialize)]
220#[serde(tag = "event_type")]
221pub enum InteractionStreamEvent {
222 #[serde(rename = "interaction.start")]
224 InteractionStart {
225 interaction: Option<Interaction>,
227 event_id: Option<String>,
229 },
230
231 #[serde(rename = "interaction.complete")]
233 InteractionComplete {
234 interaction: Option<Interaction>,
236 event_id: Option<String>,
238 },
239
240 #[serde(rename = "interaction.status_update")]
242 InteractionStatusUpdate {
243 interaction_id: Option<String>,
245 status: Option<InteractionsStatus>,
247 event_id: Option<String>,
249 },
250
251 #[serde(rename = "content.start")]
253 ContentStart {
254 index: Option<u32>,
256 content: Option<Content>,
258 event_id: Option<String>,
260 },
261
262 #[serde(rename = "content.delta")]
264 ContentDelta {
265 index: Option<u32>,
267 event_id: Option<String>,
269 delta: Option<Delta>,
271 },
272
273 #[serde(rename = "content.stop")]
275 ContentStop {
276 index: Option<u32>,
278 event_id: Option<String>,
280 },
281
282 #[serde(rename = "error")]
284 Error {
285 error: Option<InteractionsError>,
287 event_id: Option<String>,
289 },
290}
291
292#[skip_serializing_none]
295#[derive(Debug, Clone, Deserialize, Serialize)]
296#[serde(tag = "type", rename_all = "snake_case")]
297pub enum Delta {
298 Text {
300 text: Option<String>,
301 annotations: Option<Vec<Annotation>>,
302 },
303 Image {
305 data: Option<String>,
306 uri: Option<String>,
307 mime_type: Option<ImageMimeType>,
308 resolution: Option<MediaResolution>,
309 },
310 Audio {
312 data: Option<String>,
313 uri: Option<String>,
314 mime_type: Option<AudioMimeType>,
315 },
316 Document {
318 data: Option<String>,
319 uri: Option<String>,
320 mime_type: Option<DocumentMimeType>,
321 },
322 Video {
324 data: Option<String>,
325 uri: Option<String>,
326 mime_type: Option<VideoMimeType>,
327 resolution: Option<MediaResolution>,
328 },
329 ThoughtSummary {
331 content: Option<ThoughtSummaryContent>,
332 },
333 ThoughtSignature { signature: Option<String> },
335 FunctionCall {
337 name: Option<String>,
338 arguments: Option<String>,
339 id: Option<String>,
340 },
341 FunctionResult {
343 name: Option<String>,
344 is_error: Option<bool>,
345 result: Option<Value>,
346 call_id: Option<String>,
347 },
348 CodeExecutionCall {
350 arguments: Option<CodeExecutionArguments>,
351 id: Option<String>,
352 },
353 CodeExecutionResult {
355 result: Option<String>,
356 is_error: Option<bool>,
357 signature: Option<String>,
358 call_id: Option<String>,
359 },
360 UrlContextCall {
362 arguments: Option<UrlContextArguments>,
363 id: Option<String>,
364 },
365 UrlContextResult {
367 signature: Option<String>,
368 result: Option<Vec<UrlContextResultData>>,
369 is_error: Option<bool>,
370 call_id: Option<String>,
371 },
372 GoogleSearchCall {
374 arguments: Option<GoogleSearchArguments>,
375 id: Option<String>,
376 },
377 GoogleSearchResult {
379 signature: Option<String>,
380 result: Option<Vec<GoogleSearchResultData>>,
381 is_error: Option<bool>,
382 call_id: Option<String>,
383 },
384 FileSearchCall { id: Option<String> },
386 FileSearchResult {
388 result: Option<Vec<FileSearchResultData>>,
389 },
390 McpServerToolCall {
392 name: Option<String>,
393 server_name: Option<String>,
394 arguments: Option<Value>,
395 id: Option<String>,
396 },
397 McpServerToolResult {
399 name: Option<String>,
400 server_name: Option<String>,
401 result: Option<Value>,
402 call_id: Option<String>,
403 },
404}
405
406#[skip_serializing_none]
408#[derive(Debug, Clone, Deserialize, Serialize)]
409pub struct InteractionsError {
410 pub code: Option<String>,
412 pub message: Option<String>,
414}
415
416#[skip_serializing_none]
422#[derive(Debug, Clone, Default, Deserialize, Serialize)]
423pub struct InteractionsGetParams {
424 pub stream: Option<bool>,
426 pub last_event_id: Option<String>,
428 pub api_version: Option<String>,
430}
431
432#[skip_serializing_none]
434#[derive(Debug, Clone, Default, Deserialize, Serialize)]
435pub struct InteractionsDeleteParams {
436 pub api_version: Option<String>,
438}
439
440#[skip_serializing_none]
442#[derive(Debug, Clone, Default, Deserialize, Serialize)]
443pub struct InteractionsCancelParams {
444 pub api_version: Option<String>,
446}
447
448#[skip_serializing_none]
455#[derive(Debug, Clone, Deserialize, Serialize)]
456#[serde(tag = "type", rename_all = "snake_case")]
457pub enum InteractionsTool {
458 Function(Function),
460 GoogleSearch {},
462 CodeExecution {},
464 UrlContext {},
466 McpServer {
468 name: Option<String>,
469 url: Option<String>,
470 headers: Option<HashMap<String, String>>,
471 allowed_tools: Option<AllowedTools>,
472 },
473 FileSearch {
475 file_search_store_names: Option<Vec<String>>,
477 top_k: Option<u32>,
479 metadata_filter: Option<String>,
481 },
482}
483
484#[skip_serializing_none]
486#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
487pub struct AllowedTools {
488 pub mode: Option<ToolChoiceType>,
490 pub tools: Option<Vec<String>>,
492}
493
494#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
495#[serde(rename_all = "snake_case")]
496pub enum ToolChoiceType {
497 Auto,
498 Any,
499 None,
500 Validated,
501}
502
503#[skip_serializing_none]
508#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
509pub struct GenerationConfig {
510 #[validate(range(min = 0.0, max = 2.0))]
511 pub temperature: Option<f32>,
512
513 #[validate(custom(function = "validate_top_p_value"))]
514 pub top_p: Option<f32>,
515
516 pub seed: Option<i64>,
517
518 #[validate(custom(function = "validate_stop_sequences"))]
519 pub stop_sequences: Option<Vec<String>>,
520
521 pub tool_choice: Option<ToolChoice>,
522
523 pub thinking_level: Option<ThinkingLevel>,
524
525 pub thinking_summaries: Option<ThinkingSummaries>,
526
527 #[validate(range(min = 1))]
528 pub max_output_tokens: Option<u32>,
529
530 pub speech_config: Option<Vec<SpeechConfig>>,
531
532 pub image_config: Option<ImageConfig>,
533}
534
535#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
536#[serde(rename_all = "snake_case")]
537pub enum ThinkingLevel {
538 Minimal,
539 Low,
540 Medium,
541 High,
542}
543
544#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
545#[serde(rename_all = "snake_case")]
546pub enum ThinkingSummaries {
547 Auto,
548 None,
549}
550
551#[derive(Debug, Clone, Deserialize, Serialize)]
553#[serde(untagged)]
554pub enum ToolChoice {
555 Type(ToolChoiceType),
556 Config(ToolChoiceConfig),
557}
558
559#[skip_serializing_none]
560#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
561pub struct ToolChoiceConfig {
562 pub allowed_tools: Option<AllowedTools>,
563}
564
565#[skip_serializing_none]
566#[derive(Debug, Clone, Deserialize, Serialize)]
567pub struct SpeechConfig {
568 pub voice: Option<String>,
569 pub language: Option<String>,
570 pub speaker: Option<String>,
571}
572
573#[skip_serializing_none]
574#[derive(Debug, Clone, Deserialize, Serialize)]
575pub struct ImageConfig {
576 pub aspect_ratio: Option<AspectRatio>,
577 pub image_size: Option<ImageSize>,
578}
579
580#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
581pub enum AspectRatio {
582 #[serde(rename = "1:1")]
583 Square,
584 #[serde(rename = "2:3")]
585 Portrait2x3,
586 #[serde(rename = "3:2")]
587 Landscape3x2,
588 #[serde(rename = "3:4")]
589 Portrait3x4,
590 #[serde(rename = "4:3")]
591 Landscape4x3,
592 #[serde(rename = "4:5")]
593 Portrait4x5,
594 #[serde(rename = "5:4")]
595 Landscape5x4,
596 #[serde(rename = "9:16")]
597 Portrait9x16,
598 #[serde(rename = "16:9")]
599 Landscape16x9,
600 #[serde(rename = "21:9")]
601 UltraWide,
602}
603
604#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
605pub enum ImageSize {
606 #[serde(rename = "1K")]
607 OneK,
608 #[serde(rename = "2K")]
609 TwoK,
610 #[serde(rename = "4K")]
611 FourK,
612}
613
614#[derive(Debug, Clone, Deserialize, Serialize)]
617#[serde(tag = "type", rename_all = "snake_case")]
618pub enum AgentConfig {
619 Dynamic {},
621 #[serde(rename = "deep-research")]
623 DeepResearch {
624 #[serde(skip_serializing_if = "Option::is_none")]
626 thinking_summaries: Option<ThinkingSummaries>,
627 },
628}
629
630#[derive(Debug, Clone, Deserialize, Serialize)]
637#[serde(untagged)]
638pub enum InteractionsInput {
639 Text(String),
641 Content(Content),
643 Contents(Vec<Content>),
645 Turns(Vec<Turn>),
647}
648
649#[skip_serializing_none]
652#[derive(Debug, Clone, Deserialize, Serialize)]
653pub struct Turn {
654 pub role: Option<String>,
656 pub content: Option<TurnContent>,
658}
659
660#[derive(Debug, Clone, Deserialize, Serialize)]
662#[serde(untagged)]
663pub enum TurnContent {
664 Contents(Vec<Content>),
665 Text(String),
666}
667
668#[skip_serializing_none]
671#[derive(Debug, Clone, Deserialize, Serialize)]
672#[serde(tag = "type", rename_all = "snake_case")]
673pub enum Content {
674 Text {
676 text: Option<String>,
677 annotations: Option<Vec<Annotation>>,
678 },
679
680 Image {
682 data: Option<String>,
683 uri: Option<String>,
684 mime_type: Option<ImageMimeType>,
685 resolution: Option<MediaResolution>,
686 },
687
688 Audio {
690 data: Option<String>,
691 uri: Option<String>,
692 mime_type: Option<AudioMimeType>,
693 },
694
695 Document {
697 data: Option<String>,
698 uri: Option<String>,
699 mime_type: Option<DocumentMimeType>,
700 },
701
702 Video {
704 data: Option<String>,
705 uri: Option<String>,
706 mime_type: Option<VideoMimeType>,
707 resolution: Option<MediaResolution>,
708 },
709
710 Thought {
712 signature: Option<String>,
713 summary: Option<Vec<ThoughtSummaryContent>>,
714 },
715
716 FunctionCall {
718 name: String,
719 arguments: Value,
720 id: String,
721 },
722
723 FunctionResult {
725 name: Option<String>,
726 is_error: Option<bool>,
727 result: Value,
728 call_id: String,
729 },
730
731 CodeExecutionCall {
733 arguments: Option<CodeExecutionArguments>,
734 id: Option<String>,
735 },
736
737 CodeExecutionResult {
739 result: Option<String>,
740 is_error: Option<bool>,
741 signature: Option<String>,
742 call_id: Option<String>,
743 },
744
745 UrlContextCall {
747 arguments: Option<UrlContextArguments>,
748 id: Option<String>,
749 },
750
751 UrlContextResult {
753 signature: Option<String>,
754 result: Option<Vec<UrlContextResultData>>,
755 is_error: Option<bool>,
756 call_id: Option<String>,
757 },
758
759 GoogleSearchCall {
761 arguments: Option<GoogleSearchArguments>,
762 id: Option<String>,
763 },
764
765 GoogleSearchResult {
767 signature: Option<String>,
768 result: Option<Vec<GoogleSearchResultData>>,
769 is_error: Option<bool>,
770 call_id: Option<String>,
771 },
772
773 FileSearchCall { id: Option<String> },
775
776 FileSearchResult {
778 result: Option<Vec<FileSearchResultData>>,
779 },
780
781 McpServerToolCall {
783 name: String,
784 server_name: String,
785 arguments: Value,
786 id: String,
787 },
788
789 McpServerToolResult {
791 name: Option<String>,
792 server_name: Option<String>,
793 result: Value,
794 call_id: String,
795 },
796}
797
798#[skip_serializing_none]
800#[derive(Debug, Clone, Deserialize, Serialize)]
801#[serde(tag = "type", rename_all = "snake_case")]
802pub enum ThoughtSummaryContent {
803 Text {
805 text: Option<String>,
806 annotations: Option<Vec<Annotation>>,
807 },
808 Image {
810 data: Option<String>,
811 uri: Option<String>,
812 mime_type: Option<ImageMimeType>,
813 resolution: Option<MediaResolution>,
814 },
815}
816
817#[skip_serializing_none]
819#[derive(Debug, Clone, Deserialize, Serialize)]
820pub struct Annotation {
821 pub start_index: Option<u32>,
823 pub end_index: Option<u32>,
825 pub source: Option<String>,
827}
828
829#[skip_serializing_none]
831#[derive(Debug, Clone, Deserialize, Serialize)]
832pub struct UrlContextArguments {
833 pub urls: Option<Vec<String>>,
835}
836
837#[skip_serializing_none]
839#[derive(Debug, Clone, Deserialize, Serialize)]
840pub struct UrlContextResultData {
841 pub url: Option<String>,
843 pub status: Option<UrlContextStatus>,
845}
846
847#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
849#[serde(rename_all = "snake_case")]
850pub enum UrlContextStatus {
851 Success,
852 Error,
853 Paywall,
854 Unsafe,
855}
856
857#[skip_serializing_none]
859#[derive(Debug, Clone, Deserialize, Serialize)]
860pub struct GoogleSearchArguments {
861 pub queries: Option<Vec<String>>,
863}
864
865#[skip_serializing_none]
867#[derive(Debug, Clone, Deserialize, Serialize)]
868pub struct GoogleSearchResultData {
869 pub url: Option<String>,
871 pub title: Option<String>,
873 pub rendered_content: Option<String>,
875}
876
877#[skip_serializing_none]
879#[derive(Debug, Clone, Deserialize, Serialize)]
880pub struct FileSearchResultData {
881 pub title: Option<String>,
883 pub text: Option<String>,
885 pub file_search_store: Option<String>,
887}
888
889#[skip_serializing_none]
891#[derive(Debug, Clone, Deserialize, Serialize)]
892pub struct CodeExecutionArguments {
893 pub language: Option<CodeExecutionLanguage>,
895 pub code: Option<String>,
897}
898
899#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
901#[serde(rename_all = "snake_case")]
902pub enum CodeExecutionLanguage {
903 Python,
904}
905
906#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
908#[serde(rename_all = "snake_case")]
909pub enum MediaResolution {
910 Low,
911 Medium,
912 High,
913 UltraHigh,
914}
915
916#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
918pub enum ImageMimeType {
919 #[serde(rename = "image/png")]
920 Png,
921 #[serde(rename = "image/jpeg")]
922 Jpeg,
923 #[serde(rename = "image/webp")]
924 Webp,
925 #[serde(rename = "image/heic")]
926 Heic,
927 #[serde(rename = "image/heif")]
928 Heif,
929}
930
931impl ImageMimeType {
932 pub fn as_str(&self) -> &'static str {
933 match self {
934 ImageMimeType::Png => "image/png",
935 ImageMimeType::Jpeg => "image/jpeg",
936 ImageMimeType::Webp => "image/webp",
937 ImageMimeType::Heic => "image/heic",
938 ImageMimeType::Heif => "image/heif",
939 }
940 }
941}
942
943#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
945pub enum AudioMimeType {
946 #[serde(rename = "audio/wav")]
947 Wav,
948 #[serde(rename = "audio/mp3")]
949 Mp3,
950 #[serde(rename = "audio/aiff")]
951 Aiff,
952 #[serde(rename = "audio/aac")]
953 Aac,
954 #[serde(rename = "audio/ogg")]
955 Ogg,
956 #[serde(rename = "audio/flac")]
957 Flac,
958}
959
960impl AudioMimeType {
961 pub fn as_str(&self) -> &'static str {
962 match self {
963 AudioMimeType::Wav => "audio/wav",
964 AudioMimeType::Mp3 => "audio/mp3",
965 AudioMimeType::Aiff => "audio/aiff",
966 AudioMimeType::Aac => "audio/aac",
967 AudioMimeType::Ogg => "audio/ogg",
968 AudioMimeType::Flac => "audio/flac",
969 }
970 }
971}
972
973#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
975pub enum DocumentMimeType {
976 #[serde(rename = "application/pdf")]
977 Pdf,
978}
979
980impl DocumentMimeType {
981 pub fn as_str(&self) -> &'static str {
982 match self {
983 DocumentMimeType::Pdf => "application/pdf",
984 }
985 }
986}
987
988#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
990pub enum VideoMimeType {
991 #[serde(rename = "video/mp4")]
992 Mp4,
993 #[serde(rename = "video/mpeg")]
994 Mpeg,
995 #[serde(rename = "video/mov")]
996 Mov,
997 #[serde(rename = "video/avi")]
998 Avi,
999 #[serde(rename = "video/x-flv")]
1000 Flv,
1001 #[serde(rename = "video/mpg")]
1002 Mpg,
1003 #[serde(rename = "video/webm")]
1004 Webm,
1005 #[serde(rename = "video/wmv")]
1006 Wmv,
1007 #[serde(rename = "video/3gpp")]
1008 ThreeGpp,
1009}
1010
1011impl VideoMimeType {
1012 pub fn as_str(&self) -> &'static str {
1013 match self {
1014 VideoMimeType::Mp4 => "video/mp4",
1015 VideoMimeType::Mpeg => "video/mpeg",
1016 VideoMimeType::Mov => "video/mov",
1017 VideoMimeType::Avi => "video/avi",
1018 VideoMimeType::Flv => "video/x-flv",
1019 VideoMimeType::Mpg => "video/mpg",
1020 VideoMimeType::Webm => "video/webm",
1021 VideoMimeType::Wmv => "video/wmv",
1022 VideoMimeType::ThreeGpp => "video/3gpp",
1023 }
1024 }
1025}
1026
1027#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
1032#[serde(rename_all = "snake_case")]
1033pub enum InteractionsStatus {
1034 #[default]
1035 InProgress,
1036 RequiresAction,
1037 Completed,
1038 Failed,
1039 Cancelled,
1040}
1041
1042#[skip_serializing_none]
1048#[derive(Debug, Clone, Deserialize, Serialize)]
1049pub struct ModalityTokens {
1050 pub modality: Option<ResponseModality>,
1051 pub tokens: Option<u32>,
1052}
1053
1054#[skip_serializing_none]
1055#[derive(Debug, Clone, Deserialize, Serialize)]
1056pub struct InteractionsUsage {
1057 pub total_input_tokens: Option<u32>,
1058 pub input_tokens_by_modality: Option<Vec<ModalityTokens>>,
1059 pub total_cached_tokens: Option<u32>,
1060 pub cached_tokens_by_modality: Option<Vec<ModalityTokens>>,
1061 pub total_output_tokens: Option<u32>,
1062 pub output_tokens_by_modality: Option<Vec<ModalityTokens>>,
1063 pub total_tool_use_tokens: Option<u32>,
1064 pub tool_use_tokens_by_modality: Option<Vec<ModalityTokens>>,
1065 pub total_thought_tokens: Option<u32>,
1066 pub total_tokens: Option<u32>,
1067}
1068
1069#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
1070#[serde(rename_all = "snake_case")]
1071pub enum ResponseModality {
1072 Text,
1073 Image,
1074 Audio,
1075}
1076
1077fn is_option_blank(v: Option<&String>) -> bool {
1078 v.map(|s| s.trim().is_empty()).unwrap_or(true)
1079}
1080
1081fn validate_interactions_request(req: &InteractionsRequest) -> Result<(), ValidationError> {
1082 if is_option_blank(req.model.as_ref()) && is_option_blank(req.agent.as_ref()) {
1084 return Err(ValidationError::new("model_or_agent_required"));
1085 }
1086 if !is_option_blank(req.model.as_ref()) && !is_option_blank(req.agent.as_ref()) {
1087 let mut e = ValidationError::new("model_and_agent_mutually_exclusive");
1088 e.message = Some("Cannot set both model and agent. Provide exactly one.".into());
1089 return Err(e);
1090 }
1091
1092 if req.response_format.is_some() && is_option_blank(req.response_mime_type.as_ref()) {
1094 return Err(ValidationError::new("response_mime_type_required"));
1095 }
1096
1097 if !is_option_blank(req.agent.as_ref()) && !req.background {
1099 let mut e = ValidationError::new("agent_requires_background");
1100 e.message = Some("Agent interactions require background mode to be enabled.".into());
1101 return Err(e);
1102 }
1103 if !is_option_blank(req.model.as_ref()) && req.background {
1104 let mut e = ValidationError::new("background_requires_agent");
1105 e.message = Some("Background mode is only supported for agent interactions.".into());
1106 return Err(e);
1107 }
1108
1109 if req.background && req.stream {
1111 let mut e = ValidationError::new("background_conflicts_with_stream");
1112 e.message = Some("Cannot set both background and stream to true.".into());
1113 return Err(e);
1114 }
1115 Ok(())
1116}
1117
1118fn validate_tools(tools: &[InteractionsTool]) -> Result<(), ValidationError> {
1119 if tools
1121 .iter()
1122 .any(|t| matches!(t, InteractionsTool::FileSearch { .. }))
1123 {
1124 return Err(ValidationError::new("file_search_tool_not_supported"));
1125 }
1126 Ok(())
1127}
1128
1129fn validate_input(input: &InteractionsInput) -> Result<(), ValidationError> {
1130 let empty_msg = match input {
1132 InteractionsInput::Text(s) if s.trim().is_empty() => Some("Input text cannot be empty"),
1133 InteractionsInput::Content(content) if is_content_empty(content) => {
1134 Some("Input content cannot be empty")
1135 }
1136 InteractionsInput::Contents(contents) if contents.is_empty() => {
1137 Some("Input content array cannot be empty")
1138 }
1139 InteractionsInput::Contents(contents) if contents.iter().any(is_content_empty) => {
1140 Some("Input content array contains empty content items")
1141 }
1142 InteractionsInput::Turns(turns) if turns.is_empty() => {
1143 Some("Input turns array cannot be empty")
1144 }
1145 InteractionsInput::Turns(turns) if turns.iter().any(is_turn_empty) => {
1146 Some("Input turns array contains empty turn items")
1147 }
1148 _ => None,
1149 };
1150 if let Some(msg) = empty_msg {
1151 let mut e = ValidationError::new("input_cannot_be_empty");
1152 e.message = Some(msg.into());
1153 return Err(e);
1154 }
1155
1156 fn has_file_search_content(content: &Content) -> bool {
1158 matches!(
1159 content,
1160 Content::FileSearchCall { .. } | Content::FileSearchResult { .. }
1161 )
1162 }
1163
1164 fn check_turn(turn: &Turn) -> bool {
1165 if let Some(content) = &turn.content {
1166 match content {
1167 TurnContent::Contents(contents) => contents.iter().any(has_file_search_content),
1168 TurnContent::Text(_) => false,
1169 }
1170 } else {
1171 false
1172 }
1173 }
1174
1175 let has_file_search = match input {
1176 InteractionsInput::Text(_) => false,
1177 InteractionsInput::Content(content) => has_file_search_content(content),
1178 InteractionsInput::Contents(contents) => contents.iter().any(has_file_search_content),
1179 InteractionsInput::Turns(turns) => turns.iter().any(check_turn),
1180 };
1181
1182 if has_file_search {
1183 return Err(ValidationError::new("file_search_content_not_supported"));
1184 }
1185 Ok(())
1186}
1187
1188fn is_content_empty(content: &Content) -> bool {
1189 match content {
1190 Content::Text { text, .. } => is_option_blank(text.as_ref()),
1191 Content::Image { data, uri, .. }
1192 | Content::Audio { data, uri, .. }
1193 | Content::Document { data, uri, .. }
1194 | Content::Video { data, uri, .. } => {
1195 is_option_blank(data.as_ref()) && is_option_blank(uri.as_ref())
1196 }
1197 Content::CodeExecutionCall { id, .. }
1198 | Content::UrlContextCall { id, .. }
1199 | Content::GoogleSearchCall { id, .. } => is_option_blank(id.as_ref()),
1200 Content::CodeExecutionResult { call_id, .. }
1201 | Content::UrlContextResult { call_id, .. }
1202 | Content::GoogleSearchResult { call_id, .. } => is_option_blank(call_id.as_ref()),
1203 _ => false,
1204 }
1205}
1206
1207fn is_turn_empty(turn: &Turn) -> bool {
1208 match &turn.content {
1209 None => true,
1210 Some(TurnContent::Text(s)) => s.trim().is_empty(),
1211 Some(TurnContent::Contents(contents)) => {
1212 contents.is_empty() || contents.iter().any(is_content_empty)
1213 }
1214 }
1215}
1216
1217fn validate_stop_sequences(seqs: &[String]) -> Result<(), ValidationError> {
1218 if seqs.len() > 5 {
1219 let mut e = ValidationError::new("too_many_stop_sequences");
1220 e.message = Some("Maximum 5 stop sequences allowed".into());
1221 return Err(e);
1222 }
1223 if seqs.iter().any(|s| s.trim().is_empty()) {
1224 let mut e = ValidationError::new("stop_sequences_cannot_be_empty");
1225 e.message = Some("Stop sequences cannot contain empty strings".into());
1226 return Err(e);
1227 }
1228 Ok(())
1229}