Skip to main content

rig/providers/gemini/interactions_api/
mod.rs

1//! Google Gemini Interactions API integration.
2//! From <https://ai.google.dev/api/interactions-api>
3
4use crate::OneOrMany;
5use crate::completion::{self, CompletionError, CompletionRequest, GetTokenUsage};
6use crate::http_client::HttpClientExt;
7use crate::message::{self, MimeType, Reasoning};
8use crate::telemetry::SpanCombinator;
9use serde_json::{Map, Value};
10use tracing::{Level, enabled, info_span};
11use tracing_futures::Instrument;
12use url::form_urlencoded;
13
14use super::client::InteractionsClient;
15
16/// Streaming helpers for the Interactions API.
17pub mod streaming;
18pub use interactions_api_types::*;
19
20// =================================================================
21// Rig Implementation Types
22// =================================================================
23
24/// Completion model wrapper for the Gemini Interactions API.
25#[derive(Clone, Debug)]
26pub struct InteractionsCompletionModel<T = reqwest::Client> {
27    pub(crate) client: InteractionsClient<T>,
28    pub model: String,
29}
30
31impl<T> InteractionsCompletionModel<T> {
32    /// Create a new Interactions completion model for the given client and model name.
33    pub fn new(client: InteractionsClient<T>, model: impl Into<String>) -> Self {
34        Self {
35            client,
36            model: model.into(),
37        }
38    }
39
40    /// Create a new Interactions completion model using a string model name.
41    pub fn with_model(client: InteractionsClient<T>, model: &str) -> Self {
42        Self {
43            client,
44            model: model.to_string(),
45        }
46    }
47
48    /// Use the GenerateContent API instead of Interactions.
49    pub fn generate_content_api(self) -> super::completion::CompletionModel<T> {
50        super::completion::CompletionModel::with_model(
51            self.client.generate_content_api(),
52            &self.model,
53        )
54    }
55
56    pub(crate) fn create_completion_request(
57        &self,
58        completion_request: CompletionRequest,
59        stream_override: Option<bool>,
60    ) -> Result<CreateInteractionRequest, CompletionError> {
61        create_request_body(self.model.clone(), completion_request, stream_override)
62    }
63}
64
65impl<T> InteractionsCompletionModel<T>
66where
67    T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
68{
69    /// Create an interaction and return the raw response payload.
70    pub async fn create_interaction(
71        &self,
72        completion_request: CompletionRequest,
73    ) -> Result<Interaction, CompletionError> {
74        let request = self.create_completion_request(completion_request, Some(false))?;
75        self.client.create_interaction(request).await
76    }
77
78    /// Fetch an interaction by ID for polling background tasks.
79    pub async fn get_interaction(
80        &self,
81        interaction_id: impl AsRef<str>,
82    ) -> Result<Interaction, CompletionError> {
83        self.client.get_interaction(interaction_id).await
84    }
85
86    /// Start an interaction and stream raw SSE events.
87    pub async fn stream_interaction_events(
88        &self,
89        completion_request: CompletionRequest,
90    ) -> Result<streaming::InteractionEventStream, CompletionError> {
91        let request = self.create_completion_request(completion_request, Some(true))?;
92        self.client.stream_interaction_events(request).await
93    }
94
95    /// Resume an interaction stream by ID and optional last event ID.
96    pub async fn stream_interaction_events_by_id(
97        &self,
98        interaction_id: impl AsRef<str>,
99        last_event_id: Option<&str>,
100    ) -> Result<streaming::InteractionEventStream, CompletionError> {
101        self.client
102            .stream_interaction_events_by_id(interaction_id, last_event_id)
103            .await
104    }
105}
106
107impl<T> completion::CompletionModel for InteractionsCompletionModel<T>
108where
109    T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
110{
111    type Response = Interaction;
112    type StreamingResponse = streaming::StreamingCompletionResponse;
113    type Client = InteractionsClient<T>;
114
115    fn make(client: &Self::Client, model: impl Into<String>) -> Self {
116        Self::new(client.clone(), model)
117    }
118
119    async fn completion(
120        &self,
121        completion_request: CompletionRequest,
122    ) -> Result<completion::CompletionResponse<Interaction>, CompletionError> {
123        let span = if tracing::Span::current().is_disabled() {
124            info_span!(
125                target: "rig::completions",
126                "interactions",
127                gen_ai.operation.name = "interactions",
128                gen_ai.provider.name = "gcp.gemini",
129                gen_ai.request.model = self.model,
130                gen_ai.system_instructions = &completion_request.preamble,
131                gen_ai.response.id = tracing::field::Empty,
132                gen_ai.response.model = tracing::field::Empty,
133                gen_ai.usage.output_tokens = tracing::field::Empty,
134                gen_ai.usage.input_tokens = tracing::field::Empty,
135            )
136        } else {
137            tracing::Span::current()
138        };
139
140        let request = self.create_completion_request(completion_request, Some(false))?;
141
142        if enabled!(Level::TRACE) {
143            tracing::trace!(
144                target: "rig::completions",
145                "Gemini interactions completion request: {}",
146                serde_json::to_string_pretty(&request)?
147            );
148        }
149
150        let body = serde_json::to_vec(&request)?;
151        let request = self
152            .client
153            .post("/v1beta/interactions")?
154            .body(body)
155            .map_err(|e| CompletionError::HttpError(e.into()))?;
156
157        async move {
158            let response = self.client.send::<_, Vec<u8>>(request).await?;
159
160            if response.status().is_success() {
161                let response_body = response
162                    .into_body()
163                    .await
164                    .map_err(CompletionError::HttpError)?;
165
166                let response_text = String::from_utf8_lossy(&response_body).to_string();
167
168                let response: Interaction =
169                    serde_json::from_slice(&response_body).map_err(|err| {
170                        tracing::error!(
171                            error = %err,
172                            body = %response_text,
173                            "Failed to deserialize Gemini interactions response"
174                        );
175                        CompletionError::JsonError(err)
176                    })?;
177
178                let span = tracing::Span::current();
179                span.record_response_metadata(&response);
180                span.record_token_usage(&response);
181
182                if enabled!(Level::TRACE) {
183                    tracing::trace!(
184                        target: "rig::completions",
185                        "Gemini interactions completion response: {}",
186                        serde_json::to_string_pretty(&response)?
187                    );
188                }
189
190                response.try_into()
191            } else {
192                let text = String::from_utf8_lossy(
193                    &response
194                        .into_body()
195                        .await
196                        .map_err(CompletionError::HttpError)?,
197                )
198                .into();
199
200                Err(CompletionError::ProviderError(text))
201            }
202        }
203        .instrument(span)
204        .await
205    }
206
207    async fn stream(
208        &self,
209        request: CompletionRequest,
210    ) -> Result<
211        crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
212        CompletionError,
213    > {
214        InteractionsCompletionModel::stream(self, request).await
215    }
216}
217
218impl<T> InteractionsClient<T>
219where
220    T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
221{
222    /// Create a new interaction and return the raw response payload.
223    pub async fn create_interaction(
224        &self,
225        request: CreateInteractionRequest,
226    ) -> Result<Interaction, CompletionError> {
227        if request.stream == Some(true) {
228            return Err(CompletionError::RequestError(Box::new(
229                std::io::Error::new(
230                    std::io::ErrorKind::InvalidInput,
231                    "stream=true requires stream_interaction_events",
232                ),
233            )));
234        }
235
236        let body = serde_json::to_vec(&request)?;
237        let request = self
238            .post("/v1beta/interactions")?
239            .body(body)
240            .map_err(|e| CompletionError::HttpError(e.into()))?;
241
242        send_interaction_request(self, request).await
243    }
244
245    /// Fetch an interaction by ID (useful for polling background tasks).
246    pub async fn get_interaction(
247        &self,
248        interaction_id: impl AsRef<str>,
249    ) -> Result<Interaction, CompletionError> {
250        let path = format!("/v1beta/interactions/{}", interaction_id.as_ref());
251        let request = self
252            .get(path)?
253            .body(Vec::new())
254            .map_err(|e| CompletionError::HttpError(e.into()))?;
255
256        send_interaction_request(self, request).await
257    }
258
259    /// Start an interaction and stream raw SSE events.
260    pub async fn stream_interaction_events(
261        &self,
262        mut request: CreateInteractionRequest,
263    ) -> Result<streaming::InteractionEventStream, CompletionError> {
264        request.stream = Some(true);
265        let body = serde_json::to_vec(&request)?;
266        let request = self
267            .post_sse("/v1beta/interactions")?
268            .header("Content-Type", "application/json")
269            .body(body)
270            .map_err(|e| CompletionError::HttpError(e.into()))?;
271
272        Ok(streaming::stream_interaction_events(self.clone(), request))
273    }
274
275    /// Resume an interaction stream by ID and optional last event ID.
276    pub async fn stream_interaction_events_by_id(
277        &self,
278        interaction_id: impl AsRef<str>,
279        last_event_id: Option<&str>,
280    ) -> Result<streaming::InteractionEventStream, CompletionError> {
281        let path = build_interaction_stream_path(interaction_id.as_ref(), last_event_id);
282        let request = self
283            .get_sse(path)?
284            .body(Vec::new())
285            .map_err(|e| CompletionError::HttpError(e.into()))?;
286
287        Ok(streaming::stream_interaction_events(self.clone(), request))
288    }
289}
290
291pub(crate) fn create_request_body(
292    model: String,
293    completion_request: CompletionRequest,
294    stream_override: Option<bool>,
295) -> Result<CreateInteractionRequest, CompletionError> {
296    let mut history = Vec::new();
297    if let Some(docs) = completion_request.normalized_documents() {
298        history.push(docs);
299    }
300    history.extend(completion_request.chat_history);
301    let (history_system, history) = split_system_messages_from_history(history);
302
303    let turns = history
304        .into_iter()
305        .map(Turn::try_from)
306        .collect::<Result<Vec<_>, _>>()
307        .map_err(|err| CompletionError::RequestError(Box::new(err)))?;
308
309    let input = InteractionInput::Turns(turns);
310
311    let raw_params = completion_request
312        .additional_params
313        .unwrap_or_else(|| Value::Object(Map::new()));
314
315    let mut params: AdditionalParameters = serde_json::from_value(raw_params)?;
316
317    let mut generation_config = params.generation_config.take().unwrap_or_default();
318    if let Some(temp) = completion_request.temperature {
319        generation_config.temperature = Some(temp);
320    }
321    if let Some(max_tokens) = completion_request.max_tokens {
322        generation_config.max_output_tokens = Some(max_tokens);
323    }
324    if let Some(tool_choice) = completion_request.tool_choice {
325        generation_config.tool_choice = Some(tool_choice.try_into()?);
326    }
327    let generation_config = if generation_config.is_empty() {
328        None
329    } else {
330        Some(generation_config)
331    };
332
333    let system_instruction = completion_request
334        .preamble
335        .or_else(|| {
336            if history_system.is_empty() {
337                None
338            } else {
339                Some(history_system.join("\n\n"))
340            }
341        })
342        .or(params.system_instruction.take());
343
344    let mut tools = Vec::new();
345    if !completion_request.tools.is_empty() {
346        tools.extend(
347            completion_request
348                .tools
349                .into_iter()
350                .map(Tool::try_from)
351                .collect::<Result<Vec<_>, _>>()?,
352        );
353    }
354    if let Some(mut extra_tools) = params.tools.take() {
355        tools.append(&mut extra_tools);
356    }
357    let tools = if tools.is_empty() { None } else { Some(tools) };
358
359    let stream = stream_override.or(params.stream.take());
360
361    let (agent, agent_config) = if params.agent.is_some() {
362        (params.agent.take(), params.agent_config.take())
363    } else {
364        (None, None)
365    };
366
367    let response_format = params.response_format.take();
368    let response_mime_type = params.response_mime_type.take();
369
370    if response_format.is_some() && response_mime_type.is_none() {
371        return Err(CompletionError::RequestError(Box::new(
372            std::io::Error::new(
373                std::io::ErrorKind::InvalidInput,
374                "response_mime_type is required when response_format is set",
375            ),
376        )));
377    }
378
379    Ok(CreateInteractionRequest {
380        model: if agent.is_some() { None } else { Some(model) },
381        agent,
382        input,
383        system_instruction,
384        tools,
385        response_format,
386        response_mime_type,
387        stream,
388        store: params.store.take(),
389        background: params.background.take(),
390        generation_config,
391        agent_config,
392        response_modalities: params.response_modalities.take(),
393        previous_interaction_id: params.previous_interaction_id.take(),
394        additional_params: params.additional_params.take(),
395    })
396}
397
398fn split_system_messages_from_history(
399    history: Vec<completion::Message>,
400) -> (Vec<String>, Vec<completion::Message>) {
401    let mut system = Vec::new();
402    let mut remaining = Vec::new();
403
404    for message in history {
405        match message {
406            completion::Message::System { content } => system.push(content),
407            other => remaining.push(other),
408        }
409    }
410
411    (system, remaining)
412}
413
414async fn send_interaction_request<T>(
415    client: &InteractionsClient<T>,
416    request: crate::http_client::Request<Vec<u8>>,
417) -> Result<Interaction, CompletionError>
418where
419    T: HttpClientExt + Clone + std::fmt::Debug + Default + 'static,
420{
421    let response = client.send::<_, Vec<u8>>(request).await?;
422
423    if response.status().is_success() {
424        let response_body = response
425            .into_body()
426            .await
427            .map_err(CompletionError::HttpError)?;
428
429        let response_text = String::from_utf8_lossy(&response_body).to_string();
430
431        let response: Interaction = serde_json::from_slice(&response_body).map_err(|err| {
432            tracing::error!(
433                error = %err,
434                body = %response_text,
435                "Failed to deserialize Gemini interactions response"
436            );
437            CompletionError::JsonError(err)
438        })?;
439
440        Ok(response)
441    } else {
442        let text = String::from_utf8_lossy(
443            &response
444                .into_body()
445                .await
446                .map_err(CompletionError::HttpError)?,
447        )
448        .into();
449
450        Err(CompletionError::ProviderError(text))
451    }
452}
453
454fn build_interaction_stream_path(interaction_id: &str, last_event_id: Option<&str>) -> String {
455    let mut serializer = form_urlencoded::Serializer::new(String::new());
456    serializer.append_pair("stream", "true");
457    if let Some(last_event_id) = last_event_id {
458        serializer.append_pair("last_event_id", last_event_id);
459    }
460    format!(
461        "/v1beta/interactions/{}?{}",
462        interaction_id,
463        serializer.finish()
464    )
465}
466
467impl TryFrom<Interaction> for completion::CompletionResponse<Interaction> {
468    type Error = CompletionError;
469
470    fn try_from(response: Interaction) -> Result<Self, Self::Error> {
471        if response.outputs.is_empty() {
472            let status = response.status.as_ref().map(|status| format!("{status:?}"));
473            let message = match status {
474                Some(status) => format!(
475                    "Interaction contained no outputs (status: {status}). Use get_interaction for background tasks."
476                ),
477                None => "Interaction contained no outputs".to_string(),
478            };
479            return Err(CompletionError::ResponseError(message));
480        }
481
482        let content = response
483            .outputs
484            .iter()
485            .cloned()
486            .filter_map(|output| match assistant_content_from_output(output) {
487                Ok(Some(content)) => Some(Ok(content)),
488                Ok(None) => None,
489                Err(err) => Some(Err(err)),
490            })
491            .collect::<Result<Vec<_>, _>>()?;
492
493        let choice = OneOrMany::many(content).map_err(|_| {
494            CompletionError::ResponseError(
495                "Response contained no message or tool call (empty)".to_owned(),
496            )
497        })?;
498
499        let usage = response
500            .usage
501            .as_ref()
502            .and_then(|usage| usage.token_usage())
503            .unwrap_or_default();
504
505        Ok(completion::CompletionResponse {
506            choice,
507            usage,
508            raw_response: response,
509            message_id: None,
510        })
511    }
512}
513
514fn assistant_content_from_output(
515    output: Content,
516) -> Result<Option<completion::AssistantContent>, CompletionError> {
517    match output {
518        Content::Text(TextContent { text, .. }) => {
519            Ok(Some(completion::AssistantContent::text(text)))
520        }
521        Content::FunctionCall(FunctionCallContent {
522            name,
523            arguments,
524            id,
525            ..
526        }) => {
527            let Some(name) = name else {
528                return Ok(None);
529            };
530            let call_id = id.unwrap_or_else(|| name.clone());
531            Ok(Some(completion::AssistantContent::tool_call_with_call_id(
532                name.clone(),
533                call_id,
534                name,
535                arguments.unwrap_or(Value::Object(Map::new())),
536            )))
537        }
538        Content::Thought(ThoughtContent {
539            summary, signature, ..
540        }) => {
541            let mut reasoning_content = summary
542                .unwrap_or_default()
543                .into_iter()
544                .filter_map(|content| match content {
545                    ThoughtSummaryContent::Text(text) => Some(message::ReasoningContent::Text {
546                        text: text.text,
547                        signature: None,
548                    }),
549                    _ => None,
550                })
551                .collect::<Vec<_>>();
552
553            if reasoning_content.is_empty() {
554                return Ok(None);
555            }
556
557            if let Some(signature) = signature
558                && let Some(message::ReasoningContent::Text {
559                    signature: first_signature,
560                    ..
561                }) = reasoning_content
562                    .iter_mut()
563                    .find(|content| matches!(content, message::ReasoningContent::Text { .. }))
564            {
565                *first_signature = Some(signature);
566            }
567
568            Ok(Some(completion::AssistantContent::Reasoning(Reasoning {
569                id: None,
570                content: reasoning_content,
571            })))
572        }
573        Content::Image(ImageContent {
574            data,
575            uri,
576            mime_type,
577            ..
578        }) => {
579            let Some(mime_type) = mime_type else {
580                return Err(CompletionError::ResponseError(
581                    "Image output missing mime_type".to_owned(),
582                ));
583            };
584
585            let media_type =
586                message::ImageMediaType::from_mime_type(&mime_type).ok_or_else(|| {
587                    CompletionError::ResponseError(format!(
588                        "Unsupported image output mime type {mime_type}"
589                    ))
590                })?;
591
592            let image = if let Some(data) = data {
593                message::AssistantContent::image_base64(
594                    data,
595                    Some(media_type),
596                    Some(message::ImageDetail::default()),
597                )
598            } else if let Some(uri) = uri {
599                completion::AssistantContent::Image(message::Image {
600                    data: message::DocumentSourceKind::Url(uri),
601                    media_type: Some(media_type),
602                    detail: Some(message::ImageDetail::default()),
603                    additional_params: None,
604                })
605            } else {
606                return Err(CompletionError::ResponseError(
607                    "Image output missing data or uri".to_owned(),
608                ));
609            };
610
611            Ok(Some(image))
612        }
613        _ => Ok(None),
614    }
615}
616
617fn split_data_uri(
618    src: message::DocumentSourceKind,
619) -> Result<(Option<String>, Option<String>), message::MessageError> {
620    match src {
621        message::DocumentSourceKind::Url(uri) => Ok((None, Some(uri))),
622        message::DocumentSourceKind::Base64(data) | message::DocumentSourceKind::String(data) => {
623            Ok((Some(data), None))
624        }
625        message::DocumentSourceKind::Raw(_) => Err(message::MessageError::ConversionError(
626            "Raw content is not supported, encode as base64 first".to_string(),
627        )),
628        message::DocumentSourceKind::Unknown => Err(message::MessageError::ConversionError(
629            "Unknown content source".to_string(),
630        )),
631    }
632}
633
634/// Raw request/response types and convenience helpers for the Gemini Interactions API.
635pub mod interactions_api_types {
636    use super::split_data_uri;
637    use crate::completion::{CompletionError, GetTokenUsage, Usage};
638    use crate::message::{self, MimeType};
639    use crate::telemetry::ProviderResponseExt;
640    use serde::{Deserialize, Serialize};
641    use serde_json::{Value, json};
642
643    // =================================================================
644    // Request / Response Types
645    // =================================================================
646
647    /// Optional parameters for creating an interaction.
648    #[derive(Debug, Deserialize, Serialize, Default, Clone)]
649    #[serde(rename_all = "snake_case")]
650    pub struct AdditionalParameters {
651        pub agent: Option<String>,
652        pub agent_config: Option<AgentConfig>,
653        pub background: Option<bool>,
654        pub generation_config: Option<GenerationConfig>,
655        pub previous_interaction_id: Option<String>,
656        pub response_modalities: Option<Vec<ResponseModality>>,
657        pub response_format: Option<Value>,
658        pub response_mime_type: Option<String>,
659        pub store: Option<bool>,
660        pub stream: Option<bool>,
661        pub system_instruction: Option<String>,
662        pub tools: Option<Vec<Tool>>,
663        #[serde(flatten, skip_serializing_if = "Option::is_none")]
664        pub additional_params: Option<Value>,
665    }
666
667    /// Request body for the create interaction endpoint.
668    #[derive(Debug, Deserialize, Serialize, Clone)]
669    #[serde(rename_all = "snake_case")]
670    pub struct CreateInteractionRequest {
671        #[serde(skip_serializing_if = "Option::is_none")]
672        pub model: Option<String>,
673        #[serde(skip_serializing_if = "Option::is_none")]
674        pub agent: Option<String>,
675        pub input: InteractionInput,
676        #[serde(skip_serializing_if = "Option::is_none")]
677        pub system_instruction: Option<String>,
678        #[serde(skip_serializing_if = "Option::is_none")]
679        pub tools: Option<Vec<Tool>>,
680        #[serde(skip_serializing_if = "Option::is_none")]
681        pub response_format: Option<Value>,
682        #[serde(skip_serializing_if = "Option::is_none")]
683        pub response_mime_type: Option<String>,
684        #[serde(skip_serializing_if = "Option::is_none")]
685        pub stream: Option<bool>,
686        #[serde(skip_serializing_if = "Option::is_none")]
687        pub store: Option<bool>,
688        #[serde(skip_serializing_if = "Option::is_none")]
689        pub background: Option<bool>,
690        #[serde(skip_serializing_if = "Option::is_none")]
691        pub generation_config: Option<GenerationConfig>,
692        #[serde(skip_serializing_if = "Option::is_none")]
693        pub agent_config: Option<AgentConfig>,
694        #[serde(skip_serializing_if = "Option::is_none")]
695        pub response_modalities: Option<Vec<ResponseModality>>,
696        #[serde(skip_serializing_if = "Option::is_none")]
697        pub previous_interaction_id: Option<String>,
698        #[serde(flatten, skip_serializing_if = "Option::is_none")]
699        pub additional_params: Option<Value>,
700    }
701
702    /// Interaction response payload.
703    #[derive(Clone, Debug, Deserialize, Serialize, Default)]
704    #[serde(rename_all = "snake_case")]
705    pub struct Interaction {
706        #[serde(default)]
707        pub id: String,
708        #[serde(skip_serializing_if = "Option::is_none")]
709        pub model: Option<String>,
710        #[serde(skip_serializing_if = "Option::is_none")]
711        pub agent: Option<String>,
712        #[serde(skip_serializing_if = "Option::is_none")]
713        pub status: Option<InteractionStatus>,
714        #[serde(skip_serializing_if = "Option::is_none")]
715        pub object: Option<String>,
716        #[serde(skip_serializing_if = "Option::is_none")]
717        pub created: Option<String>,
718        #[serde(skip_serializing_if = "Option::is_none")]
719        pub updated: Option<String>,
720        #[serde(skip_serializing_if = "Option::is_none")]
721        pub role: Option<String>,
722        #[serde(default)]
723        pub outputs: Vec<Content>,
724        #[serde(skip_serializing_if = "Option::is_none")]
725        pub usage: Option<InteractionUsage>,
726        #[serde(skip_serializing_if = "Option::is_none")]
727        pub system_instruction: Option<String>,
728        #[serde(skip_serializing_if = "Option::is_none")]
729        pub tools: Option<Vec<Tool>>,
730        #[serde(skip_serializing_if = "Option::is_none")]
731        pub background: Option<bool>,
732        #[serde(skip_serializing_if = "Option::is_none")]
733        pub response_modalities: Option<Vec<ResponseModality>>,
734        #[serde(skip_serializing_if = "Option::is_none")]
735        pub response_format: Option<Value>,
736        #[serde(skip_serializing_if = "Option::is_none")]
737        pub response_mime_type: Option<String>,
738        #[serde(skip_serializing_if = "Option::is_none")]
739        pub previous_interaction_id: Option<String>,
740        #[serde(skip_serializing_if = "Option::is_none")]
741        pub input: Option<InteractionInput>,
742    }
743
744    impl GetTokenUsage for Interaction {
745        fn token_usage(&self) -> Option<Usage> {
746            self.usage.as_ref().and_then(|usage| usage.token_usage())
747        }
748    }
749
750    impl ProviderResponseExt for Interaction {
751        type OutputMessage = Content;
752        type Usage = InteractionUsage;
753
754        fn get_response_id(&self) -> Option<String> {
755            if self.id.is_empty() {
756                None
757            } else {
758                Some(self.id.clone())
759            }
760        }
761
762        fn get_response_model_name(&self) -> Option<String> {
763            self.model.clone()
764        }
765
766        fn get_output_messages(&self) -> Vec<Self::OutputMessage> {
767            self.outputs.clone()
768        }
769
770        fn get_text_response(&self) -> Option<String> {
771            let text = self
772                .outputs
773                .iter()
774                .filter_map(|content| match content {
775                    Content::Text(text) => Some(text.text.clone()),
776                    _ => None,
777                })
778                .collect::<Vec<_>>()
779                .join("\n");
780
781            if text.is_empty() { None } else { Some(text) }
782        }
783
784        fn get_usage(&self) -> Option<Self::Usage> {
785            self.usage.clone()
786        }
787    }
788
789    /// Groups Google Search tool calls and results for a single interaction.
790    #[derive(Clone, Debug, Default)]
791    pub struct GoogleSearchExchange {
792        /// Call identifier used to match calls to results.
793        pub call_id: Option<String>,
794        /// One or more Google Search tool calls.
795        pub calls: Vec<GoogleSearchCallContent>,
796        /// One or more Google Search tool results.
797        pub results: Vec<GoogleSearchResultContent>,
798    }
799
800    impl GoogleSearchExchange {
801        /// Collects all queries from the stored Google Search tool calls.
802        pub fn queries(&self) -> Vec<String> {
803            let mut queries = Vec::new();
804            for call in &self.calls {
805                if let Some(args) = &call.arguments
806                    && let Some(call_queries) = &args.queries
807                {
808                    queries.extend(call_queries.clone());
809                }
810            }
811            queries
812        }
813
814        /// Collects all Google Search result entries from tool results.
815        pub fn result_items(&self) -> Vec<GoogleSearchResult> {
816            let mut items = Vec::new();
817            for result in &self.results {
818                if let Some(entries) = &result.result {
819                    items.extend(entries.clone());
820                }
821            }
822            items
823        }
824    }
825
826    /// Groups URL context tool calls and results for a single interaction.
827    #[derive(Clone, Debug, Default)]
828    pub struct UrlContextExchange {
829        /// Call identifier used to match calls to results.
830        pub call_id: Option<String>,
831        /// One or more URL context tool calls.
832        pub calls: Vec<UrlContextCallContent>,
833        /// One or more URL context tool results.
834        pub results: Vec<UrlContextResultContent>,
835    }
836
837    impl UrlContextExchange {
838        /// Collects all URLs from the stored URL context tool calls.
839        pub fn urls(&self) -> Vec<String> {
840            let mut urls = Vec::new();
841            for call in &self.calls {
842                if let Some(args) = &call.arguments
843                    && let Some(call_urls) = &args.urls
844                {
845                    urls.extend(call_urls.clone());
846                }
847            }
848            urls
849        }
850
851        /// Collects all URL context result entries from tool results.
852        pub fn result_items(&self) -> Vec<UrlContextResult> {
853            let mut items = Vec::new();
854            for result in &self.results {
855                if let Some(entries) = &result.result {
856                    items.extend(entries.clone());
857                }
858            }
859            items
860        }
861    }
862
863    /// Groups code execution tool calls and results for a single interaction.
864    #[derive(Clone, Debug, Default)]
865    pub struct CodeExecutionExchange {
866        /// Call identifier used to match calls to results.
867        pub call_id: Option<String>,
868        /// One or more code execution tool calls.
869        pub calls: Vec<CodeExecutionCallContent>,
870        /// One or more code execution tool results.
871        pub results: Vec<CodeExecutionResultContent>,
872    }
873
874    impl CodeExecutionExchange {
875        /// Collects all code snippets from the stored code execution tool calls.
876        pub fn code_snippets(&self) -> Vec<String> {
877            let mut snippets = Vec::new();
878            for call in &self.calls {
879                if let Some(args) = &call.arguments
880                    && let Some(code) = &args.code
881                {
882                    snippets.push(code.clone());
883                }
884            }
885            snippets
886        }
887
888        /// Collects all code execution outputs from tool results.
889        pub fn outputs(&self) -> Vec<String> {
890            let mut outputs = Vec::new();
891            for result in &self.results {
892                if let Some(output) = &result.result {
893                    outputs.push(output.clone());
894                }
895            }
896            outputs
897        }
898    }
899
900    impl Interaction {
901        /// Groups Google Search tool calls and results by call_id.
902        ///
903        /// When a call_id is missing, results are grouped with the most recent
904        /// call (identified or not) as a best-effort fallback.
905        pub fn google_search_exchanges(&self) -> Vec<GoogleSearchExchange> {
906            let mut exchanges: Vec<GoogleSearchExchange> = Vec::new();
907            let mut last_call_index: Option<usize> = None;
908
909            for content in &self.outputs {
910                match content {
911                    Content::GoogleSearchCall(call) => {
912                        let index = if let Some(call_id) = call.id.as_ref() {
913                            if let Some(index) = exchanges
914                                .iter()
915                                .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
916                            {
917                                exchanges[index].calls.push(call.clone());
918                                index
919                            } else {
920                                exchanges.push(GoogleSearchExchange {
921                                    call_id: Some(call_id.clone()),
922                                    calls: vec![call.clone()],
923                                    results: Vec::new(),
924                                });
925                                exchanges.len() - 1
926                            }
927                        } else {
928                            exchanges.push(GoogleSearchExchange {
929                                call_id: None,
930                                calls: vec![call.clone()],
931                                results: Vec::new(),
932                            });
933                            exchanges.len() - 1
934                        };
935                        last_call_index = Some(index);
936                    }
937                    Content::GoogleSearchResult(result) => {
938                        if let Some(call_id) = result.call_id.as_ref() {
939                            if let Some(index) = exchanges
940                                .iter()
941                                .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
942                            {
943                                exchanges[index].results.push(result.clone());
944                            } else {
945                                exchanges.push(GoogleSearchExchange {
946                                    call_id: Some(call_id.clone()),
947                                    calls: Vec::new(),
948                                    results: vec![result.clone()],
949                                });
950                            }
951                        } else if let Some(index) = last_call_index {
952                            exchanges[index].results.push(result.clone());
953                        } else {
954                            exchanges.push(GoogleSearchExchange {
955                                call_id: None,
956                                calls: Vec::new(),
957                                results: vec![result.clone()],
958                            });
959                            last_call_index = Some(exchanges.len() - 1);
960                        }
961                    }
962                    _ => {}
963                }
964            }
965
966            exchanges
967        }
968
969        /// Collects Google Search tool call contents from the interaction outputs.
970        pub fn google_search_call_contents(&self) -> Vec<GoogleSearchCallContent> {
971            self.google_search_exchanges()
972                .into_iter()
973                .flat_map(|exchange| exchange.calls)
974                .collect()
975        }
976
977        /// Collects Google Search result contents from the interaction outputs.
978        pub fn google_search_result_contents(&self) -> Vec<GoogleSearchResultContent> {
979            self.google_search_exchanges()
980                .into_iter()
981                .flat_map(|exchange| exchange.results)
982                .collect()
983        }
984
985        /// Collects all Google Search queries from tool calls in the outputs.
986        pub fn google_search_queries(&self) -> Vec<String> {
987            self.google_search_exchanges()
988                .into_iter()
989                .flat_map(|exchange| exchange.queries())
990                .collect()
991        }
992
993        /// Collects all Google Search result entries from tool results in the outputs.
994        pub fn google_search_results(&self) -> Vec<GoogleSearchResult> {
995            self.google_search_exchanges()
996                .into_iter()
997                .flat_map(|exchange| exchange.result_items())
998                .collect()
999        }
1000
1001        /// Groups URL context tool calls and results by call_id.
1002        ///
1003        /// When a call_id is missing, results are grouped with the most recent
1004        /// call (identified or not) as a best-effort fallback.
1005        pub fn url_context_exchanges(&self) -> Vec<UrlContextExchange> {
1006            let mut exchanges: Vec<UrlContextExchange> = Vec::new();
1007            let mut last_call_index: Option<usize> = None;
1008
1009            for content in &self.outputs {
1010                match content {
1011                    Content::UrlContextCall(call) => {
1012                        let index = if let Some(call_id) = call.id.as_ref() {
1013                            if let Some(index) = exchanges
1014                                .iter()
1015                                .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1016                            {
1017                                exchanges[index].calls.push(call.clone());
1018                                index
1019                            } else {
1020                                exchanges.push(UrlContextExchange {
1021                                    call_id: Some(call_id.clone()),
1022                                    calls: vec![call.clone()],
1023                                    results: Vec::new(),
1024                                });
1025                                exchanges.len() - 1
1026                            }
1027                        } else {
1028                            exchanges.push(UrlContextExchange {
1029                                call_id: None,
1030                                calls: vec![call.clone()],
1031                                results: Vec::new(),
1032                            });
1033                            exchanges.len() - 1
1034                        };
1035                        last_call_index = Some(index);
1036                    }
1037                    Content::UrlContextResult(result) => {
1038                        if let Some(call_id) = result.call_id.as_ref() {
1039                            if let Some(index) = exchanges
1040                                .iter()
1041                                .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1042                            {
1043                                exchanges[index].results.push(result.clone());
1044                            } else {
1045                                exchanges.push(UrlContextExchange {
1046                                    call_id: Some(call_id.clone()),
1047                                    calls: Vec::new(),
1048                                    results: vec![result.clone()],
1049                                });
1050                            }
1051                        } else if let Some(index) = last_call_index {
1052                            exchanges[index].results.push(result.clone());
1053                        } else {
1054                            exchanges.push(UrlContextExchange {
1055                                call_id: None,
1056                                calls: Vec::new(),
1057                                results: vec![result.clone()],
1058                            });
1059                            last_call_index = Some(exchanges.len() - 1);
1060                        }
1061                    }
1062                    _ => {}
1063                }
1064            }
1065
1066            exchanges
1067        }
1068
1069        /// Collects URL context tool call contents from the interaction outputs.
1070        pub fn url_context_call_contents(&self) -> Vec<UrlContextCallContent> {
1071            self.url_context_exchanges()
1072                .into_iter()
1073                .flat_map(|exchange| exchange.calls)
1074                .collect()
1075        }
1076
1077        /// Collects URL context result contents from the interaction outputs.
1078        pub fn url_context_result_contents(&self) -> Vec<UrlContextResultContent> {
1079            self.url_context_exchanges()
1080                .into_iter()
1081                .flat_map(|exchange| exchange.results)
1082                .collect()
1083        }
1084
1085        /// Collects all URLs from URL context tool calls in the outputs.
1086        pub fn url_context_urls(&self) -> Vec<String> {
1087            self.url_context_exchanges()
1088                .into_iter()
1089                .flat_map(|exchange| exchange.urls())
1090                .collect()
1091        }
1092
1093        /// Collects all URL context result entries from tool results in the outputs.
1094        pub fn url_context_results(&self) -> Vec<UrlContextResult> {
1095            self.url_context_exchanges()
1096                .into_iter()
1097                .flat_map(|exchange| exchange.result_items())
1098                .collect()
1099        }
1100
1101        /// Groups code execution tool calls and results by call_id.
1102        ///
1103        /// When a call_id is missing, results are grouped with the most recent
1104        /// call (identified or not) as a best-effort fallback.
1105        pub fn code_execution_exchanges(&self) -> Vec<CodeExecutionExchange> {
1106            let mut exchanges: Vec<CodeExecutionExchange> = Vec::new();
1107            let mut last_call_index: Option<usize> = None;
1108
1109            for content in &self.outputs {
1110                match content {
1111                    Content::CodeExecutionCall(call) => {
1112                        let index = if let Some(call_id) = call.id.as_ref() {
1113                            if let Some(index) = exchanges
1114                                .iter()
1115                                .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1116                            {
1117                                exchanges[index].calls.push(call.clone());
1118                                index
1119                            } else {
1120                                exchanges.push(CodeExecutionExchange {
1121                                    call_id: Some(call_id.clone()),
1122                                    calls: vec![call.clone()],
1123                                    results: Vec::new(),
1124                                });
1125                                exchanges.len() - 1
1126                            }
1127                        } else {
1128                            exchanges.push(CodeExecutionExchange {
1129                                call_id: None,
1130                                calls: vec![call.clone()],
1131                                results: Vec::new(),
1132                            });
1133                            exchanges.len() - 1
1134                        };
1135                        last_call_index = Some(index);
1136                    }
1137                    Content::CodeExecutionResult(result) => {
1138                        if let Some(call_id) = result.call_id.as_ref() {
1139                            if let Some(index) = exchanges
1140                                .iter()
1141                                .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
1142                            {
1143                                exchanges[index].results.push(result.clone());
1144                            } else {
1145                                exchanges.push(CodeExecutionExchange {
1146                                    call_id: Some(call_id.clone()),
1147                                    calls: Vec::new(),
1148                                    results: vec![result.clone()],
1149                                });
1150                            }
1151                        } else if let Some(index) = last_call_index {
1152                            exchanges[index].results.push(result.clone());
1153                        } else {
1154                            exchanges.push(CodeExecutionExchange {
1155                                call_id: None,
1156                                calls: Vec::new(),
1157                                results: vec![result.clone()],
1158                            });
1159                            last_call_index = Some(exchanges.len() - 1);
1160                        }
1161                    }
1162                    _ => {}
1163                }
1164            }
1165
1166            exchanges
1167        }
1168
1169        /// Collects code execution tool call contents from the interaction outputs.
1170        pub fn code_execution_call_contents(&self) -> Vec<CodeExecutionCallContent> {
1171            self.code_execution_exchanges()
1172                .into_iter()
1173                .flat_map(|exchange| exchange.calls)
1174                .collect()
1175        }
1176
1177        /// Collects code execution result contents from the interaction outputs.
1178        pub fn code_execution_result_contents(&self) -> Vec<CodeExecutionResultContent> {
1179            self.code_execution_exchanges()
1180                .into_iter()
1181                .flat_map(|exchange| exchange.results)
1182                .collect()
1183        }
1184
1185        /// Collects all code snippets from code execution calls in the outputs.
1186        pub fn code_execution_snippets(&self) -> Vec<String> {
1187            self.code_execution_exchanges()
1188                .into_iter()
1189                .flat_map(|exchange| exchange.code_snippets())
1190                .collect()
1191        }
1192
1193        /// Collects all code execution outputs from tool results in the outputs.
1194        pub fn code_execution_outputs(&self) -> Vec<String> {
1195            self.code_execution_exchanges()
1196                .into_iter()
1197                .flat_map(|exchange| exchange.outputs())
1198                .collect()
1199        }
1200
1201        /// Returns concatenated text outputs with inline citations appended.
1202        pub fn text_with_inline_citations(&self) -> Option<String> {
1203            let text = self
1204                .outputs
1205                .iter()
1206                .filter_map(|content| match content {
1207                    Content::Text(text) => Some(text.with_inline_citations()),
1208                    _ => None,
1209                })
1210                .collect::<Vec<_>>()
1211                .join("\n");
1212
1213            if text.is_empty() { None } else { Some(text) }
1214        }
1215
1216        /// Returns true when the interaction is in a terminal state.
1217        pub fn is_terminal(&self) -> bool {
1218            self.status
1219                .as_ref()
1220                .is_some_and(InteractionStatus::is_terminal)
1221        }
1222
1223        /// Returns true when the interaction completed successfully.
1224        pub fn is_completed(&self) -> bool {
1225            matches!(self.status, Some(InteractionStatus::Completed))
1226        }
1227    }
1228
1229    /// Lifecycle status of an interaction.
1230    #[derive(Clone, Debug, Deserialize, Serialize)]
1231    #[serde(rename_all = "snake_case")]
1232    pub enum InteractionStatus {
1233        InProgress,
1234        RequiresAction,
1235        Completed,
1236        Failed,
1237        Cancelled,
1238    }
1239
1240    impl InteractionStatus {
1241        /// Returns true if the status is terminal.
1242        pub fn is_terminal(&self) -> bool {
1243            matches!(
1244                self,
1245                InteractionStatus::Completed
1246                    | InteractionStatus::Failed
1247                    | InteractionStatus::Cancelled
1248            )
1249        }
1250    }
1251
1252    /// Token usage metadata for an interaction.
1253    #[derive(Clone, Debug, Deserialize, Serialize, Default)]
1254    #[serde(rename_all = "snake_case")]
1255    pub struct InteractionUsage {
1256        #[serde(skip_serializing_if = "Option::is_none")]
1257        pub total_input_tokens: Option<u64>,
1258        #[serde(skip_serializing_if = "Option::is_none")]
1259        pub total_output_tokens: Option<u64>,
1260        #[serde(skip_serializing_if = "Option::is_none")]
1261        pub total_tokens: Option<u64>,
1262    }
1263
1264    impl GetTokenUsage for InteractionUsage {
1265        fn token_usage(&self) -> Option<Usage> {
1266            let mut usage = Usage::new();
1267            usage.input_tokens = self.total_input_tokens.unwrap_or_default();
1268            usage.output_tokens = self.total_output_tokens.unwrap_or_default();
1269            usage.total_tokens = self
1270                .total_tokens
1271                .unwrap_or(usage.input_tokens + usage.output_tokens);
1272            Some(usage)
1273        }
1274    }
1275
1276    /// Input payload accepted by the Interactions API.
1277    #[derive(Clone, Debug, Deserialize, Serialize)]
1278    #[serde(untagged)]
1279    pub enum InteractionInput {
1280        Text(String),
1281        Content(Content),
1282        Turns(Vec<Turn>),
1283        Contents(Vec<Content>),
1284    }
1285
1286    /// Role for a conversation turn.
1287    #[derive(Clone, Debug, Deserialize, Serialize)]
1288    #[serde(rename_all = "lowercase")]
1289    pub enum Role {
1290        User,
1291        Model,
1292    }
1293
1294    /// Single conversational turn with role and content.
1295    #[derive(Clone, Debug, Deserialize, Serialize)]
1296    pub struct Turn {
1297        pub role: Role,
1298        pub content: TurnContent,
1299    }
1300
1301    /// Content for a single turn.
1302    #[derive(Clone, Debug, Deserialize, Serialize)]
1303    #[serde(untagged)]
1304    pub enum TurnContent {
1305        Text(String),
1306        Contents(Vec<Content>),
1307    }
1308
1309    impl TryFrom<crate::completion::Message> for Turn {
1310        type Error = message::MessageError;
1311
1312        fn try_from(message: crate::completion::Message) -> Result<Self, Self::Error> {
1313            match message {
1314                crate::completion::Message::System { content } => Ok(Self {
1315                    role: Role::User,
1316                    content: TurnContent::Text(content),
1317                }),
1318                crate::completion::Message::User { content } => {
1319                    let contents = content
1320                        .into_iter()
1321                        .map(Content::try_from)
1322                        .collect::<Result<Vec<_>, _>>()?;
1323                    Ok(Self {
1324                        role: Role::User,
1325                        content: TurnContent::Contents(contents),
1326                    })
1327                }
1328                crate::completion::Message::Assistant { content, .. } => {
1329                    let contents = content
1330                        .into_iter()
1331                        .map(Content::try_from)
1332                        .collect::<Result<Vec<_>, _>>()?;
1333                    Ok(Self {
1334                        role: Role::Model,
1335                        content: TurnContent::Contents(contents),
1336                    })
1337                }
1338            }
1339        }
1340    }
1341
1342    // =================================================================
1343    // Content
1344    // =================================================================
1345
1346    /// Text annotation metadata for citations.
1347    #[derive(Clone, Debug, Deserialize, Serialize)]
1348    pub struct Annotation {
1349        #[serde(skip_serializing_if = "Option::is_none")]
1350        pub start_index: Option<i64>,
1351        #[serde(skip_serializing_if = "Option::is_none")]
1352        pub end_index: Option<i64>,
1353        #[serde(skip_serializing_if = "Option::is_none")]
1354        pub source: Option<String>,
1355    }
1356
1357    /// Normalized citation extracted from an annotation.
1358    #[derive(Clone, Debug)]
1359    pub struct Citation {
1360        pub start_index: usize,
1361        pub end_index: usize,
1362        pub source: String,
1363    }
1364
1365    /// Text content item.
1366    #[derive(Clone, Debug, Deserialize, Serialize)]
1367    pub struct TextContent {
1368        pub text: String,
1369        #[serde(skip_serializing_if = "Option::is_none")]
1370        pub annotations: Option<Vec<Annotation>>,
1371    }
1372
1373    impl TextContent {
1374        /// Collects citations extracted from annotations.
1375        pub fn citations(&self) -> Vec<Citation> {
1376            let mut citations = Vec::new();
1377            let Some(annotations) = self.annotations.as_ref() else {
1378                return citations;
1379            };
1380
1381            for annotation in annotations {
1382                let (Some(start), Some(end), Some(source)) = (
1383                    annotation.start_index,
1384                    annotation.end_index,
1385                    annotation.source.as_ref(),
1386                ) else {
1387                    continue;
1388                };
1389
1390                if start < 0 || end < 0 {
1391                    continue;
1392                }
1393                let start = start as usize;
1394                let end = end as usize;
1395                if end <= start || end > self.text.len() {
1396                    continue;
1397                }
1398                if !self.text.is_char_boundary(start) || !self.text.is_char_boundary(end) {
1399                    continue;
1400                }
1401
1402                citations.push(Citation {
1403                    start_index: start,
1404                    end_index: end,
1405                    source: source.clone(),
1406                });
1407            }
1408
1409            citations.sort_by(|a, b| {
1410                a.start_index
1411                    .cmp(&b.start_index)
1412                    .then_with(|| a.end_index.cmp(&b.end_index))
1413            });
1414
1415            citations
1416        }
1417
1418        /// Returns the text with inline citations appended after annotated spans.
1419        pub fn with_inline_citations(&self) -> String {
1420            let citations = self.citations();
1421            if citations.is_empty() {
1422                return self.text.clone();
1423            }
1424
1425            let mut source_order = Vec::new();
1426            for citation in &citations {
1427                if !source_order.contains(&citation.source) {
1428                    source_order.push(citation.source.clone());
1429                }
1430            }
1431
1432            let mut inserts = citations
1433                .iter()
1434                .map(|citation| {
1435                    let index = source_order
1436                        .iter()
1437                        .position(|source| source == &citation.source)
1438                        .map(|idx| idx + 1)
1439                        .unwrap_or(0);
1440                    (
1441                        citation.start_index,
1442                        citation.end_index,
1443                        index,
1444                        &citation.source,
1445                    )
1446                })
1447                .collect::<Vec<_>>();
1448
1449            inserts.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| b.0.cmp(&a.0)));
1450
1451            let mut text = self.text.clone();
1452            for (_, end, index, source) in inserts {
1453                if index == 0 {
1454                    continue;
1455                }
1456                let citation = format!("[{}]({})", index, source);
1457                text.insert_str(end, &citation);
1458            }
1459
1460            text
1461        }
1462    }
1463
1464    /// Image content item.
1465    #[derive(Clone, Debug, Deserialize, Serialize)]
1466    pub struct ImageContent {
1467        #[serde(skip_serializing_if = "Option::is_none")]
1468        pub data: Option<String>,
1469        #[serde(skip_serializing_if = "Option::is_none")]
1470        pub uri: Option<String>,
1471        #[serde(skip_serializing_if = "Option::is_none")]
1472        pub mime_type: Option<String>,
1473        #[serde(skip_serializing_if = "Option::is_none")]
1474        pub resolution: Option<MediaResolution>,
1475    }
1476
1477    /// Audio content item.
1478    #[derive(Clone, Debug, Deserialize, Serialize)]
1479    pub struct AudioContent {
1480        #[serde(skip_serializing_if = "Option::is_none")]
1481        pub data: Option<String>,
1482        #[serde(skip_serializing_if = "Option::is_none")]
1483        pub uri: Option<String>,
1484        #[serde(skip_serializing_if = "Option::is_none")]
1485        pub mime_type: Option<String>,
1486    }
1487
1488    /// Document content item.
1489    #[derive(Clone, Debug, Deserialize, Serialize)]
1490    pub struct DocumentContent {
1491        #[serde(skip_serializing_if = "Option::is_none")]
1492        pub data: Option<String>,
1493        #[serde(skip_serializing_if = "Option::is_none")]
1494        pub uri: Option<String>,
1495        #[serde(skip_serializing_if = "Option::is_none")]
1496        pub mime_type: Option<String>,
1497    }
1498
1499    /// Video content item.
1500    #[derive(Clone, Debug, Deserialize, Serialize)]
1501    pub struct VideoContent {
1502        #[serde(skip_serializing_if = "Option::is_none")]
1503        pub data: Option<String>,
1504        #[serde(skip_serializing_if = "Option::is_none")]
1505        pub uri: Option<String>,
1506        #[serde(skip_serializing_if = "Option::is_none")]
1507        pub mime_type: Option<String>,
1508        #[serde(skip_serializing_if = "Option::is_none")]
1509        pub resolution: Option<MediaResolution>,
1510    }
1511
1512    /// Thought summary content.
1513    #[derive(Clone, Debug, Deserialize, Serialize)]
1514    pub struct ThoughtContent {
1515        #[serde(skip_serializing_if = "Option::is_none")]
1516        pub signature: Option<String>,
1517        #[serde(skip_serializing_if = "Option::is_none")]
1518        pub summary: Option<Vec<ThoughtSummaryContent>>,
1519    }
1520
1521    /// Thought summary item.
1522    #[derive(Clone, Debug, Deserialize, Serialize)]
1523    #[serde(untagged)]
1524    pub enum ThoughtSummaryContent {
1525        Text(TextContent),
1526        Image(ImageContent),
1527    }
1528
1529    /// Function call content item.
1530    #[derive(Clone, Debug, Deserialize, Serialize)]
1531    pub struct FunctionCallContent {
1532        #[serde(skip_serializing_if = "Option::is_none")]
1533        pub name: Option<String>,
1534        #[serde(skip_serializing_if = "Option::is_none")]
1535        pub arguments: Option<Value>,
1536        #[serde(skip_serializing_if = "Option::is_none")]
1537        pub id: Option<String>,
1538    }
1539
1540    /// Function result content item.
1541    #[derive(Clone, Debug, Deserialize, Serialize)]
1542    pub struct FunctionResultContent {
1543        #[serde(skip_serializing_if = "Option::is_none")]
1544        pub name: Option<String>,
1545        #[serde(skip_serializing_if = "Option::is_none")]
1546        pub is_error: Option<bool>,
1547        #[serde(skip_serializing_if = "Option::is_none")]
1548        pub result: Option<Value>,
1549        #[serde(skip_serializing_if = "Option::is_none")]
1550        pub call_id: Option<String>,
1551    }
1552
1553    /// Arguments for a code execution call.
1554    #[derive(Clone, Debug, Deserialize, Serialize)]
1555    pub struct CodeExecutionCallArguments {
1556        #[serde(skip_serializing_if = "Option::is_none")]
1557        pub language: Option<String>,
1558        #[serde(skip_serializing_if = "Option::is_none")]
1559        pub code: Option<String>,
1560    }
1561
1562    /// Code execution call content item.
1563    #[derive(Clone, Debug, Deserialize, Serialize)]
1564    pub struct CodeExecutionCallContent {
1565        #[serde(skip_serializing_if = "Option::is_none")]
1566        pub arguments: Option<CodeExecutionCallArguments>,
1567        #[serde(skip_serializing_if = "Option::is_none")]
1568        pub id: Option<String>,
1569    }
1570
1571    /// Code execution result content item.
1572    #[derive(Clone, Debug, Deserialize, Serialize)]
1573    pub struct CodeExecutionResultContent {
1574        #[serde(skip_serializing_if = "Option::is_none")]
1575        pub result: Option<String>,
1576        #[serde(skip_serializing_if = "Option::is_none")]
1577        pub is_error: Option<bool>,
1578        #[serde(skip_serializing_if = "Option::is_none")]
1579        pub signature: Option<String>,
1580        #[serde(skip_serializing_if = "Option::is_none")]
1581        pub call_id: Option<String>,
1582    }
1583
1584    /// Arguments for a URL context call.
1585    #[derive(Clone, Debug, Deserialize, Serialize)]
1586    pub struct UrlContextCallArguments {
1587        #[serde(skip_serializing_if = "Option::is_none")]
1588        pub urls: Option<Vec<String>>,
1589    }
1590
1591    /// URL context call content item.
1592    #[derive(Clone, Debug, Deserialize, Serialize)]
1593    pub struct UrlContextCallContent {
1594        #[serde(skip_serializing_if = "Option::is_none")]
1595        pub arguments: Option<UrlContextCallArguments>,
1596        #[serde(skip_serializing_if = "Option::is_none")]
1597        pub id: Option<String>,
1598    }
1599
1600    /// URL context result entry.
1601    #[derive(Clone, Debug, Deserialize, Serialize)]
1602    pub struct UrlContextResult {
1603        #[serde(skip_serializing_if = "Option::is_none")]
1604        pub url: Option<String>,
1605        #[serde(skip_serializing_if = "Option::is_none")]
1606        pub status: Option<String>,
1607    }
1608
1609    /// URL context result content item.
1610    #[derive(Clone, Debug, Deserialize, Serialize)]
1611    pub struct UrlContextResultContent {
1612        #[serde(skip_serializing_if = "Option::is_none")]
1613        pub signature: Option<String>,
1614        #[serde(skip_serializing_if = "Option::is_none")]
1615        pub result: Option<Vec<UrlContextResult>>,
1616        #[serde(skip_serializing_if = "Option::is_none")]
1617        pub is_error: Option<bool>,
1618        #[serde(skip_serializing_if = "Option::is_none")]
1619        pub call_id: Option<String>,
1620    }
1621
1622    /// Arguments for a Google Search call.
1623    #[derive(Clone, Debug, Deserialize, Serialize)]
1624    pub struct GoogleSearchCallArguments {
1625        #[serde(skip_serializing_if = "Option::is_none")]
1626        pub queries: Option<Vec<String>>,
1627    }
1628
1629    /// Google Search call content item.
1630    #[derive(Clone, Debug, Deserialize, Serialize)]
1631    pub struct GoogleSearchCallContent {
1632        #[serde(skip_serializing_if = "Option::is_none")]
1633        pub arguments: Option<GoogleSearchCallArguments>,
1634        #[serde(skip_serializing_if = "Option::is_none")]
1635        pub id: Option<String>,
1636    }
1637
1638    /// Google Search result entry.
1639    #[derive(Clone, Debug, Deserialize, Serialize)]
1640    pub struct GoogleSearchResult {
1641        #[serde(skip_serializing_if = "Option::is_none")]
1642        pub url: Option<String>,
1643        #[serde(skip_serializing_if = "Option::is_none")]
1644        pub title: Option<String>,
1645        #[serde(skip_serializing_if = "Option::is_none")]
1646        pub rendered_content: Option<String>,
1647    }
1648
1649    /// Google Search result content item.
1650    #[derive(Clone, Debug, Deserialize, Serialize)]
1651    pub struct GoogleSearchResultContent {
1652        #[serde(skip_serializing_if = "Option::is_none")]
1653        pub signature: Option<String>,
1654        #[serde(skip_serializing_if = "Option::is_none")]
1655        pub result: Option<Vec<GoogleSearchResult>>,
1656        #[serde(skip_serializing_if = "Option::is_none")]
1657        pub is_error: Option<bool>,
1658        #[serde(skip_serializing_if = "Option::is_none")]
1659        pub call_id: Option<String>,
1660    }
1661
1662    /// MCP server tool call content item.
1663    #[derive(Clone, Debug, Deserialize, Serialize)]
1664    pub struct McpServerToolCallContent {
1665        #[serde(skip_serializing_if = "Option::is_none")]
1666        pub name: Option<String>,
1667        #[serde(skip_serializing_if = "Option::is_none")]
1668        pub server_name: Option<String>,
1669        #[serde(skip_serializing_if = "Option::is_none")]
1670        pub arguments: Option<Value>,
1671        #[serde(skip_serializing_if = "Option::is_none")]
1672        pub id: Option<String>,
1673    }
1674
1675    /// MCP server tool result content item.
1676    #[derive(Clone, Debug, Deserialize, Serialize)]
1677    pub struct McpServerToolResultContent {
1678        #[serde(skip_serializing_if = "Option::is_none")]
1679        pub name: Option<String>,
1680        #[serde(skip_serializing_if = "Option::is_none")]
1681        pub server_name: Option<String>,
1682        #[serde(skip_serializing_if = "Option::is_none")]
1683        pub result: Option<Value>,
1684        #[serde(skip_serializing_if = "Option::is_none")]
1685        pub call_id: Option<String>,
1686    }
1687
1688    /// File search result entry.
1689    #[derive(Clone, Debug, Deserialize, Serialize)]
1690    pub struct FileSearchResult {
1691        pub title: String,
1692        pub text: String,
1693        pub file_search_store: String,
1694    }
1695
1696    /// File search result content item.
1697    #[derive(Clone, Debug, Deserialize, Serialize)]
1698    pub struct FileSearchResultContent {
1699        #[serde(skip_serializing_if = "Option::is_none")]
1700        pub result: Option<Vec<FileSearchResult>>,
1701    }
1702
1703    /// Content item produced or consumed by the Interactions API.
1704    #[derive(Clone, Debug, Deserialize, Serialize)]
1705    #[serde(tag = "type", rename_all = "snake_case")]
1706    pub enum Content {
1707        Text(TextContent),
1708        Image(ImageContent),
1709        Audio(AudioContent),
1710        Document(DocumentContent),
1711        Video(VideoContent),
1712        Thought(ThoughtContent),
1713        FunctionCall(FunctionCallContent),
1714        FunctionResult(FunctionResultContent),
1715        CodeExecutionCall(CodeExecutionCallContent),
1716        CodeExecutionResult(CodeExecutionResultContent),
1717        UrlContextCall(UrlContextCallContent),
1718        UrlContextResult(UrlContextResultContent),
1719        GoogleSearchCall(GoogleSearchCallContent),
1720        GoogleSearchResult(GoogleSearchResultContent),
1721        McpServerToolCall(McpServerToolCallContent),
1722        McpServerToolResult(McpServerToolResultContent),
1723        FileSearchResult(FileSearchResultContent),
1724    }
1725
1726    impl TryFrom<message::UserContent> for Content {
1727        type Error = message::MessageError;
1728
1729        fn try_from(content: message::UserContent) -> Result<Self, Self::Error> {
1730            match content {
1731                message::UserContent::Text(message::Text { text }) => Ok(Self::Text(TextContent {
1732                    text,
1733                    annotations: None,
1734                })),
1735                message::UserContent::ToolResult(message::ToolResult {
1736                    id,
1737                    call_id,
1738                    content,
1739                }) => {
1740                    let Some(call_id) = call_id else {
1741                        return Err(message::MessageError::ConversionError(
1742                            "Tool results require call_id for Gemini Interactions API".to_string(),
1743                        ));
1744                    };
1745
1746                    let content = content.first();
1747
1748                    let message::ToolResultContent::Text(text) = content else {
1749                        return Err(message::MessageError::ConversionError(
1750                            "Tool result content must be text".to_string(),
1751                        ));
1752                    };
1753
1754                    let result: Value = serde_json::from_str(&text.text).unwrap_or_else(|error| {
1755                        tracing::trace!(?error, "Tool result is not valid JSON; sending as string");
1756                        json!(text.text)
1757                    });
1758
1759                    Ok(Self::FunctionResult(FunctionResultContent {
1760                        name: Some(id),
1761                        is_error: None,
1762                        result: Some(result),
1763                        call_id: Some(call_id),
1764                    }))
1765                }
1766                message::UserContent::Image(message::Image {
1767                    data, media_type, ..
1768                }) => {
1769                    let media_type = media_type.ok_or_else(|| {
1770                        message::MessageError::ConversionError(
1771                            "Media type for image is required for Gemini".to_string(),
1772                        )
1773                    })?;
1774                    let mime_type = media_type.to_mime_type().to_string();
1775                    let (data, uri) = split_data_uri(data)?;
1776                    Ok(Self::Image(ImageContent {
1777                        data,
1778                        uri,
1779                        mime_type: Some(mime_type),
1780                        resolution: None,
1781                    }))
1782                }
1783                message::UserContent::Audio(message::Audio {
1784                    data, media_type, ..
1785                }) => {
1786                    let media_type = media_type.ok_or_else(|| {
1787                        message::MessageError::ConversionError(
1788                            "Media type for audio is required for Gemini".to_string(),
1789                        )
1790                    })?;
1791                    let mime_type = media_type.to_mime_type().to_string();
1792                    let (data, uri) = split_data_uri(data)?;
1793                    Ok(Self::Audio(AudioContent {
1794                        data,
1795                        uri,
1796                        mime_type: Some(mime_type),
1797                    }))
1798                }
1799                message::UserContent::Video(message::Video {
1800                    data, media_type, ..
1801                }) => {
1802                    let media_type = media_type.ok_or_else(|| {
1803                        message::MessageError::ConversionError(
1804                            "Media type for video is required for Gemini".to_string(),
1805                        )
1806                    })?;
1807                    let mime_type = media_type.to_mime_type().to_string();
1808                    let (data, uri) = split_data_uri(data)?;
1809                    Ok(Self::Video(VideoContent {
1810                        data,
1811                        uri,
1812                        mime_type: Some(mime_type),
1813                        resolution: None,
1814                    }))
1815                }
1816                message::UserContent::Document(message::Document {
1817                    data, media_type, ..
1818                }) => {
1819                    let media_type = media_type.ok_or_else(|| {
1820                        message::MessageError::ConversionError(
1821                            "Media type for document is required for Gemini".to_string(),
1822                        )
1823                    })?;
1824                    let mime_type = media_type.to_mime_type().to_string();
1825                    let (data, uri) = split_data_uri(data)?;
1826                    Ok(Self::Document(DocumentContent {
1827                        data,
1828                        uri,
1829                        mime_type: Some(mime_type),
1830                    }))
1831                }
1832            }
1833        }
1834    }
1835
1836    impl TryFrom<message::AssistantContent> for Content {
1837        type Error = message::MessageError;
1838
1839        fn try_from(content: message::AssistantContent) -> Result<Self, Self::Error> {
1840            match content {
1841                message::AssistantContent::Text(message::Text { text }) => {
1842                    Ok(Self::Text(TextContent {
1843                        text,
1844                        annotations: None,
1845                    }))
1846                }
1847                message::AssistantContent::ToolCall(tool_call) => {
1848                    let call_id = tool_call.call_id.unwrap_or_else(|| tool_call.id.clone());
1849                    Ok(Self::FunctionCall(FunctionCallContent {
1850                        name: Some(tool_call.function.name),
1851                        arguments: Some(tool_call.function.arguments),
1852                        id: Some(call_id),
1853                    }))
1854                }
1855                message::AssistantContent::Reasoning(message::Reasoning { content, .. }) => {
1856                    let mut signature = None;
1857                    let summary = content
1858                        .into_iter()
1859                        .map(|reasoning_content| {
1860                            let text = match reasoning_content {
1861                                message::ReasoningContent::Text {
1862                                    text,
1863                                    signature: content_signature,
1864                                } => {
1865                                    if signature.is_none() {
1866                                        signature = content_signature;
1867                                    }
1868                                    text
1869                                }
1870                                message::ReasoningContent::Summary(text)
1871                                | message::ReasoningContent::Encrypted(text) => text,
1872                                message::ReasoningContent::Redacted { data } => data,
1873                            };
1874
1875                            ThoughtSummaryContent::Text(TextContent {
1876                                text,
1877                                annotations: None,
1878                            })
1879                        })
1880                        .collect();
1881
1882                    Ok(Self::Thought(ThoughtContent {
1883                        signature,
1884                        summary: Some(summary),
1885                    }))
1886                }
1887                message::AssistantContent::Image(message::Image {
1888                    data, media_type, ..
1889                }) => {
1890                    let media_type = media_type.ok_or_else(|| {
1891                        message::MessageError::ConversionError(
1892                            "Media type for image is required for Gemini".to_string(),
1893                        )
1894                    })?;
1895                    let mime_type = media_type.to_mime_type().to_string();
1896                    let (data, uri) = split_data_uri(data)?;
1897                    Ok(Self::Image(ImageContent {
1898                        data,
1899                        uri,
1900                        mime_type: Some(mime_type),
1901                        resolution: None,
1902                    }))
1903                }
1904            }
1905        }
1906    }
1907
1908    // =================================================================
1909    // Tools / Config
1910    // =================================================================
1911
1912    /// Response modalities supported by the model.
1913    #[derive(Clone, Debug, Deserialize, Serialize)]
1914    #[serde(rename_all = "snake_case")]
1915    pub enum ResponseModality {
1916        Text,
1917        Image,
1918        Audio,
1919    }
1920
1921    /// Thinking depth hint for generation.
1922    #[derive(Clone, Debug, Deserialize, Serialize)]
1923    #[serde(rename_all = "snake_case")]
1924    pub enum ThinkingLevel {
1925        Minimal,
1926        Low,
1927        Medium,
1928        High,
1929    }
1930
1931    /// Thinking summary behavior.
1932    #[derive(Clone, Debug, Deserialize, Serialize)]
1933    #[serde(rename_all = "snake_case")]
1934    pub enum ThinkingSummaries {
1935        Auto,
1936        None,
1937    }
1938
1939    /// Speech synthesis configuration.
1940    #[derive(Clone, Debug, Deserialize, Serialize)]
1941    #[serde(rename_all = "snake_case")]
1942    pub struct SpeechConfig {
1943        #[serde(skip_serializing_if = "Option::is_none")]
1944        pub voice: Option<String>,
1945        #[serde(skip_serializing_if = "Option::is_none")]
1946        pub language: Option<String>,
1947        #[serde(skip_serializing_if = "Option::is_none")]
1948        pub speaker: Option<String>,
1949    }
1950
1951    /// Generation configuration for the Interactions API.
1952    #[derive(Clone, Debug, Deserialize, Serialize, Default)]
1953    #[serde(rename_all = "snake_case")]
1954    pub struct GenerationConfig {
1955        #[serde(skip_serializing_if = "Option::is_none")]
1956        pub temperature: Option<f64>,
1957        #[serde(skip_serializing_if = "Option::is_none")]
1958        pub top_p: Option<f64>,
1959        #[serde(skip_serializing_if = "Option::is_none")]
1960        pub seed: Option<u64>,
1961        #[serde(skip_serializing_if = "Option::is_none")]
1962        pub stop_sequences: Option<Vec<String>>,
1963        #[serde(skip_serializing_if = "Option::is_none")]
1964        pub tool_choice: Option<ToolChoice>,
1965        #[serde(skip_serializing_if = "Option::is_none")]
1966        pub thinking_level: Option<ThinkingLevel>,
1967        #[serde(skip_serializing_if = "Option::is_none")]
1968        pub thinking_summaries: Option<ThinkingSummaries>,
1969        #[serde(skip_serializing_if = "Option::is_none")]
1970        pub max_output_tokens: Option<u64>,
1971        #[serde(skip_serializing_if = "Option::is_none")]
1972        pub speech_config: Option<Vec<SpeechConfig>>,
1973    }
1974
1975    impl GenerationConfig {
1976        /// Returns true when no generation fields are set.
1977        pub fn is_empty(&self) -> bool {
1978            self.temperature.is_none()
1979                && self.top_p.is_none()
1980                && self.seed.is_none()
1981                && self.stop_sequences.is_none()
1982                && self.tool_choice.is_none()
1983                && self.thinking_level.is_none()
1984                && self.thinking_summaries.is_none()
1985                && self.max_output_tokens.is_none()
1986                && self.speech_config.is_none()
1987        }
1988    }
1989
1990    /// Tool selection strategy.
1991    #[derive(Clone, Debug, Deserialize, Serialize)]
1992    #[serde(untagged)]
1993    pub enum ToolChoice {
1994        Type(ToolChoiceType),
1995        Config(ToolChoiceConfig),
1996    }
1997
1998    /// Tool selection mode.
1999    #[derive(Clone, Debug, Deserialize, Serialize)]
2000    #[serde(rename_all = "snake_case")]
2001    pub enum ToolChoiceType {
2002        Auto,
2003        Any,
2004        None,
2005        Validated,
2006    }
2007
2008    /// Tool selection configuration.
2009    #[derive(Clone, Debug, Deserialize, Serialize)]
2010    pub struct ToolChoiceConfig {
2011        pub allowed_tools: AllowedTools,
2012    }
2013
2014    /// Allowed tools for tool selection.
2015    #[derive(Clone, Debug, Deserialize, Serialize)]
2016    pub struct AllowedTools {
2017        #[serde(skip_serializing_if = "Option::is_none")]
2018        pub mode: Option<ToolChoiceType>,
2019        #[serde(skip_serializing_if = "Option::is_none")]
2020        pub tools: Option<Vec<String>>,
2021    }
2022
2023    /// Tool definition for Interactions API.
2024    #[derive(Clone, Debug, Deserialize, Serialize)]
2025    #[serde(tag = "type", rename_all = "snake_case")]
2026    pub enum Tool {
2027        Function(FunctionTool),
2028        GoogleSearch,
2029        CodeExecution,
2030        UrlContext,
2031        ComputerUse(ComputerUseTool),
2032        McpServer(McpServerTool),
2033        FileSearch(FileSearchTool),
2034    }
2035
2036    /// Function tool definition.
2037    #[derive(Clone, Debug, Deserialize, Serialize)]
2038    pub struct FunctionTool {
2039        #[serde(skip_serializing_if = "Option::is_none")]
2040        pub name: Option<String>,
2041        #[serde(skip_serializing_if = "Option::is_none")]
2042        pub description: Option<String>,
2043        #[serde(skip_serializing_if = "Option::is_none")]
2044        pub parameters: Option<Value>,
2045    }
2046
2047    /// Computer use tool configuration.
2048    #[derive(Clone, Debug, Deserialize, Serialize)]
2049    pub struct ComputerUseTool {
2050        #[serde(skip_serializing_if = "Option::is_none")]
2051        pub environment: Option<String>,
2052        #[serde(skip_serializing_if = "Option::is_none")]
2053        pub excluded_predefined_functions: Option<Vec<String>>,
2054    }
2055
2056    /// MCP server tool configuration.
2057    #[derive(Clone, Debug, Deserialize, Serialize)]
2058    pub struct McpServerTool {
2059        #[serde(skip_serializing_if = "Option::is_none")]
2060        pub name: Option<String>,
2061        #[serde(skip_serializing_if = "Option::is_none")]
2062        pub url: Option<String>,
2063        #[serde(skip_serializing_if = "Option::is_none")]
2064        pub headers: Option<Value>,
2065        #[serde(skip_serializing_if = "Option::is_none")]
2066        pub allowed_tools: Option<AllowedTools>,
2067    }
2068
2069    /// File search tool configuration.
2070    #[derive(Clone, Debug, Deserialize, Serialize)]
2071    pub struct FileSearchTool {
2072        #[serde(skip_serializing_if = "Option::is_none")]
2073        pub file_search_store_names: Option<Vec<String>>,
2074        #[serde(skip_serializing_if = "Option::is_none")]
2075        pub top_k: Option<u64>,
2076        #[serde(skip_serializing_if = "Option::is_none")]
2077        pub metadata_filter: Option<String>,
2078    }
2079
2080    impl TryFrom<crate::completion::ToolDefinition> for Tool {
2081        type Error = CompletionError;
2082
2083        fn try_from(tool: crate::completion::ToolDefinition) -> Result<Self, Self::Error> {
2084            Ok(Tool::Function(FunctionTool {
2085                name: Some(tool.name),
2086                description: Some(tool.description),
2087                parameters: Some(tool.parameters),
2088            }))
2089        }
2090    }
2091
2092    impl TryFrom<message::ToolChoice> for ToolChoice {
2093        type Error = CompletionError;
2094
2095        fn try_from(tool_choice: message::ToolChoice) -> Result<Self, Self::Error> {
2096            match tool_choice {
2097                message::ToolChoice::Auto => Ok(ToolChoice::Type(ToolChoiceType::Auto)),
2098                message::ToolChoice::None => Ok(ToolChoice::Type(ToolChoiceType::None)),
2099                message::ToolChoice::Required => Ok(ToolChoice::Type(ToolChoiceType::Any)),
2100                message::ToolChoice::Specific { function_names } => {
2101                    Ok(ToolChoice::Config(ToolChoiceConfig {
2102                        allowed_tools: AllowedTools {
2103                            mode: Some(ToolChoiceType::Validated),
2104                            tools: Some(function_names),
2105                        },
2106                    }))
2107                }
2108            }
2109        }
2110    }
2111
2112    /// Agent configuration for Interactions API.
2113    #[derive(Clone, Debug, Deserialize, Serialize)]
2114    #[serde(tag = "type", rename_all = "kebab-case")]
2115    pub enum AgentConfig {
2116        Dynamic,
2117        DeepResearch {
2118            #[serde(skip_serializing_if = "Option::is_none")]
2119            thinking_summaries: Option<ThinkingSummaries>,
2120        },
2121    }
2122
2123    /// Media resolution hint for multimodal content.
2124    #[derive(Clone, Debug, Deserialize, Serialize)]
2125    #[serde(rename_all = "snake_case")]
2126    pub enum MediaResolution {
2127        Low,
2128        Medium,
2129        High,
2130        UltraHigh,
2131    }
2132
2133    // =================================================================
2134    // Streaming Events
2135    // =================================================================
2136
2137    /// Server-sent event payloads for streaming interactions.
2138    #[derive(Clone, Debug, Deserialize, Serialize)]
2139    #[serde(tag = "event_type")]
2140    pub enum InteractionSseEvent {
2141        #[serde(rename = "interaction.start")]
2142        InteractionStart {
2143            interaction: Interaction,
2144            #[serde(skip_serializing_if = "Option::is_none")]
2145            event_id: Option<String>,
2146        },
2147        #[serde(rename = "interaction.complete")]
2148        InteractionComplete {
2149            interaction: Interaction,
2150            #[serde(skip_serializing_if = "Option::is_none")]
2151            event_id: Option<String>,
2152        },
2153        #[serde(rename = "interaction.status_update")]
2154        InteractionStatusUpdate {
2155            interaction_id: String,
2156            status: InteractionStatus,
2157            #[serde(skip_serializing_if = "Option::is_none")]
2158            event_id: Option<String>,
2159        },
2160        #[serde(rename = "content.start")]
2161        ContentStart {
2162            index: i32,
2163            content: Content,
2164            #[serde(skip_serializing_if = "Option::is_none")]
2165            event_id: Option<String>,
2166        },
2167        #[serde(rename = "content.delta")]
2168        ContentDelta {
2169            index: i32,
2170            delta: ContentDelta,
2171            #[serde(skip_serializing_if = "Option::is_none")]
2172            event_id: Option<String>,
2173        },
2174        #[serde(rename = "content.stop")]
2175        ContentStop {
2176            index: i32,
2177            #[serde(skip_serializing_if = "Option::is_none")]
2178            event_id: Option<String>,
2179        },
2180        #[serde(rename = "error")]
2181        Error {
2182            error: ErrorEvent,
2183            #[serde(skip_serializing_if = "Option::is_none")]
2184            event_id: Option<String>,
2185        },
2186    }
2187
2188    /// Error payload for streaming events.
2189    #[derive(Clone, Debug, Deserialize, Serialize)]
2190    pub struct ErrorEvent {
2191        pub code: String,
2192        pub message: String,
2193    }
2194
2195    /// Content delta item in streaming events.
2196    #[derive(Clone, Debug, Deserialize, Serialize)]
2197    #[serde(tag = "type", rename_all = "snake_case")]
2198    pub enum ContentDelta {
2199        Text(TextDelta),
2200        Image(ImageDelta),
2201        Audio(AudioDelta),
2202        Document(DocumentDelta),
2203        Video(VideoDelta),
2204        ThoughtSummary(ThoughtSummaryDelta),
2205        ThoughtSignature(ThoughtSignatureDelta),
2206        FunctionCall(FunctionCallDelta),
2207        FunctionResult(FunctionResultDelta),
2208        CodeExecutionCall(CodeExecutionCallDelta),
2209        CodeExecutionResult(CodeExecutionResultDelta),
2210        UrlContextCall(UrlContextCallDelta),
2211        UrlContextResult(UrlContextResultDelta),
2212        GoogleSearchCall(GoogleSearchCallDelta),
2213        GoogleSearchResult(GoogleSearchResultDelta),
2214        McpServerToolCall(McpServerToolCallDelta),
2215        McpServerToolResult(McpServerToolResultDelta),
2216        FileSearchResult(FileSearchResultDelta),
2217    }
2218
2219    /// Streaming text delta.
2220    #[derive(Clone, Debug, Deserialize, Serialize)]
2221    pub struct TextDelta {
2222        #[serde(skip_serializing_if = "Option::is_none")]
2223        pub text: Option<String>,
2224        #[serde(skip_serializing_if = "Option::is_none")]
2225        pub annotations: Option<Vec<Annotation>>,
2226    }
2227
2228    /// Streaming image delta.
2229    #[derive(Clone, Debug, Deserialize, Serialize)]
2230    pub struct ImageDelta {
2231        #[serde(skip_serializing_if = "Option::is_none")]
2232        pub data: Option<String>,
2233        #[serde(skip_serializing_if = "Option::is_none")]
2234        pub uri: Option<String>,
2235        #[serde(skip_serializing_if = "Option::is_none")]
2236        pub mime_type: Option<String>,
2237        #[serde(skip_serializing_if = "Option::is_none")]
2238        pub resolution: Option<MediaResolution>,
2239    }
2240
2241    /// Streaming audio delta.
2242    #[derive(Clone, Debug, Deserialize, Serialize)]
2243    pub struct AudioDelta {
2244        #[serde(skip_serializing_if = "Option::is_none")]
2245        pub data: Option<String>,
2246        #[serde(skip_serializing_if = "Option::is_none")]
2247        pub uri: Option<String>,
2248        #[serde(skip_serializing_if = "Option::is_none")]
2249        pub mime_type: Option<String>,
2250    }
2251
2252    /// Streaming document delta.
2253    #[derive(Clone, Debug, Deserialize, Serialize)]
2254    pub struct DocumentDelta {
2255        #[serde(skip_serializing_if = "Option::is_none")]
2256        pub data: Option<String>,
2257        #[serde(skip_serializing_if = "Option::is_none")]
2258        pub uri: Option<String>,
2259        #[serde(skip_serializing_if = "Option::is_none")]
2260        pub mime_type: Option<String>,
2261    }
2262
2263    /// Streaming video delta.
2264    #[derive(Clone, Debug, Deserialize, Serialize)]
2265    pub struct VideoDelta {
2266        #[serde(skip_serializing_if = "Option::is_none")]
2267        pub data: Option<String>,
2268        #[serde(skip_serializing_if = "Option::is_none")]
2269        pub uri: Option<String>,
2270        #[serde(skip_serializing_if = "Option::is_none")]
2271        pub mime_type: Option<String>,
2272        #[serde(skip_serializing_if = "Option::is_none")]
2273        pub resolution: Option<MediaResolution>,
2274    }
2275
2276    /// Streaming thought summary delta.
2277    #[derive(Clone, Debug, Deserialize, Serialize)]
2278    pub struct ThoughtSummaryDelta {
2279        pub content: ThoughtSummaryContent,
2280    }
2281
2282    /// Streaming thought signature delta.
2283    #[derive(Clone, Debug, Deserialize, Serialize)]
2284    pub struct ThoughtSignatureDelta {
2285        pub signature: String,
2286    }
2287
2288    /// Streaming function call delta.
2289    #[derive(Clone, Debug, Deserialize, Serialize)]
2290    pub struct FunctionCallDelta {
2291        #[serde(skip_serializing_if = "Option::is_none")]
2292        pub name: Option<String>,
2293        #[serde(skip_serializing_if = "Option::is_none")]
2294        pub arguments: Option<Value>,
2295        #[serde(skip_serializing_if = "Option::is_none")]
2296        pub id: Option<String>,
2297    }
2298
2299    /// Streaming function result delta.
2300    #[derive(Clone, Debug, Deserialize, Serialize)]
2301    pub struct FunctionResultDelta {
2302        #[serde(skip_serializing_if = "Option::is_none")]
2303        pub name: Option<String>,
2304        #[serde(skip_serializing_if = "Option::is_none")]
2305        pub result: Option<Value>,
2306        #[serde(skip_serializing_if = "Option::is_none")]
2307        pub call_id: Option<String>,
2308        #[serde(skip_serializing_if = "Option::is_none")]
2309        pub is_error: Option<bool>,
2310    }
2311
2312    /// Streaming code execution call delta.
2313    #[derive(Clone, Debug, Deserialize, Serialize)]
2314    pub struct CodeExecutionCallDelta {
2315        #[serde(skip_serializing_if = "Option::is_none")]
2316        pub arguments: Option<CodeExecutionCallArguments>,
2317        #[serde(skip_serializing_if = "Option::is_none")]
2318        pub id: Option<String>,
2319    }
2320
2321    /// Streaming code execution result delta.
2322    #[derive(Clone, Debug, Deserialize, Serialize)]
2323    pub struct CodeExecutionResultDelta {
2324        #[serde(skip_serializing_if = "Option::is_none")]
2325        pub result: Option<String>,
2326        #[serde(skip_serializing_if = "Option::is_none")]
2327        pub is_error: Option<bool>,
2328        #[serde(skip_serializing_if = "Option::is_none")]
2329        pub signature: Option<String>,
2330        #[serde(skip_serializing_if = "Option::is_none")]
2331        pub call_id: Option<String>,
2332    }
2333
2334    /// Streaming URL context call delta.
2335    #[derive(Clone, Debug, Deserialize, Serialize)]
2336    pub struct UrlContextCallDelta {
2337        #[serde(skip_serializing_if = "Option::is_none")]
2338        pub arguments: Option<UrlContextCallArguments>,
2339        #[serde(skip_serializing_if = "Option::is_none")]
2340        pub id: Option<String>,
2341    }
2342
2343    /// Streaming URL context result delta.
2344    #[derive(Clone, Debug, Deserialize, Serialize)]
2345    pub struct UrlContextResultDelta {
2346        #[serde(skip_serializing_if = "Option::is_none")]
2347        pub result: Option<Vec<UrlContextResult>>,
2348        #[serde(skip_serializing_if = "Option::is_none")]
2349        pub signature: Option<String>,
2350        #[serde(skip_serializing_if = "Option::is_none")]
2351        pub is_error: Option<bool>,
2352        #[serde(skip_serializing_if = "Option::is_none")]
2353        pub call_id: Option<String>,
2354    }
2355
2356    /// Streaming Google Search call delta.
2357    #[derive(Clone, Debug, Deserialize, Serialize)]
2358    pub struct GoogleSearchCallDelta {
2359        #[serde(skip_serializing_if = "Option::is_none")]
2360        pub arguments: Option<GoogleSearchCallArguments>,
2361        #[serde(skip_serializing_if = "Option::is_none")]
2362        pub id: Option<String>,
2363    }
2364
2365    /// Streaming Google Search result delta.
2366    #[derive(Clone, Debug, Deserialize, Serialize)]
2367    pub struct GoogleSearchResultDelta {
2368        #[serde(skip_serializing_if = "Option::is_none")]
2369        pub result: Option<Vec<GoogleSearchResult>>,
2370        #[serde(skip_serializing_if = "Option::is_none")]
2371        pub signature: Option<String>,
2372        #[serde(skip_serializing_if = "Option::is_none")]
2373        pub is_error: Option<bool>,
2374        #[serde(skip_serializing_if = "Option::is_none")]
2375        pub call_id: Option<String>,
2376    }
2377
2378    /// Streaming MCP server tool call delta.
2379    #[derive(Clone, Debug, Deserialize, Serialize)]
2380    pub struct McpServerToolCallDelta {
2381        #[serde(skip_serializing_if = "Option::is_none")]
2382        pub name: Option<String>,
2383        #[serde(skip_serializing_if = "Option::is_none")]
2384        pub server_name: Option<String>,
2385        #[serde(skip_serializing_if = "Option::is_none")]
2386        pub arguments: Option<Value>,
2387        #[serde(skip_serializing_if = "Option::is_none")]
2388        pub id: Option<String>,
2389    }
2390
2391    /// Streaming MCP server tool result delta.
2392    #[derive(Clone, Debug, Deserialize, Serialize)]
2393    pub struct McpServerToolResultDelta {
2394        #[serde(skip_serializing_if = "Option::is_none")]
2395        pub name: Option<String>,
2396        #[serde(skip_serializing_if = "Option::is_none")]
2397        pub server_name: Option<String>,
2398        #[serde(skip_serializing_if = "Option::is_none")]
2399        pub result: Option<Value>,
2400        #[serde(skip_serializing_if = "Option::is_none")]
2401        pub call_id: Option<String>,
2402    }
2403
2404    /// Streaming file search result delta.
2405    #[derive(Clone, Debug, Deserialize, Serialize)]
2406    pub struct FileSearchResultDelta {
2407        #[serde(skip_serializing_if = "Option::is_none")]
2408        pub result: Option<Vec<FileSearchResult>>,
2409    }
2410}
2411
2412#[cfg(test)]
2413mod tests {
2414    use super::*;
2415    use crate::OneOrMany;
2416    use crate::completion::{CompletionRequest, Message};
2417    use crate::message::{self, ToolChoice as MessageToolChoice};
2418    use serde_json::json;
2419
2420    #[test]
2421    fn test_create_request_body_simple() {
2422        let prompt = Message::User {
2423            content: OneOrMany::one(message::UserContent::text("Hello")),
2424        };
2425
2426        let request = CompletionRequest {
2427            model: None,
2428            preamble: Some("Be precise.".to_string()),
2429            chat_history: OneOrMany::one(prompt),
2430            documents: vec![],
2431            tools: vec![],
2432            temperature: Some(0.7),
2433            max_tokens: Some(128),
2434            tool_choice: Some(MessageToolChoice::Required),
2435            additional_params: None,
2436            output_schema: None,
2437        };
2438
2439        let result = create_request_body("gemini-2.5-flash".to_string(), request, Some(false))
2440            .expect("request should build");
2441
2442        assert_eq!(result.model.as_deref(), Some("gemini-2.5-flash"));
2443        assert!(result.agent.is_none());
2444        assert_eq!(result.stream, Some(false));
2445        assert_eq!(result.system_instruction.as_deref(), Some("Be precise."));
2446
2447        let config = result.generation_config.expect("generation config missing");
2448        assert_eq!(config.temperature, Some(0.7));
2449        assert_eq!(config.max_output_tokens, Some(128));
2450        assert!(matches!(
2451            config.tool_choice,
2452            Some(ToolChoice::Type(ToolChoiceType::Any))
2453        ));
2454
2455        let InteractionInput::Turns(turns) = result.input else {
2456            panic!("expected turns input");
2457        };
2458        assert_eq!(turns.len(), 1);
2459        let turn = &turns[0];
2460        assert!(matches!(turn.role, Role::User));
2461        let TurnContent::Contents(contents) = &turn.content else {
2462            panic!("expected content array");
2463        };
2464        assert_eq!(contents.len(), 1);
2465        match &contents[0] {
2466            Content::Text(TextContent { text, .. }) => assert_eq!(text, "Hello"),
2467            other => panic!("unexpected content: {other:?}"),
2468        }
2469    }
2470
2471    #[test]
2472    fn test_tool_result_requires_call_id() {
2473        let content = message::UserContent::ToolResult(message::ToolResult {
2474            id: "get_weather".to_string(),
2475            call_id: None,
2476            content: OneOrMany::one(message::ToolResultContent::text("ok")),
2477        });
2478
2479        let err = Content::try_from(content).expect_err("should require call_id");
2480        assert!(format!("{err}").contains("call_id"));
2481    }
2482
2483    #[test]
2484    fn test_response_function_call_mapping() {
2485        let interaction = Interaction {
2486            id: "interaction-1".to_string(),
2487            outputs: vec![Content::FunctionCall(FunctionCallContent {
2488                name: Some("get_weather".to_string()),
2489                arguments: Some(json!({"location": "Paris"})),
2490                id: Some("call-123".to_string()),
2491            })],
2492            usage: Some(InteractionUsage {
2493                total_input_tokens: Some(5),
2494                total_output_tokens: Some(7),
2495                total_tokens: Some(12),
2496            }),
2497            ..Default::default()
2498        };
2499
2500        let response: completion::CompletionResponse<Interaction> =
2501            interaction.try_into().expect("conversion should succeed");
2502
2503        let choice = response.choice.first();
2504        match choice {
2505            completion::AssistantContent::ToolCall(tool_call) => {
2506                assert_eq!(tool_call.function.name, "get_weather");
2507                assert_eq!(tool_call.call_id.as_deref(), Some("call-123"));
2508            }
2509            other => panic!("unexpected content: {other:?}"),
2510        }
2511
2512        assert_eq!(response.usage.input_tokens, 5);
2513        assert_eq!(response.usage.output_tokens, 7);
2514        assert_eq!(response.usage.total_tokens, 12);
2515    }
2516
2517    #[test]
2518    fn test_google_search_tool_serialization() {
2519        let tool = Tool::GoogleSearch;
2520        let value = serde_json::to_value(tool).expect("tool should serialize");
2521        assert_eq!(value, json!({ "type": "google_search" }));
2522    }
2523
2524    #[test]
2525    fn test_url_context_tool_serialization() {
2526        let tool = Tool::UrlContext;
2527        let value = serde_json::to_value(tool).expect("tool should serialize");
2528        assert_eq!(value, json!({ "type": "url_context" }));
2529    }
2530
2531    #[test]
2532    fn test_code_execution_tool_serialization() {
2533        let tool = Tool::CodeExecution;
2534        let value = serde_json::to_value(tool).expect("tool should serialize");
2535        assert_eq!(value, json!({ "type": "code_execution" }));
2536    }
2537
2538    #[test]
2539    fn test_google_search_helpers() {
2540        let interaction = Interaction {
2541            outputs: vec![
2542                Content::GoogleSearchCall(GoogleSearchCallContent {
2543                    arguments: Some(GoogleSearchCallArguments {
2544                        queries: Some(vec!["query-one".to_string(), "query-two".to_string()]),
2545                    }),
2546                    id: Some("call-1".to_string()),
2547                }),
2548                Content::GoogleSearchResult(GoogleSearchResultContent {
2549                    result: Some(vec![GoogleSearchResult {
2550                        url: Some("https://example.com".to_string()),
2551                        title: Some("Example One".to_string()),
2552                        rendered_content: None,
2553                    }]),
2554                    signature: None,
2555                    is_error: None,
2556                    call_id: Some("call-1".to_string()),
2557                }),
2558                Content::GoogleSearchCall(GoogleSearchCallContent {
2559                    arguments: Some(GoogleSearchCallArguments {
2560                        queries: Some(vec!["query-three".to_string()]),
2561                    }),
2562                    id: Some("call-2".to_string()),
2563                }),
2564                Content::GoogleSearchResult(GoogleSearchResultContent {
2565                    result: Some(vec![GoogleSearchResult {
2566                        url: Some("https://example.org".to_string()),
2567                        title: Some("Example Two".to_string()),
2568                        rendered_content: None,
2569                    }]),
2570                    signature: None,
2571                    is_error: None,
2572                    call_id: Some("call-2".to_string()),
2573                }),
2574            ],
2575            ..Default::default()
2576        };
2577
2578        let exchanges = interaction.google_search_exchanges();
2579        assert_eq!(exchanges.len(), 2);
2580        assert_eq!(exchanges[0].call_id.as_deref(), Some("call-1"));
2581        assert_eq!(
2582            exchanges[0].queries(),
2583            vec!["query-one".to_string(), "query-two".to_string()]
2584        );
2585        let exchange_results = exchanges[0].result_items();
2586        assert_eq!(exchange_results.len(), 1);
2587        assert_eq!(exchange_results[0].title.as_deref(), Some("Example One"));
2588
2589        assert_eq!(exchanges[1].call_id.as_deref(), Some("call-2"));
2590        assert_eq!(exchanges[1].queries(), vec!["query-three".to_string()]);
2591        let exchange_results = exchanges[1].result_items();
2592        assert_eq!(exchange_results.len(), 1);
2593        assert_eq!(exchange_results[0].title.as_deref(), Some("Example Two"));
2594
2595        let queries = interaction.google_search_queries();
2596        assert_eq!(queries, vec!["query-one", "query-two", "query-three"]);
2597
2598        let results = interaction.google_search_results();
2599        assert_eq!(results.len(), 2);
2600        assert_eq!(results[0].title.as_deref(), Some("Example One"));
2601        assert_eq!(results[1].title.as_deref(), Some("Example Two"));
2602
2603        let call_contents = interaction.google_search_call_contents();
2604        assert_eq!(call_contents.len(), 2);
2605        assert_eq!(call_contents[0].id.as_deref(), Some("call-1"));
2606        assert_eq!(call_contents[1].id.as_deref(), Some("call-2"));
2607
2608        let result_contents = interaction.google_search_result_contents();
2609        assert_eq!(result_contents.len(), 2);
2610        assert_eq!(result_contents[0].call_id.as_deref(), Some("call-1"));
2611        assert_eq!(result_contents[1].call_id.as_deref(), Some("call-2"));
2612    }
2613
2614    #[test]
2615    fn test_google_search_helpers_without_call_id() {
2616        let interaction = Interaction {
2617            outputs: vec![
2618                Content::GoogleSearchCall(GoogleSearchCallContent {
2619                    arguments: Some(GoogleSearchCallArguments {
2620                        queries: Some(vec!["query-one".to_string()]),
2621                    }),
2622                    id: None,
2623                }),
2624                Content::GoogleSearchResult(GoogleSearchResultContent {
2625                    result: Some(vec![GoogleSearchResult {
2626                        url: Some("https://example.com".to_string()),
2627                        title: Some("Example One".to_string()),
2628                        rendered_content: None,
2629                    }]),
2630                    signature: None,
2631                    is_error: None,
2632                    call_id: None,
2633                }),
2634                Content::GoogleSearchCall(GoogleSearchCallContent {
2635                    arguments: Some(GoogleSearchCallArguments {
2636                        queries: Some(vec!["query-two".to_string()]),
2637                    }),
2638                    id: Some("call-2".to_string()),
2639                }),
2640                Content::GoogleSearchResult(GoogleSearchResultContent {
2641                    result: Some(vec![GoogleSearchResult {
2642                        url: Some("https://example.org".to_string()),
2643                        title: Some("Example Two".to_string()),
2644                        rendered_content: None,
2645                    }]),
2646                    signature: None,
2647                    is_error: None,
2648                    call_id: None,
2649                }),
2650            ],
2651            ..Default::default()
2652        };
2653
2654        let exchanges = interaction.google_search_exchanges();
2655        assert_eq!(exchanges.len(), 2);
2656
2657        let no_id = exchanges
2658            .iter()
2659            .find(|exchange| exchange.call_id.is_none())
2660            .expect("expected no-id exchange");
2661        assert_eq!(no_id.calls.len(), 1);
2662        assert_eq!(no_id.results.len(), 1);
2663
2664        let with_id = exchanges
2665            .iter()
2666            .find(|exchange| exchange.call_id.as_deref() == Some("call-2"))
2667            .expect("expected call-2 exchange");
2668        assert_eq!(with_id.calls.len(), 1);
2669        assert_eq!(with_id.results.len(), 1);
2670    }
2671
2672    #[test]
2673    fn test_url_context_helpers() {
2674        let interaction = Interaction {
2675            outputs: vec![
2676                Content::UrlContextCall(UrlContextCallContent {
2677                    arguments: Some(UrlContextCallArguments {
2678                        urls: Some(vec![
2679                            "https://example.com".to_string(),
2680                            "https://example.org".to_string(),
2681                        ]),
2682                    }),
2683                    id: Some("call-1".to_string()),
2684                }),
2685                Content::UrlContextResult(UrlContextResultContent {
2686                    result: Some(vec![UrlContextResult {
2687                        url: Some("https://example.com".to_string()),
2688                        status: Some("success".to_string()),
2689                    }]),
2690                    signature: None,
2691                    is_error: None,
2692                    call_id: Some("call-1".to_string()),
2693                }),
2694            ],
2695            ..Default::default()
2696        };
2697
2698        let exchanges = interaction.url_context_exchanges();
2699        assert_eq!(exchanges.len(), 1);
2700        assert_eq!(exchanges[0].call_id.as_deref(), Some("call-1"));
2701        assert_eq!(
2702            exchanges[0].urls(),
2703            vec!["https://example.com", "https://example.org"]
2704        );
2705        let results = exchanges[0].result_items();
2706        assert_eq!(results.len(), 1);
2707        assert_eq!(results[0].status.as_deref(), Some("success"));
2708
2709        let urls = interaction.url_context_urls();
2710        assert_eq!(urls, vec!["https://example.com", "https://example.org"]);
2711
2712        let results = interaction.url_context_results();
2713        assert_eq!(results.len(), 1);
2714        assert_eq!(results[0].url.as_deref(), Some("https://example.com"));
2715
2716        let call_contents = interaction.url_context_call_contents();
2717        assert_eq!(call_contents.len(), 1);
2718        assert_eq!(call_contents[0].id.as_deref(), Some("call-1"));
2719
2720        let result_contents = interaction.url_context_result_contents();
2721        assert_eq!(result_contents.len(), 1);
2722        assert_eq!(result_contents[0].call_id.as_deref(), Some("call-1"));
2723    }
2724
2725    #[test]
2726    fn test_url_context_helpers_without_call_id() {
2727        let interaction = Interaction {
2728            outputs: vec![
2729                Content::UrlContextCall(UrlContextCallContent {
2730                    arguments: Some(UrlContextCallArguments {
2731                        urls: Some(vec!["https://example.com".to_string()]),
2732                    }),
2733                    id: None,
2734                }),
2735                Content::UrlContextResult(UrlContextResultContent {
2736                    result: Some(vec![UrlContextResult {
2737                        url: Some("https://example.com".to_string()),
2738                        status: Some("success".to_string()),
2739                    }]),
2740                    signature: None,
2741                    is_error: None,
2742                    call_id: None,
2743                }),
2744                Content::UrlContextCall(UrlContextCallContent {
2745                    arguments: Some(UrlContextCallArguments {
2746                        urls: Some(vec!["https://example.org".to_string()]),
2747                    }),
2748                    id: Some("call-2".to_string()),
2749                }),
2750                Content::UrlContextResult(UrlContextResultContent {
2751                    result: Some(vec![UrlContextResult {
2752                        url: Some("https://example.org".to_string()),
2753                        status: Some("success".to_string()),
2754                    }]),
2755                    signature: None,
2756                    is_error: None,
2757                    call_id: None,
2758                }),
2759            ],
2760            ..Default::default()
2761        };
2762
2763        let exchanges = interaction.url_context_exchanges();
2764        assert_eq!(exchanges.len(), 2);
2765
2766        let no_id = exchanges
2767            .iter()
2768            .find(|exchange| exchange.call_id.is_none())
2769            .expect("expected no-id exchange");
2770        assert_eq!(no_id.calls.len(), 1);
2771        assert_eq!(no_id.results.len(), 1);
2772
2773        let with_id = exchanges
2774            .iter()
2775            .find(|exchange| exchange.call_id.as_deref() == Some("call-2"))
2776            .expect("expected call-2 exchange");
2777        assert_eq!(with_id.calls.len(), 1);
2778        assert_eq!(with_id.results.len(), 1);
2779    }
2780
2781    #[test]
2782    fn test_code_execution_helpers() {
2783        let interaction = Interaction {
2784            outputs: vec![
2785                Content::CodeExecutionCall(CodeExecutionCallContent {
2786                    arguments: Some(CodeExecutionCallArguments {
2787                        language: Some("python".to_string()),
2788                        code: Some("print(2 + 2)".to_string()),
2789                    }),
2790                    id: Some("call-1".to_string()),
2791                }),
2792                Content::CodeExecutionResult(CodeExecutionResultContent {
2793                    result: Some("4\n".to_string()),
2794                    signature: None,
2795                    is_error: None,
2796                    call_id: Some("call-1".to_string()),
2797                }),
2798            ],
2799            ..Default::default()
2800        };
2801
2802        let exchanges = interaction.code_execution_exchanges();
2803        assert_eq!(exchanges.len(), 1);
2804        assert_eq!(exchanges[0].call_id.as_deref(), Some("call-1"));
2805        assert_eq!(exchanges[0].code_snippets(), vec!["print(2 + 2)"]);
2806        assert_eq!(exchanges[0].outputs(), vec!["4\n"]);
2807
2808        let calls = interaction.code_execution_call_contents();
2809        assert_eq!(calls.len(), 1);
2810        assert_eq!(calls[0].id.as_deref(), Some("call-1"));
2811
2812        let results = interaction.code_execution_result_contents();
2813        assert_eq!(results.len(), 1);
2814        assert_eq!(results[0].call_id.as_deref(), Some("call-1"));
2815
2816        let snippets = interaction.code_execution_snippets();
2817        assert_eq!(snippets, vec!["print(2 + 2)"]);
2818
2819        let outputs = interaction.code_execution_outputs();
2820        assert_eq!(outputs, vec!["4\n"]);
2821    }
2822
2823    #[test]
2824    fn test_code_execution_helpers_without_call_id() {
2825        let interaction = Interaction {
2826            outputs: vec![
2827                Content::CodeExecutionCall(CodeExecutionCallContent {
2828                    arguments: Some(CodeExecutionCallArguments {
2829                        language: Some("python".to_string()),
2830                        code: Some("print(1 + 1)".to_string()),
2831                    }),
2832                    id: None,
2833                }),
2834                Content::CodeExecutionResult(CodeExecutionResultContent {
2835                    result: Some("2\n".to_string()),
2836                    signature: None,
2837                    is_error: None,
2838                    call_id: None,
2839                }),
2840                Content::CodeExecutionCall(CodeExecutionCallContent {
2841                    arguments: Some(CodeExecutionCallArguments {
2842                        language: Some("python".to_string()),
2843                        code: Some("print(2 + 2)".to_string()),
2844                    }),
2845                    id: Some("call-2".to_string()),
2846                }),
2847                Content::CodeExecutionResult(CodeExecutionResultContent {
2848                    result: Some("4\n".to_string()),
2849                    signature: None,
2850                    is_error: None,
2851                    call_id: None,
2852                }),
2853            ],
2854            ..Default::default()
2855        };
2856
2857        let exchanges = interaction.code_execution_exchanges();
2858        assert_eq!(exchanges.len(), 2);
2859
2860        let no_id = exchanges
2861            .iter()
2862            .find(|exchange| exchange.call_id.is_none())
2863            .expect("expected no-id exchange");
2864        assert_eq!(no_id.calls.len(), 1);
2865        assert_eq!(no_id.results.len(), 1);
2866
2867        let with_id = exchanges
2868            .iter()
2869            .find(|exchange| exchange.call_id.as_deref() == Some("call-2"))
2870            .expect("expected call-2 exchange");
2871        assert_eq!(with_id.calls.len(), 1);
2872        assert_eq!(with_id.results.len(), 1);
2873    }
2874
2875    #[test]
2876    fn test_interaction_status_helpers() {
2877        let mut interaction = Interaction {
2878            status: Some(InteractionStatus::InProgress),
2879            ..Default::default()
2880        };
2881        assert!(!interaction.is_terminal());
2882        assert!(!interaction.is_completed());
2883
2884        interaction.status = Some(InteractionStatus::Completed);
2885        assert!(interaction.is_terminal());
2886        assert!(interaction.is_completed());
2887
2888        interaction.status = Some(InteractionStatus::Failed);
2889        assert!(interaction.is_terminal());
2890        assert!(!interaction.is_completed());
2891    }
2892
2893    #[test]
2894    fn test_build_interaction_stream_path() {
2895        let path = build_interaction_stream_path("interaction-123", None);
2896        assert_eq!(path, "/v1beta/interactions/interaction-123?stream=true");
2897
2898        let path = build_interaction_stream_path("interaction-123", Some("event-456"));
2899        assert_eq!(
2900            path,
2901            "/v1beta/interactions/interaction-123?stream=true&last_event_id=event-456"
2902        );
2903    }
2904
2905    #[test]
2906    fn test_inline_citations_from_annotations() {
2907        let text_content = TextContent {
2908            text: "Hello world".to_string(),
2909            annotations: Some(vec![
2910                Annotation {
2911                    start_index: Some(6),
2912                    end_index: Some(11),
2913                    source: Some("https://example.com".to_string()),
2914                },
2915                Annotation {
2916                    start_index: Some(0),
2917                    end_index: Some(5),
2918                    source: Some("https://hello.example".to_string()),
2919                },
2920            ]),
2921        };
2922
2923        let cited = text_content.with_inline_citations();
2924        assert_eq!(
2925            cited,
2926            "Hello[1](https://hello.example) world[2](https://example.com)"
2927        );
2928
2929        let interaction = Interaction {
2930            outputs: vec![Content::Text(text_content)],
2931            ..Default::default()
2932        };
2933
2934        let cited_text = interaction.text_with_inline_citations();
2935        assert_eq!(
2936            cited_text.as_deref(),
2937            Some("Hello[1](https://hello.example) world[2](https://example.com)")
2938        );
2939    }
2940}