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}