Skip to main content

rig_core/providers/gemini/interactions_api/
mod.rs

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