Skip to main content

rig_core/providers/gemini/interactions_api/
mod.rs

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