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