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