genai_rs/
response.rs

1//! Response types for the Interactions API.
2//!
3//! This module contains `InteractionResponse` and related types for handling
4//! API responses, including helper methods for extracting content.
5
6use base64::Engine;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Deserializer, Serialize};
9use std::collections::BTreeSet;
10use std::fmt;
11
12use crate::content::{
13    Annotation, CodeExecutionLanguage, Content, FileSearchResultItem, GoogleSearchResultItem,
14};
15use crate::errors::GenaiError;
16use crate::request::Turn;
17use crate::tools::Tool;
18
19// =============================================================================
20// Token Count Deserialization Helpers
21// =============================================================================
22
23/// Deserializes a token count as `u32`, warning if the JSON value is negative.
24///
25/// Token counts should never be negative, but we handle this gracefully per
26/// Evergreen principles. Negative values are clamped to 0 with a warning log.
27fn deserialize_token_count<'de, D>(deserializer: D) -> Result<u32, D::Error>
28where
29    D: Deserializer<'de>,
30{
31    let value = i64::deserialize(deserializer)?;
32    if value < 0 {
33        tracing::warn!(
34            "Received negative token count from API: {}. Clamping to 0.",
35            value
36        );
37        Ok(0)
38    } else if value > u32::MAX as i64 {
39        tracing::warn!(
40            "Token count exceeds u32::MAX: {}. Clamping to u32::MAX.",
41            value
42        );
43        Ok(u32::MAX)
44    } else {
45        Ok(value as u32)
46    }
47}
48
49/// Deserializes an optional token count as `Option<u32>`, warning if negative.
50fn deserialize_optional_token_count<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
51where
52    D: Deserializer<'de>,
53{
54    let value: Option<i64> = Option::deserialize(deserializer)?;
55    match value {
56        None => Ok(None),
57        Some(v) if v < 0 => {
58            tracing::warn!(
59                "Received negative token count from API: {}. Clamping to 0.",
60                v
61            );
62            Ok(Some(0))
63        }
64        Some(v) if v > u32::MAX as i64 => {
65            tracing::warn!("Token count exceeds u32::MAX: {}. Clamping to u32::MAX.", v);
66            Ok(Some(u32::MAX))
67        }
68        Some(v) => Ok(Some(v as u32)),
69    }
70}
71
72/// Status of an interaction.
73///
74/// This enum is marked `#[non_exhaustive]` for forward compatibility.
75/// New status values may be added by the API in future versions.
76///
77/// # Unknown Status Handling
78///
79/// When the API returns a status value that this library doesn't recognize,
80/// it will be captured in the `Unknown` variant with the original status
81/// string preserved. This follows the Evergreen philosophy of graceful
82/// degradation and data preservation.
83#[derive(Clone, Debug, PartialEq)]
84#[non_exhaustive]
85pub enum InteractionStatus {
86    Completed,
87    InProgress,
88    RequiresAction,
89    Failed,
90    Cancelled,
91    /// Unknown status (for forward compatibility).
92    ///
93    /// This variant captures any unrecognized status values from the API,
94    /// allowing the library to handle new statuses gracefully.
95    ///
96    /// The `status_type` field contains the unrecognized status string,
97    /// and `data` contains the JSON value (typically the same string).
98    Unknown {
99        /// The unrecognized status string from the API
100        status_type: String,
101        /// The raw JSON value, preserved for debugging
102        data: serde_json::Value,
103    },
104}
105
106impl InteractionStatus {
107    /// Check if this is an unknown status.
108    #[must_use]
109    pub const fn is_unknown(&self) -> bool {
110        matches!(self, Self::Unknown { .. })
111    }
112
113    /// Returns the status type name if this is an unknown status.
114    ///
115    /// Returns `None` for known statuses.
116    #[must_use]
117    pub fn unknown_status_type(&self) -> Option<&str> {
118        match self {
119            Self::Unknown { status_type, .. } => Some(status_type),
120            _ => None,
121        }
122    }
123
124    /// Returns the raw JSON data if this is an unknown status.
125    ///
126    /// Returns `None` for known statuses.
127    #[must_use]
128    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
129        match self {
130            Self::Unknown { data, .. } => Some(data),
131            _ => None,
132        }
133    }
134}
135
136impl Serialize for InteractionStatus {
137    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138    where
139        S: serde::Serializer,
140    {
141        match self {
142            Self::Completed => serializer.serialize_str("completed"),
143            Self::InProgress => serializer.serialize_str("in_progress"),
144            Self::RequiresAction => serializer.serialize_str("requires_action"),
145            Self::Failed => serializer.serialize_str("failed"),
146            Self::Cancelled => serializer.serialize_str("cancelled"),
147            Self::Unknown { status_type, .. } => serializer.serialize_str(status_type),
148        }
149    }
150}
151
152impl<'de> Deserialize<'de> for InteractionStatus {
153    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
154    where
155        D: serde::Deserializer<'de>,
156    {
157        let value = serde_json::Value::deserialize(deserializer)?;
158
159        match value.as_str() {
160            Some("completed") => Ok(Self::Completed),
161            Some("in_progress") => Ok(Self::InProgress),
162            Some("requires_action") => Ok(Self::RequiresAction),
163            Some("failed") => Ok(Self::Failed),
164            Some("cancelled") => Ok(Self::Cancelled),
165            Some(other) => {
166                tracing::warn!(
167                    "Encountered unknown InteractionStatus '{}'. \
168                     This may indicate a new API feature. \
169                     The status will be preserved in the Unknown variant.",
170                    other
171                );
172                Ok(Self::Unknown {
173                    status_type: other.to_string(),
174                    data: value,
175                })
176            }
177            None => {
178                // Non-string value - preserve it in Unknown
179                let status_type = format!("<non-string: {}>", value);
180                tracing::warn!(
181                    "InteractionStatus received non-string value: {}. \
182                     Preserving in Unknown variant.",
183                    value
184                );
185                Ok(Self::Unknown {
186                    status_type,
187                    data: value,
188                })
189            }
190        }
191    }
192}
193
194/// Token count for a specific modality.
195///
196/// Used in per-modality breakdowns like [`UsageMetadata::input_tokens_by_modality`].
197///
198/// # Example
199///
200/// ```no_run
201/// # use genai_rs::UsageMetadata;
202/// # let usage: UsageMetadata = Default::default();
203/// if let Some(breakdown) = &usage.input_tokens_by_modality {
204///     for modality_tokens in breakdown {
205///         println!("{}: {} tokens", modality_tokens.modality, modality_tokens.tokens);
206///     }
207/// }
208/// ```
209#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
210pub struct ModalityTokens {
211    /// The modality type (e.g., "text", "image", "audio").
212    ///
213    /// Uses string for forward compatibility with new modalities per Evergreen principles.
214    pub modality: String,
215    /// Token count for this modality.
216    ///
217    /// Uses `u32` since token counts are never negative. If the API returns a negative
218    /// value (which would be a bug), it's clamped to 0 with a warning log.
219    #[serde(deserialize_with = "deserialize_token_count")]
220    pub tokens: u32,
221}
222
223/// Token usage information from the Interactions API.
224///
225/// All token counts use `u32` since they're never negative. If the API returns
226/// a negative value (which would be a bug), it's clamped to 0 with a warning log.
227#[derive(Clone, Deserialize, Serialize, Debug, Default, PartialEq)]
228#[serde(default)]
229pub struct UsageMetadata {
230    /// Total number of input tokens (prompt tokens sent to the model)
231    #[serde(
232        skip_serializing_if = "Option::is_none",
233        deserialize_with = "deserialize_optional_token_count"
234    )]
235    pub total_input_tokens: Option<u32>,
236    /// Total number of output tokens (tokens generated by the model)
237    #[serde(
238        skip_serializing_if = "Option::is_none",
239        deserialize_with = "deserialize_optional_token_count"
240    )]
241    pub total_output_tokens: Option<u32>,
242    /// Total number of tokens (input + output)
243    #[serde(
244        skip_serializing_if = "Option::is_none",
245        deserialize_with = "deserialize_optional_token_count"
246    )]
247    pub total_tokens: Option<u32>,
248    /// Total number of cached tokens (from context caching, reduces billing)
249    #[serde(
250        skip_serializing_if = "Option::is_none",
251        deserialize_with = "deserialize_optional_token_count"
252    )]
253    pub total_cached_tokens: Option<u32>,
254    /// Total number of reasoning tokens (populated for thinking models like gemini-2.0-flash-thinking)
255    #[serde(
256        skip_serializing_if = "Option::is_none",
257        deserialize_with = "deserialize_optional_token_count"
258    )]
259    pub total_reasoning_tokens: Option<u32>,
260    /// Total number of thought tokens (thinking model internal reasoning, distinct from reasoning_tokens)
261    ///
262    /// This field appears in API responses for models using the thinking/reasoning features.
263    /// Note: This may overlap with or complement `total_reasoning_tokens` - both are included
264    /// to accurately reflect the wire format returned by the API.
265    #[serde(
266        skip_serializing_if = "Option::is_none",
267        deserialize_with = "deserialize_optional_token_count"
268    )]
269    pub total_thought_tokens: Option<u32>,
270    /// Total number of tokens used for tool/function calling overhead
271    #[serde(
272        skip_serializing_if = "Option::is_none",
273        deserialize_with = "deserialize_optional_token_count"
274    )]
275    pub total_tool_use_tokens: Option<u32>,
276
277    // =========================================================================
278    // Per-Modality Breakdowns
279    // =========================================================================
280    /// Input token counts broken down by modality (text, image, audio).
281    ///
282    /// Useful for understanding cost distribution in multi-modal prompts.
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub input_tokens_by_modality: Option<Vec<ModalityTokens>>,
285
286    /// Output token counts broken down by modality.
287    ///
288    /// Useful for understanding output cost distribution in multi-modal responses.
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub output_tokens_by_modality: Option<Vec<ModalityTokens>>,
291
292    /// Cached token counts broken down by modality.
293    ///
294    /// Shows which modalities benefit from context caching.
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub cached_tokens_by_modality: Option<Vec<ModalityTokens>>,
297
298    /// Tool use token counts broken down by modality.
299    ///
300    /// Shows tool invocation overhead per modality.
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub tool_use_tokens_by_modality: Option<Vec<ModalityTokens>>,
303}
304
305impl UsageMetadata {
306    /// Returns true if any usage data is present
307    #[must_use]
308    pub fn has_data(&self) -> bool {
309        self.total_tokens.is_some()
310            || self.total_input_tokens.is_some()
311            || self.total_output_tokens.is_some()
312            || self.total_cached_tokens.is_some()
313            || self.total_reasoning_tokens.is_some()
314            || self.total_thought_tokens.is_some()
315            || self.total_tool_use_tokens.is_some()
316            || self.input_tokens_by_modality.is_some()
317            || self.output_tokens_by_modality.is_some()
318            || self.cached_tokens_by_modality.is_some()
319            || self.tool_use_tokens_by_modality.is_some()
320    }
321
322    /// Returns total thought tokens (thinking model internal reasoning)
323    ///
324    /// This may be populated for thinking models. See also `total_reasoning_tokens`
325    /// which may contain related but distinct token counts.
326    #[must_use]
327    pub fn thought_tokens(&self) -> Option<u32> {
328        self.total_thought_tokens
329    }
330
331    /// Returns the input token count for a specific modality.
332    ///
333    /// # Arguments
334    ///
335    /// * `modality` - The modality name (e.g., "TEXT", "IMAGE", "AUDIO")
336    ///
337    /// # Returns
338    ///
339    /// The token count for the specified modality, or `None` if the modality
340    /// is not present in the breakdown or if modality data is unavailable.
341    ///
342    /// # Example
343    ///
344    /// ```no_run
345    /// # use genai_rs::UsageMetadata;
346    /// # let usage: UsageMetadata = Default::default();
347    /// if let Some(image_tokens) = usage.input_tokens_for_modality("IMAGE") {
348    ///     println!("Image input cost: {} tokens", image_tokens);
349    /// }
350    /// ```
351    #[must_use]
352    pub fn input_tokens_for_modality(&self, modality: &str) -> Option<u32> {
353        self.input_tokens_by_modality
354            .as_ref()?
355            .iter()
356            .find(|m| m.modality == modality)
357            .map(|m| m.tokens)
358    }
359
360    /// Returns the cache hit rate as a fraction (0.0 to 1.0).
361    ///
362    /// The cache hit rate is the ratio of cached tokens to total input tokens.
363    /// A higher rate indicates better cache utilization and lower costs.
364    ///
365    /// # Returns
366    ///
367    /// - `Some(rate)` where `rate` is between 0.0 and 1.0
368    /// - `None` if either `total_cached_tokens` or `total_input_tokens` is unavailable,
369    ///   or if `total_input_tokens` is zero
370    ///
371    /// # Example
372    ///
373    /// ```no_run
374    /// # use genai_rs::UsageMetadata;
375    /// # let usage: UsageMetadata = Default::default();
376    /// if let Some(rate) = usage.cache_hit_rate() {
377    ///     println!("Cache hit rate: {:.1}%", rate * 100.0);
378    /// }
379    /// ```
380    #[must_use]
381    pub fn cache_hit_rate(&self) -> Option<f32> {
382        let cached = self.total_cached_tokens? as f32;
383        let total = self.total_input_tokens? as f32;
384        if total > 0.0 {
385            Some(cached / total)
386        } else {
387            None
388        }
389    }
390
391    /// Accumulates usage from another `UsageMetadata` into this one.
392    ///
393    /// This is useful for aggregating token counts across multiple API calls,
394    /// such as in auto-function calling loops where each iteration reports
395    /// its own usage.
396    ///
397    /// For each field, if the other has a value:
398    /// - If self has a value, adds the other's value
399    /// - If self has None, takes the other's value
400    ///
401    /// Note: `*_by_modality` fields are not accumulated (would require complex merging).
402    pub(crate) fn accumulate(&mut self, other: &UsageMetadata) {
403        fn add_option(a: &mut Option<u32>, b: Option<u32>) {
404            if let Some(b_val) = b {
405                *a = Some(a.unwrap_or(0).saturating_add(b_val));
406            }
407        }
408
409        add_option(&mut self.total_input_tokens, other.total_input_tokens);
410        add_option(&mut self.total_output_tokens, other.total_output_tokens);
411        add_option(&mut self.total_tokens, other.total_tokens);
412        add_option(&mut self.total_cached_tokens, other.total_cached_tokens);
413        add_option(
414            &mut self.total_reasoning_tokens,
415            other.total_reasoning_tokens,
416        );
417        add_option(&mut self.total_thought_tokens, other.total_thought_tokens);
418        add_option(&mut self.total_tool_use_tokens, other.total_tool_use_tokens);
419        // Note: *_by_modality fields are not accumulated as they would require
420        // complex merging logic. If needed, callers can handle these separately.
421    }
422}
423
424// =============================================================================
425// Metadata Types (Google Search grounding, URL context)
426// =============================================================================
427
428/// Grounding metadata returned when using the GoogleSearch tool.
429///
430/// Contains search queries executed by the model and web sources that
431/// ground the response in real-time information.
432///
433/// # Example
434///
435/// ```no_run
436/// # use genai_rs::InteractionResponse;
437/// # let response: InteractionResponse = todo!();
438/// if let Some(metadata) = response.google_search_metadata() {
439///     println!("Search queries: {:?}", metadata.web_search_queries);
440///     for chunk in &metadata.grounding_chunks {
441///         println!("Source: {} - {}", chunk.web.title, chunk.web.uri);
442///     }
443/// }
444/// ```
445#[derive(Clone, Deserialize, Serialize, Debug, Default, PartialEq)]
446#[serde(default, rename_all = "camelCase")]
447pub struct GroundingMetadata {
448    /// Search queries that were executed by the model
449    pub web_search_queries: Vec<String>,
450
451    /// Web sources referenced in the response
452    pub grounding_chunks: Vec<GroundingChunk>,
453}
454
455/// A web source referenced in grounding.
456#[derive(Clone, Deserialize, Serialize, Debug, Default, PartialEq)]
457pub struct GroundingChunk {
458    /// Web resource information
459    #[serde(default)]
460    pub web: WebSource,
461}
462
463/// Web source details (URI, title, and domain).
464#[derive(Clone, Deserialize, Serialize, Debug, Default, PartialEq, Eq)]
465#[serde(default, rename_all = "camelCase")]
466pub struct WebSource {
467    /// URI of the web page
468    pub uri: String,
469    /// Title of the source
470    pub title: String,
471    /// Domain of the web page (e.g., "wikipedia.org")
472    pub domain: String,
473}
474
475/// Metadata returned when using the UrlContext tool.
476///
477/// Contains retrieval status for each URL that was processed.
478/// This is useful for verification and debugging URL fetches.
479///
480/// # Example
481///
482/// ```no_run
483/// # use genai_rs::InteractionResponse;
484/// # let response: InteractionResponse = todo!();
485/// if let Some(metadata) = response.url_context_metadata() {
486///     for entry in &metadata.url_metadata {
487///         println!("URL: {} - Status: {:?}", entry.retrieved_url, entry.url_retrieval_status);
488///     }
489/// }
490/// ```
491#[derive(Clone, Deserialize, Serialize, Debug, Default, PartialEq)]
492#[serde(default, rename_all = "camelCase")]
493pub struct UrlContextMetadata {
494    /// Metadata for each URL that was processed
495    pub url_metadata: Vec<UrlMetadataEntry>,
496}
497
498/// Retrieval status for a single URL processed by the UrlContext tool.
499#[derive(Clone, Deserialize, Serialize, Debug, Default, PartialEq, Eq)]
500#[serde(default, rename_all = "camelCase")]
501pub struct UrlMetadataEntry {
502    /// The URL that was retrieved
503    pub retrieved_url: String,
504    /// Status of the retrieval attempt
505    pub url_retrieval_status: UrlRetrievalStatus,
506}
507
508/// Status of a URL retrieval attempt.
509///
510/// # Forward Compatibility (Evergreen Philosophy)
511///
512/// This enum is marked `#[non_exhaustive]`, which means:
513/// - Match statements must include a wildcard arm (`_ => ...`)
514/// - New variants may be added in minor version updates without breaking your code
515///
516/// When the API returns a status value that this library doesn't recognize,
517/// it will be captured as `UrlRetrievalStatus::Unknown` rather than causing a
518/// deserialization error. This follows the
519/// [Evergreen spec](https://github.com/google-deepmind/evergreen-spec)
520/// philosophy of graceful degradation.
521#[derive(Clone, Debug, Default, PartialEq, Eq)]
522#[non_exhaustive]
523pub enum UrlRetrievalStatus {
524    /// Status not specified
525    #[default]
526    Unspecified,
527    /// URL content was successfully retrieved
528    Success,
529    /// URL failed safety/content moderation checks
530    Unsafe,
531    /// URL retrieval failed for other reasons
532    Error,
533    /// Unknown status (for forward compatibility).
534    ///
535    /// This variant captures any unrecognized status values from the API,
536    /// allowing the library to handle new statuses gracefully.
537    ///
538    /// The `status_type` field contains the unrecognized status string,
539    /// and `data` contains the full JSON value for debugging.
540    Unknown {
541        /// The unrecognized status string from the API
542        status_type: String,
543        /// The raw JSON value, preserved for debugging
544        data: serde_json::Value,
545    },
546}
547
548impl UrlRetrievalStatus {
549    /// Check if this is an unknown status.
550    #[must_use]
551    pub const fn is_unknown(&self) -> bool {
552        matches!(self, Self::Unknown { .. })
553    }
554
555    /// Returns the status type name if this is an unknown status.
556    ///
557    /// Returns `None` for known statuses.
558    #[must_use]
559    pub fn unknown_status_type(&self) -> Option<&str> {
560        match self {
561            Self::Unknown { status_type, .. } => Some(status_type),
562            _ => None,
563        }
564    }
565
566    /// Returns the raw JSON data if this is an unknown status.
567    ///
568    /// Returns `None` for known statuses.
569    #[must_use]
570    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
571        match self {
572            Self::Unknown { data, .. } => Some(data),
573            _ => None,
574        }
575    }
576
577    /// Returns true if retrieval was successful.
578    #[must_use]
579    pub const fn is_success(&self) -> bool {
580        matches!(self, Self::Success)
581    }
582
583    /// Returns true if retrieval failed for any reason.
584    ///
585    /// **Note:** This returns `true` only for `Error` and `Unsafe` variants.
586    /// The `Unknown` variant is NOT treated as an error because:
587    /// 1. URL retrieval failures are non-critical (graceful degradation)
588    /// 2. An `Unknown` status may represent a new success state from the API
589    #[must_use]
590    pub const fn is_error(&self) -> bool {
591        matches!(self, Self::Error | Self::Unsafe)
592    }
593}
594
595impl Serialize for UrlRetrievalStatus {
596    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
597    where
598        S: serde::Serializer,
599    {
600        match self {
601            Self::Unspecified => serializer.serialize_str("URL_RETRIEVAL_STATUS_UNSPECIFIED"),
602            Self::Success => serializer.serialize_str("URL_RETRIEVAL_STATUS_SUCCESS"),
603            Self::Unsafe => serializer.serialize_str("URL_RETRIEVAL_STATUS_UNSAFE"),
604            Self::Error => serializer.serialize_str("URL_RETRIEVAL_STATUS_ERROR"),
605            Self::Unknown { status_type, .. } => serializer.serialize_str(status_type),
606        }
607    }
608}
609
610impl<'de> Deserialize<'de> for UrlRetrievalStatus {
611    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
612    where
613        D: serde::Deserializer<'de>,
614    {
615        let value = serde_json::Value::deserialize(deserializer)?;
616
617        match value.as_str() {
618            Some("URL_RETRIEVAL_STATUS_UNSPECIFIED") => Ok(Self::Unspecified),
619            Some("URL_RETRIEVAL_STATUS_SUCCESS") => Ok(Self::Success),
620            Some("URL_RETRIEVAL_STATUS_UNSAFE") => Ok(Self::Unsafe),
621            Some("URL_RETRIEVAL_STATUS_ERROR") => Ok(Self::Error),
622            Some(other) => {
623                tracing::warn!(
624                    "Encountered unknown UrlRetrievalStatus '{}'. \
625                     This may indicate a new API feature. \
626                     The status will be preserved in the Unknown variant.",
627                    other
628                );
629                Ok(Self::Unknown {
630                    status_type: other.to_string(),
631                    data: value,
632                })
633            }
634            None => {
635                // Non-string value - preserve it in Unknown
636                let status_type = format!("<non-string: {}>", value);
637                tracing::warn!(
638                    "UrlRetrievalStatus received non-string value: {}. \
639                     Preserving in Unknown variant.",
640                    value
641                );
642                Ok(Self::Unknown {
643                    status_type,
644                    data: value,
645                })
646            }
647        }
648    }
649}
650
651impl fmt::Display for UrlRetrievalStatus {
652    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
653        match self {
654            Self::Unspecified => write!(f, "URL_RETRIEVAL_STATUS_UNSPECIFIED"),
655            Self::Success => write!(f, "URL_RETRIEVAL_STATUS_SUCCESS"),
656            Self::Unsafe => write!(f, "URL_RETRIEVAL_STATUS_UNSAFE"),
657            Self::Error => write!(f, "URL_RETRIEVAL_STATUS_ERROR"),
658            Self::Unknown { status_type, .. } => write!(f, "{}", status_type),
659        }
660    }
661}
662
663// =============================================================================
664// Image Info Type
665// =============================================================================
666
667/// Information about an image in the response.
668///
669/// This is a view type that provides convenient access to image data
670/// in the response, with automatic base64 decoding.
671///
672/// # Example
673///
674/// ```no_run
675/// use genai_rs::Client;
676///
677/// # #[tokio::main]
678/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
679/// let client = Client::new("api-key".to_string());
680///
681/// let response = client
682///     .interaction()
683///     .with_model("gemini-3-flash-preview")
684///     .with_text("A cat playing with yarn")
685///     .with_image_output()
686///     .create()
687///     .await?;
688///
689/// for image in response.images() {
690///     let bytes = image.bytes()?;
691///     let filename = format!("image.{}", image.extension());
692///     std::fs::write(&filename, bytes)?;
693/// }
694/// # Ok(())
695/// # }
696/// ```
697#[derive(Debug, Clone)]
698pub struct ImageInfo<'a> {
699    data: &'a str,
700    mime_type: Option<&'a str>,
701}
702
703impl ImageInfo<'_> {
704    /// Decodes and returns the image bytes.
705    ///
706    /// # Errors
707    ///
708    /// Returns an error if the base64 data is invalid.
709    #[must_use = "this `Result` should be used to handle potential decode errors"]
710    pub fn bytes(&self) -> Result<Vec<u8>, GenaiError> {
711        base64::engine::general_purpose::STANDARD
712            .decode(self.data)
713            .map_err(|e| GenaiError::InvalidInput(format!("Invalid base64 image data: {}", e)))
714    }
715
716    /// Returns the MIME type of the image, if available.
717    #[must_use]
718    pub fn mime_type(&self) -> Option<&str> {
719        self.mime_type
720    }
721
722    /// Returns a file extension suitable for this image's MIME type.
723    ///
724    /// Returns "png" as default if MIME type is unknown or unrecognized.
725    /// Logs a warning for unrecognized MIME types to surface API evolution
726    /// (following the project's Evergreen philosophy).
727    #[must_use]
728    pub fn extension(&self) -> &str {
729        match self.mime_type {
730            Some("image/jpeg") | Some("image/jpg") => "jpg",
731            Some("image/png") => "png",
732            Some("image/webp") => "webp",
733            Some("image/gif") => "gif",
734            Some(unknown) => {
735                tracing::warn!(
736                    "Unknown image MIME type '{}', defaulting to 'png' extension. \
737                     Consider updating genai-rs to handle this type.",
738                    unknown
739                );
740                "png"
741            }
742            None => "png", // No MIME type provided, default to png
743        }
744    }
745}
746
747// =============================================================================
748// Audio Info Type
749// =============================================================================
750
751/// Information about audio content in the response.
752///
753/// This is a view type that provides convenient access to audio data
754/// in the response, with automatic base64 decoding.
755///
756/// # Example
757///
758/// ```no_run
759/// use genai_rs::Client;
760///
761/// # #[tokio::main]
762/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
763/// let client = Client::new("api-key".to_string());
764///
765/// let response = client
766///     .interaction()
767///     .with_model("gemini-2.5-pro-preview-tts")
768///     .with_text("Hello, world!")
769///     .with_audio_output()
770///     .with_voice("Kore")
771///     .create()
772///     .await?;
773///
774/// for audio in response.audios() {
775///     let bytes = audio.bytes()?;
776///     let filename = format!("audio.{}", audio.extension());
777///     std::fs::write(&filename, bytes)?;
778/// }
779/// # Ok(())
780/// # }
781/// ```
782#[derive(Debug, Clone)]
783pub struct AudioInfo<'a> {
784    data: &'a str,
785    mime_type: Option<&'a str>,
786}
787
788impl AudioInfo<'_> {
789    /// Decodes and returns the audio bytes.
790    ///
791    /// # Errors
792    ///
793    /// Returns an error if the base64 data is invalid.
794    #[must_use = "this `Result` should be used to handle potential decode errors"]
795    pub fn bytes(&self) -> Result<Vec<u8>, GenaiError> {
796        base64::engine::general_purpose::STANDARD
797            .decode(self.data)
798            .map_err(|e| GenaiError::InvalidInput(format!("Invalid base64 audio data: {}", e)))
799    }
800
801    /// Returns the MIME type of the audio, if available.
802    #[must_use]
803    pub fn mime_type(&self) -> Option<&str> {
804        self.mime_type
805    }
806
807    /// Returns a file extension suitable for this audio's MIME type.
808    ///
809    /// Returns "wav" as default if MIME type is unknown or unrecognized.
810    /// Logs a warning for unrecognized MIME types to surface API evolution
811    /// (following the project's Evergreen philosophy).
812    #[must_use]
813    pub fn extension(&self) -> &str {
814        match self.mime_type {
815            Some("audio/wav") | Some("audio/x-wav") => "wav",
816            Some("audio/mp3") | Some("audio/mpeg") => "mp3",
817            Some("audio/ogg") => "ogg",
818            Some("audio/flac") => "flac",
819            Some("audio/aac") => "aac",
820            Some("audio/webm") => "webm",
821            // PCM/L16 format from TTS - raw audio data
822            Some(mime) if mime.starts_with("audio/L16") => "pcm",
823            Some(unknown) => {
824                tracing::warn!(
825                    "Unknown audio MIME type '{}', defaulting to 'wav' extension. \
826                     Consider updating genai-rs to handle this type.",
827                    unknown
828                );
829                "wav"
830            }
831            None => "wav", // No MIME type provided, default to wav
832        }
833    }
834}
835
836// =============================================================================
837// Function Call/Result Info Types
838// =============================================================================
839
840/// Information about a function call requested by the model.
841///
842/// Returned by [`InteractionResponse::function_calls()`] for convenient access
843/// to function call details.
844///
845/// This is a **view type** that borrows data from the underlying [`InteractionResponse`].
846/// It implements [`Serialize`] for logging and debugging purposes, but not `Deserialize`
847/// since it's not meant to be constructed directly—use the response helper methods instead.
848///
849/// # Example
850///
851/// ```no_run
852/// # use genai_rs::InteractionResponse;
853/// # let response: InteractionResponse = todo!();
854/// for call in response.function_calls() {
855///     println!("Function: {} with args: {}", call.name, call.args);
856///     if let Some(id) = call.id {
857///         println!("  Call ID: {}", id);
858///     }
859/// }
860/// ```
861#[derive(Debug, Clone, PartialEq, Serialize)]
862pub struct FunctionCallInfo<'a> {
863    /// Unique identifier for this function call (used when sending results back)
864    pub id: Option<&'a str>,
865    /// Name of the function to call
866    pub name: &'a str,
867    /// Arguments to pass to the function
868    pub args: &'a serde_json::Value,
869}
870
871impl FunctionCallInfo<'_> {
872    /// Convert to an owned version that doesn't borrow from the response.
873    ///
874    /// Use this when you need to store function call data beyond the lifetime
875    /// of the response, such as for event emission, trajectory recording,
876    /// or passing to async tasks.
877    ///
878    /// # Example
879    ///
880    /// ```no_run
881    /// # use genai_rs::InteractionResponse;
882    /// # let response: InteractionResponse = todo!();
883    /// // Store function calls for later processing
884    /// let owned_calls: Vec<_> = response.function_calls()
885    ///     .into_iter()
886    ///     .map(|call| call.to_owned())
887    ///     .collect();
888    /// ```
889    #[must_use]
890    pub fn to_owned(&self) -> OwnedFunctionCallInfo {
891        OwnedFunctionCallInfo {
892            id: self.id.map(String::from),
893            name: self.name.to_string(),
894            args: self.args.clone(),
895        }
896    }
897}
898
899/// Owned version of [`FunctionCallInfo`] for storing beyond response lifetime.
900///
901/// This type owns all its data, making it suitable for:
902/// - Event emission with function call metadata
903/// - Trajectory/replay recording
904/// - Passing to async tasks or storing in collections
905///
906/// # Example
907///
908/// ```no_run
909/// # use genai_rs::InteractionResponse;
910/// # let response: InteractionResponse = todo!();
911/// let owned_calls: Vec<_> = response.function_calls()
912///     .into_iter()
913///     .map(|call| call.to_owned())
914///     .collect();
915///
916/// // owned_calls can now outlive `response`
917/// for call in owned_calls {
918///     println!("Function: {} with args: {}", call.name, call.args);
919/// }
920/// ```
921#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
922pub struct OwnedFunctionCallInfo {
923    /// Unique identifier for this function call (used when sending results back)
924    pub id: Option<String>,
925    /// Name of the function to call
926    pub name: String,
927    /// Arguments to pass to the function
928    pub args: serde_json::Value,
929}
930
931/// Information about a function result in the response.
932///
933/// Returned by [`InteractionResponse::function_results()`] for convenient access
934/// to function result details.
935///
936/// This is a **view type** that borrows data from the underlying [`InteractionResponse`].
937/// It implements [`Serialize`] for logging and debugging purposes, but not `Deserialize`
938/// since it's not meant to be constructed directly—use the response helper methods instead.
939///
940/// # Example
941///
942/// ```no_run
943/// # use genai_rs::InteractionResponse;
944/// # let response: InteractionResponse = todo!();
945/// for result in response.function_results() {
946///     if let Some(name) = result.name {
947///         println!("Function {} returned: {}", name, result.result);
948///     }
949/// }
950/// ```
951#[derive(Debug, Clone, PartialEq, Serialize)]
952pub struct FunctionResultInfo<'a> {
953    /// Name of the function that was called (optional per API spec)
954    pub name: Option<&'a str>,
955    /// The call_id from the FunctionCall this result responds to
956    pub call_id: &'a str,
957    /// The result returned by the function
958    pub result: &'a serde_json::Value,
959    /// Whether this result indicates an error
960    pub is_error: Option<bool>,
961}
962
963/// Information about a code execution call requested by the model.
964///
965/// Returned by [`InteractionResponse::code_execution_calls()`] for convenient access
966/// to code execution details.
967///
968/// This is a **view type** that borrows data from the underlying [`InteractionResponse`].
969/// It implements [`Serialize`] for logging and debugging purposes, but not `Deserialize`
970/// since it's not meant to be constructed directly—use the response helper methods instead.
971///
972/// # Example
973///
974/// ```no_run
975/// # use genai_rs::InteractionResponse;
976/// # let response: InteractionResponse = todo!();
977/// for call in response.code_execution_calls() {
978///     println!("Executing {} code (id: {:?})", call.language, call.id);
979///     println!("Code: {}", call.code);
980/// }
981/// ```
982#[derive(Debug, Clone, PartialEq, Serialize)]
983#[non_exhaustive]
984pub struct CodeExecutionCallInfo<'a> {
985    /// Unique identifier for this code execution call (optional per API spec)
986    pub id: Option<&'a str>,
987    /// Programming language (currently only Python is supported)
988    pub language: CodeExecutionLanguage,
989    /// Source code to execute
990    pub code: &'a str,
991}
992
993/// Information about a code execution result.
994///
995/// Returned by [`InteractionResponse::code_execution_results()`] for convenient access
996/// to code execution results.
997///
998/// This is a **view type** that borrows data from the underlying [`InteractionResponse`].
999/// It implements [`Serialize`] for logging and debugging purposes, but not `Deserialize`
1000/// since it's not meant to be constructed directly—use the response helper methods instead.
1001///
1002/// # Example
1003///
1004/// ```no_run
1005/// # use genai_rs::InteractionResponse;
1006/// # let response: InteractionResponse = todo!();
1007/// for result in response.code_execution_results() {
1008///     println!("Call {:?} completed: is_error={}", result.call_id, result.is_error);
1009///     if !result.is_error {
1010///         println!("Output: {}", result.result);
1011///     }
1012/// }
1013/// ```
1014#[derive(Debug, Clone, PartialEq, Serialize)]
1015#[non_exhaustive]
1016pub struct CodeExecutionResultInfo<'a> {
1017    /// The call_id matching the CodeExecutionCall this result is for (optional per API spec)
1018    pub call_id: Option<&'a str>,
1019    /// Whether the code execution resulted in an error
1020    pub is_error: bool,
1021    /// The output of the code execution (stdout for success, error message for failure)
1022    pub result: &'a str,
1023}
1024
1025/// Information about a URL context result.
1026///
1027/// Returned by [`InteractionResponse::url_context_results()`] for convenient access
1028/// to URL context results.
1029///
1030/// This is a **view type** that borrows data from the underlying [`InteractionResponse`].
1031/// It implements [`Serialize`] for logging and debugging purposes, but not `Deserialize`
1032/// since it's not meant to be constructed directly—use the response helper methods instead.
1033///
1034/// # Example
1035///
1036/// ```no_run
1037/// # use genai_rs::InteractionResponse;
1038/// # let response: InteractionResponse = todo!();
1039/// for result in response.url_context_results() {
1040///     println!("Call ID: {}", result.call_id);
1041///     for item in result.items {
1042///         println!("  URL: {} - Status: {}", item.url, item.status);
1043///     }
1044/// }
1045/// ```
1046#[derive(Debug, Clone, PartialEq, Serialize)]
1047#[non_exhaustive]
1048pub struct UrlContextResultInfo<'a> {
1049    /// The ID of the corresponding UrlContextCall
1050    pub call_id: &'a str,
1051    /// The result items containing URL and status for each fetched URL
1052    pub items: &'a [crate::UrlContextResultItem],
1053}
1054
1055/// Response from creating or retrieving an interaction
1056#[derive(Clone, Deserialize, Serialize, Debug)]
1057#[serde(rename_all = "camelCase")]
1058pub struct InteractionResponse {
1059    /// Unique identifier for this interaction.
1060    ///
1061    /// This field is `None` when the interaction was created with `store=false`,
1062    /// since non-stored interactions are not assigned an ID by the API.
1063    #[serde(skip_serializing_if = "Option::is_none")]
1064    pub id: Option<String>,
1065
1066    /// Model name if a model was used
1067    #[serde(skip_serializing_if = "Option::is_none")]
1068    pub model: Option<String>,
1069
1070    /// Agent name if an agent was used
1071    #[serde(skip_serializing_if = "Option::is_none")]
1072    pub agent: Option<String>,
1073
1074    /// The input that was provided (array of content objects)
1075    #[serde(default)]
1076    pub input: Vec<Content>,
1077
1078    /// The outputs generated by the model/agent (array of content objects)
1079    #[serde(default)]
1080    pub outputs: Vec<Content>,
1081
1082    /// Current status of the interaction
1083    pub status: InteractionStatus,
1084
1085    /// Token usage information
1086    #[serde(skip_serializing_if = "Option::is_none")]
1087    pub usage: Option<UsageMetadata>,
1088
1089    /// Tools that were available for this interaction
1090    #[serde(skip_serializing_if = "Option::is_none")]
1091    pub tools: Option<Vec<Tool>>,
1092
1093    /// Grounding metadata when using GoogleSearch tool
1094    #[serde(skip_serializing_if = "Option::is_none")]
1095    pub grounding_metadata: Option<GroundingMetadata>,
1096
1097    /// URL context metadata when using UrlContext tool
1098    #[serde(skip_serializing_if = "Option::is_none")]
1099    pub url_context_metadata: Option<UrlContextMetadata>,
1100
1101    /// Previous interaction ID if this was a follow-up
1102    #[serde(skip_serializing_if = "Option::is_none")]
1103    pub previous_interaction_id: Option<String>,
1104
1105    /// Timestamp when the interaction was created (ISO 8601 UTC)
1106    #[serde(skip_serializing_if = "Option::is_none")]
1107    pub created: Option<DateTime<Utc>>,
1108
1109    /// Timestamp when the interaction was last updated (ISO 8601 UTC)
1110    #[serde(skip_serializing_if = "Option::is_none")]
1111    pub updated: Option<DateTime<Utc>>,
1112}
1113
1114impl InteractionResponse {
1115    // =========================================================================
1116    // Text Content Helpers
1117    // =========================================================================
1118
1119    /// Extract the first text content from outputs
1120    ///
1121    /// Returns the first text found in the outputs vector.
1122    /// Useful for simple queries where you expect a single text response.
1123    ///
1124    /// # Example
1125    /// ```no_run
1126    /// # use genai_rs::InteractionResponse;
1127    /// # let response: InteractionResponse = todo!();
1128    /// if let Some(text) = response.as_text() {
1129    ///     println!("Response: {}", text);
1130    /// }
1131    /// ```
1132    #[must_use]
1133    pub fn as_text(&self) -> Option<&str> {
1134        self.outputs.iter().find_map(|content| {
1135            if let Content::Text { text: Some(t), .. } = content {
1136                Some(t.as_str())
1137            } else {
1138                None
1139            }
1140        })
1141    }
1142
1143    /// Extract all text contents concatenated
1144    ///
1145    /// Combines all text outputs into a single string.
1146    /// Useful when the model returns multiple text chunks.
1147    ///
1148    /// # Example
1149    /// ```no_run
1150    /// # use genai_rs::InteractionResponse;
1151    /// # let response: InteractionResponse = todo!();
1152    /// let full_text = response.all_text();
1153    /// println!("Complete response: {}", full_text);
1154    /// ```
1155    #[must_use]
1156    pub fn all_text(&self) -> String {
1157        self.outputs
1158            .iter()
1159            .filter_map(|content| {
1160                if let Content::Text { text: Some(t), .. } = content {
1161                    Some(t.as_str())
1162                } else {
1163                    None
1164                }
1165            })
1166            .collect::<Vec<_>>()
1167            .join("")
1168    }
1169
1170    // =========================================================================
1171    // Annotation Helpers (Citation Support)
1172    // =========================================================================
1173
1174    /// Check if response contains annotations (citations).
1175    ///
1176    /// Returns `true` if any text output contains source annotations.
1177    /// Annotations are typically present when grounding tools like
1178    /// `GoogleSearch` or `UrlContext` were used.
1179    ///
1180    /// # Example
1181    ///
1182    /// ```no_run
1183    /// # use genai_rs::InteractionResponse;
1184    /// # let response: InteractionResponse = todo!();
1185    /// if response.has_annotations() {
1186    ///     println!("Response includes {} citations", response.all_annotations().count());
1187    /// }
1188    /// ```
1189    #[must_use]
1190    pub fn has_annotations(&self) -> bool {
1191        self.outputs.iter().any(|c| c.annotations().is_some())
1192    }
1193
1194    /// Returns all annotations from text outputs.
1195    ///
1196    /// Collects all [`Annotation`] references from all text outputs in the response.
1197    /// Annotations link specific text spans to their sources, enabling citation tracking.
1198    ///
1199    /// # Example
1200    ///
1201    /// ```no_run
1202    /// # use genai_rs::InteractionResponse;
1203    /// # let response: InteractionResponse = todo!();
1204    /// let text = response.all_text();
1205    /// for annotation in response.all_annotations() {
1206    ///     if let Some(span) = annotation.extract_span(&text) {
1207    ///         println!("'{}' sourced from: {:?}", span, annotation.source);
1208    ///     }
1209    /// }
1210    /// ```
1211    pub fn all_annotations(&self) -> impl Iterator<Item = &Annotation> {
1212        self.outputs
1213            .iter()
1214            .filter_map(|c| c.annotations())
1215            .flatten()
1216    }
1217
1218    // =========================================================================
1219    // Image Content Helpers
1220    // =========================================================================
1221
1222    /// Returns the decoded bytes of the first image in the response.
1223    ///
1224    /// This is a convenience method for the common case of extracting a single
1225    /// generated image. For multiple images, use [`images()`](Self::images).
1226    ///
1227    /// # Errors
1228    ///
1229    /// Returns an error if the base64 data is invalid.
1230    ///
1231    /// # Example
1232    ///
1233    /// ```no_run
1234    /// use genai_rs::Client;
1235    ///
1236    /// # #[tokio::main]
1237    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1238    /// let client = Client::new("api-key".to_string());
1239    ///
1240    /// let response = client
1241    ///     .interaction()
1242    ///     .with_model("gemini-3-flash-preview")
1243    ///     .with_text("A sunset over mountains")
1244    ///     .with_image_output()
1245    ///     .create()
1246    ///     .await?;
1247    ///
1248    /// if let Some(bytes) = response.first_image_bytes()? {
1249    ///     std::fs::write("sunset.png", &bytes)?;
1250    ///     println!("Saved {} bytes", bytes.len());
1251    /// }
1252    /// # Ok(())
1253    /// # }
1254    /// ```
1255    pub fn first_image_bytes(&self) -> Result<Option<Vec<u8>>, GenaiError> {
1256        for output in &self.outputs {
1257            if let Content::Image {
1258                data: Some(base64_data),
1259                ..
1260            } = output
1261            {
1262                let bytes = base64::engine::general_purpose::STANDARD
1263                    .decode(base64_data)
1264                    .map_err(|e| {
1265                        GenaiError::InvalidInput(format!("Invalid base64 image data: {}", e))
1266                    })?;
1267                return Ok(Some(bytes));
1268            }
1269        }
1270        Ok(None)
1271    }
1272
1273    /// Returns an iterator over all images in the response.
1274    ///
1275    /// Each item is an [`ImageInfo`] that provides access to the image data,
1276    /// MIME type, and convenience methods for decoding.
1277    ///
1278    /// # Example
1279    ///
1280    /// ```no_run
1281    /// use genai_rs::Client;
1282    ///
1283    /// # #[tokio::main]
1284    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1285    /// let client = Client::new("api-key".to_string());
1286    ///
1287    /// let response = client
1288    ///     .interaction()
1289    ///     .with_model("gemini-3-flash-preview")
1290    ///     .with_text("Generate 3 variations of a cat")
1291    ///     .with_image_output()
1292    ///     .create()
1293    ///     .await?;
1294    ///
1295    /// for (i, image) in response.images().enumerate() {
1296    ///     let bytes = image.bytes()?;
1297    ///     let filename = format!("cat_{}.{}", i, image.extension());
1298    ///     std::fs::write(&filename, bytes)?;
1299    /// }
1300    /// # Ok(())
1301    /// # }
1302    /// ```
1303    pub fn images(&self) -> impl Iterator<Item = ImageInfo<'_>> {
1304        self.outputs.iter().filter_map(|output| {
1305            if let Content::Image {
1306                data: Some(base64_data),
1307                mime_type,
1308                ..
1309            } = output
1310            {
1311                Some(ImageInfo {
1312                    data: base64_data.as_str(),
1313                    mime_type: mime_type.as_deref(),
1314                })
1315            } else {
1316                None
1317            }
1318        })
1319    }
1320
1321    /// Check if the response contains any images.
1322    ///
1323    /// # Example
1324    ///
1325    /// ```no_run
1326    /// use genai_rs::Client;
1327    ///
1328    /// # #[tokio::main]
1329    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1330    /// # let client = Client::new("api-key".to_string());
1331    /// # let response = client.interaction().with_model("gemini-3-flash-preview")
1332    /// #     .with_text("A cat").with_image_output().create().await?;
1333    /// if response.has_images() {
1334    ///     for image in response.images() {
1335    ///         let bytes = image.bytes()?;
1336    ///         // process images...
1337    ///     }
1338    /// }
1339    /// # Ok(())
1340    /// # }
1341    /// ```
1342    #[must_use]
1343    pub fn has_images(&self) -> bool {
1344        self.outputs
1345            .iter()
1346            .any(|output| matches!(output, Content::Image { data: Some(_), .. }))
1347    }
1348
1349    // =========================================================================
1350    // Audio Helpers
1351    // =========================================================================
1352
1353    /// Returns the first audio content in the response.
1354    ///
1355    /// This is a convenience method for the common case of extracting a single
1356    /// generated audio. For multiple audio outputs, use [`audios()`](Self::audios).
1357    ///
1358    /// # Example
1359    ///
1360    /// ```no_run
1361    /// use genai_rs::Client;
1362    ///
1363    /// # #[tokio::main]
1364    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1365    /// let client = Client::new("api-key".to_string());
1366    ///
1367    /// let response = client
1368    ///     .interaction()
1369    ///     .with_model("gemini-2.5-pro-preview-tts")
1370    ///     .with_text("Hello, world!")
1371    ///     .with_audio_output()
1372    ///     .with_voice("Kore")
1373    ///     .create()
1374    ///     .await?;
1375    ///
1376    /// if let Some(audio) = response.first_audio() {
1377    ///     let bytes = audio.bytes()?;
1378    ///     std::fs::write("speech.wav", &bytes)?;
1379    /// }
1380    /// # Ok(())
1381    /// # }
1382    /// ```
1383    #[must_use]
1384    pub fn first_audio(&self) -> Option<AudioInfo<'_>> {
1385        self.audios().next()
1386    }
1387
1388    /// Returns an iterator over all audio content in the response.
1389    ///
1390    /// Each [`AudioInfo`] provides methods for accessing the audio data,
1391    /// MIME type, and a suitable file extension.
1392    ///
1393    /// # Example
1394    ///
1395    /// ```no_run
1396    /// use genai_rs::Client;
1397    ///
1398    /// # #[tokio::main]
1399    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1400    /// let client = Client::new("api-key".to_string());
1401    ///
1402    /// let response = client
1403    ///     .interaction()
1404    ///     .with_model("gemini-2.5-pro-preview-tts")
1405    ///     .with_text("Generate multiple audio segments")
1406    ///     .with_audio_output()
1407    ///     .create()
1408    ///     .await?;
1409    ///
1410    /// for (i, audio) in response.audios().enumerate() {
1411    ///     let bytes = audio.bytes()?;
1412    ///     let filename = format!("audio_{}.{}", i, audio.extension());
1413    ///     std::fs::write(&filename, bytes)?;
1414    /// }
1415    /// # Ok(())
1416    /// # }
1417    /// ```
1418    pub fn audios(&self) -> impl Iterator<Item = AudioInfo<'_>> {
1419        self.outputs.iter().filter_map(|output| {
1420            if let Content::Audio {
1421                data: Some(base64_data),
1422                mime_type,
1423                ..
1424            } = output
1425            {
1426                Some(AudioInfo {
1427                    data: base64_data.as_str(),
1428                    mime_type: mime_type.as_deref(),
1429                })
1430            } else {
1431                None
1432            }
1433        })
1434    }
1435
1436    /// Check if the response contains any audio content.
1437    ///
1438    /// # Example
1439    ///
1440    /// ```no_run
1441    /// use genai_rs::Client;
1442    ///
1443    /// # #[tokio::main]
1444    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1445    /// # let client = Client::new("api-key".to_string());
1446    /// # let response = client.interaction().with_model("gemini-2.5-pro-preview-tts")
1447    /// #     .with_text("Hello").with_audio_output().create().await?;
1448    /// if response.has_audio() {
1449    ///     for audio in response.audios() {
1450    ///         let bytes = audio.bytes()?;
1451    ///         // process audio...
1452    ///     }
1453    /// }
1454    /// # Ok(())
1455    /// # }
1456    /// ```
1457    #[must_use]
1458    pub fn has_audio(&self) -> bool {
1459        self.outputs
1460            .iter()
1461            .any(|output| matches!(output, Content::Audio { data: Some(_), .. }))
1462    }
1463
1464    // =========================================================================
1465    // Function Calling Helpers
1466    // =========================================================================
1467
1468    /// Extract function calls from outputs
1469    ///
1470    /// Returns a vector of [`FunctionCallInfo`] structs with named fields for
1471    /// convenient access to function call details.
1472    ///
1473    /// # Example
1474    ///
1475    /// ```no_run
1476    /// # use genai_rs::InteractionResponse;
1477    /// # let response: InteractionResponse = todo!();
1478    /// for call in response.function_calls() {
1479    ///     println!("Function: {} with args: {}", call.name, call.args);
1480    ///     if let Some(id) = call.id {
1481    ///         // Use call.id when sending results back to the model
1482    ///         println!("  Call ID: {}", id);
1483    ///     }
1484    /// }
1485    /// ```
1486    #[must_use]
1487    pub fn function_calls(&self) -> Vec<FunctionCallInfo<'_>> {
1488        self.outputs
1489            .iter()
1490            .filter_map(|content| {
1491                if let Content::FunctionCall { id, name, args } = content {
1492                    Some(FunctionCallInfo {
1493                        id: id.as_ref().map(|s| s.as_str()),
1494                        name: name.as_str(),
1495                        args,
1496                    })
1497                } else {
1498                    None
1499                }
1500            })
1501            .collect()
1502    }
1503
1504    /// Check if response contains text
1505    ///
1506    /// Returns true if any output contains text content.
1507    #[must_use]
1508    pub fn has_text(&self) -> bool {
1509        self.outputs
1510            .iter()
1511            .any(|c| matches!(c, Content::Text { text: Some(_), .. }))
1512    }
1513
1514    /// Check if response contains function calls
1515    ///
1516    /// Returns true if any output contains a function call.
1517    #[must_use]
1518    pub fn has_function_calls(&self) -> bool {
1519        self.outputs
1520            .iter()
1521            .any(|c| matches!(c, Content::FunctionCall { .. }))
1522    }
1523
1524    /// Check if response contains function results
1525    ///
1526    /// Returns true if any output contains a function result.
1527    ///
1528    /// # Example
1529    ///
1530    /// ```no_run
1531    /// # use genai_rs::InteractionResponse;
1532    /// # let response: InteractionResponse = todo!();
1533    /// if response.has_function_results() {
1534    ///     for result in response.function_results() {
1535    ///         println!("Function {:?} returned data", result.name);
1536    ///     }
1537    /// }
1538    /// ```
1539    #[must_use]
1540    pub fn has_function_results(&self) -> bool {
1541        self.outputs
1542            .iter()
1543            .any(|c| matches!(c, Content::FunctionResult { .. }))
1544    }
1545
1546    /// Extract function results from outputs
1547    ///
1548    /// Returns a vector of [`FunctionResultInfo`] structs with named fields for
1549    /// convenient access to function result details.
1550    ///
1551    /// # Example
1552    ///
1553    /// ```no_run
1554    /// # use genai_rs::InteractionResponse;
1555    /// # let response: InteractionResponse = todo!();
1556    /// for result in response.function_results() {
1557    ///     println!("Function {:?} (call_id: {}) returned: {}",
1558    ///         result.name, result.call_id, result.result);
1559    /// }
1560    /// ```
1561    #[must_use]
1562    pub fn function_results(&self) -> Vec<FunctionResultInfo<'_>> {
1563        self.outputs
1564            .iter()
1565            .filter_map(|content| {
1566                if let Content::FunctionResult {
1567                    name,
1568                    call_id,
1569                    result,
1570                    is_error,
1571                } = content
1572                {
1573                    Some(FunctionResultInfo {
1574                        name: name.as_deref(),
1575                        call_id: call_id.as_str(),
1576                        result,
1577                        is_error: *is_error,
1578                    })
1579                } else {
1580                    None
1581                }
1582            })
1583            .collect()
1584    }
1585
1586    // =========================================================================
1587    // Thinking/Reasoning Helpers
1588    // =========================================================================
1589
1590    /// Check if response contains thoughts (internal reasoning)
1591    ///
1592    /// Returns true if any output contains thought content with a signature.
1593    #[must_use]
1594    pub fn has_thoughts(&self) -> bool {
1595        self.outputs
1596            .iter()
1597            .any(|c| matches!(c, Content::Thought { signature: Some(_) }))
1598    }
1599
1600    /// Get an iterator over all thought signatures (internal reasoning verification).
1601    ///
1602    /// Returns the cryptographic signature of each `Thought` variant in the outputs.
1603    /// These signatures are used to verify the model's reasoning process when
1604    /// thinking mode is enabled via `with_thinking_level()`.
1605    ///
1606    /// **Note:** The actual thought text is not exposed by the API - only signatures
1607    /// for verification purposes.
1608    ///
1609    /// # Example
1610    /// ```no_run
1611    /// # use genai_rs::InteractionResponse;
1612    /// # let response: InteractionResponse = todo!();
1613    /// for signature in response.thought_signatures() {
1614    ///     println!("Thought signature: {}", signature);
1615    /// }
1616    /// ```
1617    pub fn thought_signatures(&self) -> impl Iterator<Item = &str> {
1618        self.outputs.iter().filter_map(|c| match c {
1619            Content::Thought { signature: Some(s) } => Some(s.as_str()),
1620            _ => None,
1621        })
1622    }
1623
1624    // =========================================================================
1625    // Unknown Content Helpers (Evergreen Forward Compatibility)
1626    // =========================================================================
1627
1628    /// Check if response contains unknown content types.
1629    ///
1630    /// Returns `true` if any output contains an [`Content::Unknown`] variant.
1631    /// This indicates the API returned content types that this library version doesn't
1632    /// recognize.
1633    ///
1634    /// # When to Use
1635    ///
1636    /// Call this after receiving a response to detect if you might be missing content:
1637    ///
1638    /// ```no_run
1639    /// # use genai_rs::InteractionResponse;
1640    /// # let response: InteractionResponse = todo!();
1641    /// if response.has_unknown() {
1642    ///     eprintln!("Warning: Response contains unknown content types");
1643    ///     for (content_type, data) in response.unknown_content() {
1644    ///         eprintln!("  - {}: {:?}", content_type, data);
1645    ///     }
1646    /// }
1647    /// ```
1648    #[must_use]
1649    pub fn has_unknown(&self) -> bool {
1650        self.outputs
1651            .iter()
1652            .any(|c| matches!(c, Content::Unknown { .. }))
1653    }
1654
1655    /// Get all unknown content as (content_type, data) tuples.
1656    ///
1657    /// Returns a vector of references to the type names and JSON data for all
1658    /// [`Content::Unknown`] variants in the outputs.
1659    ///
1660    /// # Example
1661    ///
1662    /// ```no_run
1663    /// # use genai_rs::InteractionResponse;
1664    /// # let response: InteractionResponse = todo!();
1665    /// for (content_type, data) in response.unknown_content() {
1666    ///     println!("Unknown type '{}': {}", content_type, data);
1667    /// }
1668    /// ```
1669    #[must_use]
1670    pub fn unknown_content(&self) -> Vec<(&str, &serde_json::Value)> {
1671        self.outputs
1672            .iter()
1673            .filter_map(|content| {
1674                if let Content::Unknown { content_type, data } = content {
1675                    Some((content_type.as_str(), data))
1676                } else {
1677                    None
1678                }
1679            })
1680            .collect()
1681    }
1682
1683    // =========================================================================
1684    // Google Search Metadata Helpers
1685    // =========================================================================
1686
1687    /// Check if response has grounding metadata from Google Search.
1688    ///
1689    /// Returns true if the response was grounded using the GoogleSearch tool.
1690    ///
1691    /// This checks for both:
1692    /// - Explicit `grounding_metadata` field (future API support)
1693    /// - GoogleSearchCall/GoogleSearchResult outputs (current Interactions API)
1694    ///
1695    /// # Example
1696    ///
1697    /// ```no_run
1698    /// # use genai_rs::InteractionResponse;
1699    /// # let response: InteractionResponse = todo!();
1700    /// if response.has_google_search_metadata() {
1701    ///     println!("Response is grounded with web sources");
1702    /// }
1703    /// ```
1704    #[must_use]
1705    pub fn has_google_search_metadata(&self) -> bool {
1706        self.grounding_metadata.is_some()
1707            || self.has_google_search_calls()
1708            || self.has_google_search_results()
1709    }
1710
1711    /// Get Google Search grounding metadata if explicitly present.
1712    ///
1713    /// **Note:** The Interactions API embeds Google Search data in outputs rather than
1714    /// a top-level `grounding_metadata` field. Use [`google_search_calls()`](Self::google_search_calls)
1715    /// and [`google_search_results()`](Self::google_search_results) to access the search
1716    /// data from outputs. This method returns `None` for Interactions API responses.
1717    ///
1718    /// Returns the grounding metadata containing search queries and web sources
1719    /// when the GoogleSearch tool was used and the API provides explicit metadata.
1720    ///
1721    /// # Example
1722    ///
1723    /// ```no_run
1724    /// # use genai_rs::InteractionResponse;
1725    /// # let response: InteractionResponse = todo!();
1726    /// // For Interactions API, use direct output accessors:
1727    /// for query in response.google_search_calls() {
1728    ///     println!("Search query: {}", query);
1729    /// }
1730    /// for result in response.google_search_results() {
1731    ///     println!("Source: {} - {}", result.title, result.url);
1732    /// }
1733    /// ```
1734    #[must_use]
1735    pub fn google_search_metadata(&self) -> Option<&GroundingMetadata> {
1736        self.grounding_metadata.as_ref()
1737    }
1738
1739    // =========================================================================
1740    // URL Context Metadata Helpers
1741    // =========================================================================
1742
1743    /// Check if response has URL context metadata.
1744    ///
1745    /// Returns true if the UrlContext tool was used and metadata is available.
1746    ///
1747    /// # Example
1748    ///
1749    /// ```no_run
1750    /// # use genai_rs::InteractionResponse;
1751    /// # let response: InteractionResponse = todo!();
1752    /// if response.has_url_context_metadata() {
1753    ///     println!("Response includes URL context");
1754    /// }
1755    /// ```
1756    #[must_use]
1757    pub fn has_url_context_metadata(&self) -> bool {
1758        self.url_context_metadata.is_some()
1759    }
1760
1761    /// Get URL context metadata if present.
1762    ///
1763    /// Returns metadata about URLs that were fetched when the UrlContext tool was used,
1764    /// including retrieval status for each URL.
1765    ///
1766    /// # Example
1767    ///
1768    /// ```no_run
1769    /// # use genai_rs::InteractionResponse;
1770    /// # let response: InteractionResponse = todo!();
1771    /// if let Some(metadata) = response.url_context_metadata() {
1772    ///     for entry in &metadata.url_metadata {
1773    ///         println!("URL: {} - Status: {:?}", entry.retrieved_url, entry.url_retrieval_status);
1774    ///     }
1775    /// }
1776    /// ```
1777    #[must_use]
1778    pub fn url_context_metadata(&self) -> Option<&UrlContextMetadata> {
1779        self.url_context_metadata.as_ref()
1780    }
1781
1782    // =========================================================================
1783    // Code Execution Tool Helpers
1784    // =========================================================================
1785
1786    /// Check if response contains code execution calls
1787    #[must_use]
1788    pub fn has_code_execution_calls(&self) -> bool {
1789        self.outputs
1790            .iter()
1791            .any(|c| matches!(c, Content::CodeExecutionCall { .. }))
1792    }
1793
1794    /// Get the first code execution call, if any.
1795    ///
1796    /// Convenience method for the common case where you just want to see
1797    /// the first code the model wants to execute.
1798    ///
1799    /// # Example
1800    ///
1801    /// ```no_run
1802    /// # use genai_rs::InteractionResponse;
1803    /// # let response: InteractionResponse = todo!();
1804    /// if let Some(call) = response.code_execution_call() {
1805    ///     println!("Model wants to run {} code (id: {:?}):\n{}", call.language, call.id, call.code);
1806    /// }
1807    /// ```
1808    #[must_use]
1809    pub fn code_execution_call(&self) -> Option<CodeExecutionCallInfo<'_>> {
1810        self.outputs.iter().find_map(|content| {
1811            if let Content::CodeExecutionCall { id, language, code } = content {
1812                Some(CodeExecutionCallInfo {
1813                    id: id.as_deref(),
1814                    language: language.clone(),
1815                    code: code.as_str(),
1816                })
1817            } else {
1818                None
1819            }
1820        })
1821    }
1822
1823    /// Extract all code execution calls from outputs
1824    ///
1825    /// Returns a vector of [`CodeExecutionCallInfo`] structs with named fields for
1826    /// convenient access to code execution details.
1827    ///
1828    /// # Example
1829    ///
1830    /// ```no_run
1831    /// # use genai_rs::{InteractionResponse, CodeExecutionLanguage};
1832    /// # let response: InteractionResponse = todo!();
1833    /// for call in response.code_execution_calls() {
1834    ///     match call.language {
1835    ///         CodeExecutionLanguage::Python => println!("Python (id: {:?}):\n{}", call.id, call.code),
1836    ///         _ => println!("Other (id: {:?}):\n{}", call.id, call.code),
1837    ///     }
1838    /// }
1839    /// ```
1840    #[must_use]
1841    pub fn code_execution_calls(&self) -> Vec<CodeExecutionCallInfo<'_>> {
1842        self.outputs
1843            .iter()
1844            .filter_map(|content| {
1845                if let Content::CodeExecutionCall { id, language, code } = content {
1846                    Some(CodeExecutionCallInfo {
1847                        id: id.as_deref(),
1848                        language: language.clone(),
1849                        code: code.as_str(),
1850                    })
1851                } else {
1852                    None
1853                }
1854            })
1855            .collect()
1856    }
1857
1858    /// Check if response contains code execution results
1859    #[must_use]
1860    pub fn has_code_execution_results(&self) -> bool {
1861        self.outputs
1862            .iter()
1863            .any(|c| matches!(c, Content::CodeExecutionResult { .. }))
1864    }
1865
1866    /// Extract code execution results from outputs
1867    ///
1868    /// Returns a vector of [`CodeExecutionResultInfo`] structs with named fields for
1869    /// convenient access to code execution results.
1870    ///
1871    /// # Example
1872    ///
1873    /// ```no_run
1874    /// # use genai_rs::InteractionResponse;
1875    /// # let response: InteractionResponse = todo!();
1876    /// for result in response.code_execution_results() {
1877    ///     if !result.is_error {
1878    ///         println!("Code output (call_id: {:?}): {}", result.call_id, result.result);
1879    ///     } else {
1880    ///         eprintln!("Code failed: {}", result.result);
1881    ///     }
1882    /// }
1883    /// ```
1884    #[must_use]
1885    pub fn code_execution_results(&self) -> Vec<CodeExecutionResultInfo<'_>> {
1886        self.outputs
1887            .iter()
1888            .filter_map(|content| {
1889                if let Content::CodeExecutionResult {
1890                    call_id,
1891                    is_error,
1892                    result,
1893                } = content
1894                {
1895                    Some(CodeExecutionResultInfo {
1896                        call_id: call_id.as_deref(),
1897                        is_error: *is_error,
1898                        result: result.as_str(),
1899                    })
1900                } else {
1901                    None
1902                }
1903            })
1904            .collect()
1905    }
1906
1907    /// Get the first successful code execution output, if any.
1908    ///
1909    /// This is a convenience method for the common case where you just want the
1910    /// output from successful code execution without handling errors.
1911    ///
1912    /// # Example
1913    ///
1914    /// ```no_run
1915    /// # use genai_rs::InteractionResponse;
1916    /// # let response: InteractionResponse = todo!();
1917    /// if let Some(output) = response.successful_code_output() {
1918    ///     println!("Result: {}", output);
1919    /// }
1920    /// ```
1921    #[must_use]
1922    pub fn successful_code_output(&self) -> Option<&str> {
1923        self.outputs.iter().find_map(|content| {
1924            if let Content::CodeExecutionResult {
1925                is_error, result, ..
1926            } = content
1927            {
1928                if !is_error {
1929                    Some(result.as_str())
1930                } else {
1931                    None
1932                }
1933            } else {
1934                None
1935            }
1936        })
1937    }
1938
1939    // =========================================================================
1940    // Google Search Output Content Helpers
1941    // =========================================================================
1942
1943    /// Check if response contains Google Search calls
1944    ///
1945    /// Returns true if the model performed any Google Search queries.
1946    ///
1947    /// # Example
1948    ///
1949    /// ```no_run
1950    /// # use genai_rs::InteractionResponse;
1951    /// # let response: InteractionResponse = todo!();
1952    /// if response.has_google_search_calls() {
1953    ///     println!("Model searched: {:?}", response.google_search_calls());
1954    /// }
1955    /// ```
1956    #[must_use]
1957    pub fn has_google_search_calls(&self) -> bool {
1958        self.outputs
1959            .iter()
1960            .any(|c| matches!(c, Content::GoogleSearchCall { .. }))
1961    }
1962
1963    /// Get the first Google Search query, if any.
1964    ///
1965    /// Convenience method for the common case where you just want to see
1966    /// the first search query performed by the model.
1967    ///
1968    /// # Example
1969    ///
1970    /// ```no_run
1971    /// # use genai_rs::InteractionResponse;
1972    /// # let response: InteractionResponse = todo!();
1973    /// if let Some(query) = response.google_search_call() {
1974    ///     println!("Model searched for: {}", query);
1975    /// }
1976    /// ```
1977    #[must_use]
1978    pub fn google_search_call(&self) -> Option<&str> {
1979        self.outputs.iter().find_map(|content| {
1980            if let Content::GoogleSearchCall { queries, .. } = content {
1981                // Return first non-empty query
1982                queries.iter().find(|q| !q.is_empty()).map(|q| q.as_str())
1983            } else {
1984                None
1985            }
1986        })
1987    }
1988
1989    /// Extract all Google Search queries from outputs
1990    ///
1991    /// Returns a vector of search query strings (flattened from all search calls).
1992    ///
1993    /// # Example
1994    ///
1995    /// ```no_run
1996    /// # use genai_rs::InteractionResponse;
1997    /// # let response: InteractionResponse = todo!();
1998    /// for query in response.google_search_calls() {
1999    ///     println!("Searched for: {}", query);
2000    /// }
2001    /// ```
2002    #[must_use]
2003    pub fn google_search_calls(&self) -> Vec<&str> {
2004        self.outputs
2005            .iter()
2006            .filter_map(|content| {
2007                if let Content::GoogleSearchCall { queries, .. } = content {
2008                    Some(queries.iter().map(|q| q.as_str()))
2009                } else {
2010                    None
2011                }
2012            })
2013            .flatten()
2014            .collect()
2015    }
2016
2017    /// Check if response contains Google Search results
2018    #[must_use]
2019    pub fn has_google_search_results(&self) -> bool {
2020        self.outputs
2021            .iter()
2022            .any(|c| matches!(c, Content::GoogleSearchResult { .. }))
2023    }
2024
2025    /// Extract Google Search result items from outputs
2026    ///
2027    /// Returns a vector of references to the search result items with title/URL info.
2028    #[must_use]
2029    pub fn google_search_results(&self) -> Vec<&GoogleSearchResultItem> {
2030        self.outputs
2031            .iter()
2032            .filter_map(|content| {
2033                if let Content::GoogleSearchResult { result, .. } = content {
2034                    Some(result.iter())
2035                } else {
2036                    None
2037                }
2038            })
2039            .flatten()
2040            .collect()
2041    }
2042
2043    // =========================================================================
2044    // URL Context Output Content Helpers
2045    // =========================================================================
2046
2047    /// Check if response contains URL context calls
2048    ///
2049    /// Returns true if the model requested any URLs for context.
2050    ///
2051    /// # Example
2052    ///
2053    /// ```no_run
2054    /// # use genai_rs::InteractionResponse;
2055    /// # let response: InteractionResponse = todo!();
2056    /// if response.has_url_context_calls() {
2057    ///     let urls = response.url_context_call_urls();
2058    ///     println!("Model fetched: {:?}", urls);
2059    /// }
2060    /// ```
2061    #[must_use]
2062    pub fn has_url_context_calls(&self) -> bool {
2063        self.outputs
2064            .iter()
2065            .any(|c| matches!(c, Content::UrlContextCall { .. }))
2066    }
2067
2068    /// Get the ID of the first URL context call, if any.
2069    ///
2070    /// Convenience method for the common case where you just want to get
2071    /// the call ID for the first URL context call.
2072    ///
2073    /// # Example
2074    ///
2075    /// ```no_run
2076    /// # use genai_rs::InteractionResponse;
2077    /// # let response: InteractionResponse = todo!();
2078    /// if let Some(call_id) = response.url_context_call_id() {
2079    ///     println!("URL context call ID: {}", call_id);
2080    /// }
2081    /// ```
2082    #[must_use]
2083    pub fn url_context_call_id(&self) -> Option<&str> {
2084        self.outputs.iter().find_map(|content| {
2085            if let Content::UrlContextCall { id, .. } = content {
2086                Some(id.as_str())
2087            } else {
2088                None
2089            }
2090        })
2091    }
2092
2093    /// Extract URL context call URLs from outputs
2094    ///
2095    /// Returns a vector of all URLs that were requested for fetching across all UrlContextCalls.
2096    ///
2097    /// # Example
2098    ///
2099    /// ```no_run
2100    /// # use genai_rs::InteractionResponse;
2101    /// # let response: InteractionResponse = todo!();
2102    /// for url in response.url_context_call_urls() {
2103    ///     println!("Requested URL: {}", url);
2104    /// }
2105    /// ```
2106    #[must_use]
2107    pub fn url_context_call_urls(&self) -> Vec<&str> {
2108        self.outputs
2109            .iter()
2110            .filter_map(|content| {
2111                if let Content::UrlContextCall { urls, .. } = content {
2112                    Some(urls.iter().map(String::as_str).collect::<Vec<_>>())
2113                } else {
2114                    None
2115                }
2116            })
2117            .flatten()
2118            .collect()
2119    }
2120
2121    /// Check if response contains URL context results
2122    #[must_use]
2123    pub fn has_url_context_results(&self) -> bool {
2124        self.outputs
2125            .iter()
2126            .any(|c| matches!(c, Content::UrlContextResult { .. }))
2127    }
2128
2129    /// Extract URL context results from outputs
2130    ///
2131    /// Returns a vector of [`UrlContextResultInfo`] structs with named fields for
2132    /// convenient access to URL context results.
2133    ///
2134    /// # Example
2135    ///
2136    /// ```no_run
2137    /// # use genai_rs::InteractionResponse;
2138    /// # let response: InteractionResponse = todo!();
2139    /// for result in response.url_context_results() {
2140    ///     println!("Call ID: {}", result.call_id);
2141    ///     for item in result.items {
2142    ///         println!("  URL: {} - Status: {}", item.url, item.status);
2143    ///     }
2144    /// }
2145    /// ```
2146    #[must_use]
2147    pub fn url_context_results(&self) -> Vec<UrlContextResultInfo<'_>> {
2148        self.outputs
2149            .iter()
2150            .filter_map(|content| {
2151                if let Content::UrlContextResult { call_id, result } = content {
2152                    Some(UrlContextResultInfo {
2153                        call_id: call_id.as_str(),
2154                        items: result,
2155                    })
2156                } else {
2157                    None
2158                }
2159            })
2160            .collect()
2161    }
2162
2163    // =========================================================================
2164    // File Search Output Content Helpers
2165    // =========================================================================
2166
2167    /// Check if response contains file search results
2168    ///
2169    /// Returns true if the model returned any file search results from semantic retrieval.
2170    ///
2171    /// # Example
2172    ///
2173    /// ```no_run
2174    /// # use genai_rs::InteractionResponse;
2175    /// # let response: InteractionResponse = todo!();
2176    /// if response.has_file_search_results() {
2177    ///     println!("Found {} search matches", response.file_search_results().len());
2178    /// }
2179    /// ```
2180    #[must_use]
2181    pub fn has_file_search_results(&self) -> bool {
2182        self.outputs
2183            .iter()
2184            .any(|c| matches!(c, Content::FileSearchResult { .. }))
2185    }
2186
2187    /// Extract file search result items from outputs
2188    ///
2189    /// Returns a vector of references to the file search result items with title/text/store info.
2190    ///
2191    /// # Example
2192    ///
2193    /// ```no_run
2194    /// # use genai_rs::InteractionResponse;
2195    /// # let response: InteractionResponse = todo!();
2196    /// for result in response.file_search_results() {
2197    ///     println!("{}: {}", result.title, result.text);
2198    /// }
2199    /// ```
2200    #[must_use]
2201    pub fn file_search_results(&self) -> Vec<&FileSearchResultItem> {
2202        self.outputs
2203            .iter()
2204            .filter_map(|content| {
2205                if let Content::FileSearchResult { result, .. } = content {
2206                    Some(result.iter())
2207                } else {
2208                    None
2209                }
2210            })
2211            .flatten()
2212            .collect()
2213    }
2214
2215    // =========================================================================
2216    // Summary and Diagnostics
2217    // =========================================================================
2218
2219    /// Get a summary of content types present in outputs.
2220    ///
2221    /// Returns a [`ContentSummary`] with counts for each content type.
2222    /// Useful for debugging, logging, or detecting unexpected content.
2223    ///
2224    /// # Example
2225    ///
2226    /// ```no_run
2227    /// # use genai_rs::InteractionResponse;
2228    /// # let response: InteractionResponse = todo!();
2229    /// let summary = response.content_summary();
2230    /// println!("Response has {} text outputs", summary.text_count);
2231    /// if summary.unknown_count > 0 {
2232    ///     println!("Warning: {} unknown types: {:?}",
2233    ///         summary.unknown_count, summary.unknown_types);
2234    /// }
2235    /// ```
2236    #[must_use]
2237    pub fn content_summary(&self) -> ContentSummary {
2238        let mut summary = ContentSummary::default();
2239        let mut unknown_types_set = BTreeSet::new();
2240
2241        for content in &self.outputs {
2242            match content {
2243                Content::Text { .. } => summary.text_count += 1,
2244                Content::Thought { .. } => summary.thought_count += 1,
2245                Content::ThoughtSignature { .. } => {
2246                    // ThoughtSignature typically only appears during streaming,
2247                    // not in final outputs. Count with thoughts if present.
2248                    summary.thought_count += 1
2249                }
2250                Content::Image { .. } => summary.image_count += 1,
2251                Content::Audio { .. } => summary.audio_count += 1,
2252                Content::Video { .. } => summary.video_count += 1,
2253                Content::Document { .. } => summary.document_count += 1,
2254                Content::FunctionCall { .. } => summary.function_call_count += 1,
2255                Content::FunctionResult { .. } => summary.function_result_count += 1,
2256                Content::CodeExecutionCall { .. } => summary.code_execution_call_count += 1,
2257                Content::CodeExecutionResult { .. } => summary.code_execution_result_count += 1,
2258                Content::GoogleSearchCall { .. } => summary.google_search_call_count += 1,
2259                Content::GoogleSearchResult { .. } => summary.google_search_result_count += 1,
2260                Content::UrlContextCall { .. } => summary.url_context_call_count += 1,
2261                Content::UrlContextResult { .. } => summary.url_context_result_count += 1,
2262                Content::FileSearchResult { .. } => summary.file_search_result_count += 1,
2263                Content::ComputerUseCall { .. } => summary.computer_use_call_count += 1,
2264                Content::ComputerUseResult { .. } => summary.computer_use_result_count += 1,
2265                Content::Unknown { content_type, .. } => {
2266                    summary.unknown_count += 1;
2267                    unknown_types_set.insert(content_type.clone());
2268                }
2269            }
2270        }
2271
2272        // BTreeSet maintains sorted order, so no need to sort
2273        summary.unknown_types = unknown_types_set.into_iter().collect();
2274        summary
2275    }
2276
2277    // =========================================================================
2278    // Token Usage Helpers
2279    // =========================================================================
2280
2281    /// Get the number of input (prompt) tokens used.
2282    ///
2283    /// Returns `None` if usage metadata is not available.
2284    ///
2285    /// # Example
2286    ///
2287    /// ```no_run
2288    /// # use genai_rs::InteractionResponse;
2289    /// # let response: InteractionResponse = todo!();
2290    /// if let Some(tokens) = response.input_tokens() {
2291    ///     println!("Input tokens: {}", tokens);
2292    /// }
2293    /// ```
2294    #[must_use]
2295    pub fn input_tokens(&self) -> Option<u32> {
2296        self.usage.as_ref().and_then(|u| u.total_input_tokens)
2297    }
2298
2299    /// Get the number of output tokens generated.
2300    ///
2301    /// Returns `None` if usage metadata is not available.
2302    ///
2303    /// # Example
2304    ///
2305    /// ```no_run
2306    /// # use genai_rs::InteractionResponse;
2307    /// # let response: InteractionResponse = todo!();
2308    /// if let Some(tokens) = response.output_tokens() {
2309    ///     println!("Output tokens: {}", tokens);
2310    /// }
2311    /// ```
2312    #[must_use]
2313    pub fn output_tokens(&self) -> Option<u32> {
2314        self.usage.as_ref().and_then(|u| u.total_output_tokens)
2315    }
2316
2317    /// Get the total number of tokens used (input + output).
2318    ///
2319    /// Returns `None` if usage metadata is not available.
2320    ///
2321    /// # Example
2322    ///
2323    /// ```no_run
2324    /// # use genai_rs::InteractionResponse;
2325    /// # let response: InteractionResponse = todo!();
2326    /// if let Some(tokens) = response.total_tokens() {
2327    ///     println!("Total tokens: {}", tokens);
2328    /// }
2329    /// ```
2330    #[must_use]
2331    pub fn total_tokens(&self) -> Option<u32> {
2332        self.usage.as_ref().and_then(|u| u.total_tokens)
2333    }
2334
2335    /// Get the number of reasoning tokens used (for thinking models).
2336    ///
2337    /// Reasoning tokens are used when thinking mode is enabled
2338    /// (e.g., via `with_thinking_level()` on supported models).
2339    /// Returns `None` if usage metadata is not available or thinking wasn't used.
2340    ///
2341    /// # Example
2342    ///
2343    /// ```no_run
2344    /// # use genai_rs::InteractionResponse;
2345    /// # let response: InteractionResponse = todo!();
2346    /// if let Some(tokens) = response.reasoning_tokens() {
2347    ///     println!("Reasoning tokens: {}", tokens);
2348    /// }
2349    /// ```
2350    #[must_use]
2351    pub fn reasoning_tokens(&self) -> Option<u32> {
2352        self.usage.as_ref().and_then(|u| u.total_reasoning_tokens)
2353    }
2354
2355    /// Get the number of cached tokens used (from context caching).
2356    ///
2357    /// Cached tokens reduce billing costs when reusing context.
2358    /// Returns `None` if usage metadata is not available or caching wasn't used.
2359    ///
2360    /// # Example
2361    ///
2362    /// ```no_run
2363    /// # use genai_rs::InteractionResponse;
2364    /// # let response: InteractionResponse = todo!();
2365    /// if let Some(tokens) = response.cached_tokens() {
2366    ///     println!("Cached tokens: {} (reduces cost)", tokens);
2367    /// }
2368    /// ```
2369    #[must_use]
2370    pub fn cached_tokens(&self) -> Option<u32> {
2371        self.usage.as_ref().and_then(|u| u.total_cached_tokens)
2372    }
2373
2374    /// Get the number of tool use tokens consumed.
2375    ///
2376    /// Tool use tokens represent overhead from function calling.
2377    /// Returns `None` if usage metadata is not available or tools weren't used.
2378    ///
2379    /// # Example
2380    ///
2381    /// ```no_run
2382    /// # use genai_rs::InteractionResponse;
2383    /// # let response: InteractionResponse = todo!();
2384    /// if let Some(tokens) = response.tool_use_tokens() {
2385    ///     println!("Tool use overhead: {} tokens", tokens);
2386    /// }
2387    /// ```
2388    #[must_use]
2389    pub fn tool_use_tokens(&self) -> Option<u32> {
2390        self.usage.as_ref().and_then(|u| u.total_tool_use_tokens)
2391    }
2392
2393    // =========================================================================
2394    // Timestamp Helpers
2395    // =========================================================================
2396
2397    /// Get the timestamp when this interaction was created.
2398    ///
2399    /// Returns `None` if the interaction was created with `store=false` or
2400    /// if the API didn't include timestamp information.
2401    ///
2402    /// # Example
2403    ///
2404    /// ```no_run
2405    /// # use genai_rs::InteractionResponse;
2406    /// # let response: InteractionResponse = todo!();
2407    /// if let Some(created) = response.created() {
2408    ///     println!("Created at: {}", created.to_rfc3339());
2409    /// }
2410    /// ```
2411    #[must_use]
2412    pub fn created(&self) -> Option<DateTime<Utc>> {
2413        self.created
2414    }
2415
2416    /// Get the timestamp when this interaction was last updated.
2417    ///
2418    /// Returns `None` if the interaction was created with `store=false` or
2419    /// if the API didn't include timestamp information.
2420    ///
2421    /// # Example
2422    ///
2423    /// ```no_run
2424    /// # use genai_rs::InteractionResponse;
2425    /// # let response: InteractionResponse = todo!();
2426    /// if let Some(updated) = response.updated() {
2427    ///     println!("Last updated: {}", updated.to_rfc3339());
2428    /// }
2429    /// ```
2430    #[must_use]
2431    pub fn updated(&self) -> Option<DateTime<Utc>> {
2432        self.updated
2433    }
2434
2435    // =========================================================================
2436    // Multi-Turn Helpers
2437    // =========================================================================
2438
2439    /// Converts this response's outputs to a model turn for multi-turn conversations.
2440    ///
2441    /// This enables seamless multi-turn patterns by allowing response outputs to be
2442    /// directly included in subsequent requests as conversation history.
2443    ///
2444    /// # Example
2445    ///
2446    /// ```no_run
2447    /// # use genai_rs::{Client, Turn};
2448    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2449    /// let client = Client::new("api-key".to_string());
2450    ///
2451    /// // First turn
2452    /// let response1 = client.interaction()
2453    ///     .with_model("gemini-3-flash-preview")
2454    ///     .with_text("What is 2+2?")
2455    ///     .create()
2456    ///     .await?;
2457    ///
2458    /// // Use response as model turn in follow-up
2459    /// let response2 = client.interaction()
2460    ///     .with_model("gemini-3-flash-preview")
2461    ///     .with_history(vec![
2462    ///         Turn::user("What is 2+2?"),
2463    ///         response1.as_model_turn(),
2464    ///         Turn::user("Now multiply that by 3"),
2465    ///     ])
2466    ///     .create()
2467    ///     .await?;
2468    /// # Ok(())
2469    /// # }
2470    /// ```
2471    #[must_use]
2472    pub fn as_model_turn(&self) -> Turn {
2473        Turn::model(self.outputs.clone())
2474    }
2475}
2476
2477/// Summary of content types present in an interaction response.
2478///
2479/// Returned by [`InteractionResponse::content_summary`]. Provides a quick overview
2480/// of what content types are present, including any unknown types.
2481///
2482/// # Example
2483///
2484/// ```no_run
2485/// # use genai_rs::InteractionResponse;
2486/// # let response: InteractionResponse = todo!();
2487/// let summary = response.content_summary();
2488///
2489/// // Check for unexpected content
2490/// if summary.unknown_count > 0 {
2491///     tracing::warn!(
2492///         "Response contains {} unknown content types: {:?}",
2493///         summary.unknown_count,
2494///         summary.unknown_types
2495///     );
2496/// }
2497///
2498/// // Log content breakdown
2499/// tracing::debug!(
2500///     "Content: {} text, {} thoughts, {} function calls",
2501///     summary.text_count,
2502///     summary.thought_count,
2503///     summary.function_call_count
2504/// );
2505/// ```
2506#[derive(Clone, Debug, Default, PartialEq, Eq)]
2507pub struct ContentSummary {
2508    /// Number of text content items
2509    pub text_count: usize,
2510    /// Number of thought content items
2511    pub thought_count: usize,
2512    /// Number of image content items
2513    pub image_count: usize,
2514    /// Number of audio content items
2515    pub audio_count: usize,
2516    /// Number of video content items
2517    pub video_count: usize,
2518    /// Number of document content items (PDF files)
2519    pub document_count: usize,
2520    /// Number of function call content items
2521    pub function_call_count: usize,
2522    /// Number of function result content items
2523    pub function_result_count: usize,
2524    /// Number of code execution call content items
2525    pub code_execution_call_count: usize,
2526    /// Number of code execution result content items
2527    pub code_execution_result_count: usize,
2528    /// Number of Google Search call content items
2529    pub google_search_call_count: usize,
2530    /// Number of Google Search result content items
2531    pub google_search_result_count: usize,
2532    /// Number of URL context call content items
2533    pub url_context_call_count: usize,
2534    /// Number of URL context result content items
2535    pub url_context_result_count: usize,
2536    /// Number of file search result content items
2537    pub file_search_result_count: usize,
2538    /// Number of computer use call content items
2539    pub computer_use_call_count: usize,
2540    /// Number of computer use result content items
2541    pub computer_use_result_count: usize,
2542    /// Number of unknown content items
2543    pub unknown_count: usize,
2544    /// List of unique unknown type names encountered (sorted alphabetically)
2545    pub unknown_types: Vec<String>,
2546}
2547
2548impl fmt::Display for ContentSummary {
2549    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2550        let mut parts = Vec::new();
2551
2552        if self.text_count > 0 {
2553            parts.push(format!("{} text", self.text_count));
2554        }
2555        if self.thought_count > 0 {
2556            parts.push(format!("{} thought", self.thought_count));
2557        }
2558        if self.image_count > 0 {
2559            parts.push(format!("{} image", self.image_count));
2560        }
2561        if self.audio_count > 0 {
2562            parts.push(format!("{} audio", self.audio_count));
2563        }
2564        if self.video_count > 0 {
2565            parts.push(format!("{} video", self.video_count));
2566        }
2567        if self.function_call_count > 0 {
2568            parts.push(format!("{} function_call", self.function_call_count));
2569        }
2570        if self.function_result_count > 0 {
2571            parts.push(format!("{} function_result", self.function_result_count));
2572        }
2573        if self.code_execution_call_count > 0 {
2574            parts.push(format!(
2575                "{} code_execution_call",
2576                self.code_execution_call_count
2577            ));
2578        }
2579        if self.code_execution_result_count > 0 {
2580            parts.push(format!(
2581                "{} code_execution_result",
2582                self.code_execution_result_count
2583            ));
2584        }
2585        if self.google_search_call_count > 0 {
2586            parts.push(format!(
2587                "{} google_search_call",
2588                self.google_search_call_count
2589            ));
2590        }
2591        if self.google_search_result_count > 0 {
2592            parts.push(format!(
2593                "{} google_search_result",
2594                self.google_search_result_count
2595            ));
2596        }
2597        if self.url_context_call_count > 0 {
2598            parts.push(format!("{} url_context_call", self.url_context_call_count));
2599        }
2600        if self.url_context_result_count > 0 {
2601            parts.push(format!(
2602                "{} url_context_result",
2603                self.url_context_result_count
2604            ));
2605        }
2606        if self.file_search_result_count > 0 {
2607            parts.push(format!(
2608                "{} file_search_result",
2609                self.file_search_result_count
2610            ));
2611        }
2612        if self.computer_use_call_count > 0 {
2613            parts.push(format!(
2614                "{} computer_use_call",
2615                self.computer_use_call_count
2616            ));
2617        }
2618        if self.computer_use_result_count > 0 {
2619            parts.push(format!(
2620                "{} computer_use_result",
2621                self.computer_use_result_count
2622            ));
2623        }
2624        if self.unknown_count > 0 {
2625            parts.push(format!(
2626                "{} unknown ({:?})",
2627                self.unknown_count, self.unknown_types
2628            ));
2629        }
2630
2631        if parts.is_empty() {
2632            write!(f, "empty")
2633        } else {
2634            write!(f, "{}", parts.join(", "))
2635        }
2636    }
2637}
2638
2639#[cfg(test)]
2640mod tests {
2641    use super::*;
2642
2643    fn minimal_response(usage: Option<UsageMetadata>) -> InteractionResponse {
2644        InteractionResponse {
2645            id: None,
2646            model: None,
2647            agent: None,
2648            input: vec![],
2649            outputs: vec![],
2650            status: InteractionStatus::Completed,
2651            usage,
2652            tools: None,
2653            grounding_metadata: None,
2654            url_context_metadata: None,
2655            previous_interaction_id: None,
2656            created: None,
2657            updated: None,
2658        }
2659    }
2660
2661    #[test]
2662    fn test_token_helpers_with_usage() {
2663        let response = minimal_response(Some(UsageMetadata {
2664            total_input_tokens: Some(100),
2665            total_output_tokens: Some(50),
2666            total_tokens: Some(150),
2667            total_cached_tokens: Some(25),
2668            total_reasoning_tokens: Some(10),
2669            total_tool_use_tokens: Some(5),
2670            ..Default::default()
2671        }));
2672
2673        assert_eq!(response.input_tokens(), Some(100));
2674        assert_eq!(response.output_tokens(), Some(50));
2675        assert_eq!(response.total_tokens(), Some(150));
2676        assert_eq!(response.cached_tokens(), Some(25));
2677        assert_eq!(response.reasoning_tokens(), Some(10));
2678        assert_eq!(response.tool_use_tokens(), Some(5));
2679    }
2680
2681    #[test]
2682    fn test_token_helpers_without_usage() {
2683        let response = minimal_response(None);
2684
2685        assert_eq!(response.input_tokens(), None);
2686        assert_eq!(response.output_tokens(), None);
2687        assert_eq!(response.total_tokens(), None);
2688        assert_eq!(response.cached_tokens(), None);
2689        assert_eq!(response.reasoning_tokens(), None);
2690        assert_eq!(response.tool_use_tokens(), None);
2691    }
2692
2693    #[test]
2694    fn test_token_helpers_with_partial_usage() {
2695        // Test case where only some token counts are available
2696        let response = minimal_response(Some(UsageMetadata {
2697            total_input_tokens: Some(100),
2698            total_output_tokens: Some(50),
2699            total_tokens: Some(150),
2700            total_cached_tokens: None,
2701            total_reasoning_tokens: None,
2702            total_tool_use_tokens: None,
2703            ..Default::default()
2704        }));
2705
2706        assert_eq!(response.input_tokens(), Some(100));
2707        assert_eq!(response.output_tokens(), Some(50));
2708        assert_eq!(response.total_tokens(), Some(150));
2709        assert_eq!(response.cached_tokens(), None);
2710        assert_eq!(response.reasoning_tokens(), None);
2711        assert_eq!(response.tool_use_tokens(), None);
2712    }
2713
2714    // =========================================================================
2715    // ModalityTokens Tests
2716    // =========================================================================
2717
2718    #[test]
2719    fn test_modality_tokens_serialization() {
2720        let tokens = ModalityTokens {
2721            modality: "TEXT".to_string(),
2722            tokens: 100,
2723        };
2724
2725        let json = serde_json::to_string(&tokens).unwrap();
2726        assert!(json.contains("\"modality\":\"TEXT\""));
2727        assert!(json.contains("\"tokens\":100"));
2728
2729        // Roundtrip
2730        let deserialized: ModalityTokens = serde_json::from_str(&json).unwrap();
2731        assert_eq!(deserialized.modality, "TEXT");
2732        assert_eq!(deserialized.tokens, 100);
2733    }
2734
2735    #[test]
2736    fn test_modality_tokens_deserialization() {
2737        let json = r#"{"modality": "IMAGE", "tokens": 500}"#;
2738        let tokens: ModalityTokens = serde_json::from_str(json).unwrap();
2739        assert_eq!(tokens.modality, "IMAGE");
2740        assert_eq!(tokens.tokens, 500);
2741    }
2742
2743    #[test]
2744    fn test_input_tokens_for_modality() {
2745        let usage = UsageMetadata {
2746            input_tokens_by_modality: Some(vec![
2747                ModalityTokens {
2748                    modality: "TEXT".to_string(),
2749                    tokens: 100,
2750                },
2751                ModalityTokens {
2752                    modality: "IMAGE".to_string(),
2753                    tokens: 500,
2754                },
2755                ModalityTokens {
2756                    modality: "AUDIO".to_string(),
2757                    tokens: 200,
2758                },
2759            ]),
2760            ..Default::default()
2761        };
2762
2763        assert_eq!(usage.input_tokens_for_modality("TEXT"), Some(100));
2764        assert_eq!(usage.input_tokens_for_modality("IMAGE"), Some(500));
2765        assert_eq!(usage.input_tokens_for_modality("AUDIO"), Some(200));
2766        assert_eq!(usage.input_tokens_for_modality("VIDEO"), None);
2767    }
2768
2769    #[test]
2770    fn test_input_tokens_for_modality_none() {
2771        let usage = UsageMetadata::default();
2772        assert_eq!(usage.input_tokens_for_modality("TEXT"), None);
2773    }
2774
2775    #[test]
2776    fn test_cache_hit_rate() {
2777        // 25% cache hit rate
2778        let usage = UsageMetadata {
2779            total_input_tokens: Some(100),
2780            total_cached_tokens: Some(25),
2781            ..Default::default()
2782        };
2783        let rate = usage.cache_hit_rate().unwrap();
2784        assert!((rate - 0.25).abs() < f32::EPSILON);
2785
2786        // 100% cache hit rate
2787        let usage = UsageMetadata {
2788            total_input_tokens: Some(100),
2789            total_cached_tokens: Some(100),
2790            ..Default::default()
2791        };
2792        let rate = usage.cache_hit_rate().unwrap();
2793        assert!((rate - 1.0).abs() < f32::EPSILON);
2794
2795        // 0% cache hit rate
2796        let usage = UsageMetadata {
2797            total_input_tokens: Some(100),
2798            total_cached_tokens: Some(0),
2799            ..Default::default()
2800        };
2801        let rate = usage.cache_hit_rate().unwrap();
2802        assert!((rate - 0.0).abs() < f32::EPSILON);
2803    }
2804
2805    #[test]
2806    fn test_cache_hit_rate_none_cases() {
2807        // Missing cached tokens
2808        let usage = UsageMetadata {
2809            total_input_tokens: Some(100),
2810            total_cached_tokens: None,
2811            ..Default::default()
2812        };
2813        assert!(usage.cache_hit_rate().is_none());
2814
2815        // Missing input tokens
2816        let usage = UsageMetadata {
2817            total_input_tokens: None,
2818            total_cached_tokens: Some(25),
2819            ..Default::default()
2820        };
2821        assert!(usage.cache_hit_rate().is_none());
2822
2823        // Zero input tokens (avoid division by zero)
2824        let usage = UsageMetadata {
2825            total_input_tokens: Some(0),
2826            total_cached_tokens: Some(0),
2827            ..Default::default()
2828        };
2829        assert!(usage.cache_hit_rate().is_none());
2830    }
2831
2832    #[test]
2833    fn test_has_data_with_modality_breakdowns() {
2834        // Only modality breakdowns present
2835        let usage = UsageMetadata {
2836            input_tokens_by_modality: Some(vec![ModalityTokens {
2837                modality: "TEXT".to_string(),
2838                tokens: 100,
2839            }]),
2840            ..Default::default()
2841        };
2842        assert!(usage.has_data());
2843
2844        // Empty default
2845        let usage = UsageMetadata::default();
2846        assert!(!usage.has_data());
2847    }
2848
2849    #[test]
2850    fn test_usage_metadata_with_modality_breakdowns_serialization() {
2851        let usage = UsageMetadata {
2852            total_input_tokens: Some(600),
2853            total_output_tokens: Some(100),
2854            input_tokens_by_modality: Some(vec![
2855                ModalityTokens {
2856                    modality: "TEXT".to_string(),
2857                    tokens: 100,
2858                },
2859                ModalityTokens {
2860                    modality: "IMAGE".to_string(),
2861                    tokens: 500,
2862                },
2863            ]),
2864            output_tokens_by_modality: Some(vec![ModalityTokens {
2865                modality: "TEXT".to_string(),
2866                tokens: 100,
2867            }]),
2868            ..Default::default()
2869        };
2870
2871        let json = serde_json::to_string(&usage).unwrap();
2872        assert!(json.contains("input_tokens_by_modality"));
2873        assert!(json.contains("output_tokens_by_modality"));
2874
2875        // Roundtrip
2876        let deserialized: UsageMetadata = serde_json::from_str(&json).unwrap();
2877        assert_eq!(deserialized.total_input_tokens, Some(600));
2878        assert_eq!(
2879            deserialized
2880                .input_tokens_by_modality
2881                .as_ref()
2882                .unwrap()
2883                .len(),
2884            2
2885        );
2886        assert_eq!(
2887            deserialized
2888                .output_tokens_by_modality
2889                .as_ref()
2890                .unwrap()
2891                .len(),
2892            1
2893        );
2894    }
2895
2896    // =========================================================================
2897    // Token Count Deserialization Edge Cases
2898    // =========================================================================
2899
2900    #[test]
2901    fn test_negative_token_count_clamped_to_zero() {
2902        // When API returns negative token counts (shouldn't happen but be defensive)
2903        let json = r#"{"total_input_tokens": -100, "total_output_tokens": 50}"#;
2904        let usage: UsageMetadata = serde_json::from_str(json).unwrap();
2905
2906        // Negative values are clamped to 0
2907        assert_eq!(usage.total_input_tokens, Some(0));
2908        assert_eq!(usage.total_output_tokens, Some(50));
2909    }
2910
2911    #[test]
2912    fn test_modality_tokens_negative_clamped() {
2913        let json = r#"{"modality": "TEXT", "tokens": -50}"#;
2914        let tokens: ModalityTokens = serde_json::from_str(json).unwrap();
2915
2916        assert_eq!(tokens.modality, "TEXT");
2917        assert_eq!(tokens.tokens, 0);
2918    }
2919
2920    #[test]
2921    fn test_large_token_count_clamped_to_u32_max() {
2922        // Value larger than u32::MAX (4,294,967,295)
2923        let json = r#"{"total_input_tokens": 5000000000}"#;
2924        let usage: UsageMetadata = serde_json::from_str(json).unwrap();
2925
2926        assert_eq!(usage.total_input_tokens, Some(u32::MAX));
2927    }
2928
2929    #[test]
2930    fn test_valid_token_counts_unchanged() {
2931        let json = r#"{
2932            "total_input_tokens": 100,
2933            "total_output_tokens": 50,
2934            "total_tokens": 150,
2935            "total_cached_tokens": 25
2936        }"#;
2937        let usage: UsageMetadata = serde_json::from_str(json).unwrap();
2938
2939        assert_eq!(usage.total_input_tokens, Some(100));
2940        assert_eq!(usage.total_output_tokens, Some(50));
2941        assert_eq!(usage.total_tokens, Some(150));
2942        assert_eq!(usage.total_cached_tokens, Some(25));
2943    }
2944
2945    // =========================================================================
2946    // Image Helper Tests
2947    // =========================================================================
2948
2949    fn make_response_with_image(base64_data: &str, mime_type: Option<&str>) -> InteractionResponse {
2950        InteractionResponse {
2951            id: Some("test-id".to_string()),
2952            model: Some("test-model".to_string()),
2953            agent: None,
2954            input: vec![],
2955            outputs: vec![Content::Image {
2956                data: Some(base64_data.to_string()),
2957                mime_type: mime_type.map(String::from),
2958                uri: None,
2959                resolution: None,
2960            }],
2961            status: InteractionStatus::Completed,
2962            usage: None,
2963            tools: None,
2964            grounding_metadata: None,
2965            url_context_metadata: None,
2966            previous_interaction_id: None,
2967            created: None,
2968            updated: None,
2969        }
2970    }
2971
2972    fn make_response_no_images() -> InteractionResponse {
2973        InteractionResponse {
2974            id: Some("test-id".to_string()),
2975            model: Some("test-model".to_string()),
2976            agent: None,
2977            input: vec![],
2978            outputs: vec![Content::Text {
2979                text: Some("Hello".to_string()),
2980                annotations: None,
2981            }],
2982            status: InteractionStatus::Completed,
2983            usage: None,
2984            tools: None,
2985            grounding_metadata: None,
2986            url_context_metadata: None,
2987            previous_interaction_id: None,
2988            created: None,
2989            updated: None,
2990        }
2991    }
2992
2993    #[test]
2994    fn test_first_image_bytes_success() {
2995        // Base64 for "test"
2996        let base64_data = "dGVzdA==";
2997        let response = make_response_with_image(base64_data, Some("image/png"));
2998
2999        let result = response.first_image_bytes();
3000        assert!(result.is_ok());
3001        let bytes = result.unwrap();
3002        assert!(bytes.is_some());
3003        assert_eq!(bytes.unwrap(), b"test");
3004    }
3005
3006    #[test]
3007    fn test_first_image_bytes_no_images() {
3008        let response = make_response_no_images();
3009
3010        let result = response.first_image_bytes();
3011        assert!(result.is_ok());
3012        assert!(result.unwrap().is_none());
3013    }
3014
3015    #[test]
3016    fn test_first_image_bytes_invalid_base64() {
3017        let response = make_response_with_image("not-valid-base64!!!", Some("image/png"));
3018
3019        let result = response.first_image_bytes();
3020        assert!(result.is_err());
3021        let err = result.unwrap_err().to_string();
3022        assert!(err.contains("Invalid base64"));
3023    }
3024
3025    #[test]
3026    fn test_images_iterator() {
3027        // Create response with multiple images
3028        let response = InteractionResponse {
3029            id: Some("test-id".to_string()),
3030            model: Some("test-model".to_string()),
3031            agent: None,
3032            input: vec![],
3033            outputs: vec![
3034                Content::Image {
3035                    data: Some("dGVzdDE=".to_string()), // "test1"
3036                    mime_type: Some("image/png".to_string()),
3037                    uri: None,
3038                    resolution: None,
3039                },
3040                Content::Text {
3041                    text: Some("text between".to_string()),
3042                    annotations: None,
3043                },
3044                Content::Image {
3045                    data: Some("dGVzdDI=".to_string()), // "test2"
3046                    mime_type: Some("image/jpeg".to_string()),
3047                    uri: None,
3048                    resolution: None,
3049                },
3050            ],
3051            status: InteractionStatus::Completed,
3052            usage: None,
3053            tools: None,
3054            grounding_metadata: None,
3055            url_context_metadata: None,
3056            previous_interaction_id: None,
3057            created: None,
3058            updated: None,
3059        };
3060
3061        let images: Vec<_> = response.images().collect();
3062        assert_eq!(images.len(), 2);
3063
3064        assert_eq!(images[0].bytes().unwrap(), b"test1");
3065        assert_eq!(images[0].mime_type(), Some("image/png"));
3066        assert_eq!(images[0].extension(), "png");
3067
3068        assert_eq!(images[1].bytes().unwrap(), b"test2");
3069        assert_eq!(images[1].mime_type(), Some("image/jpeg"));
3070        assert_eq!(images[1].extension(), "jpg");
3071    }
3072
3073    #[test]
3074    fn test_has_images() {
3075        let response_with = make_response_with_image("dGVzdA==", Some("image/png"));
3076        assert!(response_with.has_images());
3077
3078        let response_without = make_response_no_images();
3079        assert!(!response_without.has_images());
3080    }
3081
3082    #[test]
3083    fn test_image_info_extension() {
3084        let check = |mime: Option<&str>, expected: &str| {
3085            let info = ImageInfo {
3086                data: "",
3087                mime_type: mime,
3088            };
3089            assert_eq!(info.extension(), expected);
3090        };
3091
3092        check(Some("image/jpeg"), "jpg");
3093        check(Some("image/jpg"), "jpg");
3094        check(Some("image/png"), "png");
3095        check(Some("image/webp"), "webp");
3096        check(Some("image/gif"), "gif");
3097        check(Some("image/unknown"), "png"); // default
3098        check(None, "png"); // default
3099    }
3100
3101    #[test]
3102    fn test_image_info_bytes_invalid_base64() {
3103        let info = ImageInfo {
3104            data: "not-valid-base64!!!",
3105            mime_type: Some("image/png"),
3106        };
3107        let result = info.bytes();
3108        assert!(result.is_err());
3109        let err = result.unwrap_err().to_string();
3110        assert!(err.contains("Invalid base64"));
3111    }
3112
3113    #[test]
3114    fn test_image_info_extension_unknown_mime_type() {
3115        // This test documents Evergreen-compliant behavior:
3116        // Unknown MIME types default to "png" and log a warning (not verified here)
3117        // to surface API evolution without breaking user code.
3118        let info = ImageInfo {
3119            data: "",
3120            mime_type: Some("image/future-format"),
3121        };
3122        assert_eq!(info.extension(), "png");
3123
3124        // Completely novel MIME type also defaults gracefully
3125        let info2 = ImageInfo {
3126            data: "",
3127            mime_type: Some("application/octet-stream"),
3128        };
3129        assert_eq!(info2.extension(), "png");
3130    }
3131
3132    // =========================================================================
3133    // AudioInfo Tests
3134    // =========================================================================
3135
3136    #[test]
3137    fn test_audio_info_extension() {
3138        let check = |mime: Option<&str>, expected: &str| {
3139            let info = AudioInfo {
3140                data: "",
3141                mime_type: mime,
3142            };
3143            assert_eq!(info.extension(), expected);
3144        };
3145
3146        check(Some("audio/wav"), "wav");
3147        check(Some("audio/x-wav"), "wav");
3148        check(Some("audio/mp3"), "mp3");
3149        check(Some("audio/mpeg"), "mp3");
3150        check(Some("audio/ogg"), "ogg");
3151        check(Some("audio/flac"), "flac");
3152        check(Some("audio/aac"), "aac");
3153        check(Some("audio/webm"), "webm");
3154        // PCM/L16 format from TTS API
3155        check(Some("audio/L16;codec=pcm;rate=24000"), "pcm");
3156        check(Some("audio/L16"), "pcm");
3157        check(Some("audio/unknown"), "wav"); // default
3158        check(None, "wav"); // default
3159    }
3160
3161    #[test]
3162    fn test_audio_info_bytes_valid_base64() {
3163        // Base64 for "test"
3164        let info = AudioInfo {
3165            data: "dGVzdA==",
3166            mime_type: Some("audio/wav"),
3167        };
3168        let result = info.bytes();
3169        assert!(result.is_ok());
3170        assert_eq!(result.unwrap(), b"test");
3171    }
3172
3173    #[test]
3174    fn test_audio_info_bytes_invalid_base64() {
3175        let info = AudioInfo {
3176            data: "not-valid-base64!!!",
3177            mime_type: Some("audio/wav"),
3178        };
3179        let result = info.bytes();
3180        assert!(result.is_err());
3181        let err = result.unwrap_err().to_string();
3182        assert!(err.contains("Invalid base64"));
3183    }
3184
3185    #[test]
3186    fn test_audio_info_extension_unknown_mime_type() {
3187        // Evergreen-compliant behavior: unknown MIME types default to "wav"
3188        // and log a warning to surface API evolution.
3189        let info = AudioInfo {
3190            data: "",
3191            mime_type: Some("audio/future-format"),
3192        };
3193        assert_eq!(info.extension(), "wav");
3194    }
3195
3196    #[test]
3197    fn test_usage_metadata_accumulate_both_have_values() {
3198        let mut usage1 = UsageMetadata {
3199            total_input_tokens: Some(100),
3200            total_output_tokens: Some(50),
3201            total_tokens: Some(150),
3202            ..Default::default()
3203        };
3204        let usage2 = UsageMetadata {
3205            total_input_tokens: Some(200),
3206            total_output_tokens: Some(75),
3207            total_tokens: Some(275),
3208            ..Default::default()
3209        };
3210
3211        usage1.accumulate(&usage2);
3212
3213        assert_eq!(usage1.total_input_tokens, Some(300));
3214        assert_eq!(usage1.total_output_tokens, Some(125));
3215        assert_eq!(usage1.total_tokens, Some(425));
3216    }
3217
3218    #[test]
3219    fn test_usage_metadata_accumulate_self_has_none() {
3220        let mut usage1 = UsageMetadata::default();
3221        let usage2 = UsageMetadata {
3222            total_input_tokens: Some(200),
3223            total_output_tokens: Some(75),
3224            ..Default::default()
3225        };
3226
3227        usage1.accumulate(&usage2);
3228
3229        assert_eq!(usage1.total_input_tokens, Some(200));
3230        assert_eq!(usage1.total_output_tokens, Some(75));
3231    }
3232
3233    #[test]
3234    fn test_usage_metadata_accumulate_other_has_none() {
3235        let mut usage1 = UsageMetadata {
3236            total_input_tokens: Some(100),
3237            total_output_tokens: Some(50),
3238            ..Default::default()
3239        };
3240        let usage2 = UsageMetadata::default();
3241
3242        usage1.accumulate(&usage2);
3243
3244        // Values should remain unchanged
3245        assert_eq!(usage1.total_input_tokens, Some(100));
3246        assert_eq!(usage1.total_output_tokens, Some(50));
3247    }
3248
3249    #[test]
3250    fn test_usage_metadata_accumulate_all_fields() {
3251        let mut usage1 = UsageMetadata {
3252            total_input_tokens: Some(100),
3253            total_output_tokens: Some(50),
3254            total_tokens: Some(150),
3255            total_cached_tokens: Some(20),
3256            total_reasoning_tokens: Some(10),
3257            total_thought_tokens: Some(5),
3258            total_tool_use_tokens: Some(15),
3259            ..Default::default()
3260        };
3261        let usage2 = UsageMetadata {
3262            total_input_tokens: Some(200),
3263            total_output_tokens: Some(100),
3264            total_tokens: Some(300),
3265            total_cached_tokens: Some(40),
3266            total_reasoning_tokens: Some(20),
3267            total_thought_tokens: Some(10),
3268            total_tool_use_tokens: Some(30),
3269            ..Default::default()
3270        };
3271
3272        usage1.accumulate(&usage2);
3273
3274        assert_eq!(usage1.total_input_tokens, Some(300));
3275        assert_eq!(usage1.total_output_tokens, Some(150));
3276        assert_eq!(usage1.total_tokens, Some(450));
3277        assert_eq!(usage1.total_cached_tokens, Some(60));
3278        assert_eq!(usage1.total_reasoning_tokens, Some(30));
3279        assert_eq!(usage1.total_thought_tokens, Some(15));
3280        assert_eq!(usage1.total_tool_use_tokens, Some(45));
3281    }
3282
3283    #[test]
3284    fn test_usage_metadata_accumulate_zero_values() {
3285        let mut usage1 = UsageMetadata {
3286            total_input_tokens: Some(0),
3287            total_output_tokens: Some(50),
3288            ..Default::default()
3289        };
3290        let usage2 = UsageMetadata {
3291            total_input_tokens: Some(100),
3292            total_output_tokens: Some(0),
3293            ..Default::default()
3294        };
3295
3296        usage1.accumulate(&usage2);
3297
3298        assert_eq!(usage1.total_input_tokens, Some(100));
3299        assert_eq!(usage1.total_output_tokens, Some(50));
3300    }
3301
3302    #[test]
3303    fn test_usage_metadata_accumulate_saturating() {
3304        // Test that we don't overflow - use saturating_add
3305        let mut usage1 = UsageMetadata {
3306            total_input_tokens: Some(u32::MAX - 10),
3307            ..Default::default()
3308        };
3309        let usage2 = UsageMetadata {
3310            total_input_tokens: Some(100),
3311            ..Default::default()
3312        };
3313
3314        usage1.accumulate(&usage2);
3315
3316        // Should saturate at u32::MAX, not wrap around
3317        assert_eq!(usage1.total_input_tokens, Some(u32::MAX));
3318    }
3319}