Skip to main content

adk_gemini/
models.rs

1//! # Core Gemini API Primitives
2//!
3//! This module contains the fundamental building blocks used across the Gemini API.
4//! These core data structures are shared by multiple modules and form the foundation
5//! for constructing requests and parsing responses.
6//!
7//! ## Core Types
8//!
9//! - [`Role`] - Represents the speaker in a conversation (User or Model)
10//! - [`Part`] - Content fragments that make up messages (text, images, function calls)
11//! - [`Blob`] - Binary data with MIME type for inline content
12//! - [`Content`] - Container for parts with optional role assignment
13//! - [`Message`] - Complete message with content and explicit role
14//! - [`Modality`] - Output format types (text, image, audio)
15//!
16//! ## Usage
17//!
18//! These types are typically used in combination with the domain-specific modules:
19//! - `generation` - For content generation requests and responses
20//! - `embedding` - For text embedding operations
21//! - `safety` - For content moderation settings
22//! - `tools` - For function calling capabilities
23//! - `batch` - For batch processing operations
24//! - `cache` - For content caching
25//! - `files` - For file management
26
27#![allow(clippy::enum_variant_names)]
28
29use serde::{Deserialize, Serialize, de};
30
31/// Role of a message in a conversation
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33#[serde(rename_all = "lowercase")]
34pub enum Role {
35    /// Message from the user
36    User,
37    /// Message from the model
38    Model,
39}
40
41/// Content part that can be included in a message
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43#[serde(untagged)]
44pub enum Part {
45    /// Text content
46    Text {
47        /// The text content
48        text: String,
49        /// Whether this is a thought summary (Gemini 2.5 series only)
50        #[serde(skip_serializing_if = "Option::is_none")]
51        thought: Option<bool>,
52        /// The thought signature (Gemini 2.5+ thinking models only).
53        /// Preserved from responses and echoed back in conversation history for Gemini 3.x thought signature support.
54        #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
55        thought_signature: Option<String>,
56    },
57    InlineData {
58        /// The blob data
59        #[serde(rename = "inlineData")]
60        inline_data: Blob,
61    },
62    /// File data referenced by URI
63    FileData {
64        #[serde(rename = "fileData")]
65        file_data: FileDataRef,
66    },
67    /// Function call from the model
68    FunctionCall {
69        /// The function call details
70        #[serde(rename = "functionCall")]
71        function_call: super::tools::FunctionCall,
72        /// The thought signature (Gemini 2.5+ thinking models only).
73        /// Preserved from responses and echoed back in conversation history for Gemini 3.x thought signature support.
74        #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
75        thought_signature: Option<String>,
76    },
77    /// Function response (results from executing a function call)
78    FunctionResponse {
79        /// The function response details
80        #[serde(rename = "functionResponse")]
81        function_response: super::tools::FunctionResponse,
82        /// The thought signature (Gemini 3.x thinking models).
83        /// Must be echoed back on function response parts when thinking is active.
84        #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
85        thought_signature: Option<String>,
86    },
87    /// Server-side tool call from Gemini 3 (built-in tool invocation)
88    ToolCall {
89        #[serde(rename = "toolCall")]
90        tool_call: serde_json::Value,
91        /// The thought signature (Gemini 3.x thinking models).
92        /// Must be preserved and echoed back in conversation history.
93        #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
94        thought_signature: Option<String>,
95    },
96    /// Server-side tool response from Gemini 3 (built-in tool result)
97    ToolResponse {
98        #[serde(rename = "toolResponse")]
99        tool_response: serde_json::Value,
100        /// The thought signature (Gemini 3.x thinking models).
101        /// Must be preserved and echoed back in conversation history.
102        #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
103        thought_signature: Option<String>,
104    },
105    /// Generated code emitted by Gemini code execution.
106    ExecutableCode {
107        #[serde(rename = "executableCode")]
108        executable_code: serde_json::Value,
109        /// The thought signature (Gemini 3.x thinking models).
110        /// Must be preserved and echoed back in conversation history.
111        #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
112        thought_signature: Option<String>,
113    },
114    /// Result emitted by Gemini code execution.
115    CodeExecutionResult {
116        #[serde(rename = "codeExecutionResult")]
117        code_execution_result: serde_json::Value,
118        /// The thought signature (Gemini 3.x thinking models).
119        /// Must be preserved and echoed back in conversation history.
120        #[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
121        thought_signature: Option<String>,
122    },
123}
124
125/// Blob for a message part
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
127#[serde(rename_all = "camelCase")]
128pub struct Blob {
129    /// The MIME type of the data
130    pub mime_type: String,
131    /// Base64 encoded data
132    pub data: String,
133}
134
135impl Blob {
136    /// Create a new blob with mime type and data
137    pub fn new(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
138        Self { mime_type: mime_type.into(), data: data.into() }
139    }
140}
141
142/// Reference to an external file by URI, used in Gemini wire format.
143///
144/// # Example
145///
146/// ```rust
147/// use adk_gemini::FileDataRef;
148///
149/// let file_ref = FileDataRef {
150///     mime_type: "application/pdf".to_string(),
151///     file_uri: "gs://my-bucket/report.pdf".to_string(),
152/// };
153/// ```
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155#[serde(rename_all = "camelCase")]
156pub struct FileDataRef {
157    pub mime_type: String,
158    pub file_uri: String,
159}
160
161/// Content of a message
162#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
163#[serde(rename_all = "camelCase")]
164pub struct Content {
165    /// Parts of the content
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub parts: Option<Vec<Part>>,
168    /// Role of the content
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub role: Option<Role>,
171}
172
173impl Content {
174    /// Create a new text content
175    pub fn text(text: impl Into<String>) -> Self {
176        Self {
177            parts: Some(vec![Part::Text {
178                text: text.into(),
179                thought: None,
180                thought_signature: None,
181            }]),
182            role: None,
183        }
184    }
185
186    /// Create a new content with a function call
187    pub fn function_call(function_call: super::tools::FunctionCall) -> Self {
188        Self {
189            parts: Some(vec![Part::FunctionCall { function_call, thought_signature: None }]),
190            role: None,
191        }
192    }
193
194    /// Create a new content with a function call and thought signature
195    pub fn function_call_with_thought(
196        function_call: super::tools::FunctionCall,
197        thought_signature: impl Into<String>,
198    ) -> Self {
199        Self {
200            parts: Some(vec![Part::FunctionCall {
201                function_call,
202                thought_signature: Some(thought_signature.into()),
203            }]),
204            role: None,
205        }
206    }
207
208    /// Create a new text content with thought signature
209    pub fn text_with_thought_signature(
210        text: impl Into<String>,
211        thought_signature: impl Into<String>,
212    ) -> Self {
213        Self {
214            parts: Some(vec![Part::Text {
215                text: text.into(),
216                thought: None,
217                thought_signature: Some(thought_signature.into()),
218            }]),
219            role: None,
220        }
221    }
222
223    /// Create a new thought content with thought signature
224    pub fn thought_with_signature(
225        text: impl Into<String>,
226        thought_signature: impl Into<String>,
227    ) -> Self {
228        Self {
229            parts: Some(vec![Part::Text {
230                text: text.into(),
231                thought: Some(true),
232                thought_signature: Some(thought_signature.into()),
233            }]),
234            role: None,
235        }
236    }
237
238    /// Create a new content with a function response
239    pub fn function_response(function_response: super::tools::FunctionResponse) -> Self {
240        Self {
241            parts: Some(vec![Part::FunctionResponse {
242                function_response,
243                thought_signature: None,
244            }]),
245            role: None,
246        }
247    }
248
249    /// Create a new content with a function response from name and JSON value
250    pub fn function_response_json(name: impl Into<String>, response: serde_json::Value) -> Self {
251        Self {
252            parts: Some(vec![Part::FunctionResponse {
253                function_response: super::tools::FunctionResponse::new(name, response),
254                thought_signature: None,
255            }]),
256            role: None,
257        }
258    }
259
260    /// Create a new content with inline data (blob data)
261    pub fn inline_data(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
262        Self {
263            parts: Some(vec![Part::InlineData { inline_data: Blob::new(mime_type, data) }]),
264            role: None,
265        }
266    }
267
268    /// Create function response content with multimodal parts.
269    ///
270    /// The `FunctionResponse` carries its multimodal data (inline images, file references)
271    /// in its own `parts` field, matching the Gemini wire format where `inlineData`/`fileData`
272    /// entries are nested inside the `functionResponse` object.
273    pub fn function_response_multimodal(function_response: super::tools::FunctionResponse) -> Self {
274        Self {
275            parts: Some(vec![Part::FunctionResponse {
276                function_response,
277                thought_signature: None,
278            }]),
279            role: None,
280        }
281    }
282
283    /// Add a role to this content
284    pub fn with_role(mut self, role: Role) -> Self {
285        self.role = Some(role);
286        self
287    }
288}
289
290/// Message in a conversation
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct Message {
293    /// Content of the message
294    pub content: Content,
295    /// Role of the message
296    pub role: Role,
297}
298
299impl Message {
300    /// Create a new user message with text content
301    pub fn user(text: impl Into<String>) -> Self {
302        Self { content: Content::text(text).with_role(Role::User), role: Role::User }
303    }
304
305    /// Create a new model message with text content
306    pub fn model(text: impl Into<String>) -> Self {
307        Self { content: Content::text(text).with_role(Role::Model), role: Role::Model }
308    }
309
310    /// Create a new embedding message with text content
311    pub fn embed(text: impl Into<String>) -> Self {
312        Self { content: Content::text(text), role: Role::Model }
313    }
314
315    /// Create a new function message with function response content from JSON
316    pub fn function(name: impl Into<String>, response: serde_json::Value) -> Self {
317        Self {
318            content: Content::function_response_json(name, response).with_role(Role::Model),
319            role: Role::Model,
320        }
321    }
322
323    /// Create a new function message with function response from a JSON string
324    pub fn function_str(
325        name: impl Into<String>,
326        response: impl Into<String>,
327    ) -> Result<Self, serde_json::Error> {
328        let response_str = response.into();
329        let json = serde_json::from_str(&response_str)?;
330        Ok(Self {
331            content: Content::function_response_json(name, json).with_role(Role::Model),
332            role: Role::Model,
333        })
334    }
335}
336
337/// Content modality type - specifies the format of model output
338#[derive(Debug, Clone, Serialize, PartialEq)]
339#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
340pub enum Modality {
341    /// Default value.
342    ModalityUnspecified,
343    /// Indicates the model should return text.
344    Text,
345    /// Indicates the model should return images.
346    Image,
347    /// Indicates the model should return audio.
348    Audio,
349    /// Indicates the model should return video.
350    Video,
351    /// Indicates document content (PDFs, etc.)
352    Document,
353    /// Unknown or future modality types
354    Unknown,
355}
356
357impl Modality {
358    fn from_wire_str(value: &str) -> Self {
359        match value {
360            "MODALITY_UNSPECIFIED" => Self::ModalityUnspecified,
361            "TEXT" => Self::Text,
362            "IMAGE" => Self::Image,
363            "AUDIO" => Self::Audio,
364            "VIDEO" => Self::Video,
365            "DOCUMENT" => Self::Document,
366            _ => Self::Unknown,
367        }
368    }
369
370    fn from_wire_number(value: i64) -> Self {
371        match value {
372            0 => Self::ModalityUnspecified,
373            1 => Self::Text,
374            2 => Self::Image,
375            3 => Self::Video,
376            4 => Self::Audio,
377            5 => Self::Document,
378            _ => Self::Unknown,
379        }
380    }
381}
382
383impl<'de> Deserialize<'de> for Modality {
384    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
385    where
386        D: serde::Deserializer<'de>,
387    {
388        let value = serde_json::Value::deserialize(deserializer)?;
389        match value {
390            serde_json::Value::String(s) => Ok(Self::from_wire_str(&s)),
391            serde_json::Value::Number(n) => n
392                .as_i64()
393                .map(Self::from_wire_number)
394                .ok_or_else(|| de::Error::custom("modality must be an integer-compatible number")),
395            _ => Err(de::Error::custom("modality must be a string or integer")),
396        }
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_tool_call_deserialize_and_roundtrip() {
406        let json = r#"{"toolCall": {"name": "google_search", "args": {"query": "rust lang"}}}"#;
407        let part: Part = serde_json::from_str(json).expect("should deserialize toolCall");
408        match &part {
409            Part::ToolCall { tool_call, .. } => {
410                assert_eq!(tool_call["name"], "google_search");
411                assert_eq!(tool_call["args"]["query"], "rust lang");
412            }
413            other => panic!("expected Part::ToolCall, got {other:?}"),
414        }
415        // Round-trip
416        let serialized = serde_json::to_string(&part).expect("should serialize");
417        let deserialized: Part =
418            serde_json::from_str(&serialized).expect("should deserialize again");
419        assert_eq!(part, deserialized);
420    }
421
422    #[test]
423    fn test_tool_response_deserialize_and_roundtrip() {
424        let json = r#"{"toolResponse": {"name": "google_search", "output": {"results": []}}, "thoughtSignature": "sig_123"}"#;
425        let part: Part = serde_json::from_str(json).expect("should deserialize toolResponse");
426        match &part {
427            Part::ToolResponse { tool_response, thought_signature } => {
428                assert_eq!(tool_response["name"], "google_search");
429                assert_eq!(tool_response["output"]["results"], serde_json::json!([]));
430                assert_eq!(thought_signature.as_deref(), Some("sig_123"));
431            }
432            other => panic!("expected Part::ToolResponse, got {other:?}"),
433        }
434        // Round-trip
435        let serialized = serde_json::to_string(&part).expect("should serialize");
436        let deserialized: Part =
437            serde_json::from_str(&serialized).expect("should deserialize again");
438        assert_eq!(part, deserialized);
439    }
440
441    #[test]
442    fn test_code_execution_parts_preserve_thought_signature() {
443        let executable = serde_json::json!({
444            "executableCode": { "language": "python", "code": "print(1)" },
445            "thoughtSignature": "sig_exec"
446        });
447        let result = serde_json::json!({
448            "codeExecutionResult": { "outcome": "OUTCOME_OK", "output": "1" },
449            "thoughtSignature": "sig_result"
450        });
451
452        let executable_part: Part =
453            serde_json::from_value(executable).expect("should deserialize executable code");
454        let result_part: Part =
455            serde_json::from_value(result).expect("should deserialize code execution result");
456
457        match executable_part {
458            Part::ExecutableCode { thought_signature, .. } => {
459                assert_eq!(thought_signature.as_deref(), Some("sig_exec"));
460            }
461            other => panic!("expected Part::ExecutableCode, got {other:?}"),
462        }
463
464        match result_part {
465            Part::CodeExecutionResult { thought_signature, .. } => {
466                assert_eq!(thought_signature.as_deref(), Some("sig_result"));
467            }
468            other => panic!("expected Part::CodeExecutionResult, got {other:?}"),
469        }
470    }
471
472    // ===== Multimodal function response tests =====
473
474    #[test]
475    fn test_file_data_ref_serde_round_trip() {
476        let file_ref = FileDataRef {
477            mime_type: "application/pdf".to_string(),
478            file_uri: "gs://bucket/report.pdf".to_string(),
479        };
480        let json = serde_json::to_string(&file_ref).unwrap();
481        assert!(json.contains("mimeType"));
482        assert!(json.contains("fileUri"));
483        let deserialized: FileDataRef = serde_json::from_str(&json).unwrap();
484        assert_eq!(file_ref, deserialized);
485    }
486
487    #[test]
488    fn test_part_file_data_serde_round_trip() {
489        let part = Part::FileData {
490            file_data: FileDataRef {
491                mime_type: "image/jpeg".to_string(),
492                file_uri: "https://example.com/img.jpg".to_string(),
493            },
494        };
495        let json = serde_json::to_string(&part).unwrap();
496        assert!(json.contains("fileData"));
497        let deserialized: Part = serde_json::from_str(&json).unwrap();
498        assert_eq!(part, deserialized);
499    }
500
501    #[test]
502    fn test_function_response_new_backward_compat() {
503        let fr =
504            super::super::tools::FunctionResponse::new("tool", serde_json::json!({"ok": true}));
505        let json = serde_json::to_string(&fr).unwrap();
506        // Should only have name and response — no inline_data or file_data keys
507        let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&json).unwrap();
508        assert!(map.contains_key("name"));
509        assert!(map.contains_key("response"));
510        assert!(!map.contains_key("inline_data"));
511        assert!(!map.contains_key("file_data"));
512    }
513
514    #[test]
515    fn test_function_response_with_inline_data_constructor() {
516        let blobs = vec![Blob::new("image/png", "base64data")];
517        let fr = super::super::tools::FunctionResponse::with_inline_data(
518            "chart",
519            serde_json::json!({"status": "ok"}),
520            blobs.clone(),
521        );
522        assert_eq!(fr.name, "chart");
523        assert_eq!(fr.parts.len(), 1);
524        assert!(matches!(
525            &fr.parts[0],
526            super::super::tools::FunctionResponsePart::InlineData { inline_data }
527            if inline_data == &blobs[0]
528        ));
529    }
530
531    #[test]
532    fn test_function_response_with_file_data_constructor() {
533        let files = vec![FileDataRef {
534            mime_type: "application/pdf".to_string(),
535            file_uri: "gs://b/f.pdf".to_string(),
536        }];
537        let fr = super::super::tools::FunctionResponse::with_file_data(
538            "doc",
539            serde_json::json!({"ok": true}),
540            files.clone(),
541        );
542        assert_eq!(fr.name, "doc");
543        assert_eq!(fr.parts.len(), 1);
544        assert!(matches!(
545            &fr.parts[0],
546            super::super::tools::FunctionResponsePart::FileData { file_data }
547            if file_data == &files[0]
548        ));
549    }
550
551    #[test]
552    fn test_function_response_inline_data_only_constructor() {
553        let blobs = vec![Blob::new("audio/wav", "audiodata")];
554        let fr =
555            super::super::tools::FunctionResponse::inline_data_only("audio_tool", blobs.clone());
556        assert_eq!(fr.name, "audio_tool");
557        assert!(fr.response.is_none());
558        assert_eq!(fr.parts.len(), 1);
559    }
560
561    #[test]
562    fn test_content_function_response_multimodal_parts_nested() {
563        use super::super::tools::FunctionResponsePart;
564        let blobs = [Blob::new("image/png", "img1"), Blob::new("image/jpeg", "img2")];
565        let files = [FileDataRef {
566            mime_type: "application/pdf".to_string(),
567            file_uri: "gs://b/f.pdf".to_string(),
568        }];
569        let mut fr_parts: Vec<FunctionResponsePart> = blobs
570            .iter()
571            .map(|b| FunctionResponsePart::InlineData { inline_data: b.clone() })
572            .collect();
573        fr_parts
574            .extend(files.iter().map(|f| FunctionResponsePart::FileData { file_data: f.clone() }));
575        let fr = super::super::tools::FunctionResponse {
576            name: "tool".to_string(),
577            response: Some(serde_json::json!({"ok": true})),
578            parts: fr_parts,
579        };
580        let content = Content::function_response_multimodal(fr);
581        let content_parts = content.parts.unwrap();
582        // Single FunctionResponse part in the Content
583        assert_eq!(content_parts.len(), 1);
584        assert!(matches!(&content_parts[0], Part::FunctionResponse { .. }));
585        // The multimodal data is nested inside the FunctionResponse
586        if let Part::FunctionResponse { function_response, .. } = &content_parts[0] {
587            // 2 inline + 1 file = 3 nested parts
588            assert_eq!(function_response.parts.len(), 3);
589        } else {
590            panic!("expected FunctionResponse part");
591        }
592    }
593
594    #[test]
595    fn test_multimodal_function_response_wire_format() {
596        // Verify the serialized JSON matches the Gemini API wire format:
597        // The `parts` array with `inlineData` lives INSIDE the `functionResponse` object.
598        use super::super::tools::FunctionResponsePart;
599        let fr = super::super::tools::FunctionResponse {
600            name: "get_image".to_string(),
601            response: Some(serde_json::json!({"image_ref": {"$ref": "photo.jpg"}})),
602            parts: vec![FunctionResponsePart::InlineData {
603                inline_data: Blob::new("image/jpeg", "base64encodeddata"),
604            }],
605        };
606
607        let part = Part::FunctionResponse { function_response: fr, thought_signature: None };
608        let json = serde_json::to_value(&part).unwrap();
609
610        // The functionResponse object should contain name, response, AND parts
611        let fr_obj = &json["functionResponse"];
612        assert_eq!(fr_obj["name"], "get_image");
613        assert!(fr_obj["response"].is_object());
614        assert!(fr_obj["parts"].is_array());
615        assert_eq!(fr_obj["parts"].as_array().unwrap().len(), 1);
616
617        // The nested part should have inlineData with mimeType and data
618        let inline = &fr_obj["parts"][0]["inlineData"];
619        assert_eq!(inline["mimeType"], "image/jpeg");
620        assert_eq!(inline["data"], "base64encodeddata");
621    }
622
623    #[test]
624    fn test_json_only_function_response_has_no_parts_key() {
625        // When there are no multimodal parts, the `parts` key should be absent
626        let fr = super::super::tools::FunctionResponse::new(
627            "simple_tool",
628            serde_json::json!({"result": "ok"}),
629        );
630        let part = Part::FunctionResponse { function_response: fr, thought_signature: None };
631        let json = serde_json::to_string(&part).unwrap();
632        // Should NOT contain "parts" key at all
633        assert!(
634            !json.contains(r#""parts""#),
635            "JSON-only response should not have parts key: {json}"
636        );
637    }
638}