Skip to main content

rig_core/providers/gemini/interactions_api/
mod.rs

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