Skip to main content

rig_core/providers/gemini/interactions_api/
mod.rs

1//! Wires and payload types for the [Gemini Interactions API](https://ai.google.dev/api/interactions-api).
2//!
3//! ```no_run
4//! use rig_core::providers::gemini::{Gemini, completion::GEMINI_2_5_FLASH};
5//!
6//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
7//! let wire = Gemini::from_env()?.interactions(GEMINI_2_5_FLASH);
8//! # Ok(())
9//! # }
10//! ```
11
12use crate::completion::CompletionRequest;
13use crate::error::EncodeError;
14use crate::message::{self, MimeType};
15use crate::telemetry::GenAiOperation;
16use crate::wire::{Descriptor, Mode};
17use base64::{Engine, prelude::BASE64_STANDARD};
18use serde_json::{Map, Value};
19use url::form_urlencoded;
20
21/// Streaming helpers for the Interactions API.
22pub mod streaming;
23pub use interactions_api_types::*;
24
25/// Gemini provider name used in normalized records and telemetry.
26pub(crate) const PROVIDER_NAME: &str = "gcp.gemini";
27
28/// The issuer of Gemini's reasoning, which is the only reasoning this wire
29/// replays.
30pub(crate) const ISSUER: crate::message::Issuer =
31    crate::message::Issuer::from_static(PROVIDER_NAME);
32
33/// Create interactions with `POST /v1beta/interactions`.
34/// Streaming mode sets `alt=sse` and `stream: true`; unary mode reads a whole resource.
35#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
36pub struct Interactions {
37    /// The key and the API root.
38    pub provider: crate::providers::gemini::GeminiConfig,
39    /// The model to address.
40    pub model: String,
41}
42
43impl Interactions {
44    /// The wire for `model`.
45    pub fn new(provider: crate::providers::gemini::GeminiConfig, model: impl Into<String>) -> Self {
46        Self {
47            provider,
48            model: model.into(),
49        }
50    }
51}
52
53impl crate::wire::Wire for Interactions {
54    type Op = crate::operation::Completion;
55    type Payload = crate::wire::Encoded;
56    type Frame = crate::wire::WireFrame;
57    type Decoder<'id> = streaming::InteractionsDecoder<'id>;
58
59    fn describe(&self) -> Descriptor<'_> {
60        Descriptor::new(PROVIDER_NAME)
61            .model(self.model.as_str())
62            .telemetry(|mode| match mode {
63                Mode::Unary => GenAiOperation::Interactions,
64                Mode::Streaming => GenAiOperation::InteractionsStreaming,
65            })
66    }
67
68    fn encode(
69        &self,
70        request: CompletionRequest,
71        mode: crate::wire::Mode,
72    ) -> Result<crate::wire::Encoded, EncodeError> {
73        let request = request.replayable_to(&[ISSUER])?;
74        // `stream` is part of the request body on this wire, so the mode is
75        // in the bytes as well as in the path.
76        let streaming = matches!(mode, crate::wire::Mode::Streaming);
77        let body = create_request_body(self.model.clone(), request, Some(streaming))?;
78        crate::providers::internal::trace_json(
79            if streaming {
80                crate::providers::internal::LogTarget::Streaming
81            } else {
82                crate::providers::internal::LogTarget::Completions
83            },
84            "Gemini interactions completion request",
85            &body,
86        );
87        let (path, framing) = if streaming {
88            (
89                "/v1beta/interactions?alt=sse",
90                crate::http_client::framing::Framing::Sse,
91            )
92        } else {
93            (
94                "/v1beta/interactions",
95                crate::http_client::framing::Framing::Whole,
96            )
97        };
98        let request = http::Request::post(self.provider.interactions_uri(path))
99            .header("Content-Type", "application/json")
100            .header(
101                crate::providers::gemini::GeminiConfig::INTERACTIONS_KEY_HEADER,
102                self.provider.api_key.expose(),
103            )
104            .body(crate::wire::Body::Bytes(serde_json::to_vec(&body)?))?;
105        // Gemini supplies no transport request-id response header.
106        Ok(crate::wire::Encoded::new(request, framing))
107    }
108
109    fn decoder<'id>(&self) -> Self::Decoder<'id> {
110        streaming::InteractionsDecoder::default()
111    }
112}
113
114/// Read an existing interaction resource or resume its event stream.
115/// Unary mode retrieves the resource once. Streaming mode resumes after
116/// `last_event_id`, or from the beginning if no event id is supplied.
117#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
118pub struct InteractionResume {
119    /// The key and the API root.
120    pub provider: crate::providers::gemini::GeminiConfig,
121    /// The interaction to read.
122    pub interaction_id: String,
123    /// The last event the consumer saw, so a resumed stream does not
124    /// redeliver it. `None` resumes from the beginning, as the API defaults.
125    pub last_event_id: Option<String>,
126}
127
128impl InteractionResume {
129    /// The wire for the interaction `interaction_id`.
130    pub fn new(
131        provider: crate::providers::gemini::GeminiConfig,
132        interaction_id: impl Into<String>,
133    ) -> Self {
134        Self {
135            provider,
136            interaction_id: interaction_id.into(),
137            last_event_id: None,
138        }
139    }
140
141    /// Resume a streamed read after the event `last_event_id`.
142    pub fn after_event(mut self, last_event_id: impl Into<String>) -> Self {
143        self.last_event_id = Some(last_event_id.into());
144        self
145    }
146}
147
148impl crate::wire::Wire for InteractionResume {
149    type Op = crate::operation::Completion;
150    type Payload = crate::wire::Encoded;
151    type Frame = crate::wire::WireFrame;
152    type Decoder<'id> = streaming::InteractionsDecoder<'id>;
153
154    /// The interaction names its own model; this wire addresses no model id.
155    fn describe(&self) -> Descriptor<'_> {
156        Descriptor::new(PROVIDER_NAME).telemetry(|mode| match mode {
157            Mode::Unary => GenAiOperation::Interactions,
158            Mode::Streaming => GenAiOperation::InteractionsStreaming,
159        })
160    }
161
162    /// Reads an existing interaction, so the request carries no body and the
163    /// [`CompletionRequest`] contributes nothing: what to read is the wire's
164    /// own data.
165    fn encode(
166        &self,
167        _request: CompletionRequest,
168        mode: crate::wire::Mode,
169    ) -> Result<crate::wire::Encoded, EncodeError> {
170        let (path, framing) = match mode {
171            Mode::Unary => (
172                format!("/v1beta/interactions/{}", self.interaction_id),
173                crate::http_client::framing::Framing::Whole,
174            ),
175            Mode::Streaming => (
176                format!(
177                    "{}&alt=sse",
178                    build_interaction_stream_path(
179                        &self.interaction_id,
180                        self.last_event_id.as_deref(),
181                    )
182                ),
183                crate::http_client::framing::Framing::Sse,
184            ),
185        };
186        let request = http::Request::get(self.provider.interactions_uri(&path))
187            .header(
188                crate::providers::gemini::GeminiConfig::INTERACTIONS_KEY_HEADER,
189                self.provider.api_key.expose(),
190            )
191            .body(crate::wire::Body::empty())?;
192        Ok(crate::wire::Encoded::new(request, framing))
193    }
194
195    fn decoder<'id>(&self) -> Self::Decoder<'id> {
196        streaming::InteractionsDecoder::default()
197    }
198}
199
200pub(crate) fn create_request_body(
201    model: String,
202    completion_request: CompletionRequest,
203    stream_override: Option<bool>,
204) -> Result<CreateInteractionRequest, EncodeError> {
205    let chat_history = completion_request.chat_history_with_documents();
206
207    let mut history = Vec::new();
208    history.extend(chat_history);
209    let (history_system, history) = split_system_messages_from_history(history);
210
211    let tool_ids = crate::providers::internal::wire_ids::WireIds::new(&history);
212    let mut steps = Vec::new();
213    for (position, message) in history.into_iter().enumerate() {
214        let mut converted = Step::from_message(message).map_err(EncodeError::request)?;
215        tool_ids
216            .apply(
217                position,
218                converted.iter_mut().filter_map(|step| match step {
219                    Step::FunctionCall(call) => call.id.as_mut(),
220                    Step::FunctionResult(result) => result.call_id.as_mut(),
221                    _ => None,
222                }),
223            )
224            .map_err(EncodeError::request)?;
225        steps.extend(converted);
226    }
227
228    let input = InteractionInput::Steps(steps);
229
230    let raw_params = completion_request
231        .additional_params
232        .unwrap_or_else(|| Value::Object(Map::new()));
233
234    let mut params: AdditionalParameters = serde_json::from_value(raw_params)?;
235
236    let mut generation_config = params.generation_config.take().unwrap_or_default();
237    if let Some(temp) = completion_request.temperature {
238        generation_config.temperature = Some(temp);
239    }
240    if let Some(max_tokens) = completion_request.max_tokens {
241        generation_config.max_output_tokens = Some(max_tokens);
242    }
243    if let Some(tool_choice) = completion_request.tool_choice {
244        generation_config.tool_choice = Some(tool_choice.try_into()?);
245    }
246    let generation_config = if generation_config.is_empty() {
247        None
248    } else {
249        Some(generation_config)
250    };
251
252    let system_instruction = (!history_system.is_empty())
253        .then(|| history_system.join("\n\n"))
254        .or(params.system_instruction.take());
255
256    let mut tools = Vec::new();
257    if !completion_request.tools.is_empty() {
258        tools.extend(
259            completion_request
260                .tools
261                .into_iter()
262                .map(Tool::try_from)
263                .collect::<Result<Vec<_>, _>>()?,
264        );
265    }
266    if let Some(mut extra_tools) = params.tools.take() {
267        tools.append(&mut extra_tools);
268    }
269    let tools = if tools.is_empty() { None } else { Some(tools) };
270
271    let stream = stream_override.or(params.stream.take());
272
273    let (agent, agent_config) = if params.agent.is_some() {
274        (params.agent.take(), params.agent_config.take())
275    } else {
276        (None, None)
277    };
278
279    let response_format = params.response_format.take();
280    let response_mime_type = params.response_mime_type.take();
281
282    if response_format.is_some() && response_mime_type.is_none() {
283        return Err(EncodeError::request(std::io::Error::new(
284            std::io::ErrorKind::InvalidInput,
285            "response_mime_type is required when response_format is set",
286        )));
287    }
288
289    Ok(CreateInteractionRequest {
290        model: if agent.is_some() { None } else { Some(model) },
291        agent,
292        input,
293        system_instruction,
294        tools,
295        response_format,
296        response_mime_type,
297        stream,
298        store: params.store.take(),
299        background: params.background.take(),
300        generation_config,
301        agent_config,
302        response_modalities: params.response_modalities.take(),
303        previous_interaction_id: params.previous_interaction_id.take(),
304        additional_params: params.additional_params.take(),
305    })
306}
307
308use super::completion::split_system_messages_from_history;
309
310fn build_interaction_stream_path(interaction_id: &str, last_event_id: Option<&str>) -> String {
311    let mut serializer = form_urlencoded::Serializer::new(String::new());
312    serializer.append_pair("stream", "true");
313    if let Some(last_event_id) = last_event_id {
314        serializer.append_pair("last_event_id", last_event_id);
315    }
316    format!(
317        "/v1beta/interactions/{}?{}",
318        interaction_id,
319        serializer.finish()
320    )
321}
322
323/// Shared preamble for Gemini Interactions media parts: require the media
324/// type, render its MIME string, and split the source into data/uri.
325fn media_parts<M: MimeType>(
326    data: message::DocumentSourceKind,
327    media_type: Option<M>,
328    kind: &str,
329) -> Result<(Option<String>, Option<String>, String), message::MessageError> {
330    let media_type = media_type.ok_or_else(|| {
331        message::MessageError::ConversionError(format!(
332            "Media type for {kind} is required for Gemini"
333        ))
334    })?;
335    let mime_type = media_type.to_mime_type().to_string();
336    let (data, uri) = split_data_uri(data)?;
337    Ok((data, uri, mime_type))
338}
339
340fn split_data_uri(
341    src: message::DocumentSourceKind,
342) -> Result<(Option<String>, Option<String>), message::MessageError> {
343    match src {
344        message::DocumentSourceKind::Url(uri) => Ok((None, Some(uri))),
345        message::DocumentSourceKind::Base64(data) => Ok((Some(data), None)),
346        message::DocumentSourceKind::String(data) => {
347            Ok((Some(BASE64_STANDARD.encode(data.as_bytes())), None))
348        }
349        message::DocumentSourceKind::Raw(data) => Ok((Some(BASE64_STANDARD.encode(data)), None)),
350        message::DocumentSourceKind::FileId(_) => Err(message::MessageError::ConversionError(
351            "Provider file IDs are not supported for Gemini Interactions inputs".to_string(),
352        )),
353        message::DocumentSourceKind::Unknown => Err(message::MessageError::ConversionError(
354            "Unknown content source".to_string(),
355        )),
356    }
357}
358
359/// Request and response types for the Gemini Interactions API.
360///
361/// ```
362/// use rig_core::providers::gemini::interactions_api::InteractionStatus;
363///
364/// assert!(InteractionStatus::RequiresAction.is_terminal());
365/// ```
366pub mod interactions_api_types {
367    use super::{media_parts, split_data_uri};
368    use crate::completion::Usage;
369    use crate::error::EncodeError;
370    use crate::message::{self, MimeType};
371    use base64::{Engine, prelude::BASE64_STANDARD};
372    use serde::{Deserialize, Serialize};
373    use serde_json::Value;
374
375    /// Optional parameters for creating an interaction.
376    #[derive(Debug, Deserialize, Serialize, Default, Clone)]
377    #[serde(rename_all = "snake_case")]
378    pub struct AdditionalParameters {
379        pub agent: Option<String>,
380        pub agent_config: Option<AgentConfig>,
381        pub background: Option<bool>,
382        pub generation_config: Option<GenerationConfig>,
383        pub previous_interaction_id: Option<String>,
384        pub response_modalities: Option<Vec<ResponseModality>>,
385        pub response_format: Option<Value>,
386        pub response_mime_type: Option<String>,
387        pub store: Option<bool>,
388        pub stream: Option<bool>,
389        pub system_instruction: Option<String>,
390        pub tools: Option<Vec<Tool>>,
391        #[serde(flatten, skip_serializing_if = "Option::is_none")]
392        pub additional_params: Option<Value>,
393    }
394
395    /// Request body for the create interaction endpoint.
396    #[derive(Debug, Deserialize, Serialize, Clone)]
397    #[serde(rename_all = "snake_case")]
398    pub struct CreateInteractionRequest {
399        #[serde(skip_serializing_if = "Option::is_none")]
400        pub model: Option<String>,
401        #[serde(skip_serializing_if = "Option::is_none")]
402        pub agent: Option<String>,
403        pub input: InteractionInput,
404        #[serde(skip_serializing_if = "Option::is_none")]
405        pub system_instruction: Option<String>,
406        #[serde(skip_serializing_if = "Option::is_none")]
407        pub tools: Option<Vec<Tool>>,
408        #[serde(skip_serializing_if = "Option::is_none")]
409        pub response_format: Option<Value>,
410        #[serde(skip_serializing_if = "Option::is_none")]
411        pub response_mime_type: Option<String>,
412        #[serde(skip_serializing_if = "Option::is_none")]
413        pub stream: Option<bool>,
414        #[serde(skip_serializing_if = "Option::is_none")]
415        pub store: Option<bool>,
416        #[serde(skip_serializing_if = "Option::is_none")]
417        pub background: Option<bool>,
418        #[serde(skip_serializing_if = "Option::is_none")]
419        pub generation_config: Option<GenerationConfig>,
420        #[serde(skip_serializing_if = "Option::is_none")]
421        pub agent_config: Option<AgentConfig>,
422        #[serde(skip_serializing_if = "Option::is_none")]
423        pub response_modalities: Option<Vec<ResponseModality>>,
424        #[serde(skip_serializing_if = "Option::is_none")]
425        pub previous_interaction_id: Option<String>,
426        #[serde(flatten, skip_serializing_if = "Option::is_none")]
427        pub additional_params: Option<Value>,
428    }
429
430    /// Interaction response payload.
431    #[derive(Clone, Debug, Deserialize, Serialize, Default)]
432    #[serde(rename_all = "snake_case")]
433    pub struct Interaction {
434        #[serde(default)]
435        pub id: String,
436        #[serde(skip_serializing_if = "Option::is_none")]
437        pub model: Option<String>,
438        #[serde(skip_serializing_if = "Option::is_none")]
439        pub agent: Option<String>,
440        #[serde(skip_serializing_if = "Option::is_none")]
441        pub status: Option<InteractionStatus>,
442        #[serde(skip_serializing_if = "Option::is_none")]
443        pub object: Option<String>,
444        #[serde(skip_serializing_if = "Option::is_none")]
445        pub created: Option<String>,
446        #[serde(skip_serializing_if = "Option::is_none")]
447        pub updated: Option<String>,
448        #[serde(skip_serializing_if = "Option::is_none")]
449        pub role: Option<String>,
450        #[serde(default)]
451        pub steps: Vec<Step>,
452        #[serde(skip_serializing_if = "Option::is_none")]
453        pub usage: Option<InteractionUsage>,
454        #[serde(skip_serializing_if = "Option::is_none")]
455        pub system_instruction: Option<String>,
456        #[serde(skip_serializing_if = "Option::is_none")]
457        pub tools: Option<Vec<Tool>>,
458        #[serde(skip_serializing_if = "Option::is_none")]
459        pub background: Option<bool>,
460        #[serde(skip_serializing_if = "Option::is_none")]
461        pub response_modalities: Option<Vec<ResponseModality>>,
462        #[serde(skip_serializing_if = "Option::is_none")]
463        pub response_format: Option<Value>,
464        #[serde(skip_serializing_if = "Option::is_none")]
465        pub response_mime_type: Option<String>,
466        #[serde(skip_serializing_if = "Option::is_none")]
467        pub previous_interaction_id: Option<String>,
468        #[serde(skip_serializing_if = "Option::is_none")]
469        pub input: Option<InteractionInput>,
470    }
471
472    impl From<&Interaction> for Usage {
473        fn from(value: &Interaction) -> Usage {
474            value.usage.as_ref().map(Usage::from).unwrap_or_default()
475        }
476    }
477
478    impl From<Interaction> for Usage {
479        fn from(value: Interaction) -> Usage {
480            (&value).into()
481        }
482    }
483
484    /// Groups tool calls and results of one built-in tool family for a single
485    /// interaction.
486    #[derive(Clone, Debug)]
487    pub struct Exchange<C, R> {
488        /// Call identifier used to match calls to results.
489        pub call_id: Option<String>,
490        /// One or more tool calls.
491        pub calls: Vec<C>,
492        /// One or more tool results.
493        pub results: Vec<R>,
494    }
495
496    impl<C, R> Default for Exchange<C, R> {
497        fn default() -> Self {
498            Self {
499                call_id: None,
500                calls: Vec::new(),
501                results: Vec::new(),
502            }
503        }
504    }
505
506    /// A tool call content type that carries an optional call identifier.
507    trait ExchangeCall {
508        fn id(&self) -> Option<&str>;
509    }
510
511    /// A tool result content type that carries an optional call identifier.
512    trait ExchangeResult {
513        fn call_id(&self) -> Option<&str>;
514    }
515
516    macro_rules! impl_exchange_ids {
517        ($call:ty, $result:ty) => {
518            impl ExchangeCall for $call {
519                fn id(&self) -> Option<&str> {
520                    self.id.as_deref()
521                }
522            }
523            impl ExchangeResult for $result {
524                fn call_id(&self) -> Option<&str> {
525                    self.call_id.as_deref()
526                }
527            }
528        };
529    }
530
531    impl_exchange_ids!(GoogleSearchCallContent, GoogleSearchResultContent);
532    impl_exchange_ids!(UrlContextCallContent, UrlContextResultContent);
533    impl_exchange_ids!(CodeExecutionCallContent, CodeExecutionResultContent);
534
535    /// Pairs tool calls with their results by call_id.
536    ///
537    /// When a call_id is missing, results are grouped with the most recent
538    /// call (identified or not) as a best-effort fallback.
539    fn pair_exchanges<C, R>(
540        contents: &[Content],
541        as_call: impl Fn(&Content) -> Option<&C>,
542        as_result: impl Fn(&Content) -> Option<&R>,
543    ) -> Vec<Exchange<C, R>>
544    where
545        C: Clone + ExchangeCall,
546        R: Clone + ExchangeResult,
547    {
548        let mut exchanges: Vec<Exchange<C, R>> = Vec::new();
549        let mut last_call_index: Option<usize> = None;
550        let position_of = |exchanges: &[Exchange<C, R>], call_id: &str| {
551            exchanges
552                .iter()
553                .position(|exchange| exchange.call_id.as_deref() == Some(call_id))
554        };
555
556        for content in contents {
557            if let Some(call) = as_call(content) {
558                let index = match call.id() {
559                    Some(call_id) => match position_of(&exchanges, call_id) {
560                        Some(index) => {
561                            if let Some(exchange) = exchanges.get_mut(index) {
562                                exchange.calls.push(call.clone());
563                            }
564                            index
565                        }
566                        None => {
567                            exchanges.push(Exchange {
568                                call_id: Some(call_id.to_string()),
569                                calls: vec![call.clone()],
570                                results: Vec::new(),
571                            });
572                            exchanges.len() - 1
573                        }
574                    },
575                    None => {
576                        exchanges.push(Exchange {
577                            call_id: None,
578                            calls: vec![call.clone()],
579                            results: Vec::new(),
580                        });
581                        exchanges.len() - 1
582                    }
583                };
584                last_call_index = Some(index);
585            } else if let Some(result) = as_result(content) {
586                if let Some(call_id) = result.call_id() {
587                    if let Some(index) = position_of(&exchanges, call_id) {
588                        if let Some(exchange) = exchanges.get_mut(index) {
589                            exchange.results.push(result.clone());
590                        }
591                    } else {
592                        exchanges.push(Exchange {
593                            call_id: Some(call_id.to_string()),
594                            calls: Vec::new(),
595                            results: vec![result.clone()],
596                        });
597                    }
598                } else if let Some(index) = last_call_index {
599                    if let Some(exchange) = exchanges.get_mut(index) {
600                        exchange.results.push(result.clone());
601                    }
602                } else {
603                    exchanges.push(Exchange {
604                        call_id: None,
605                        calls: Vec::new(),
606                        results: vec![result.clone()],
607                    });
608                    last_call_index = Some(exchanges.len() - 1);
609                }
610            }
611        }
612
613        exchanges
614    }
615
616    /// Groups Google Search tool calls and results for a single interaction.
617    pub type GoogleSearchExchange = Exchange<GoogleSearchCallContent, GoogleSearchResultContent>;
618
619    impl GoogleSearchExchange {
620        /// Collects all queries from the stored Google Search tool calls.
621        pub fn queries(&self) -> Vec<String> {
622            self.calls
623                .iter()
624                .filter_map(|call| call.arguments.as_ref()?.queries.as_ref())
625                .flatten()
626                .cloned()
627                .collect()
628        }
629
630        /// Collects all Google Search result entries from tool results.
631        pub fn result_items(&self) -> Vec<GoogleSearchResult> {
632            self.results
633                .iter()
634                .filter_map(|result| result.result.as_ref())
635                .flatten()
636                .cloned()
637                .collect()
638        }
639    }
640
641    /// Groups URL context tool calls and results for a single interaction.
642    pub type UrlContextExchange = Exchange<UrlContextCallContent, UrlContextResultContent>;
643
644    impl UrlContextExchange {
645        /// Collects all URLs from the stored URL context tool calls.
646        pub fn urls(&self) -> Vec<String> {
647            self.calls
648                .iter()
649                .filter_map(|call| call.arguments.as_ref()?.urls.as_ref())
650                .flatten()
651                .cloned()
652                .collect()
653        }
654
655        /// Collects all URL context result entries from tool results.
656        pub fn result_items(&self) -> Vec<UrlContextResult> {
657            self.results
658                .iter()
659                .filter_map(|result| result.result.as_ref())
660                .flatten()
661                .cloned()
662                .collect()
663        }
664    }
665
666    /// Groups code execution tool calls and results for a single interaction.
667    pub type CodeExecutionExchange = Exchange<CodeExecutionCallContent, CodeExecutionResultContent>;
668
669    impl CodeExecutionExchange {
670        /// Collects all code snippets from the stored code execution tool calls.
671        pub fn code_snippets(&self) -> Vec<String> {
672            self.calls
673                .iter()
674                .filter_map(|call| call.arguments.as_ref()?.code.clone())
675                .collect()
676        }
677
678        /// Collects all code execution outputs from tool results.
679        pub fn outputs(&self) -> Vec<String> {
680            self.results
681                .iter()
682                .filter_map(|result| result.result.clone())
683                .collect()
684        }
685    }
686
687    /// Generates the `Interaction` accessor family for one built-in tool:
688    /// the call_id-grouped exchanges plus flattened views over their calls,
689    /// results, and per-exchange collector methods.
690    macro_rules! interaction_exchange_accessors {
691        (
692            $tool:literal, $exchange:ty, $call_variant:ident, $result_variant:ident,
693            $exchanges_fn:ident, $call_contents_fn:ident -> $call_ty:ty,
694            $result_contents_fn:ident -> $result_ty:ty,
695            $($flat_doc:literal $flat_fn:ident => $method:ident -> $flat_ty:ty),* $(,)?
696        ) => {
697            #[doc = concat!("Groups ", $tool, " tool calls and results by call_id.")]
698            ///
699            /// When a call_id is missing, results are grouped with the most recent
700            /// call (identified or not) as a best-effort fallback.
701            pub fn $exchanges_fn(&self) -> Vec<$exchange> {
702                pair_exchanges(
703                    &self.output_contents(),
704                    |content| match content {
705                        Content::$call_variant(call) => Some(call),
706                        _ => None,
707                    },
708                    |content| match content {
709                        Content::$result_variant(result) => Some(result),
710                        _ => None,
711                    },
712                )
713            }
714
715            #[doc = concat!("Collects ", $tool, " tool call contents from the interaction outputs.")]
716            pub fn $call_contents_fn(&self) -> Vec<$call_ty> {
717                self.$exchanges_fn()
718                    .into_iter()
719                    .flat_map(|exchange| exchange.calls)
720                    .collect()
721            }
722
723            #[doc = concat!("Collects ", $tool, " result contents from the interaction outputs.")]
724            pub fn $result_contents_fn(&self) -> Vec<$result_ty> {
725                self.$exchanges_fn()
726                    .into_iter()
727                    .flat_map(|exchange| exchange.results)
728                    .collect()
729            }
730
731            $(
732                #[doc = $flat_doc]
733                pub fn $flat_fn(&self) -> Vec<$flat_ty> {
734                    self.$exchanges_fn()
735                        .into_iter()
736                        .flat_map(|exchange| exchange.$method())
737                        .collect()
738                }
739            )*
740        };
741    }
742
743    impl Interaction {
744        pub(crate) fn output_contents(&self) -> Vec<Content> {
745            self.steps.iter().flat_map(Step::output_contents).collect()
746        }
747
748        interaction_exchange_accessors!(
749            "Google Search", GoogleSearchExchange, GoogleSearchCall, GoogleSearchResult,
750            google_search_exchanges,
751            google_search_call_contents -> GoogleSearchCallContent,
752            google_search_result_contents -> GoogleSearchResultContent,
753            "Collects all Google Search queries from tool calls in the outputs."
754                google_search_queries => queries -> String,
755            "Collects all Google Search result entries from tool results in the outputs."
756                google_search_results => result_items -> GoogleSearchResult,
757        );
758
759        interaction_exchange_accessors!(
760            "URL context", UrlContextExchange, UrlContextCall, UrlContextResult,
761            url_context_exchanges,
762            url_context_call_contents -> UrlContextCallContent,
763            url_context_result_contents -> UrlContextResultContent,
764            "Collects all URLs from URL context tool calls in the outputs."
765                url_context_urls => urls -> String,
766            "Collects all URL context result entries from tool results in the outputs."
767                url_context_results => result_items -> UrlContextResult,
768        );
769
770        interaction_exchange_accessors!(
771            "code execution", CodeExecutionExchange, CodeExecutionCall, CodeExecutionResult,
772            code_execution_exchanges,
773            code_execution_call_contents -> CodeExecutionCallContent,
774            code_execution_result_contents -> CodeExecutionResultContent,
775            "Collects all code snippets from code execution calls in the outputs."
776                code_execution_snippets => code_snippets -> String,
777            "Collects all code execution outputs from tool results in the outputs."
778                code_execution_outputs => outputs -> String,
779        );
780
781        /// Returns concatenated text outputs with inline citations appended.
782        pub fn text_with_inline_citations(&self) -> Option<String> {
783            let text = self
784                .output_contents()
785                .iter()
786                .filter_map(|content| match content {
787                    Content::Text(text) => Some(text.with_inline_citations()),
788                    _ => None,
789                })
790                .collect::<Vec<_>>()
791                .join("\n");
792
793            if text.is_empty() { None } else { Some(text) }
794        }
795
796        /// Returns true when the interaction is in a terminal state.
797        pub fn is_terminal(&self) -> bool {
798            self.status
799                .as_ref()
800                .is_some_and(InteractionStatus::is_terminal)
801        }
802
803        /// Returns true when the interaction completed successfully.
804        pub fn is_completed(&self) -> bool {
805            matches!(self.status, Some(InteractionStatus::Completed))
806        }
807    }
808
809    /// Lifecycle status of an interaction.
810    #[derive(Clone, Debug, Deserialize, Serialize)]
811    #[serde(rename_all = "snake_case")]
812    pub enum InteractionStatus {
813        InProgress,
814        RequiresAction,
815        Incomplete,
816        BudgetExceeded,
817        Completed,
818        Failed,
819        Cancelled,
820        /// An unrecognized status, preserved verbatim without rejecting the response.
821        #[serde(untagged)]
822        Unknown(String),
823    }
824
825    impl InteractionStatus {
826        /// Return false only for [`Self::InProgress`].
827        /// Stop polling on unknown statuses and handle them explicitly.
828        /// [`Self::RequiresAction`] needs caller-supplied tool results, not further polling.
829        pub fn is_terminal(&self) -> bool {
830            !matches!(self, InteractionStatus::InProgress)
831        }
832
833        /// The exact spelling the Interactions API uses for this status on the
834        /// wire.
835        ///
836        /// Spelled out rather than derived from `Debug` (which would yield
837        /// `BudgetExceeded`, not `budget_exceeded`) so the string that reaches
838        /// [`crate::completion::FinishReason::Other`] is the provider's own.
839        pub fn as_wire_str(&self) -> &str {
840            match self {
841                Self::InProgress => "in_progress",
842                Self::RequiresAction => "requires_action",
843                Self::Incomplete => "incomplete",
844                Self::BudgetExceeded => "budget_exceeded",
845                Self::Completed => "completed",
846                Self::Failed => "failed",
847                Self::Cancelled => "cancelled",
848                Self::Unknown(status) => status,
849            }
850        }
851    }
852
853    /// Normalize completed, requires-action, and budget-exceeded statuses.
854    /// Preserve every other status verbatim as `Other`.
855    pub(crate) fn map_interaction_status(
856        status: &InteractionStatus,
857    ) -> crate::completion::FinishReason {
858        match status {
859            InteractionStatus::Completed => crate::completion::FinishReason::Stop,
860            InteractionStatus::RequiresAction => crate::completion::FinishReason::ToolCalls,
861            InteractionStatus::BudgetExceeded => crate::completion::FinishReason::Length,
862            other => crate::completion::FinishReason::Other(other.as_wire_str().to_owned()),
863        }
864    }
865
866    /// Token usage metadata for an interaction.
867    #[derive(Clone, Copy, Debug, Deserialize, Serialize, Default)]
868    #[serde(rename_all = "snake_case")]
869    pub struct InteractionUsage {
870        #[serde(skip_serializing_if = "Option::is_none")]
871        pub total_input_tokens: Option<u64>,
872        #[serde(skip_serializing_if = "Option::is_none")]
873        pub total_output_tokens: Option<u64>,
874        #[serde(skip_serializing_if = "Option::is_none")]
875        pub total_tokens: Option<u64>,
876        /// Input tokens served from Gemini's cache.
877        #[serde(default, skip_serializing_if = "Option::is_none")]
878        pub total_cached_tokens: Option<u64>,
879        /// Thinking tokens, reported separately from `total_output_tokens`.
880        #[serde(default, skip_serializing_if = "Option::is_none")]
881        pub total_thought_tokens: Option<u64>,
882        /// Prompt tokens attributed to built-in tool use.
883        #[serde(default, skip_serializing_if = "Option::is_none")]
884        pub total_tool_use_tokens: Option<u64>,
885    }
886
887    /// Rig's input is `total_input_tokens` plus the tool-use tokens, its
888    /// output `total_output_tokens` plus the thought tokens, and its total
889    /// their sum; without a base count, that side and the total stay absent.
890    impl From<&InteractionUsage> for Usage {
891        fn from(value: &InteractionUsage) -> Usage {
892            let input_tokens = value
893                .total_input_tokens
894                .map(|input| input + value.total_tool_use_tokens.unwrap_or(0));
895            let output_tokens = value
896                .total_output_tokens
897                .map(|output| output + value.total_thought_tokens.unwrap_or(0));
898            Usage {
899                input_tokens,
900                output_tokens,
901                cached_input_tokens: value.total_cached_tokens,
902                reasoning_tokens: value.total_thought_tokens,
903                tool_use_prompt_tokens: value.total_tool_use_tokens,
904                total_tokens: input_tokens
905                    .zip(output_tokens)
906                    .map(|(input, output)| input + output),
907                cache_creation_input_tokens: None,
908            }
909        }
910    }
911
912    impl From<InteractionUsage> for Usage {
913        fn from(value: InteractionUsage) -> Usage {
914            (&value).into()
915        }
916    }
917
918    /// Input payload accepted by the Interactions API.
919    #[derive(Clone, Debug, Deserialize, Serialize)]
920    #[serde(untagged)]
921    pub enum InteractionInput {
922        Text(String),
923        Content(Content),
924        Steps(Vec<Step>),
925        Contents(Vec<Content>),
926    }
927
928    /// Single interaction step.
929    #[derive(Clone, Debug, Deserialize, Serialize)]
930    #[serde(tag = "type", rename_all = "snake_case")]
931    pub enum Step {
932        // Streaming announcements may omit content until later step.delta events.
933        UserInput {
934            #[serde(default)]
935            content: Vec<Content>,
936        },
937        ModelOutput {
938            #[serde(default)]
939            content: Vec<Content>,
940        },
941        Thought(ThoughtContent),
942        FunctionCall(FunctionCallContent),
943        FunctionResult(FunctionResultContent),
944        CodeExecutionCall(CodeExecutionCallContent),
945        CodeExecutionResult(CodeExecutionResultContent),
946        UrlContextCall(UrlContextCallContent),
947        UrlContextResult(UrlContextResultContent),
948        GoogleSearchCall(GoogleSearchCallContent),
949        GoogleSearchResult(GoogleSearchResultContent),
950        McpServerToolCall(McpServerToolCallContent),
951        McpServerToolResult(McpServerToolResultContent),
952        FileSearchResult(FileSearchResultContent),
953    }
954
955    impl Step {
956        fn output_contents(&self) -> Vec<Content> {
957            match self {
958                Step::UserInput { .. } => Vec::new(),
959                Step::ModelOutput { content } => content.clone(),
960                Step::Thought(content) => vec![Content::Thought(content.clone())],
961                Step::FunctionCall(content) => vec![Content::FunctionCall(content.clone())],
962                Step::FunctionResult(content) => vec![Content::FunctionResult(content.clone())],
963                Step::CodeExecutionCall(content) => {
964                    vec![Content::CodeExecutionCall(content.clone())]
965                }
966                Step::CodeExecutionResult(content) => {
967                    vec![Content::CodeExecutionResult(content.clone())]
968                }
969                Step::UrlContextCall(content) => vec![Content::UrlContextCall(content.clone())],
970                Step::UrlContextResult(content) => {
971                    vec![Content::UrlContextResult(content.clone())]
972                }
973                Step::GoogleSearchCall(content) => {
974                    vec![Content::GoogleSearchCall(content.clone())]
975                }
976                Step::GoogleSearchResult(content) => {
977                    vec![Content::GoogleSearchResult(content.clone())]
978                }
979                Step::McpServerToolCall(content) => {
980                    vec![Content::McpServerToolCall(content.clone())]
981                }
982                Step::McpServerToolResult(content) => {
983                    vec![Content::McpServerToolResult(content.clone())]
984                }
985                Step::FileSearchResult(content) => {
986                    vec![Content::FileSearchResult(content.clone())]
987                }
988            }
989        }
990    }
991
992    impl Step {
993        /// Convert a history message into ordered interaction steps.
994        /// Calls, results, and thoughts become separate steps; adjacent text and
995        /// media remain grouped. Return conversion errors for unsupported content.
996        pub(crate) fn from_message(
997            message: crate::completion::Message,
998        ) -> Result<Vec<Self>, message::MessageError> {
999            match message {
1000                crate::completion::Message::System { content } => Ok(vec![Self::UserInput {
1001                    content: vec![Content::Text(TextContent {
1002                        text: content,
1003                        annotations: None,
1004                    })],
1005                }]),
1006                crate::completion::Message::User { content } => {
1007                    let contents = content
1008                        .into_iter()
1009                        .map(Content::try_from)
1010                        .collect::<Result<Vec<_>, _>>()?;
1011                    Ok(Self::split(contents, |content| Self::UserInput { content }))
1012                }
1013                crate::completion::Message::Assistant { content, .. } => {
1014                    let contents = content
1015                        .into_iter()
1016                        // Reasoning another service issued is not replayed.
1017                        .filter(|part| match part {
1018                            crate::message::AssistantContent::Reasoning(reasoning) => {
1019                                reasoning.open(&super::ISSUER).is_some()
1020                            }
1021                            _ => true,
1022                        })
1023                        .map(Content::try_from)
1024                        .collect::<Result<Vec<_>, _>>()?;
1025                    Ok(Self::split(contents, |content| Self::ModelOutput {
1026                        content,
1027                    }))
1028                }
1029            }
1030        }
1031
1032        /// Lift the contents that are steps of their own out of `contents`,
1033        /// grouping each run of the others under `group`.
1034        fn split(contents: Vec<Content>, group: impl Fn(Vec<Content>) -> Self) -> Vec<Self> {
1035            let mut steps = Vec::new();
1036            let mut run: Vec<Content> = Vec::new();
1037            for content in contents {
1038                let own = match content {
1039                    Content::Thought(thought) => Some(Self::Thought(thought)),
1040                    Content::FunctionCall(call) => Some(Self::FunctionCall(call)),
1041                    Content::FunctionResult(result) => Some(Self::FunctionResult(result)),
1042                    Content::CodeExecutionCall(call) => Some(Self::CodeExecutionCall(call)),
1043                    Content::CodeExecutionResult(result) => Some(Self::CodeExecutionResult(result)),
1044                    Content::UrlContextCall(call) => Some(Self::UrlContextCall(call)),
1045                    Content::UrlContextResult(result) => Some(Self::UrlContextResult(result)),
1046                    Content::GoogleSearchCall(call) => Some(Self::GoogleSearchCall(call)),
1047                    Content::GoogleSearchResult(result) => Some(Self::GoogleSearchResult(result)),
1048                    Content::McpServerToolCall(call) => Some(Self::McpServerToolCall(call)),
1049                    Content::McpServerToolResult(result) => Some(Self::McpServerToolResult(result)),
1050                    Content::FileSearchResult(result) => Some(Self::FileSearchResult(result)),
1051                    grouped @ (Content::Text(_)
1052                    | Content::Image(_)
1053                    | Content::Audio(_)
1054                    | Content::Document(_)
1055                    | Content::Video(_)) => {
1056                        run.push(grouped);
1057                        None
1058                    }
1059                };
1060                if let Some(step) = own {
1061                    if !run.is_empty() {
1062                        steps.push(group(std::mem::take(&mut run)));
1063                    }
1064                    steps.push(step);
1065                }
1066            }
1067            if !run.is_empty() {
1068                steps.push(group(run));
1069            }
1070            steps
1071        }
1072    }
1073
1074    /// Text annotation metadata for citations.
1075    #[derive(Clone, Debug, Deserialize, Serialize)]
1076    pub struct Annotation {
1077        #[serde(skip_serializing_if = "Option::is_none")]
1078        pub start_index: Option<i64>,
1079        #[serde(skip_serializing_if = "Option::is_none")]
1080        pub end_index: Option<i64>,
1081        #[serde(skip_serializing_if = "Option::is_none")]
1082        pub source: Option<String>,
1083    }
1084
1085    /// Normalized citation extracted from an annotation.
1086    #[derive(Clone, Debug)]
1087    pub struct Citation {
1088        pub start_index: usize,
1089        pub end_index: usize,
1090        pub source: String,
1091    }
1092
1093    /// Text content item.
1094    #[derive(Clone, Debug, Deserialize, Serialize)]
1095    pub struct TextContent {
1096        pub text: String,
1097        #[serde(skip_serializing_if = "Option::is_none")]
1098        pub annotations: Option<Vec<Annotation>>,
1099    }
1100
1101    impl TextContent {
1102        /// Collects citations extracted from annotations.
1103        pub fn citations(&self) -> Vec<Citation> {
1104            let mut citations = Vec::new();
1105            let Some(annotations) = self.annotations.as_ref() else {
1106                return citations;
1107            };
1108
1109            for annotation in annotations {
1110                let (Some(start), Some(end), Some(source)) = (
1111                    annotation.start_index,
1112                    annotation.end_index,
1113                    annotation.source.as_ref(),
1114                ) else {
1115                    continue;
1116                };
1117
1118                if start < 0 || end < 0 {
1119                    continue;
1120                }
1121                let start = start as usize;
1122                let end = end as usize;
1123                if end <= start || end > self.text.len() {
1124                    continue;
1125                }
1126                if !self.text.is_char_boundary(start) || !self.text.is_char_boundary(end) {
1127                    continue;
1128                }
1129
1130                citations.push(Citation {
1131                    start_index: start,
1132                    end_index: end,
1133                    source: source.clone(),
1134                });
1135            }
1136
1137            citations.sort_by(|a, b| {
1138                a.start_index
1139                    .cmp(&b.start_index)
1140                    .then_with(|| a.end_index.cmp(&b.end_index))
1141            });
1142
1143            citations
1144        }
1145
1146        /// Returns the text with inline citations appended after annotated spans.
1147        pub fn with_inline_citations(&self) -> String {
1148            let citations = self.citations();
1149            if citations.is_empty() {
1150                return self.text.clone();
1151            }
1152
1153            let mut source_order = Vec::new();
1154            for citation in &citations {
1155                if !source_order.contains(&citation.source) {
1156                    source_order.push(citation.source.clone());
1157                }
1158            }
1159
1160            let mut inserts = citations
1161                .iter()
1162                .map(|citation| {
1163                    let index = source_order
1164                        .iter()
1165                        .position(|source| source == &citation.source)
1166                        .map_or(0, |idx| idx + 1);
1167                    (
1168                        citation.start_index,
1169                        citation.end_index,
1170                        index,
1171                        &citation.source,
1172                    )
1173                })
1174                .collect::<Vec<_>>();
1175
1176            inserts.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| b.0.cmp(&a.0)));
1177
1178            let mut text = self.text.clone();
1179            for (_, end, index, source) in inserts {
1180                if index == 0 {
1181                    continue;
1182                }
1183                let citation = format!("[{index}]({source})");
1184                text.insert_str(end, &citation);
1185            }
1186
1187            text
1188        }
1189    }
1190
1191    /// Image content item.
1192    #[derive(Clone, Debug, Deserialize, Serialize)]
1193    pub struct ImageContent {
1194        #[serde(skip_serializing_if = "Option::is_none")]
1195        pub data: Option<String>,
1196        #[serde(skip_serializing_if = "Option::is_none")]
1197        pub uri: Option<String>,
1198        #[serde(skip_serializing_if = "Option::is_none")]
1199        pub mime_type: Option<String>,
1200        #[serde(skip_serializing_if = "Option::is_none")]
1201        pub resolution: Option<MediaResolution>,
1202    }
1203
1204    /// Audio content item.
1205    #[derive(Clone, Debug, Deserialize, Serialize)]
1206    pub struct AudioContent {
1207        #[serde(skip_serializing_if = "Option::is_none")]
1208        pub data: Option<String>,
1209        #[serde(skip_serializing_if = "Option::is_none")]
1210        pub uri: Option<String>,
1211        #[serde(skip_serializing_if = "Option::is_none")]
1212        pub mime_type: Option<String>,
1213    }
1214
1215    /// Document content item.
1216    #[derive(Clone, Debug, Deserialize, Serialize)]
1217    pub struct DocumentContent {
1218        #[serde(skip_serializing_if = "Option::is_none")]
1219        pub data: Option<String>,
1220        #[serde(skip_serializing_if = "Option::is_none")]
1221        pub uri: Option<String>,
1222        #[serde(skip_serializing_if = "Option::is_none")]
1223        pub mime_type: Option<String>,
1224    }
1225
1226    /// Video content item.
1227    #[derive(Clone, Debug, Deserialize, Serialize)]
1228    pub struct VideoContent {
1229        #[serde(skip_serializing_if = "Option::is_none")]
1230        pub data: Option<String>,
1231        #[serde(skip_serializing_if = "Option::is_none")]
1232        pub uri: Option<String>,
1233        #[serde(skip_serializing_if = "Option::is_none")]
1234        pub mime_type: Option<String>,
1235        #[serde(skip_serializing_if = "Option::is_none")]
1236        pub resolution: Option<MediaResolution>,
1237    }
1238
1239    /// Thought summary content.
1240    #[derive(Clone, Debug, Deserialize, Serialize)]
1241    pub struct ThoughtContent {
1242        #[serde(skip_serializing_if = "Option::is_none")]
1243        pub signature: Option<String>,
1244        #[serde(skip_serializing_if = "Option::is_none")]
1245        pub summary: Option<Vec<ThoughtSummaryContent>>,
1246    }
1247
1248    /// Thought summary item with the `type` tag required for replay.
1249    #[derive(Clone, Debug, Deserialize, Serialize)]
1250    #[serde(tag = "type", rename_all = "snake_case")]
1251    pub enum ThoughtSummaryContent {
1252        Text(TextContent),
1253        Image(ImageContent),
1254    }
1255
1256    /// Function call content item.
1257    #[derive(Clone, Debug, Deserialize, Serialize)]
1258    pub struct FunctionCallContent {
1259        #[serde(skip_serializing_if = "Option::is_none")]
1260        pub name: Option<String>,
1261        #[serde(skip_serializing_if = "Option::is_none")]
1262        pub arguments: Option<Value>,
1263        #[serde(skip_serializing_if = "Option::is_none")]
1264        pub id: Option<String>,
1265    }
1266
1267    /// Function result content item.
1268    #[derive(Clone, Debug, Deserialize, Serialize)]
1269    pub struct FunctionResultContent {
1270        #[serde(skip_serializing_if = "Option::is_none")]
1271        pub name: Option<String>,
1272        #[serde(skip_serializing_if = "Option::is_none")]
1273        pub is_error: Option<bool>,
1274        #[serde(skip_serializing_if = "Option::is_none")]
1275        pub result: Option<Value>,
1276        #[serde(skip_serializing_if = "Option::is_none")]
1277        pub call_id: Option<String>,
1278    }
1279
1280    /// Arguments for a code execution call.
1281    #[derive(Clone, Debug, Deserialize, Serialize)]
1282    pub struct CodeExecutionCallArguments {
1283        #[serde(skip_serializing_if = "Option::is_none")]
1284        pub language: Option<String>,
1285        #[serde(skip_serializing_if = "Option::is_none")]
1286        pub code: Option<String>,
1287    }
1288
1289    /// Code execution call content item.
1290    #[derive(Clone, Debug, Deserialize, Serialize)]
1291    pub struct CodeExecutionCallContent {
1292        #[serde(skip_serializing_if = "Option::is_none")]
1293        pub arguments: Option<CodeExecutionCallArguments>,
1294        #[serde(skip_serializing_if = "Option::is_none")]
1295        pub id: Option<String>,
1296    }
1297
1298    /// Code execution result content item.
1299    #[derive(Clone, Debug, Deserialize, Serialize)]
1300    pub struct CodeExecutionResultContent {
1301        #[serde(skip_serializing_if = "Option::is_none")]
1302        pub result: Option<String>,
1303        #[serde(skip_serializing_if = "Option::is_none")]
1304        pub is_error: Option<bool>,
1305        #[serde(skip_serializing_if = "Option::is_none")]
1306        pub signature: Option<String>,
1307        #[serde(skip_serializing_if = "Option::is_none")]
1308        pub call_id: Option<String>,
1309    }
1310
1311    /// Arguments for a URL context call.
1312    #[derive(Clone, Debug, Deserialize, Serialize)]
1313    pub struct UrlContextCallArguments {
1314        #[serde(skip_serializing_if = "Option::is_none")]
1315        pub urls: Option<Vec<String>>,
1316    }
1317
1318    /// URL context call content item.
1319    #[derive(Clone, Debug, Deserialize, Serialize)]
1320    pub struct UrlContextCallContent {
1321        #[serde(skip_serializing_if = "Option::is_none")]
1322        pub arguments: Option<UrlContextCallArguments>,
1323        #[serde(skip_serializing_if = "Option::is_none")]
1324        pub id: Option<String>,
1325    }
1326
1327    /// URL context result entry.
1328    #[derive(Clone, Debug, Deserialize, Serialize)]
1329    pub struct UrlContextResult {
1330        #[serde(skip_serializing_if = "Option::is_none")]
1331        pub url: Option<String>,
1332        #[serde(skip_serializing_if = "Option::is_none")]
1333        pub status: Option<String>,
1334    }
1335
1336    /// URL context result content item.
1337    #[derive(Clone, Debug, Deserialize, Serialize)]
1338    pub struct UrlContextResultContent {
1339        #[serde(skip_serializing_if = "Option::is_none")]
1340        pub signature: Option<String>,
1341        #[serde(skip_serializing_if = "Option::is_none")]
1342        pub result: Option<Vec<UrlContextResult>>,
1343        #[serde(skip_serializing_if = "Option::is_none")]
1344        pub is_error: Option<bool>,
1345        #[serde(skip_serializing_if = "Option::is_none")]
1346        pub call_id: Option<String>,
1347    }
1348
1349    /// Arguments for a Google Search call.
1350    #[derive(Clone, Debug, Deserialize, Serialize)]
1351    pub struct GoogleSearchCallArguments {
1352        #[serde(skip_serializing_if = "Option::is_none")]
1353        pub queries: Option<Vec<String>>,
1354    }
1355
1356    /// Google Search call content item.
1357    #[derive(Clone, Debug, Deserialize, Serialize)]
1358    pub struct GoogleSearchCallContent {
1359        #[serde(skip_serializing_if = "Option::is_none")]
1360        pub arguments: Option<GoogleSearchCallArguments>,
1361        #[serde(skip_serializing_if = "Option::is_none")]
1362        pub id: Option<String>,
1363    }
1364
1365    /// Google Search result entry.
1366    #[derive(Clone, Debug, Deserialize, Serialize)]
1367    pub struct GoogleSearchResult {
1368        #[serde(skip_serializing_if = "Option::is_none")]
1369        pub url: Option<String>,
1370        #[serde(skip_serializing_if = "Option::is_none")]
1371        pub title: Option<String>,
1372        #[serde(skip_serializing_if = "Option::is_none")]
1373        pub rendered_content: Option<String>,
1374    }
1375
1376    /// Google Search result content item.
1377    #[derive(Clone, Debug, Deserialize, Serialize)]
1378    pub struct GoogleSearchResultContent {
1379        #[serde(skip_serializing_if = "Option::is_none")]
1380        pub signature: Option<String>,
1381        #[serde(skip_serializing_if = "Option::is_none")]
1382        pub result: Option<Vec<GoogleSearchResult>>,
1383        #[serde(skip_serializing_if = "Option::is_none")]
1384        pub is_error: Option<bool>,
1385        #[serde(skip_serializing_if = "Option::is_none")]
1386        pub call_id: Option<String>,
1387    }
1388
1389    /// MCP server tool call content item.
1390    #[derive(Clone, Debug, Deserialize, Serialize)]
1391    pub struct McpServerToolCallContent {
1392        #[serde(skip_serializing_if = "Option::is_none")]
1393        pub name: Option<String>,
1394        #[serde(skip_serializing_if = "Option::is_none")]
1395        pub server_name: Option<String>,
1396        #[serde(skip_serializing_if = "Option::is_none")]
1397        pub arguments: Option<Value>,
1398        #[serde(skip_serializing_if = "Option::is_none")]
1399        pub id: Option<String>,
1400    }
1401
1402    /// MCP server tool result content item.
1403    #[derive(Clone, Debug, Deserialize, Serialize)]
1404    pub struct McpServerToolResultContent {
1405        #[serde(skip_serializing_if = "Option::is_none")]
1406        pub name: Option<String>,
1407        #[serde(skip_serializing_if = "Option::is_none")]
1408        pub server_name: Option<String>,
1409        #[serde(skip_serializing_if = "Option::is_none")]
1410        pub result: Option<Value>,
1411        #[serde(skip_serializing_if = "Option::is_none")]
1412        pub call_id: Option<String>,
1413    }
1414
1415    /// File search result entry.
1416    #[derive(Clone, Debug, Deserialize, Serialize)]
1417    pub struct FileSearchResult {
1418        pub title: String,
1419        pub text: String,
1420        pub file_search_store: String,
1421    }
1422
1423    /// File search result content item.
1424    #[derive(Clone, Debug, Deserialize, Serialize)]
1425    pub struct FileSearchResultContent {
1426        #[serde(skip_serializing_if = "Option::is_none")]
1427        pub result: Option<Vec<FileSearchResult>>,
1428    }
1429
1430    /// Content item produced or consumed by the Interactions API.
1431    #[derive(Clone, Debug, Deserialize, Serialize)]
1432    #[serde(tag = "type", rename_all = "snake_case")]
1433    pub enum Content {
1434        Text(TextContent),
1435        Image(ImageContent),
1436        Audio(AudioContent),
1437        Document(DocumentContent),
1438        Video(VideoContent),
1439        Thought(ThoughtContent),
1440        FunctionCall(FunctionCallContent),
1441        FunctionResult(FunctionResultContent),
1442        CodeExecutionCall(CodeExecutionCallContent),
1443        CodeExecutionResult(CodeExecutionResultContent),
1444        UrlContextCall(UrlContextCallContent),
1445        UrlContextResult(UrlContextResultContent),
1446        GoogleSearchCall(GoogleSearchCallContent),
1447        GoogleSearchResult(GoogleSearchResultContent),
1448        McpServerToolCall(McpServerToolCallContent),
1449        McpServerToolResult(McpServerToolResultContent),
1450        FileSearchResult(FileSearchResultContent),
1451    }
1452
1453    fn rich_function_result_block(
1454        content: message::ToolResultContent,
1455    ) -> Result<Value, message::MessageError> {
1456        let content = match content {
1457            message::ToolResultContent::Text(text) => Content::Text(TextContent {
1458                text: text.text,
1459                annotations: None,
1460            }),
1461            message::ToolResultContent::Json { value } => Content::Text(TextContent {
1462                text: value.to_string(),
1463                annotations: None,
1464            }),
1465            message::ToolResultContent::Image(message::Image {
1466                data, media_type, ..
1467            }) => {
1468                let media_type = media_type.ok_or_else(|| {
1469                    message::MessageError::ConversionError(
1470                        "Image media type is required for Gemini Interactions tool results"
1471                            .to_string(),
1472                    )
1473                })?;
1474                let (data, uri) = split_data_uri(data)?;
1475
1476                Content::Image(ImageContent {
1477                    data,
1478                    uri,
1479                    mime_type: Some(media_type.to_mime_type().to_string()),
1480                    resolution: None,
1481                })
1482            }
1483        };
1484
1485        serde_json::to_value(content).map_err(|err| {
1486            message::MessageError::ConversionError(format!(
1487                "Failed to serialize Gemini Interactions tool result content: {err}"
1488            ))
1489        })
1490    }
1491
1492    impl TryFrom<message::UserContent> for Content {
1493        type Error = message::MessageError;
1494
1495        fn try_from(content: message::UserContent) -> Result<Self, Self::Error> {
1496            match content {
1497                message::UserContent::Text(message::Text { text, .. }) => {
1498                    Ok(Self::Text(TextContent {
1499                        text,
1500                        annotations: None,
1501                    }))
1502                }
1503                message::UserContent::ToolResult(tool_result) => {
1504                    // The wire requires a call id even when the original provider issued none.
1505                    let call_id = tool_result.call.wire().into_owned();
1506                    let name = tool_result.name;
1507
1508                    let mut contents = tool_result.content.into_iter().collect::<Vec<_>>();
1509                    let result = if contents.len() == 1 {
1510                        let content = contents.pop().ok_or_else(|| {
1511                            message::MessageError::ConversionError(
1512                                "Tool result content must not be empty".to_string(),
1513                            )
1514                        })?;
1515
1516                        match content {
1517                            message::ToolResultContent::Text(text) => Value::String(text.text),
1518                            // A scalar or array JSON result is wrapped as the
1519                            // generate wire wraps it (`{"result": value}`): sent
1520                            // as a text block it is a multimodal response, which
1521                            // the models refuse.
1522                            message::ToolResultContent::Json { value } => match value {
1523                                value @ (Value::String(_) | Value::Object(_)) => value,
1524                                value @ (Value::Null
1525                                | Value::Bool(_)
1526                                | Value::Number(_)
1527                                | Value::Array(_)) => serde_json::json!({ "result": value }),
1528                            },
1529                            rich_content => {
1530                                Value::Array(vec![rich_function_result_block(rich_content)?])
1531                            }
1532                        }
1533                    } else {
1534                        Value::Array(
1535                            contents
1536                                .into_iter()
1537                                .map(rich_function_result_block)
1538                                .collect::<Result<Vec<_>, _>>()?,
1539                        )
1540                    };
1541
1542                    Ok(Self::FunctionResult(FunctionResultContent {
1543                        name: Some(name.into()),
1544                        is_error: None,
1545                        result: Some(result),
1546                        call_id: Some(call_id),
1547                    }))
1548                }
1549                message::UserContent::Image(message::Image {
1550                    data, media_type, ..
1551                }) => {
1552                    let (data, uri, mime_type) = media_parts(data, media_type, "image")?;
1553                    Ok(Self::Image(ImageContent {
1554                        data,
1555                        uri,
1556                        mime_type: Some(mime_type),
1557                        resolution: None,
1558                    }))
1559                }
1560                message::UserContent::Audio(message::Audio {
1561                    data, media_type, ..
1562                }) => {
1563                    let (data, uri, mime_type) = media_parts(data, media_type, "audio")?;
1564                    Ok(Self::Audio(AudioContent {
1565                        data,
1566                        uri,
1567                        mime_type: Some(mime_type),
1568                    }))
1569                }
1570                message::UserContent::Video(message::Video {
1571                    data, media_type, ..
1572                }) => {
1573                    let (data, uri, mime_type) = media_parts(data, media_type, "video")?;
1574                    Ok(Self::Video(VideoContent {
1575                        data,
1576                        uri,
1577                        mime_type: Some(mime_type),
1578                        resolution: None,
1579                    }))
1580                }
1581                message::UserContent::Document(message::Document {
1582                    data, media_type, ..
1583                }) => {
1584                    let media_type = media_type.ok_or_else(|| {
1585                        message::MessageError::ConversionError(
1586                            "Media type for document is required for Gemini".to_string(),
1587                        )
1588                    })?;
1589                    if matches!(media_type, message::DocumentMediaType::TXT) {
1590                        let text = match data {
1591                            message::DocumentSourceKind::String(text) => text,
1592                            message::DocumentSourceKind::Base64(data) => {
1593                                let decoded = BASE64_STANDARD.decode(data).map_err(|error| {
1594                                    message::MessageError::ConversionError(format!(
1595                                        "Failed to decode text document base64 data: {error}"
1596                                    ))
1597                                })?;
1598                                String::from_utf8(decoded).map_err(|error| {
1599                                    message::MessageError::ConversionError(format!(
1600                                        "Text document data must be UTF-8: {error}"
1601                                    ))
1602                                })?
1603                            }
1604                            message::DocumentSourceKind::Raw(data) => String::from_utf8(data)
1605                                .map_err(|error| {
1606                                    message::MessageError::ConversionError(format!(
1607                                        "Text document data must be UTF-8: {error}"
1608                                    ))
1609                                })?,
1610                            message::DocumentSourceKind::Url(_) => {
1611                                return Err(message::MessageError::ConversionError(
1612                                    "Text document URLs are not supported for Gemini Interactions inputs"
1613                                        .to_string(),
1614                                ));
1615                            }
1616                            message::DocumentSourceKind::FileId(_) => {
1617                                return Err(message::MessageError::ConversionError(
1618                                    "Provider file IDs are not supported for Gemini Interactions inputs"
1619                                        .to_string(),
1620                                ));
1621                            }
1622                            message::DocumentSourceKind::Unknown => {
1623                                return Err(message::MessageError::ConversionError(
1624                                    "Unknown content source".to_string(),
1625                                ));
1626                            }
1627                        };
1628                        return Ok(Self::Text(TextContent {
1629                            text,
1630                            annotations: None,
1631                        }));
1632                    }
1633                    let (data, uri, mime_type) = media_parts(data, Some(media_type), "document")?;
1634                    Ok(Self::Document(DocumentContent {
1635                        data,
1636                        uri,
1637                        mime_type: Some(mime_type),
1638                    }))
1639                }
1640            }
1641        }
1642    }
1643
1644    impl TryFrom<message::AssistantContent> for Content {
1645        type Error = message::MessageError;
1646
1647        fn try_from(content: message::AssistantContent) -> Result<Self, Self::Error> {
1648            match content {
1649                message::AssistantContent::Text(message::Text { text, .. }) => {
1650                    Ok(Self::Text(TextContent {
1651                        text,
1652                        annotations: None,
1653                    }))
1654                }
1655                message::AssistantContent::ToolCall(tool_call) => {
1656                    let call_id = tool_call.id.wire().into_owned();
1657                    Ok(Self::FunctionCall(FunctionCallContent {
1658                        name: Some(tool_call.function.name.into()),
1659                        arguments: Some(tool_call.function.arguments),
1660                        id: Some(call_id),
1661                    }))
1662                }
1663                message::AssistantContent::Reasoning(reasoning) => {
1664                    let content = reasoning
1665                        .open(&super::ISSUER)
1666                        .ok_or_else(|| {
1667                            message::MessageError::ConversionError(
1668                                "Gemini cannot replay reasoning another service issued".to_owned(),
1669                            )
1670                        })?
1671                        .content
1672                        .clone();
1673                    // Preserve signature-only thoughts without empty summary items,
1674                    // which the API rejects.
1675                    let signature = content.iter().find_map(|part| match part {
1676                        message::ReasoningContent::Text { signature, .. } => signature.clone(),
1677                        message::ReasoningContent::Summary(_)
1678                        | message::ReasoningContent::Encrypted(_)
1679                        | message::ReasoningContent::Redacted { .. } => None,
1680                    });
1681                    let summary: Vec<ThoughtSummaryContent> = content
1682                        .into_iter()
1683                        .map(|part| match part {
1684                            message::ReasoningContent::Text { text, .. }
1685                            | message::ReasoningContent::Summary(text)
1686                            | message::ReasoningContent::Encrypted(text) => text,
1687                            message::ReasoningContent::Redacted { data } => data,
1688                        })
1689                        .filter(|text| !text.is_empty())
1690                        .map(|text| {
1691                            ThoughtSummaryContent::Text(TextContent {
1692                                text,
1693                                annotations: None,
1694                            })
1695                        })
1696                        .collect();
1697
1698                    Ok(Self::Thought(ThoughtContent {
1699                        signature,
1700                        summary: (!summary.is_empty()).then_some(summary),
1701                    }))
1702                }
1703                message::AssistantContent::Image(message::Image {
1704                    data, media_type, ..
1705                }) => {
1706                    let media_type = media_type.ok_or_else(|| {
1707                        message::MessageError::ConversionError(
1708                            "Media type for image is required for Gemini".to_string(),
1709                        )
1710                    })?;
1711                    let mime_type = media_type.to_mime_type().to_string();
1712                    let (data, uri) = split_data_uri(data)?;
1713                    Ok(Self::Image(ImageContent {
1714                        data,
1715                        uri,
1716                        mime_type: Some(mime_type),
1717                        resolution: None,
1718                    }))
1719                }
1720            }
1721        }
1722    }
1723
1724    /// Response modalities supported by the model.
1725    #[derive(Clone, Debug, Deserialize, Serialize)]
1726    #[serde(rename_all = "snake_case")]
1727    pub enum ResponseModality {
1728        Text,
1729        Image,
1730        Audio,
1731    }
1732
1733    /// Thinking depth hint for generation.
1734    #[derive(Clone, Debug, Deserialize, Serialize)]
1735    #[serde(rename_all = "snake_case")]
1736    pub enum ThinkingLevel {
1737        Minimal,
1738        Low,
1739        Medium,
1740        High,
1741    }
1742
1743    /// Thinking summary behavior.
1744    #[derive(Clone, Debug, Deserialize, Serialize)]
1745    #[serde(rename_all = "snake_case")]
1746    pub enum ThinkingSummaries {
1747        Auto,
1748        None,
1749    }
1750
1751    /// Speech synthesis configuration.
1752    #[derive(Clone, Debug, Deserialize, Serialize)]
1753    #[serde(rename_all = "snake_case")]
1754    pub struct SpeechConfig {
1755        #[serde(skip_serializing_if = "Option::is_none")]
1756        pub voice: Option<String>,
1757        #[serde(skip_serializing_if = "Option::is_none")]
1758        pub language: Option<String>,
1759        #[serde(skip_serializing_if = "Option::is_none")]
1760        pub speaker: Option<String>,
1761    }
1762
1763    /// Generation configuration for the Interactions API.
1764    #[derive(Clone, Debug, Deserialize, Serialize, Default)]
1765    #[serde(rename_all = "snake_case")]
1766    pub struct GenerationConfig {
1767        #[serde(skip_serializing_if = "Option::is_none")]
1768        pub temperature: Option<f64>,
1769        #[serde(skip_serializing_if = "Option::is_none")]
1770        pub top_p: Option<f64>,
1771        #[serde(skip_serializing_if = "Option::is_none")]
1772        pub seed: Option<u64>,
1773        #[serde(skip_serializing_if = "Option::is_none")]
1774        pub stop_sequences: Option<Vec<String>>,
1775        #[serde(skip_serializing_if = "Option::is_none")]
1776        pub tool_choice: Option<ToolChoice>,
1777        #[serde(skip_serializing_if = "Option::is_none")]
1778        pub thinking_level: Option<ThinkingLevel>,
1779        #[serde(skip_serializing_if = "Option::is_none")]
1780        pub thinking_summaries: Option<ThinkingSummaries>,
1781        #[serde(skip_serializing_if = "Option::is_none")]
1782        pub max_output_tokens: Option<u64>,
1783        #[serde(skip_serializing_if = "Option::is_none")]
1784        pub speech_config: Option<Vec<SpeechConfig>>,
1785    }
1786
1787    impl GenerationConfig {
1788        /// Returns true when no generation fields are set.
1789        pub fn is_empty(&self) -> bool {
1790            self.temperature.is_none()
1791                && self.top_p.is_none()
1792                && self.seed.is_none()
1793                && self.stop_sequences.is_none()
1794                && self.tool_choice.is_none()
1795                && self.thinking_level.is_none()
1796                && self.thinking_summaries.is_none()
1797                && self.max_output_tokens.is_none()
1798                && self.speech_config.is_none()
1799        }
1800    }
1801
1802    /// Tool selection strategy.
1803    #[derive(Clone, Debug, Deserialize, Serialize)]
1804    #[serde(untagged)]
1805    pub enum ToolChoice {
1806        Type(ToolChoiceType),
1807        Config(ToolChoiceConfig),
1808    }
1809
1810    /// Tool selection mode.
1811    #[derive(Clone, Debug, Deserialize, Serialize)]
1812    #[serde(rename_all = "snake_case")]
1813    pub enum ToolChoiceType {
1814        Auto,
1815        Any,
1816        None,
1817        Validated,
1818    }
1819
1820    /// Tool selection configuration.
1821    #[derive(Clone, Debug, Deserialize, Serialize)]
1822    pub struct ToolChoiceConfig {
1823        pub allowed_tools: AllowedTools,
1824    }
1825
1826    /// Allowed tools for tool selection.
1827    #[derive(Clone, Debug, Deserialize, Serialize)]
1828    pub struct AllowedTools {
1829        #[serde(skip_serializing_if = "Option::is_none")]
1830        pub mode: Option<ToolChoiceType>,
1831        #[serde(skip_serializing_if = "Option::is_none")]
1832        pub tools: Option<Vec<String>>,
1833    }
1834
1835    /// Tool definition for Interactions API.
1836    #[derive(Clone, Debug, Deserialize, Serialize)]
1837    #[serde(tag = "type", rename_all = "snake_case")]
1838    pub enum Tool {
1839        Function(FunctionTool),
1840        GoogleSearch,
1841        CodeExecution,
1842        UrlContext,
1843        ComputerUse(ComputerUseTool),
1844        McpServer(McpServerTool),
1845        FileSearch(FileSearchTool),
1846    }
1847
1848    /// Function tool definition.
1849    #[derive(Clone, Debug, Deserialize, Serialize)]
1850    pub struct FunctionTool {
1851        #[serde(skip_serializing_if = "Option::is_none")]
1852        pub name: Option<String>,
1853        #[serde(skip_serializing_if = "Option::is_none")]
1854        pub description: Option<String>,
1855        #[serde(skip_serializing_if = "Option::is_none")]
1856        pub parameters: Option<Value>,
1857    }
1858
1859    /// Computer use tool configuration.
1860    #[derive(Clone, Debug, Deserialize, Serialize)]
1861    pub struct ComputerUseTool {
1862        #[serde(skip_serializing_if = "Option::is_none")]
1863        pub environment: Option<String>,
1864        #[serde(skip_serializing_if = "Option::is_none")]
1865        pub excluded_predefined_functions: Option<Vec<String>>,
1866    }
1867
1868    /// MCP server tool configuration.
1869    #[derive(Clone, Debug, Deserialize, Serialize)]
1870    pub struct McpServerTool {
1871        #[serde(skip_serializing_if = "Option::is_none")]
1872        pub name: Option<String>,
1873        #[serde(skip_serializing_if = "Option::is_none")]
1874        pub url: Option<String>,
1875        #[serde(skip_serializing_if = "Option::is_none")]
1876        pub headers: Option<Value>,
1877        #[serde(skip_serializing_if = "Option::is_none")]
1878        pub allowed_tools: Option<AllowedTools>,
1879    }
1880
1881    /// File search tool configuration.
1882    #[derive(Clone, Debug, Deserialize, Serialize)]
1883    pub struct FileSearchTool {
1884        #[serde(skip_serializing_if = "Option::is_none")]
1885        pub file_search_store_names: Option<Vec<String>>,
1886        #[serde(skip_serializing_if = "Option::is_none")]
1887        pub top_k: Option<u64>,
1888        #[serde(skip_serializing_if = "Option::is_none")]
1889        pub metadata_filter: Option<String>,
1890    }
1891
1892    impl TryFrom<crate::completion::ToolDefinition> for Tool {
1893        type Error = EncodeError;
1894
1895        fn try_from(tool: crate::completion::ToolDefinition) -> Result<Self, Self::Error> {
1896            Ok(Tool::Function(FunctionTool {
1897                name: Some(tool.name),
1898                description: Some(tool.description),
1899                parameters: Some(tool.parameters),
1900            }))
1901        }
1902    }
1903
1904    impl TryFrom<message::ToolChoice> for ToolChoice {
1905        type Error = EncodeError;
1906
1907        fn try_from(tool_choice: message::ToolChoice) -> Result<Self, Self::Error> {
1908            match tool_choice {
1909                message::ToolChoice::Auto => Ok(ToolChoice::Type(ToolChoiceType::Auto)),
1910                message::ToolChoice::None => Ok(ToolChoice::Type(ToolChoiceType::None)),
1911                message::ToolChoice::Required => Ok(ToolChoice::Type(ToolChoiceType::Any)),
1912                message::ToolChoice::Specific { function_names } => {
1913                    Ok(ToolChoice::Config(ToolChoiceConfig {
1914                        allowed_tools: AllowedTools {
1915                            mode: Some(ToolChoiceType::Validated),
1916                            tools: Some(function_names),
1917                        },
1918                    }))
1919                }
1920            }
1921        }
1922    }
1923
1924    /// Agent configuration for Interactions API.
1925    #[derive(Clone, Debug, Deserialize, Serialize)]
1926    #[serde(tag = "type", rename_all = "kebab-case")]
1927    pub enum AgentConfig {
1928        Dynamic,
1929        DeepResearch {
1930            #[serde(skip_serializing_if = "Option::is_none")]
1931            thinking_summaries: Option<ThinkingSummaries>,
1932        },
1933    }
1934
1935    /// Media resolution hint for multimodal content.
1936    #[derive(Clone, Debug, Deserialize, Serialize)]
1937    #[serde(rename_all = "snake_case")]
1938    pub enum MediaResolution {
1939        Low,
1940        Medium,
1941        High,
1942        UltraHigh,
1943    }
1944
1945    /// Server-sent event payloads for streaming interactions.
1946    #[derive(Clone, Debug, Deserialize, Serialize)]
1947    #[serde(tag = "event_type")]
1948    pub enum InteractionSseEvent {
1949        #[serde(rename = "interaction.created")]
1950        InteractionCreated {
1951            interaction: Interaction,
1952            #[serde(skip_serializing_if = "Option::is_none")]
1953            event_id: Option<String>,
1954        },
1955        #[serde(rename = "interaction.completed")]
1956        InteractionCompleted {
1957            interaction: Interaction,
1958            #[serde(skip_serializing_if = "Option::is_none")]
1959            event_id: Option<String>,
1960        },
1961        #[serde(rename = "interaction.status_update")]
1962        InteractionStatusUpdate {
1963            /// Absent for an interaction that is not stored (`store: false`).
1964            #[serde(default)]
1965            interaction_id: String,
1966            status: InteractionStatus,
1967            #[serde(skip_serializing_if = "Option::is_none")]
1968            event_id: Option<String>,
1969        },
1970        #[serde(rename = "step.start")]
1971        StepStart {
1972            index: u32,
1973            step: Step,
1974            #[serde(skip_serializing_if = "Option::is_none")]
1975            event_id: Option<String>,
1976        },
1977        #[serde(rename = "step.delta")]
1978        StepDelta {
1979            index: u32,
1980            delta: ContentDelta,
1981            #[serde(skip_serializing_if = "Option::is_none")]
1982            event_id: Option<String>,
1983        },
1984        #[serde(rename = "step.stop")]
1985        StepStop {
1986            index: u32,
1987            #[serde(skip_serializing_if = "Option::is_none")]
1988            event_id: Option<String>,
1989        },
1990        #[serde(rename = "error")]
1991        Error {
1992            error: ErrorEvent,
1993            #[serde(skip_serializing_if = "Option::is_none")]
1994            event_id: Option<String>,
1995        },
1996    }
1997
1998    /// Error payload for streaming events.
1999    #[derive(Clone, Debug, Deserialize, Serialize)]
2000    pub struct ErrorEvent {
2001        pub code: String,
2002        pub message: String,
2003    }
2004
2005    /// A tagged content delta containing a whole item or a text, argument, or thought fragment.
2006    #[derive(Clone, Debug, Deserialize, Serialize)]
2007    #[serde(tag = "type", rename_all = "snake_case")]
2008    pub enum ContentDelta {
2009        Text(TextDelta),
2010        Image(ImageContent),
2011        Audio(AudioContent),
2012        Document(DocumentContent),
2013        Video(VideoContent),
2014        ThoughtSummary(ThoughtSummaryDelta),
2015        ThoughtSignature(ThoughtSignatureDelta),
2016        FunctionCall(FunctionCallContent),
2017        ArgumentsDelta(ArgumentsDelta),
2018        FunctionResult(FunctionResultContent),
2019        CodeExecutionCall(CodeExecutionCallContent),
2020        CodeExecutionResult(CodeExecutionResultContent),
2021        UrlContextCall(UrlContextCallContent),
2022        UrlContextResult(UrlContextResultContent),
2023        GoogleSearchCall(GoogleSearchCallContent),
2024        GoogleSearchResult(GoogleSearchResultContent),
2025        McpServerToolCall(McpServerToolCallContent),
2026        McpServerToolResult(McpServerToolResultContent),
2027        FileSearchResult(FileSearchResultContent),
2028    }
2029
2030    /// Raw JSON argument fragment for the function call at the enclosing step index.
2031    #[derive(Clone, Debug, Deserialize, Serialize)]
2032    pub struct ArgumentsDelta {
2033        #[serde(skip_serializing_if = "Option::is_none")]
2034        pub arguments: Option<String>,
2035    }
2036
2037    /// Streaming text delta.
2038    #[derive(Clone, Debug, Deserialize, Serialize)]
2039    pub struct TextDelta {
2040        #[serde(skip_serializing_if = "Option::is_none")]
2041        pub text: Option<String>,
2042        #[serde(skip_serializing_if = "Option::is_none")]
2043        pub annotations: Option<Vec<Annotation>>,
2044    }
2045
2046    /// Streaming thought summary delta.
2047    #[derive(Clone, Debug, Deserialize, Serialize)]
2048    pub struct ThoughtSummaryDelta {
2049        pub content: ThoughtSummaryContent,
2050    }
2051
2052    /// Streaming thought signature delta.
2053    #[derive(Clone, Debug, Deserialize, Serialize)]
2054    pub struct ThoughtSignatureDelta {
2055        pub signature: String,
2056    }
2057}
2058
2059#[cfg(test)]
2060mod tests;
2061
2062#[cfg(test)]
2063mod interaction_usage_tests;