Skip to main content

rig/providers/gemini/interactions_api/
mod.rs

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