genai_rs/content.rs
1//! Core content types for the Interactions API.
2//!
3//! This module contains `Content`, the primary enum representing all content
4//! types that can appear in API requests and responses, along with its custom serialization
5//! and deserialization implementations.
6
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10// =============================================================================
11// Annotation Types (for Text Content with Citations)
12// =============================================================================
13
14/// An annotation linking a text span to its source.
15///
16/// Annotations provide citation information for specific portions of generated text,
17/// typically appearing when using grounding tools like `GoogleSearch` or `UrlContext`.
18///
19/// # Byte Indices
20///
21/// **Important:** The `start_index` and `end_index` fields are **byte positions** (not
22/// character indices) in the text content. This matches the UTF-8 byte offsets used
23/// by the Gemini API. For multi-byte characters (like emoji or non-ASCII text), you
24/// may need to use byte-slicing rather than character indexing.
25///
26/// # Example
27///
28/// ```no_run
29/// # use genai_rs::{InteractionResponse, Annotation};
30/// # let response: InteractionResponse = todo!();
31/// // Get all annotations from the response
32/// for annotation in response.all_annotations() {
33/// println!(
34/// "Text at bytes {}..{} sourced from: {}",
35/// annotation.start_index,
36/// annotation.end_index,
37/// annotation.source.as_deref().unwrap_or("<no source>")
38/// );
39/// }
40/// ```
41///
42/// # Extracting Annotated Text
43///
44/// To extract the annotated substring from the response text:
45///
46/// ```no_run
47/// # use genai_rs::{InteractionResponse, Annotation};
48/// # let response: InteractionResponse = todo!();
49/// # let annotation: &Annotation = todo!();
50/// if let Some(text) = response.as_text() {
51/// // Use byte slicing since indices are byte positions
52/// let bytes = text.as_bytes();
53/// if annotation.end_index <= bytes.len() {
54/// if let Ok(span) = std::str::from_utf8(&bytes[annotation.start_index..annotation.end_index]) {
55/// println!("Cited text: {}", span);
56/// }
57/// }
58/// }
59/// ```
60#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62#[non_exhaustive]
63pub struct Annotation {
64 /// Start of the segment in the text (byte position, inclusive).
65 ///
66 /// This is a byte offset into the UTF-8 encoded text, not a character index.
67 #[serde(default)]
68 pub start_index: usize,
69
70 /// End of the segment in the text (byte position, exclusive).
71 ///
72 /// This is a byte offset into the UTF-8 encoded text, not a character index.
73 #[serde(default)]
74 pub end_index: usize,
75
76 /// Source attributed for this portion of the text.
77 ///
78 /// This could be a URL, title, or other identifier depending on the grounding
79 /// tool used (e.g., `GoogleSearch` or `UrlContext`).
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub source: Option<String>,
82}
83
84impl Annotation {
85 /// Creates a new annotation with the given span indices and optional source.
86 ///
87 /// # Arguments
88 ///
89 /// * `start_index` - Start of the segment in the text (byte position, inclusive)
90 /// * `end_index` - End of the segment in the text (byte position, exclusive)
91 /// * `source` - Optional source attribution (URL, title, or identifier)
92 ///
93 /// # Example
94 ///
95 /// ```
96 /// # use genai_rs::Annotation;
97 /// let annotation = Annotation::new(0, 10, Some("https://example.com".to_string()));
98 /// assert_eq!(annotation.start_index, 0);
99 /// assert_eq!(annotation.end_index, 10);
100 /// assert!(annotation.has_source());
101 /// ```
102 #[must_use]
103 pub fn new(start_index: usize, end_index: usize, source: Option<String>) -> Self {
104 Self {
105 start_index,
106 end_index,
107 source,
108 }
109 }
110
111 /// Returns the byte length of the annotated span.
112 ///
113 /// This is equivalent to `end_index - start_index`.
114 #[must_use]
115 pub fn byte_len(&self) -> usize {
116 self.end_index.saturating_sub(self.start_index)
117 }
118
119 /// Returns `true` if this annotation has a source attribution.
120 #[must_use]
121 pub fn has_source(&self) -> bool {
122 self.source.is_some()
123 }
124
125 /// Extracts the annotated substring from the given text.
126 ///
127 /// Returns `None` if the indices are out of bounds or if the byte slice
128 /// doesn't form valid UTF-8.
129 ///
130 /// # Arguments
131 ///
132 /// * `text` - The full text content to extract from
133 ///
134 /// # Example
135 ///
136 /// ```
137 /// # use genai_rs::Annotation;
138 /// let annotation = Annotation::new(0, 5, Some("https://example.com".to_string()));
139 ///
140 /// let text = "Hello, world!";
141 /// assert_eq!(annotation.extract_span(text), Some("Hello"));
142 /// ```
143 #[must_use]
144 pub fn extract_span<'a>(&self, text: &'a str) -> Option<&'a str> {
145 let bytes = text.as_bytes();
146 if self.end_index <= bytes.len() && self.start_index <= self.end_index {
147 std::str::from_utf8(&bytes[self.start_index..self.end_index]).ok()
148 } else {
149 None
150 }
151 }
152}
153
154// =============================================================================
155// Google Search Result Item
156// =============================================================================
157
158/// A single result from a Google Search.
159///
160/// Contains the source information for a grounding chunk including the title,
161/// URL, and optionally the rendered content that was used for grounding.
162///
163/// # Example
164///
165/// ```no_run
166/// # use genai_rs::{Content, GoogleSearchResultItem};
167/// # let content: Content = todo!();
168/// if let Content::GoogleSearchResult { result, .. } = content {
169/// for item in result {
170/// println!("Source: {} - {}", item.title, item.url);
171/// }
172/// }
173/// ```
174#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(default)]
176pub struct GoogleSearchResultItem {
177 /// Title of the search result (often the domain name)
178 pub title: String,
179 /// URL of the source (typically a grounding redirect URL)
180 pub url: String,
181 /// The rendered content from the source (if available)
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub rendered_content: Option<String>,
184}
185
186impl GoogleSearchResultItem {
187 /// Creates a new GoogleSearchResultItem.
188 #[must_use]
189 pub fn new(title: impl Into<String>, url: impl Into<String>) -> Self {
190 Self {
191 title: title.into(),
192 url: url.into(),
193 rendered_content: None,
194 }
195 }
196
197 /// Returns `true` if this result has rendered content.
198 #[must_use]
199 pub fn has_rendered_content(&self) -> bool {
200 self.rendered_content.is_some()
201 }
202}
203
204// =============================================================================
205// URL Context Result Item
206// =============================================================================
207
208/// A single result from a URL Context fetch.
209///
210/// Contains the status of the URL fetch operation.
211///
212/// # Example
213///
214/// ```no_run
215/// # use genai_rs::{Content, UrlContextResultItem};
216/// # let content: Content = todo!();
217/// if let Content::UrlContextResult { result, .. } = content {
218/// for item in result {
219/// println!("URL: {} - Status: {}", item.url, item.status);
220/// }
221/// }
222/// ```
223#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(default)]
225pub struct UrlContextResultItem {
226 /// The URL that was fetched
227 pub url: String,
228 /// Status of the fetch operation (e.g., "success", "error", "unsafe")
229 pub status: String,
230}
231
232impl UrlContextResultItem {
233 /// Creates a new UrlContextResultItem.
234 #[must_use]
235 pub fn new(url: impl Into<String>, status: impl Into<String>) -> Self {
236 Self {
237 url: url.into(),
238 status: status.into(),
239 }
240 }
241
242 /// Returns `true` if the fetch was successful.
243 #[must_use]
244 pub fn is_success(&self) -> bool {
245 self.status == "success"
246 }
247
248 /// Returns `true` if the fetch failed with an error.
249 #[must_use]
250 pub fn is_error(&self) -> bool {
251 self.status == "error"
252 }
253
254 /// Returns `true` if the URL was blocked as unsafe.
255 #[must_use]
256 pub fn is_unsafe(&self) -> bool {
257 self.status == "unsafe"
258 }
259}
260
261// =============================================================================
262// File Search Result Item
263// =============================================================================
264
265/// A single result from a File Search.
266///
267/// Contains the extracted text from a semantic search match, including the title,
268/// text content, and the source file search store.
269///
270/// # Example
271///
272/// ```no_run
273/// # use genai_rs::{Content, FileSearchResultItem};
274/// # let content: Content = todo!();
275/// if let Content::FileSearchResult { result, .. } = content {
276/// for item in result {
277/// println!("Match from '{}': {}", item.store, item.text);
278/// }
279/// }
280/// ```
281#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(default)]
283pub struct FileSearchResultItem {
284 /// Title of the matched document
285 pub title: String,
286 /// Extracted text content from the semantic match
287 pub text: String,
288 /// Name of the file search store containing this result
289 #[serde(rename = "file_search_store")]
290 pub store: String,
291}
292
293impl FileSearchResultItem {
294 /// Creates a new FileSearchResultItem.
295 #[must_use]
296 pub fn new(
297 title: impl Into<String>,
298 text: impl Into<String>,
299 store: impl Into<String>,
300 ) -> Self {
301 Self {
302 title: title.into(),
303 text: text.into(),
304 store: store.into(),
305 }
306 }
307
308 /// Returns `true` if this result has any text content.
309 #[must_use]
310 pub fn has_text(&self) -> bool {
311 !self.text.is_empty()
312 }
313}
314
315/// Programming language for code execution.
316///
317/// This enum represents the programming language used in code execution requests.
318/// Currently only Python is supported by the Gemini API.
319///
320/// # Forward Compatibility (Evergreen Philosophy)
321///
322/// This enum is marked `#[non_exhaustive]`, which means:
323/// - Match statements must include a wildcard arm (`_ => ...`)
324/// - New variants may be added in minor version updates without breaking your code
325///
326/// When the API returns a language value that this library doesn't recognize,
327/// it will be captured as `CodeExecutionLanguage::Unknown` rather than causing a
328/// deserialization error. This follows the
329/// [Evergreen spec](https://github.com/google-deepmind/evergreen-spec)
330/// philosophy of graceful degradation.
331///
332/// # Example
333///
334/// ```no_run
335/// # use genai_rs::{Content, CodeExecutionLanguage};
336/// # let content: Content = todo!();
337/// if let Content::CodeExecutionCall { language, code, .. } = content {
338/// match language {
339/// CodeExecutionLanguage::Python => println!("Python code: {}", code),
340/// CodeExecutionLanguage::Unknown { language_type, .. } => {
341/// println!("Unknown language '{}': {}", language_type, code);
342/// }
343/// _ => println!("Other language: {}", code),
344/// }
345/// }
346/// ```
347#[derive(Clone, Debug, Default, PartialEq, Eq)]
348#[non_exhaustive]
349pub enum CodeExecutionLanguage {
350 /// Python programming language
351 #[default]
352 Python,
353 /// Unknown language (for forward compatibility).
354 ///
355 /// This variant captures any unrecognized language values from the API,
356 /// allowing the library to handle new languages gracefully.
357 ///
358 /// The `language_type` field contains the unrecognized language string,
359 /// and `data` contains the full JSON value for debugging.
360 Unknown {
361 /// The unrecognized language string from the API
362 language_type: String,
363 /// The raw JSON value, preserved for debugging
364 data: serde_json::Value,
365 },
366}
367
368impl CodeExecutionLanguage {
369 /// Check if this is an unknown language.
370 #[must_use]
371 pub const fn is_unknown(&self) -> bool {
372 matches!(self, Self::Unknown { .. })
373 }
374
375 /// Returns the language type name if this is an unknown language.
376 ///
377 /// Returns `None` for known languages.
378 #[must_use]
379 pub fn unknown_language_type(&self) -> Option<&str> {
380 match self {
381 Self::Unknown { language_type, .. } => Some(language_type),
382 _ => None,
383 }
384 }
385
386 /// Returns the raw JSON data if this is an unknown language.
387 ///
388 /// Returns `None` for known languages.
389 #[must_use]
390 pub fn unknown_data(&self) -> Option<&serde_json::Value> {
391 match self {
392 Self::Unknown { data, .. } => Some(data),
393 _ => None,
394 }
395 }
396}
397
398impl Serialize for CodeExecutionLanguage {
399 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
400 where
401 S: serde::Serializer,
402 {
403 match self {
404 Self::Python => serializer.serialize_str("PYTHON"),
405 Self::Unknown { language_type, .. } => serializer.serialize_str(language_type),
406 }
407 }
408}
409
410impl<'de> Deserialize<'de> for CodeExecutionLanguage {
411 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
412 where
413 D: serde::Deserializer<'de>,
414 {
415 let value = serde_json::Value::deserialize(deserializer)?;
416
417 match value.as_str() {
418 Some("PYTHON") => Ok(Self::Python),
419 Some(other) => {
420 tracing::warn!(
421 "Encountered unknown CodeExecutionLanguage '{}'. \
422 This may indicate a new API feature. \
423 The language will be preserved in the Unknown variant.",
424 other
425 );
426 Ok(Self::Unknown {
427 language_type: other.to_string(),
428 data: value,
429 })
430 }
431 None => {
432 // Non-string value - preserve it in Unknown
433 let language_type = format!("<non-string: {}>", value);
434 tracing::warn!(
435 "CodeExecutionLanguage received non-string value: {}. \
436 Preserving in Unknown variant.",
437 value
438 );
439 Ok(Self::Unknown {
440 language_type,
441 data: value,
442 })
443 }
444 }
445 }
446}
447
448impl fmt::Display for CodeExecutionLanguage {
449 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450 match self {
451 Self::Python => write!(f, "PYTHON"),
452 Self::Unknown { language_type, .. } => write!(f, "{}", language_type),
453 }
454 }
455}
456
457/// Resolution level for image and video content processing.
458///
459/// Controls the quality vs. token cost trade-off when processing images and videos.
460/// Lower resolution uses fewer tokens (lower cost), while higher resolution provides
461/// more detail for the model to analyze.
462///
463/// # Token Cost Trade-offs
464///
465/// | Resolution | Token Cost | Detail Level |
466/// |------------|------------|--------------|
467/// | Low | Lowest | Basic shapes and colors |
468/// | Medium | Moderate | Standard detail |
469/// | High | Higher | Fine details visible |
470/// | UltraHigh | Highest | Maximum fidelity |
471///
472/// # Forward Compatibility (Evergreen Philosophy)
473///
474/// This enum is marked `#[non_exhaustive]`, which means:
475/// - Match statements must include a wildcard arm (`_ => ...`)
476/// - New variants may be added in minor version updates without breaking your code
477///
478/// When the API returns a resolution value that this library doesn't recognize,
479/// it will be captured as `Resolution::Unknown` rather than causing a
480/// deserialization error. This follows the
481/// [Evergreen spec](https://github.com/google-deepmind/evergreen-spec)
482/// philosophy of graceful degradation.
483///
484/// # Example
485///
486/// ```
487/// use genai_rs::Resolution;
488///
489/// // Use Low for cheap, basic analysis
490/// let low_cost = Resolution::Low;
491///
492/// // Use High for detailed analysis
493/// let detailed = Resolution::High;
494///
495/// // Default is Medium
496/// assert_eq!(Resolution::default(), Resolution::Medium);
497/// ```
498#[derive(Clone, Debug, Default, PartialEq, Eq)]
499#[non_exhaustive]
500pub enum Resolution {
501 /// Lowest token cost, basic shapes and colors
502 Low,
503 /// Moderate token cost, standard detail (default)
504 #[default]
505 Medium,
506 /// Higher token cost, fine details visible
507 High,
508 /// Highest token cost, maximum fidelity
509 UltraHigh,
510 /// Unknown resolution (for forward compatibility).
511 ///
512 /// This variant captures any unrecognized resolution values from the API,
513 /// allowing the library to handle new resolutions gracefully.
514 ///
515 /// The `resolution_type` field contains the unrecognized resolution string,
516 /// and `data` contains the JSON value (typically the same string).
517 Unknown {
518 /// The unrecognized resolution string from the API
519 resolution_type: String,
520 /// The raw JSON value, preserved for debugging
521 data: serde_json::Value,
522 },
523}
524
525impl Resolution {
526 /// Check if this is an unknown resolution.
527 #[must_use]
528 pub const fn is_unknown(&self) -> bool {
529 matches!(self, Self::Unknown { .. })
530 }
531
532 /// Returns the resolution type name if this is an unknown resolution.
533 ///
534 /// Returns `None` for known resolutions.
535 #[must_use]
536 pub fn unknown_resolution_type(&self) -> Option<&str> {
537 match self {
538 Self::Unknown {
539 resolution_type, ..
540 } => Some(resolution_type),
541 _ => None,
542 }
543 }
544
545 /// Returns the raw JSON data if this is an unknown resolution.
546 ///
547 /// Returns `None` for known resolutions.
548 #[must_use]
549 pub fn unknown_data(&self) -> Option<&serde_json::Value> {
550 match self {
551 Self::Unknown { data, .. } => Some(data),
552 _ => None,
553 }
554 }
555}
556
557impl Serialize for Resolution {
558 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
559 where
560 S: serde::Serializer,
561 {
562 match self {
563 Self::Low => serializer.serialize_str("low"),
564 Self::Medium => serializer.serialize_str("medium"),
565 Self::High => serializer.serialize_str("high"),
566 Self::UltraHigh => serializer.serialize_str("ultra_high"),
567 Self::Unknown {
568 resolution_type, ..
569 } => serializer.serialize_str(resolution_type),
570 }
571 }
572}
573
574impl<'de> Deserialize<'de> for Resolution {
575 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
576 where
577 D: serde::Deserializer<'de>,
578 {
579 let value = serde_json::Value::deserialize(deserializer)?;
580
581 match value.as_str() {
582 Some("low") => Ok(Self::Low),
583 Some("medium") => Ok(Self::Medium),
584 Some("high") => Ok(Self::High),
585 Some("ultra_high") => Ok(Self::UltraHigh),
586 Some(other) => {
587 tracing::warn!(
588 "Encountered unknown Resolution '{}'. \
589 This may indicate a new API feature. \
590 The resolution will be preserved in the Unknown variant.",
591 other
592 );
593 Ok(Self::Unknown {
594 resolution_type: other.to_string(),
595 data: value,
596 })
597 }
598 None => {
599 // Non-string value - preserve it in Unknown
600 let resolution_type = format!("<non-string: {}>", value);
601 tracing::warn!(
602 "Resolution received non-string value: {}. \
603 Preserving in Unknown variant.",
604 value
605 );
606 Ok(Self::Unknown {
607 resolution_type,
608 data: value,
609 })
610 }
611 }
612 }
613}
614
615impl fmt::Display for Resolution {
616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617 match self {
618 Self::Low => write!(f, "low"),
619 Self::Medium => write!(f, "medium"),
620 Self::High => write!(f, "high"),
621 Self::UltraHigh => write!(f, "ultra_high"),
622 Self::Unknown {
623 resolution_type, ..
624 } => write!(f, "{}", resolution_type),
625 }
626 }
627}
628
629/// Content object for Interactions API - uses flat structure with type field.
630///
631/// This enum represents all content types that can appear in API requests and responses.
632/// It includes an `Unknown` variant for forward compatibility with new API content types.
633///
634/// # Forward Compatibility
635///
636/// This enum is marked `#[non_exhaustive]`, which means:
637/// - Match statements must include a wildcard arm (`_ => ...`)
638/// - New variants may be added in minor version updates without breaking your code
639///
640/// When the API returns a content type that this library doesn't recognize, it will be
641/// captured as `Content::Unknown` rather than causing a deserialization error.
642/// This allows your code to continue working even when Google adds new content types.
643///
644/// Use [`super::InteractionResponse::has_unknown`] and [`super::InteractionResponse::unknown_content`]
645/// to detect and inspect unknown content.
646///
647/// # Example
648///
649/// ```no_run
650/// # use genai_rs::{Content, InteractionResponse};
651/// # let response: InteractionResponse = todo!();
652/// for content in &response.outputs {
653/// match content {
654/// Content::Text { text, annotations } => {
655/// println!("Text: {:?}", text);
656/// if let Some(annots) = annotations {
657/// println!(" {} annotations", annots.len());
658/// }
659/// }
660/// Content::FunctionCall { name, .. } => println!("Function: {}", name),
661/// Content::Unknown { content_type, .. } => {
662/// println!("Unknown content type: {}", content_type);
663/// }
664/// // Required due to #[non_exhaustive] - handles future variants
665/// _ => {}
666/// }
667/// }
668/// ```
669#[derive(Clone, Debug, PartialEq)]
670#[non_exhaustive]
671pub enum Content {
672 /// Text content with optional source annotations.
673 ///
674 /// Annotations are present when grounding tools like `GoogleSearch` or `UrlContext`
675 /// provide citation information linking text spans to their sources.
676 Text {
677 /// The text content.
678 ///
679 /// This is `Option<String>` because during streaming, `content.start` events
680 /// announce the content type before any text arrives. The actual text is
681 /// delivered in subsequent `content.delta` events. For non-streaming responses
682 /// and delta events, this will always be `Some`.
683 text: Option<String>,
684 /// Source annotations for portions of the text.
685 ///
686 /// Present when the response includes citation information from grounding tools.
687 /// Use [`annotations()`](Self::annotations) for convenient access.
688 annotations: Option<Vec<Annotation>>,
689 },
690 /// Thought content (internal reasoning).
691 ///
692 /// Contains a cryptographic signature for verification of the thinking process.
693 /// The actual thought text is not exposed in the API response - only the signature
694 /// which can be used to validate that the response was generated through the
695 /// model's reasoning process.
696 ///
697 /// The `signature` field is `Option<String>` because `content.start` events
698 /// announce the type before the signature arrives via `content.delta`.
699 Thought { signature: Option<String> },
700 /// Thought signature (cryptographic signature for thought verification)
701 ///
702 /// This variant typically appears only during streaming responses, providing
703 /// a cryptographic signature that verifies the authenticity of thought content.
704 ThoughtSignature { signature: String },
705 /// Image content
706 Image {
707 data: Option<String>,
708 uri: Option<String>,
709 mime_type: Option<String>,
710 resolution: Option<Resolution>,
711 },
712 /// Audio content
713 Audio {
714 data: Option<String>,
715 uri: Option<String>,
716 mime_type: Option<String>,
717 },
718 /// Video content
719 Video {
720 data: Option<String>,
721 uri: Option<String>,
722 mime_type: Option<String>,
723 resolution: Option<Resolution>,
724 },
725 /// Document content for file-based inputs.
726 ///
727 /// PDF (`application/pdf`) is the primary supported format with full vision capabilities
728 /// for understanding text, images, charts, and tables. Other formats like TXT, Markdown,
729 /// HTML, and XML are processed as plain text only, losing visual structure.
730 Document {
731 data: Option<String>,
732 uri: Option<String>,
733 mime_type: Option<String>,
734 },
735 /// Function call (output from model)
736 FunctionCall {
737 /// Unique identifier for this function call
738 id: Option<String>,
739 name: String,
740 args: serde_json::Value,
741 },
742 /// Function result (input to model with execution result)
743 FunctionResult {
744 /// Function name (optional per API spec)
745 name: Option<String>,
746 /// The call_id from the FunctionCall being responded to
747 call_id: String,
748 result: serde_json::Value,
749 /// Indicates if the function execution resulted in an error
750 is_error: Option<bool>,
751 },
752 /// Code execution call (model requesting code execution)
753 ///
754 /// This variant appears when the model initiates code execution
755 /// via the `CodeExecution` built-in tool.
756 ///
757 /// # Example
758 ///
759 /// ```no_run
760 /// # use genai_rs::{Content, CodeExecutionLanguage};
761 /// # let content: Content = todo!();
762 /// if let Content::CodeExecutionCall { id, language, code } = content {
763 /// println!("Executing {:?} code (id: {:?}): {}", language, id, code);
764 /// }
765 /// ```
766 CodeExecutionCall {
767 /// Unique identifier for this code execution call (optional per API spec)
768 id: Option<String>,
769 /// Programming language (currently only Python is supported)
770 language: CodeExecutionLanguage,
771 /// Source code to execute
772 code: String,
773 },
774 /// Code execution result (returned after code runs)
775 ///
776 /// Contains the result of executed code from the `CodeExecution` tool.
777 ///
778 /// # Security Note
779 ///
780 /// When displaying results to end users, check `is_error` first. Error
781 /// results may contain stack traces or system information that shouldn't be exposed
782 /// directly to users without sanitization.
783 ///
784 /// # Example
785 ///
786 /// ```no_run
787 /// # use genai_rs::Content;
788 /// # let content: Content = todo!();
789 /// if let Content::CodeExecutionResult { is_error, result, .. } = content {
790 /// if is_error {
791 /// eprintln!("Error: {}", result);
792 /// } else {
793 /// println!("Result: {}", result);
794 /// }
795 /// }
796 /// ```
797 CodeExecutionResult {
798 /// The call_id matching the CodeExecutionCall this result is for (optional per API spec)
799 call_id: Option<String>,
800 /// Whether the code execution resulted in an error
801 is_error: bool,
802 /// The output of the code execution (stdout for success, error message for failure)
803 result: String,
804 },
805 /// Google Search call (model requesting a search)
806 ///
807 /// Appears when the model initiates a Google Search via the `GoogleSearch` tool.
808 /// The model may execute multiple queries in a single call.
809 GoogleSearchCall {
810 /// Unique identifier for this search call (used to match with result)
811 id: String,
812 /// Search queries executed by the model
813 queries: Vec<String>,
814 },
815 /// Google Search result (grounding data from search)
816 ///
817 /// Contains the results returned by the `GoogleSearch` built-in tool.
818 /// Each result includes a title and URL for the source.
819 GoogleSearchResult {
820 /// ID of the corresponding GoogleSearchCall
821 call_id: String,
822 /// Search results with source information
823 result: Vec<GoogleSearchResultItem>,
824 },
825 /// URL Context call (model requesting URL content)
826 ///
827 /// Appears when the model requests URL content via the `UrlContext` tool.
828 UrlContextCall {
829 /// Unique identifier for this URL context call
830 id: String,
831 /// URLs to fetch (extracted from arguments.urls in wire format)
832 urls: Vec<String>,
833 },
834 /// URL Context result (fetched content from URL)
835 ///
836 /// Contains the results from the `UrlContext` built-in tool.
837 UrlContextResult {
838 /// ID of the corresponding UrlContextCall
839 call_id: String,
840 /// Results for each URL that was fetched
841 result: Vec<UrlContextResultItem>,
842 },
843 /// File Search result (semantic search results from document stores)
844 ///
845 /// Contains the results returned by the `FileSearch` built-in tool.
846 /// Each result includes the title, extracted text, and source store name.
847 ///
848 /// # Example
849 ///
850 /// ```no_run
851 /// # use genai_rs::{Content, FileSearchResultItem};
852 /// # let content: Content = todo!();
853 /// if let Content::FileSearchResult { call_id, result } = content {
854 /// println!("Results for call {}: {} matches", call_id, result.len());
855 /// for item in result {
856 /// println!(" {}: {}", item.title, item.text);
857 /// }
858 /// }
859 /// ```
860 FileSearchResult {
861 /// ID of the corresponding file search call
862 call_id: String,
863 /// Search results with extracted text and source information
864 result: Vec<FileSearchResultItem>,
865 },
866 /// Computer use call (model requesting browser interaction)
867 ///
868 /// Appears when the model initiates browser automation via the `ComputerUse` tool.
869 ///
870 /// # Security Considerations
871 ///
872 /// Computer use calls allow the model to interact with web browsers on your behalf.
873 /// Always review calls before execution in production environments, especially when:
874 /// - Accessing sensitive websites (banking, admin panels)
875 /// - Performing state-changing operations (form submissions, purchases)
876 /// - Working with untrusted user input
877 ///
878 /// # Example
879 ///
880 /// ```no_run
881 /// # use genai_rs::Content;
882 /// # let content: Content = todo!();
883 /// if let Content::ComputerUseCall { id, action, parameters } = content {
884 /// println!("Browser action '{}' requested (id: {})", action, id);
885 /// println!("Parameters: {:?}", parameters);
886 /// }
887 /// ```
888 ComputerUseCall {
889 /// Unique identifier for this computer use call
890 id: String,
891 /// The browser action to perform (e.g., "navigate", "click", "type")
892 action: String,
893 /// Action-specific parameters
894 parameters: serde_json::Value,
895 },
896 /// Computer use result (returned after browser interaction)
897 ///
898 /// Contains the outcome of a browser action executed via the `ComputerUse` tool.
899 ///
900 /// # Security Note
901 ///
902 /// Results may contain sensitive information like:
903 /// - Screenshots of the current page
904 /// - DOM content from visited pages
905 /// - Cookie or session data
906 ///
907 /// Sanitize output before displaying to end users.
908 ///
909 /// # Example
910 ///
911 /// ```no_run
912 /// # use genai_rs::Content;
913 /// # let content: Content = todo!();
914 /// if let Content::ComputerUseResult { success, output, error, .. } = content {
915 /// if success {
916 /// println!("Action succeeded: {:?}", output);
917 /// } else {
918 /// eprintln!("Action failed: {:?}", error);
919 /// }
920 /// }
921 /// ```
922 ComputerUseResult {
923 /// References the `id` field from the corresponding `ComputerUseCall` variant.
924 call_id: String,
925 /// Whether the action succeeded
926 success: bool,
927 /// Action output data (may include page content, extracted data, etc.)
928 output: Option<serde_json::Value>,
929 /// Error message if action failed
930 error: Option<String>,
931 /// Optional screenshot data (base64-encoded image)
932 screenshot: Option<String>,
933 },
934 /// Unknown content type for forward compatibility.
935 ///
936 /// This variant captures content types that the library doesn't recognize yet.
937 /// This can happen when Google adds new features to the API before this library
938 /// is updated to support them.
939 ///
940 /// The `content_type` field contains the unrecognized type string from the API,
941 /// and `data` contains the full JSON object for inspection or debugging.
942 ///
943 /// # When This Occurs
944 ///
945 /// - New API features not yet supported by this library
946 /// - Beta features in testing
947 /// - Region-specific content types
948 ///
949 /// # Example
950 ///
951 /// ```no_run
952 /// # use genai_rs::Content;
953 /// # let content: Content = todo!();
954 /// if let Content::Unknown { content_type, data } = content {
955 /// eprintln!("Encountered unknown type '{}': {:?}", content_type, data);
956 /// }
957 /// ```
958 ///
959 /// # Serialization Behavior
960 ///
961 /// Unknown variants can be serialized back to JSON, enabling round-trip in
962 /// multi-turn conversations. The serialization follows these rules:
963 ///
964 /// 1. **Type field**: The `content_type` becomes the `"type"` field in output
965 /// 2. **Object data**: If `data` is a JSON object, its fields are flattened
966 /// into the output (excluding any existing `"type"` field to avoid duplicates)
967 /// 3. **Non-object data**: If `data` is a non-object value (array, string, etc.),
968 /// it's placed under a `"data"` key
969 /// 4. **Null data**: Omitted entirely from the output
970 ///
971 /// ## Example: Object Data (Common Case)
972 ///
973 /// ```
974 /// # use genai_rs::Content;
975 /// # use serde_json::json;
976 /// let content = Content::Unknown {
977 /// content_type: "new_feature".to_string(),
978 /// data: json!({"field1": "value1", "field2": 42}),
979 /// };
980 /// // Serializes to: {"type": "new_feature", "field1": "value1", "field2": 42}
981 /// ```
982 ///
983 /// ## Example: Duplicate Type Field
984 ///
985 /// If `data` contains a `"type"` field, it's excluded during serialization
986 /// (the `content_type` takes precedence):
987 ///
988 /// ```
989 /// # use genai_rs::Content;
990 /// # use serde_json::json;
991 /// let content = Content::Unknown {
992 /// content_type: "my_type".to_string(),
993 /// data: json!({"type": "ignored", "value": 123}),
994 /// };
995 /// // Serializes to: {"type": "my_type", "value": 123}
996 /// // Note: "type": "ignored" is not included
997 /// ```
998 ///
999 /// ## Example: Non-Object Data
1000 ///
1001 /// ```
1002 /// # use genai_rs::Content;
1003 /// # use serde_json::json;
1004 /// let content = Content::Unknown {
1005 /// content_type: "array_type".to_string(),
1006 /// data: json!([1, 2, 3]),
1007 /// };
1008 /// // Serializes to: {"type": "array_type", "data": [1, 2, 3]}
1009 /// ```
1010 ///
1011 /// # Field Ordering
1012 ///
1013 /// **Note:** Field ordering is not preserved during round-trip serialization,
1014 /// but all field **values** are fully preserved. When serializing an `Unknown`
1015 /// variant, the `"type"` field is always written first, followed by the remaining
1016 /// fields from `data`. This means the output field order may differ from the
1017 /// original API response.
1018 ///
1019 /// This has **no practical impact** on API compatibility because JSON objects
1020 /// are inherently unordered per RFC 8259. The Gemini API does not depend on
1021 /// field ordering.
1022 ///
1023 /// If you need to preserve the exact original field ordering (e.g., for logging
1024 /// or debugging purposes), access the raw `data` field directly via
1025 /// [`unknown_data()`](Self::unknown_data) instead of re-serializing the variant.
1026 ///
1027 /// # Manual Construction
1028 ///
1029 /// While Unknown variants are typically created by deserialization, you can
1030 /// construct them manually for testing or edge cases. Note that:
1031 ///
1032 /// - The `content_type` can be any string (including empty, though not recommended)
1033 /// - The `data` can be any valid JSON value
1034 /// - For multi-turn conversations, the serialized form must match what the API expects
1035 Unknown {
1036 /// The unrecognized type name from the API
1037 content_type: String,
1038 /// The full JSON data for this content, preserved for debugging
1039 data: serde_json::Value,
1040 },
1041}
1042
1043// Custom Serialize implementation for Content.
1044// This handles the Unknown variant specially by merging content_type into the data.
1045impl Serialize for Content {
1046 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1047 where
1048 S: serde::Serializer,
1049 {
1050 use serde::ser::SerializeMap;
1051
1052 match self {
1053 Self::Text { text, annotations } => {
1054 let mut map = serializer.serialize_map(None)?;
1055 map.serialize_entry("type", "text")?;
1056 if let Some(t) = text {
1057 map.serialize_entry("text", t)?;
1058 }
1059 if let Some(annots) = annotations
1060 && !annots.is_empty()
1061 {
1062 map.serialize_entry("annotations", annots)?;
1063 }
1064 map.end()
1065 }
1066 Self::Thought { signature } => {
1067 let mut map = serializer.serialize_map(None)?;
1068 map.serialize_entry("type", "thought")?;
1069 if let Some(s) = signature {
1070 map.serialize_entry("signature", s)?;
1071 }
1072 map.end()
1073 }
1074 Self::ThoughtSignature { signature } => {
1075 let mut map = serializer.serialize_map(None)?;
1076 map.serialize_entry("type", "thought_signature")?;
1077 map.serialize_entry("signature", signature)?;
1078 map.end()
1079 }
1080 Self::Image {
1081 data,
1082 uri,
1083 mime_type,
1084 resolution,
1085 } => {
1086 let mut map = serializer.serialize_map(None)?;
1087 map.serialize_entry("type", "image")?;
1088 if let Some(d) = data {
1089 map.serialize_entry("data", d)?;
1090 }
1091 if let Some(u) = uri {
1092 map.serialize_entry("uri", u)?;
1093 }
1094 if let Some(m) = mime_type {
1095 map.serialize_entry("mime_type", m)?;
1096 }
1097 if let Some(r) = resolution {
1098 map.serialize_entry("resolution", r)?;
1099 }
1100 map.end()
1101 }
1102 Self::Audio {
1103 data,
1104 uri,
1105 mime_type,
1106 } => {
1107 let mut map = serializer.serialize_map(None)?;
1108 map.serialize_entry("type", "audio")?;
1109 if let Some(d) = data {
1110 map.serialize_entry("data", d)?;
1111 }
1112 if let Some(u) = uri {
1113 map.serialize_entry("uri", u)?;
1114 }
1115 if let Some(m) = mime_type {
1116 map.serialize_entry("mime_type", m)?;
1117 }
1118 map.end()
1119 }
1120 Self::Video {
1121 data,
1122 uri,
1123 mime_type,
1124 resolution,
1125 } => {
1126 let mut map = serializer.serialize_map(None)?;
1127 map.serialize_entry("type", "video")?;
1128 if let Some(d) = data {
1129 map.serialize_entry("data", d)?;
1130 }
1131 if let Some(u) = uri {
1132 map.serialize_entry("uri", u)?;
1133 }
1134 if let Some(m) = mime_type {
1135 map.serialize_entry("mime_type", m)?;
1136 }
1137 if let Some(r) = resolution {
1138 map.serialize_entry("resolution", r)?;
1139 }
1140 map.end()
1141 }
1142 Self::Document {
1143 data,
1144 uri,
1145 mime_type,
1146 } => {
1147 let mut map = serializer.serialize_map(None)?;
1148 map.serialize_entry("type", "document")?;
1149 if let Some(d) = data {
1150 map.serialize_entry("data", d)?;
1151 }
1152 if let Some(u) = uri {
1153 map.serialize_entry("uri", u)?;
1154 }
1155 if let Some(m) = mime_type {
1156 map.serialize_entry("mime_type", m)?;
1157 }
1158 map.end()
1159 }
1160 Self::FunctionCall { id, name, args } => {
1161 let mut map = serializer.serialize_map(None)?;
1162 map.serialize_entry("type", "function_call")?;
1163 if let Some(i) = id {
1164 map.serialize_entry("id", i)?;
1165 }
1166 map.serialize_entry("name", name)?;
1167 map.serialize_entry("arguments", args)?;
1168 map.end()
1169 }
1170 Self::FunctionResult {
1171 name,
1172 call_id,
1173 result,
1174 is_error,
1175 } => {
1176 let mut map = serializer.serialize_map(None)?;
1177 map.serialize_entry("type", "function_result")?;
1178 if let Some(n) = name {
1179 map.serialize_entry("name", n)?;
1180 }
1181 map.serialize_entry("call_id", call_id)?;
1182 map.serialize_entry("result", result)?;
1183 if let Some(err) = is_error {
1184 map.serialize_entry("is_error", err)?;
1185 }
1186 map.end()
1187 }
1188 Self::CodeExecutionCall { id, language, code } => {
1189 let mut map = serializer.serialize_map(None)?;
1190 map.serialize_entry("type", "code_execution_call")?;
1191 if let Some(i) = id {
1192 map.serialize_entry("id", i)?;
1193 }
1194 // Wire format nests language and code inside arguments object
1195 let arguments = serde_json::json!({
1196 "language": language,
1197 "code": code
1198 });
1199 map.serialize_entry("arguments", &arguments)?;
1200 map.end()
1201 }
1202 Self::CodeExecutionResult {
1203 call_id,
1204 is_error,
1205 result,
1206 } => {
1207 let mut map = serializer.serialize_map(None)?;
1208 map.serialize_entry("type", "code_execution_result")?;
1209 if let Some(cid) = call_id {
1210 map.serialize_entry("call_id", cid)?;
1211 }
1212 map.serialize_entry("is_error", is_error)?;
1213 map.serialize_entry("result", result)?;
1214 map.end()
1215 }
1216 Self::GoogleSearchCall { id, queries } => {
1217 let mut map = serializer.serialize_map(None)?;
1218 map.serialize_entry("type", "google_search_call")?;
1219 map.serialize_entry("id", id)?;
1220 // Serialize as nested arguments.queries to match API format
1221 let arguments = serde_json::json!({ "queries": queries });
1222 map.serialize_entry("arguments", &arguments)?;
1223 map.end()
1224 }
1225 Self::GoogleSearchResult { call_id, result } => {
1226 let mut map = serializer.serialize_map(None)?;
1227 map.serialize_entry("type", "google_search_result")?;
1228 map.serialize_entry("call_id", call_id)?;
1229 map.serialize_entry("result", result)?;
1230 map.end()
1231 }
1232 Self::UrlContextCall { id, urls } => {
1233 let mut map = serializer.serialize_map(None)?;
1234 map.serialize_entry("type", "url_context_call")?;
1235 map.serialize_entry("id", id)?;
1236 // Wire format nests urls inside arguments object
1237 let arguments = serde_json::json!({ "urls": urls });
1238 map.serialize_entry("arguments", &arguments)?;
1239 map.end()
1240 }
1241 Self::UrlContextResult { call_id, result } => {
1242 let mut map = serializer.serialize_map(None)?;
1243 map.serialize_entry("type", "url_context_result")?;
1244 map.serialize_entry("call_id", call_id)?;
1245 map.serialize_entry("result", result)?;
1246 map.end()
1247 }
1248 Self::FileSearchResult { call_id, result } => {
1249 let mut map = serializer.serialize_map(None)?;
1250 map.serialize_entry("type", "file_search_result")?;
1251 map.serialize_entry("call_id", call_id)?;
1252 map.serialize_entry("result", result)?;
1253 map.end()
1254 }
1255 Self::ComputerUseCall {
1256 id,
1257 action,
1258 parameters,
1259 } => {
1260 let mut map = serializer.serialize_map(None)?;
1261 map.serialize_entry("type", "computer_use_call")?;
1262 map.serialize_entry("id", id)?;
1263 map.serialize_entry("action", action)?;
1264 map.serialize_entry("parameters", parameters)?;
1265 map.end()
1266 }
1267 Self::ComputerUseResult {
1268 call_id,
1269 success,
1270 output,
1271 error,
1272 screenshot,
1273 } => {
1274 let mut map = serializer.serialize_map(None)?;
1275 map.serialize_entry("type", "computer_use_result")?;
1276 map.serialize_entry("call_id", call_id)?;
1277 map.serialize_entry("success", success)?;
1278 if let Some(out) = output {
1279 map.serialize_entry("output", out)?;
1280 }
1281 if let Some(err) = error {
1282 map.serialize_entry("error", err)?;
1283 }
1284 if let Some(ss) = screenshot {
1285 map.serialize_entry("screenshot", ss)?;
1286 }
1287 map.end()
1288 }
1289 Self::Unknown { content_type, data } => {
1290 // For Unknown, merge the content_type into the data object
1291 let mut map = serializer.serialize_map(None)?;
1292 map.serialize_entry("type", content_type)?;
1293 // Flatten the data fields into the map if it's an object
1294 match data {
1295 serde_json::Value::Object(obj) => {
1296 for (key, value) in obj {
1297 if key != "type" {
1298 // Don't duplicate the type field
1299 map.serialize_entry(key, value)?;
1300 }
1301 }
1302 }
1303 // For non-object data (unlikely but possible), preserve under "data" key
1304 other if !other.is_null() => {
1305 map.serialize_entry("data", other)?;
1306 }
1307 _ => {} // Null data is omitted
1308 }
1309 map.end()
1310 }
1311 }
1312 }
1313}
1314
1315impl Content {
1316 /// Extract the text content, if this is a Text variant with non-empty text.
1317 ///
1318 /// Returns `Some` only for `Text` variants with non-empty text.
1319 /// Returns `None` for all other variants including `Thought`.
1320 #[must_use]
1321 pub fn as_text(&self) -> Option<&str> {
1322 match self {
1323 Self::Text { text: Some(t), .. } if !t.is_empty() => Some(t),
1324 _ => None,
1325 }
1326 }
1327
1328 /// Returns annotations if this is Text content with annotations.
1329 ///
1330 /// Returns `Some` with a slice of annotations only for `Text` variants that
1331 /// have non-empty annotations. Returns `None` for all other variants.
1332 ///
1333 /// Annotations are typically present when using grounding tools like
1334 /// `GoogleSearch` or `UrlContext`.
1335 ///
1336 /// # Example
1337 ///
1338 /// ```no_run
1339 /// # use genai_rs::{Content, Annotation};
1340 /// # let content: Content = todo!();
1341 /// if let Some(annotations) = content.annotations() {
1342 /// for annotation in annotations {
1343 /// println!("Source: {:?} for bytes {}..{}",
1344 /// annotation.source,
1345 /// annotation.start_index,
1346 /// annotation.end_index);
1347 /// }
1348 /// }
1349 /// ```
1350 #[must_use]
1351 pub fn annotations(&self) -> Option<&[Annotation]> {
1352 match self {
1353 Self::Text {
1354 annotations: Some(annots),
1355 ..
1356 } if !annots.is_empty() => Some(annots),
1357 _ => None,
1358 }
1359 }
1360
1361 /// Extract the thought signature, if this is a Thought variant with a signature.
1362 ///
1363 /// The signature is a cryptographic value used for verification of the thinking
1364 /// process. The actual thought text is not exposed in API responses.
1365 ///
1366 /// Returns `Some` only for `Thought` variants with a non-empty signature.
1367 /// Returns `None` for all other variants including `ThoughtSignature`.
1368 #[must_use]
1369 pub fn thought_signature(&self) -> Option<&str> {
1370 match self {
1371 Self::Thought { signature: Some(s) } if !s.is_empty() => Some(s),
1372 _ => None,
1373 }
1374 }
1375
1376 /// Check if this is a Text content type.
1377 #[must_use]
1378 pub const fn is_text(&self) -> bool {
1379 matches!(self, Self::Text { .. })
1380 }
1381
1382 /// Check if this is a Thought content type.
1383 #[must_use]
1384 pub const fn is_thought(&self) -> bool {
1385 matches!(self, Self::Thought { .. })
1386 }
1387
1388 /// Check if this is a ThoughtSignature content type.
1389 #[must_use]
1390 pub const fn is_thought_signature(&self) -> bool {
1391 matches!(self, Self::ThoughtSignature { .. })
1392 }
1393
1394 /// Check if this is a FunctionCall content type.
1395 #[must_use]
1396 pub const fn is_function_call(&self) -> bool {
1397 matches!(self, Self::FunctionCall { .. })
1398 }
1399
1400 /// Returns `true` if this is an unknown content type.
1401 ///
1402 /// Use this to check for content types that the library doesn't recognize.
1403 /// See [`Content::Unknown`] for more details.
1404 #[must_use]
1405 pub const fn is_unknown(&self) -> bool {
1406 matches!(self, Self::Unknown { .. })
1407 }
1408
1409 /// Check if this is a CodeExecutionCall content type.
1410 #[must_use]
1411 pub const fn is_code_execution_call(&self) -> bool {
1412 matches!(self, Self::CodeExecutionCall { .. })
1413 }
1414
1415 /// Check if this is a CodeExecutionResult content type.
1416 #[must_use]
1417 pub const fn is_code_execution_result(&self) -> bool {
1418 matches!(self, Self::CodeExecutionResult { .. })
1419 }
1420
1421 /// Check if this is a GoogleSearchCall content type.
1422 #[must_use]
1423 pub const fn is_google_search_call(&self) -> bool {
1424 matches!(self, Self::GoogleSearchCall { .. })
1425 }
1426
1427 /// Check if this is a GoogleSearchResult content type.
1428 #[must_use]
1429 pub const fn is_google_search_result(&self) -> bool {
1430 matches!(self, Self::GoogleSearchResult { .. })
1431 }
1432
1433 /// Check if this is a FileSearchResult content type.
1434 #[must_use]
1435 pub const fn is_file_search_result(&self) -> bool {
1436 matches!(self, Self::FileSearchResult { .. })
1437 }
1438
1439 /// Check if this is a UrlContextCall content type.
1440 #[must_use]
1441 pub const fn is_url_context_call(&self) -> bool {
1442 matches!(self, Self::UrlContextCall { .. })
1443 }
1444
1445 /// Check if this is a UrlContextResult content type.
1446 #[must_use]
1447 pub const fn is_url_context_result(&self) -> bool {
1448 matches!(self, Self::UrlContextResult { .. })
1449 }
1450
1451 /// Check if this is a ComputerUseCall content type.
1452 #[must_use]
1453 pub const fn is_computer_use_call(&self) -> bool {
1454 matches!(self, Self::ComputerUseCall { .. })
1455 }
1456
1457 /// Check if this is a ComputerUseResult content type.
1458 #[must_use]
1459 pub const fn is_computer_use_result(&self) -> bool {
1460 matches!(self, Self::ComputerUseResult { .. })
1461 }
1462
1463 /// Returns the content type name if this is an unknown content type.
1464 ///
1465 /// Returns `None` for known content types.
1466 #[must_use]
1467 pub fn unknown_content_type(&self) -> Option<&str> {
1468 match self {
1469 Self::Unknown { content_type, .. } => Some(content_type),
1470 _ => None,
1471 }
1472 }
1473
1474 /// Returns the raw JSON data if this is an unknown content type.
1475 ///
1476 /// Returns `None` for known content types.
1477 #[must_use]
1478 pub fn unknown_data(&self) -> Option<&serde_json::Value> {
1479 match self {
1480 Self::Unknown { data, .. } => Some(data),
1481 _ => None,
1482 }
1483 }
1484
1485 // =========================================================================
1486 // Content Constructors
1487 // =========================================================================
1488 //
1489 // These associated functions create Content variants for sending
1490 // to the API. They replace the standalone functions in interactions_api.rs.
1491
1492 /// Creates text content.
1493 ///
1494 /// # Example
1495 ///
1496 /// ```
1497 /// use genai_rs::Content;
1498 ///
1499 /// let content = Content::text("Hello, world!");
1500 /// assert!(content.is_text());
1501 /// ```
1502 #[must_use]
1503 pub fn text(text: impl Into<String>) -> Self {
1504 Self::Text {
1505 text: Some(text.into()),
1506 annotations: None,
1507 }
1508 }
1509
1510 /// Creates thought content with a signature.
1511 ///
1512 /// **Note:** Thought content is typically OUTPUT from the model, not user input.
1513 /// The signature is a cryptographic value for verification. This constructor
1514 /// is provided for completeness but rarely needed in typical usage.
1515 ///
1516 /// # Example
1517 ///
1518 /// ```
1519 /// use genai_rs::Content;
1520 ///
1521 /// let thought = Content::thought("signature_value_here");
1522 /// assert!(thought.is_thought());
1523 /// ```
1524 #[must_use]
1525 pub fn thought(signature: impl Into<String>) -> Self {
1526 Self::Thought {
1527 signature: Some(signature.into()),
1528 }
1529 }
1530
1531 /// Creates a function call content with an optional ID.
1532 ///
1533 /// Use this when you need to specify a call ID, typically when echoing function calls back
1534 /// in manual conversation construction.
1535 ///
1536 /// # Example
1537 ///
1538 /// ```
1539 /// use genai_rs::Content;
1540 /// use serde_json::json;
1541 ///
1542 /// let call = Content::function_call_with_id(
1543 /// Some("call_123"),
1544 /// "get_weather",
1545 /// json!({"location": "San Francisco"})
1546 /// );
1547 /// assert!(call.is_function_call());
1548 /// ```
1549 #[must_use]
1550 pub fn function_call_with_id(
1551 id: Option<impl Into<String>>,
1552 name: impl Into<String>,
1553 args: serde_json::Value,
1554 ) -> Self {
1555 Self::FunctionCall {
1556 id: id.map(|s| s.into()),
1557 name: name.into(),
1558 args,
1559 }
1560 }
1561
1562 /// Creates a function call content (without call ID).
1563 ///
1564 /// # Example
1565 ///
1566 /// ```
1567 /// use genai_rs::Content;
1568 /// use serde_json::json;
1569 ///
1570 /// let call = Content::function_call(
1571 /// "get_weather",
1572 /// json!({"location": "San Francisco"})
1573 /// );
1574 /// assert!(call.is_function_call());
1575 /// ```
1576 #[must_use]
1577 pub fn function_call(name: impl Into<String>, args: serde_json::Value) -> Self {
1578 Self::function_call_with_id(None::<String>, name, args)
1579 }
1580
1581 /// Creates a function result content.
1582 ///
1583 /// This is the correct way to send function execution results back to the Interactions API.
1584 /// The call_id must match the id from the FunctionCall you're responding to.
1585 ///
1586 /// # Panics
1587 ///
1588 /// Will log a warning if call_id is empty or whitespace-only, as this may cause
1589 /// API errors when the server tries to match the result to a function call.
1590 ///
1591 /// # Example
1592 ///
1593 /// ```
1594 /// use genai_rs::Content;
1595 /// use serde_json::json;
1596 ///
1597 /// let result = Content::function_result(
1598 /// "get_weather",
1599 /// "call_abc123",
1600 /// json!({"temperature": "72F", "conditions": "sunny"})
1601 /// );
1602 /// ```
1603 #[must_use]
1604 pub fn function_result(
1605 name: impl Into<String>,
1606 call_id: impl Into<String>,
1607 result: serde_json::Value,
1608 ) -> Self {
1609 let function_name = name.into();
1610 let call_id_str = call_id.into();
1611
1612 // Validate call_id is not empty
1613 if call_id_str.trim().is_empty() {
1614 tracing::warn!(
1615 "Empty call_id provided for function result '{}'. \
1616 This may cause the API to fail to match the result to its function call.",
1617 function_name
1618 );
1619 }
1620
1621 Self::FunctionResult {
1622 name: Some(function_name),
1623 call_id: call_id_str,
1624 result,
1625 is_error: None,
1626 }
1627 }
1628
1629 /// Creates function result content indicating an error.
1630 ///
1631 /// Use this when function execution fails and you need to report the error
1632 /// back to the model.
1633 ///
1634 /// # Example
1635 ///
1636 /// ```
1637 /// use genai_rs::Content;
1638 /// use serde_json::json;
1639 ///
1640 /// let error_result = Content::function_result_error(
1641 /// "get_weather",
1642 /// "call_abc123",
1643 /// json!({"error": "API rate limit exceeded"})
1644 /// );
1645 /// ```
1646 #[must_use]
1647 pub fn function_result_error(
1648 name: impl Into<String>,
1649 call_id: impl Into<String>,
1650 result: serde_json::Value,
1651 ) -> Self {
1652 let function_name = name.into();
1653 let call_id_str = call_id.into();
1654
1655 Self::FunctionResult {
1656 name: Some(function_name),
1657 call_id: call_id_str,
1658 result,
1659 is_error: Some(true),
1660 }
1661 }
1662
1663 /// Creates image content from base64-encoded data.
1664 ///
1665 /// # Example
1666 ///
1667 /// ```
1668 /// use genai_rs::Content;
1669 ///
1670 /// let image = Content::image_data(
1671 /// "base64encodeddata...",
1672 /// "image/png"
1673 /// );
1674 /// ```
1675 #[must_use]
1676 pub fn image_data(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
1677 Self::Image {
1678 data: Some(data.into()),
1679 uri: None,
1680 mime_type: Some(mime_type.into()),
1681 resolution: None,
1682 }
1683 }
1684
1685 /// Creates image content from base64-encoded data with specified resolution.
1686 ///
1687 /// # Resolution Trade-offs
1688 ///
1689 /// | Level | Token Cost | Detail |
1690 /// |-------|-----------|--------|
1691 /// | Low | Lowest | Basic shapes and colors |
1692 /// | Medium | Moderate | Standard detail |
1693 /// | High | Higher | Fine details visible |
1694 /// | UltraHigh | Highest | Maximum fidelity |
1695 ///
1696 /// # Example
1697 ///
1698 /// ```
1699 /// use genai_rs::{Content, Resolution};
1700 ///
1701 /// let image = Content::image_data_with_resolution(
1702 /// "base64encodeddata...",
1703 /// "image/png",
1704 /// Resolution::High
1705 /// );
1706 /// ```
1707 #[must_use]
1708 pub fn image_data_with_resolution(
1709 data: impl Into<String>,
1710 mime_type: impl Into<String>,
1711 resolution: Resolution,
1712 ) -> Self {
1713 Self::Image {
1714 data: Some(data.into()),
1715 uri: None,
1716 mime_type: Some(mime_type.into()),
1717 resolution: Some(resolution),
1718 }
1719 }
1720
1721 /// Creates image content from a URI.
1722 ///
1723 /// # Arguments
1724 ///
1725 /// * `uri` - The URI of the image
1726 /// * `mime_type` - The MIME type (required by the API for URI-based content)
1727 ///
1728 /// # Example
1729 ///
1730 /// ```
1731 /// use genai_rs::Content;
1732 ///
1733 /// let image = Content::image_uri(
1734 /// "https://example.com/image.png",
1735 /// "image/png"
1736 /// );
1737 /// ```
1738 #[must_use]
1739 pub fn image_uri(uri: impl Into<String>, mime_type: impl Into<String>) -> Self {
1740 Self::Image {
1741 data: None,
1742 uri: Some(uri.into()),
1743 mime_type: Some(mime_type.into()),
1744 resolution: None,
1745 }
1746 }
1747
1748 /// Creates image content from a URI with specified resolution.
1749 ///
1750 /// # Example
1751 ///
1752 /// ```
1753 /// use genai_rs::{Content, Resolution};
1754 ///
1755 /// let image = Content::image_uri_with_resolution(
1756 /// "https://example.com/image.png",
1757 /// "image/png",
1758 /// Resolution::Low // Use low resolution to reduce token cost
1759 /// );
1760 /// ```
1761 #[must_use]
1762 pub fn image_uri_with_resolution(
1763 uri: impl Into<String>,
1764 mime_type: impl Into<String>,
1765 resolution: Resolution,
1766 ) -> Self {
1767 Self::Image {
1768 data: None,
1769 uri: Some(uri.into()),
1770 mime_type: Some(mime_type.into()),
1771 resolution: Some(resolution),
1772 }
1773 }
1774
1775 /// Creates audio content from base64-encoded data.
1776 ///
1777 /// # Example
1778 ///
1779 /// ```
1780 /// use genai_rs::Content;
1781 ///
1782 /// let audio = Content::audio_data(
1783 /// "base64encodeddata...",
1784 /// "audio/mp3"
1785 /// );
1786 /// ```
1787 #[must_use]
1788 pub fn audio_data(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
1789 Self::Audio {
1790 data: Some(data.into()),
1791 uri: None,
1792 mime_type: Some(mime_type.into()),
1793 }
1794 }
1795
1796 /// Creates audio content from a URI.
1797 ///
1798 /// # Arguments
1799 ///
1800 /// * `uri` - The URI of the audio file
1801 /// * `mime_type` - The MIME type (required by the API for URI-based content)
1802 ///
1803 /// # Example
1804 ///
1805 /// ```
1806 /// use genai_rs::Content;
1807 ///
1808 /// let audio = Content::audio_uri(
1809 /// "https://example.com/audio.mp3",
1810 /// "audio/mp3"
1811 /// );
1812 /// ```
1813 #[must_use]
1814 pub fn audio_uri(uri: impl Into<String>, mime_type: impl Into<String>) -> Self {
1815 Self::Audio {
1816 data: None,
1817 uri: Some(uri.into()),
1818 mime_type: Some(mime_type.into()),
1819 }
1820 }
1821
1822 /// Creates video content from base64-encoded data.
1823 ///
1824 /// # Example
1825 ///
1826 /// ```
1827 /// use genai_rs::Content;
1828 ///
1829 /// let video = Content::video_data(
1830 /// "base64encodeddata...",
1831 /// "video/mp4"
1832 /// );
1833 /// ```
1834 #[must_use]
1835 pub fn video_data(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
1836 Self::Video {
1837 data: Some(data.into()),
1838 uri: None,
1839 mime_type: Some(mime_type.into()),
1840 resolution: None,
1841 }
1842 }
1843
1844 /// Creates video content from base64-encoded data with specified resolution.
1845 ///
1846 /// # Example
1847 ///
1848 /// ```
1849 /// use genai_rs::{Content, Resolution};
1850 ///
1851 /// let video = Content::video_data_with_resolution(
1852 /// "base64encodeddata...",
1853 /// "video/mp4",
1854 /// Resolution::Low // Use low resolution to reduce token cost for long videos
1855 /// );
1856 /// ```
1857 #[must_use]
1858 pub fn video_data_with_resolution(
1859 data: impl Into<String>,
1860 mime_type: impl Into<String>,
1861 resolution: Resolution,
1862 ) -> Self {
1863 Self::Video {
1864 data: Some(data.into()),
1865 uri: None,
1866 mime_type: Some(mime_type.into()),
1867 resolution: Some(resolution),
1868 }
1869 }
1870
1871 /// Creates video content from a URI.
1872 ///
1873 /// # Arguments
1874 ///
1875 /// * `uri` - The URI of the video file
1876 /// * `mime_type` - The MIME type (required by the API for URI-based content)
1877 ///
1878 /// # Example
1879 ///
1880 /// ```
1881 /// use genai_rs::Content;
1882 ///
1883 /// let video = Content::video_uri(
1884 /// "https://example.com/video.mp4",
1885 /// "video/mp4"
1886 /// );
1887 /// ```
1888 #[must_use]
1889 pub fn video_uri(uri: impl Into<String>, mime_type: impl Into<String>) -> Self {
1890 Self::Video {
1891 data: None,
1892 uri: Some(uri.into()),
1893 mime_type: Some(mime_type.into()),
1894 resolution: None,
1895 }
1896 }
1897
1898 /// Creates video content from a URI with specified resolution.
1899 ///
1900 /// # Example
1901 ///
1902 /// ```
1903 /// use genai_rs::{Content, Resolution};
1904 ///
1905 /// let video = Content::video_uri_with_resolution(
1906 /// "https://example.com/video.mp4",
1907 /// "video/mp4",
1908 /// Resolution::Medium
1909 /// );
1910 /// ```
1911 #[must_use]
1912 pub fn video_uri_with_resolution(
1913 uri: impl Into<String>,
1914 mime_type: impl Into<String>,
1915 resolution: Resolution,
1916 ) -> Self {
1917 Self::Video {
1918 data: None,
1919 uri: Some(uri.into()),
1920 mime_type: Some(mime_type.into()),
1921 resolution: Some(resolution),
1922 }
1923 }
1924
1925 /// Creates document content from base64-encoded data.
1926 ///
1927 /// Use this for PDF files and other document formats.
1928 ///
1929 /// # Example
1930 ///
1931 /// ```
1932 /// use genai_rs::Content;
1933 ///
1934 /// let document = Content::document_data(
1935 /// "base64encodeddata...",
1936 /// "application/pdf"
1937 /// );
1938 /// ```
1939 #[must_use]
1940 pub fn document_data(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
1941 Self::Document {
1942 data: Some(data.into()),
1943 uri: None,
1944 mime_type: Some(mime_type.into()),
1945 }
1946 }
1947
1948 /// Creates document content from a URI.
1949 ///
1950 /// Use this for PDF files and other document formats accessible via URI.
1951 ///
1952 /// # Arguments
1953 ///
1954 /// * `uri` - The URI of the document
1955 /// * `mime_type` - The MIME type (required by the API for URI-based content)
1956 ///
1957 /// # Example
1958 ///
1959 /// ```
1960 /// use genai_rs::Content;
1961 ///
1962 /// let document = Content::document_uri(
1963 /// "https://example.com/document.pdf",
1964 /// "application/pdf"
1965 /// );
1966 /// ```
1967 #[must_use]
1968 pub fn document_uri(uri: impl Into<String>, mime_type: impl Into<String>) -> Self {
1969 Self::Document {
1970 data: None,
1971 uri: Some(uri.into()),
1972 mime_type: Some(mime_type.into()),
1973 }
1974 }
1975
1976 /// Creates content from a URI and MIME type.
1977 ///
1978 /// The content type is inferred from the MIME type:
1979 ///
1980 /// - `image/*` → [`Content::Image`]
1981 /// - `audio/*` → [`Content::Audio`]
1982 /// - `video/*` → [`Content::Video`]
1983 /// - Other MIME types (including `application/*`, `text/*`) → [`Content::Document`]
1984 ///
1985 /// # Arguments
1986 ///
1987 /// * `uri` - The file URI (typically from the Files API)
1988 /// * `mime_type` - The MIME type of the file
1989 ///
1990 /// # Example
1991 ///
1992 /// ```
1993 /// use genai_rs::Content;
1994 ///
1995 /// // Creates Image variant for image MIME types
1996 /// let image = Content::from_uri_and_mime(
1997 /// "files/abc123",
1998 /// "image/png"
1999 /// );
2000 ///
2001 /// // Creates Document variant for PDF
2002 /// let doc = Content::from_uri_and_mime(
2003 /// "files/def456",
2004 /// "application/pdf"
2005 /// );
2006 /// ```
2007 #[must_use]
2008 pub fn from_uri_and_mime(uri: impl Into<String>, mime_type: impl Into<String>) -> Self {
2009 let uri_str = uri.into();
2010 let mime_str = mime_type.into();
2011
2012 // Choose the appropriate content type based on MIME type prefix
2013 if mime_str.starts_with("image/") {
2014 Self::Image {
2015 data: None,
2016 uri: Some(uri_str),
2017 mime_type: Some(mime_str),
2018 resolution: None,
2019 }
2020 } else if mime_str.starts_with("audio/") {
2021 Self::Audio {
2022 data: None,
2023 uri: Some(uri_str),
2024 mime_type: Some(mime_str),
2025 }
2026 } else if mime_str.starts_with("video/") {
2027 Self::Video {
2028 data: None,
2029 uri: Some(uri_str),
2030 mime_type: Some(mime_str),
2031 resolution: None,
2032 }
2033 } else {
2034 // Default to document for PDFs, text files, and other types
2035 Self::Document {
2036 data: None,
2037 uri: Some(uri_str),
2038 mime_type: Some(mime_str),
2039 }
2040 }
2041 }
2042
2043 /// Creates file content from a Files API metadata object.
2044 ///
2045 /// Use this to reference files uploaded via the Files API. The content type
2046 /// is inferred from the file's MIME type (image, audio, video, or document).
2047 ///
2048 /// # Arguments
2049 ///
2050 /// * `file` - The uploaded file metadata from the Files API
2051 ///
2052 /// # Example
2053 ///
2054 /// ```no_run
2055 /// use genai_rs::{Client, Content};
2056 ///
2057 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2058 /// let client = Client::new("api-key".to_string());
2059 ///
2060 /// let file = client.upload_file("video.mp4").await?;
2061 /// let content = Content::from_file(&file);
2062 ///
2063 /// let response = client.interaction()
2064 /// .with_model("gemini-3-flash-preview")
2065 /// .with_content(vec![
2066 /// Content::text("Describe this video"),
2067 /// content,
2068 /// ])
2069 /// .create()
2070 /// .await?;
2071 /// # Ok(())
2072 /// # }
2073 /// ```
2074 #[must_use]
2075 pub fn from_file(file: &crate::http::files::FileMetadata) -> Self {
2076 Self::from_uri_and_mime(file.uri.clone(), file.mime_type.clone())
2077 }
2078
2079 // =========================================================================
2080 // Builder Methods
2081 // =========================================================================
2082
2083 /// Sets the resolution on image or video content.
2084 ///
2085 /// This builder method enables fluent chaining for setting resolution:
2086 ///
2087 /// ```
2088 /// use genai_rs::{Content, Resolution};
2089 ///
2090 /// let image = Content::image_uri("files/abc123", "image/png")
2091 /// .with_resolution(Resolution::High);
2092 ///
2093 /// let video = Content::video_uri("files/def456", "video/mp4")
2094 /// .with_resolution(Resolution::Low);
2095 /// ```
2096 ///
2097 /// # Resolution Trade-offs
2098 ///
2099 /// | Level | Token Cost | Detail Level |
2100 /// |-------|------------|--------------|
2101 /// | Low | Lowest | Basic shapes and colors |
2102 /// | Medium | Moderate | Standard detail (default) |
2103 /// | High | Higher | Fine details visible |
2104 /// | UltraHigh | Highest | Maximum fidelity |
2105 ///
2106 /// # Behavior on Non-Media Content
2107 ///
2108 /// For content types that don't support resolution (Text, Audio, Document,
2109 /// FunctionCall, etc.), this method logs a warning and returns the content
2110 /// unchanged.
2111 #[must_use]
2112 pub fn with_resolution(self, resolution: Resolution) -> Self {
2113 match self {
2114 Self::Image {
2115 data,
2116 uri,
2117 mime_type,
2118 ..
2119 } => Self::Image {
2120 data,
2121 uri,
2122 mime_type,
2123 resolution: Some(resolution),
2124 },
2125 Self::Video {
2126 data,
2127 uri,
2128 mime_type,
2129 ..
2130 } => Self::Video {
2131 data,
2132 uri,
2133 mime_type,
2134 resolution: Some(resolution),
2135 },
2136 other => {
2137 tracing::warn!(
2138 "with_resolution() called on content type that doesn't support resolution. \
2139 Resolution is only applicable to Image and Video content."
2140 );
2141 other
2142 }
2143 }
2144 }
2145
2146 /// Creates a function result from this function call.
2147 ///
2148 /// This builder method enables fluent patterns for handling function calls:
2149 ///
2150 /// ```
2151 /// use genai_rs::Content;
2152 /// use serde_json::json;
2153 ///
2154 /// // Given a function call content
2155 /// let call = Content::function_call_with_id(
2156 /// Some("call_123"),
2157 /// "get_weather",
2158 /// json!({"location": "San Francisco"})
2159 /// );
2160 ///
2161 /// // Execute the function and create the result
2162 /// let result = call.with_result(json!({"temperature": "72F", "conditions": "sunny"}));
2163 /// assert!(matches!(result, Content::FunctionResult { .. }));
2164 /// ```
2165 ///
2166 /// # Behavior on Non-FunctionCall Content
2167 ///
2168 /// For content types other than `FunctionCall`, this method logs a warning
2169 /// and returns the original content unchanged.
2170 ///
2171 /// # Error Results
2172 ///
2173 /// To indicate a function execution error, use [`with_result_error`](Self::with_result_error)
2174 /// instead.
2175 #[must_use]
2176 pub fn with_result(self, result: serde_json::Value) -> Self {
2177 match &self {
2178 Self::FunctionCall { id, name, .. } => Self::FunctionResult {
2179 name: Some(name.clone()),
2180 call_id: id.clone().unwrap_or_default(),
2181 result,
2182 is_error: None,
2183 },
2184 _ => {
2185 tracing::warn!(
2186 "with_result() called on non-FunctionCall content type. \
2187 This method is only valid for FunctionCall content."
2188 );
2189 self
2190 }
2191 }
2192 }
2193
2194 /// Creates a function error result from this function call.
2195 ///
2196 /// Use this when function execution fails and you need to report the error
2197 /// back to the model.
2198 ///
2199 /// ```
2200 /// use genai_rs::Content;
2201 /// use serde_json::json;
2202 ///
2203 /// let call = Content::function_call_with_id(
2204 /// Some("call_123"),
2205 /// "get_weather",
2206 /// json!({"location": "San Francisco"})
2207 /// );
2208 ///
2209 /// // Report execution error
2210 /// let error = call.with_result_error(json!({"error": "API rate limit exceeded"}));
2211 /// ```
2212 #[must_use]
2213 pub fn with_result_error(self, result: serde_json::Value) -> Self {
2214 match &self {
2215 Self::FunctionCall { id, name, .. } => Self::FunctionResult {
2216 name: Some(name.clone()),
2217 call_id: id.clone().unwrap_or_default(),
2218 result,
2219 is_error: Some(true),
2220 },
2221 _ => {
2222 tracing::warn!(
2223 "with_result_error() called on non-FunctionCall content type. \
2224 This method is only valid for FunctionCall content."
2225 );
2226 self
2227 }
2228 }
2229 }
2230}
2231
2232// Custom Deserialize implementation to handle unknown content types gracefully.
2233//
2234// This tries to deserialize known types first, and falls back to Unknown for
2235// unrecognized types. This provides forward compatibility when Google adds
2236// new content types to the API.
2237impl<'de> Deserialize<'de> for Content {
2238 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2239 where
2240 D: serde::Deserializer<'de>,
2241 {
2242 #[cfg(feature = "strict-unknown")]
2243 use serde::de::Error as _;
2244
2245 // First, deserialize into a raw JSON value
2246 let value = serde_json::Value::deserialize(deserializer)?;
2247
2248 // Helper enum for deserializing known types
2249 #[derive(Deserialize)]
2250 #[serde(tag = "type", rename_all = "snake_case")]
2251 enum KnownContent {
2252 Text {
2253 text: Option<String>,
2254 #[serde(default)]
2255 annotations: Option<Vec<Annotation>>,
2256 },
2257 Thought {
2258 signature: Option<String>,
2259 },
2260 ThoughtSignature {
2261 #[serde(default)]
2262 signature: String,
2263 },
2264 Image {
2265 data: Option<String>,
2266 uri: Option<String>,
2267 mime_type: Option<String>,
2268 resolution: Option<Resolution>,
2269 },
2270 Audio {
2271 data: Option<String>,
2272 uri: Option<String>,
2273 mime_type: Option<String>,
2274 },
2275 Video {
2276 data: Option<String>,
2277 uri: Option<String>,
2278 mime_type: Option<String>,
2279 resolution: Option<Resolution>,
2280 },
2281 Document {
2282 data: Option<String>,
2283 uri: Option<String>,
2284 mime_type: Option<String>,
2285 },
2286 FunctionCall {
2287 #[serde(default)]
2288 id: Option<String>,
2289 // In streaming, content.start events may not include name yet
2290 #[serde(default)]
2291 name: Option<String>,
2292 #[serde(rename = "arguments", default)]
2293 args: serde_json::Value,
2294 },
2295 FunctionResult {
2296 name: Option<String>,
2297 call_id: String,
2298 result: serde_json::Value,
2299 is_error: Option<bool>,
2300 },
2301 CodeExecutionCall {
2302 #[serde(default)]
2303 id: Option<String>,
2304 // API returns language/code in the arguments object
2305 #[serde(default)]
2306 language: Option<CodeExecutionLanguage>,
2307 #[serde(default)]
2308 code: Option<String>,
2309 // Fallback for old API format
2310 #[serde(default)]
2311 arguments: Option<serde_json::Value>,
2312 },
2313 CodeExecutionResult {
2314 #[serde(default)]
2315 call_id: Option<String>,
2316 #[serde(default)]
2317 is_error: Option<bool>,
2318 #[serde(default)]
2319 result: Option<String>,
2320 },
2321 GoogleSearchCall {
2322 id: String,
2323 #[serde(default)]
2324 arguments: Option<serde_json::Value>,
2325 },
2326 GoogleSearchResult {
2327 call_id: String,
2328 #[serde(default)]
2329 result: Vec<GoogleSearchResultItem>,
2330 },
2331 UrlContextCall {
2332 id: String,
2333 #[serde(default)]
2334 arguments: Option<serde_json::Value>,
2335 },
2336 UrlContextResult {
2337 call_id: String,
2338 #[serde(default)]
2339 result: Vec<UrlContextResultItem>,
2340 },
2341 FileSearchResult {
2342 call_id: String,
2343 #[serde(default)]
2344 result: Vec<FileSearchResultItem>,
2345 },
2346 ComputerUseCall {
2347 id: String,
2348 action: String,
2349 #[serde(default)]
2350 parameters: Option<serde_json::Value>,
2351 },
2352 ComputerUseResult {
2353 call_id: String,
2354 success: bool,
2355 #[serde(default)]
2356 output: Option<serde_json::Value>,
2357 #[serde(default)]
2358 error: Option<String>,
2359 #[serde(default)]
2360 screenshot: Option<String>,
2361 },
2362 }
2363
2364 // Try to deserialize as a known type
2365 match serde_json::from_value::<KnownContent>(value.clone()) {
2366 Ok(known) => Ok(match known {
2367 KnownContent::Text { text, annotations } => Content::Text { text, annotations },
2368 KnownContent::Thought { signature } => Content::Thought { signature },
2369 KnownContent::ThoughtSignature { signature } => {
2370 Content::ThoughtSignature { signature }
2371 }
2372 KnownContent::Image {
2373 data,
2374 uri,
2375 mime_type,
2376 resolution,
2377 } => Content::Image {
2378 data,
2379 uri,
2380 mime_type,
2381 resolution,
2382 },
2383 KnownContent::Audio {
2384 data,
2385 uri,
2386 mime_type,
2387 } => Content::Audio {
2388 data,
2389 uri,
2390 mime_type,
2391 },
2392 KnownContent::Video {
2393 data,
2394 uri,
2395 mime_type,
2396 resolution,
2397 } => Content::Video {
2398 data,
2399 uri,
2400 mime_type,
2401 resolution,
2402 },
2403 KnownContent::Document {
2404 data,
2405 uri,
2406 mime_type,
2407 } => Content::Document {
2408 data,
2409 uri,
2410 mime_type,
2411 },
2412 KnownContent::FunctionCall { id, name, args } => {
2413 // In streaming, content.start may not include name yet;
2414 // deltas will fill it in. Use empty string as placeholder.
2415 Content::FunctionCall {
2416 id,
2417 name: name.unwrap_or_default(),
2418 args,
2419 }
2420 }
2421 KnownContent::FunctionResult {
2422 name,
2423 call_id,
2424 result,
2425 is_error,
2426 } => Content::FunctionResult {
2427 name,
2428 call_id,
2429 result,
2430 is_error,
2431 },
2432 KnownContent::CodeExecutionCall {
2433 id,
2434 language,
2435 code,
2436 arguments,
2437 } => {
2438 // Prefer direct fields, fall back to parsing arguments
2439 if let (Some(lang), Some(source)) = (language, code) {
2440 Content::CodeExecutionCall {
2441 id,
2442 language: lang,
2443 code: source,
2444 }
2445 } else if let Some(args) = arguments {
2446 // Parse old format: {"language": "PYTHON", "code": "..."}
2447 // Code is required - if missing, treat as Unknown per Evergreen philosophy
2448 let source = match args.get("code").and_then(|v| v.as_str()) {
2449 Some(code) => code.to_string(),
2450 None => {
2451 tracing::warn!(
2452 "CodeExecutionCall arguments missing required 'code' field for id: {:?}. \
2453 Treating as Unknown variant to preserve data for debugging.",
2454 id
2455 );
2456 return Ok(Content::Unknown {
2457 content_type: "code_execution_call".to_string(),
2458 data: value.clone(),
2459 });
2460 }
2461 };
2462
2463 // Language defaults to Python if missing or malformed (most common case)
2464 let lang = match args.get("language") {
2465 Some(lang_value) => {
2466 match serde_json::from_value::<CodeExecutionLanguage>(
2467 lang_value.clone(),
2468 ) {
2469 Ok(lang) => lang,
2470 Err(e) => {
2471 tracing::warn!(
2472 "CodeExecutionCall has invalid language value for id: {:?}, \
2473 defaulting to Python. Parse error: {}",
2474 id,
2475 e
2476 );
2477 CodeExecutionLanguage::Python
2478 }
2479 }
2480 }
2481 None => CodeExecutionLanguage::Python,
2482 };
2483
2484 Content::CodeExecutionCall {
2485 id,
2486 language: lang,
2487 code: source,
2488 }
2489 } else {
2490 // Malformed CodeExecutionCall - treat as Unknown to preserve data
2491 // per Evergreen philosophy (see CLAUDE.md). This avoids silently
2492 // degrading to an empty code string which could cause subtle bugs.
2493 tracing::warn!(
2494 "CodeExecutionCall missing both direct fields and arguments for id: {:?}. \
2495 Treating as Unknown variant to preserve data for debugging.",
2496 id
2497 );
2498 Content::Unknown {
2499 content_type: "code_execution_call".to_string(),
2500 data: value.clone(),
2501 }
2502 }
2503 }
2504 KnownContent::CodeExecutionResult {
2505 call_id,
2506 is_error,
2507 result,
2508 } => Content::CodeExecutionResult {
2509 call_id,
2510 // Default to false (success) when is_error is not provided
2511 is_error: is_error.unwrap_or(false),
2512 result: result.unwrap_or_default(),
2513 },
2514 KnownContent::GoogleSearchCall { id, arguments } => {
2515 // Extract queries from arguments.queries
2516 let queries = arguments
2517 .as_ref()
2518 .and_then(|args| args.get("queries"))
2519 .and_then(|q| q.as_array())
2520 .map(|arr| {
2521 arr.iter()
2522 .filter_map(|v| v.as_str().map(String::from))
2523 .collect()
2524 })
2525 .unwrap_or_default();
2526
2527 Content::GoogleSearchCall { id, queries }
2528 }
2529 KnownContent::GoogleSearchResult { call_id, result } => {
2530 Content::GoogleSearchResult { call_id, result }
2531 }
2532 KnownContent::UrlContextCall { id, arguments } => {
2533 // Extract urls from arguments.urls
2534 let urls = arguments
2535 .as_ref()
2536 .and_then(|args| args.get("urls"))
2537 .and_then(|u| u.as_array())
2538 .map(|arr| {
2539 arr.iter()
2540 .filter_map(|v| v.as_str().map(String::from))
2541 .collect()
2542 })
2543 .unwrap_or_default();
2544
2545 Content::UrlContextCall { id, urls }
2546 }
2547 KnownContent::UrlContextResult { call_id, result } => {
2548 Content::UrlContextResult { call_id, result }
2549 }
2550 KnownContent::FileSearchResult { call_id, result } => {
2551 Content::FileSearchResult { call_id, result }
2552 }
2553 KnownContent::ComputerUseCall {
2554 id,
2555 action,
2556 parameters,
2557 } => Content::ComputerUseCall {
2558 id,
2559 action,
2560 parameters: parameters.unwrap_or(serde_json::Value::Null),
2561 },
2562 KnownContent::ComputerUseResult {
2563 call_id,
2564 success,
2565 output,
2566 error,
2567 screenshot,
2568 } => Content::ComputerUseResult {
2569 call_id,
2570 success,
2571 output,
2572 error,
2573 screenshot,
2574 },
2575 }),
2576 Err(parse_error) => {
2577 // Unknown type - extract type name and preserve data
2578 let content_type = value
2579 .get("type")
2580 .and_then(|v| v.as_str())
2581 .unwrap_or("<missing type>")
2582 .to_string();
2583
2584 // Log the actual parse error for debugging - this helps distinguish
2585 // between truly unknown types and malformed known types
2586 tracing::warn!(
2587 "Encountered unknown Content type '{}'. \
2588 Parse error: {}. \
2589 This may indicate a new API feature or a malformed response. \
2590 The content will be preserved in the Unknown variant.",
2591 content_type,
2592 parse_error
2593 );
2594
2595 #[cfg(feature = "strict-unknown")]
2596 {
2597 Err(D::Error::custom(format!(
2598 "Unknown Content type '{}'. \
2599 Strict mode is enabled via the 'strict-unknown' feature flag. \
2600 Either update the library or disable strict mode.",
2601 content_type
2602 )))
2603 }
2604
2605 #[cfg(not(feature = "strict-unknown"))]
2606 {
2607 Ok(Content::Unknown {
2608 content_type,
2609 data: value,
2610 })
2611 }
2612 }
2613 }
2614 }
2615}