Skip to main content

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