gemini_rust/
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};
30
31use crate::{File, FileHandle, FilesError};
32
33/// Role of a message in a conversation
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35#[serde(rename_all = "lowercase")]
36pub enum Role {
37    /// Message from the user
38    User,
39    /// Message from the model
40    Model,
41}
42
43/// Content part that can be included in a message
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
45#[serde(untagged)]
46pub enum Part {
47    /// Text content
48    Text {
49        /// The text content
50        text: String,
51        /// Whether this is a thought summary (Gemini 2.5 series only)
52        #[serde(skip_serializing_if = "Option::is_none")]
53        thought: Option<bool>,
54        /// The thought signature for the text (Gemini 2.5 series only)
55        #[serde(rename = "thoughtSignature", skip_serializing_if = "Option::is_none")]
56        thought_signature: Option<String>,
57    },
58    InlineData {
59        /// The blob data
60        #[serde(rename = "inlineData")]
61        inline_data: Blob,
62        /// Per-part media resolution override.
63        /// If specified, overrides the global media_resolution setting for this specific inline data.
64        #[serde(skip_serializing_if = "Option::is_none")]
65        media_resolution: Option<super::generation::model::MediaResolution>,
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 for the function call (Gemini 2.5 series only)
73        #[serde(rename = "thoughtSignature", skip_serializing_if = "Option::is_none")]
74        thought_signature: Option<String>,
75    },
76    /// Function response (results from executing a function call)
77    FunctionResponse {
78        /// The function response details
79        #[serde(rename = "functionResponse")]
80        function_response: super::tools::FunctionResponse,
81    },
82    /// File reference for previously uploaded files
83    FileData {
84        #[serde(rename = "fileData")]
85        file_data: FileData,
86    },
87    /// Code generated by the model
88    ExecutableCode {
89        /// The executable code details
90        #[serde(rename = "executableCode")]
91        executable_code: super::tools::ExecutableCode,
92    },
93    /// Result of code execution
94    CodeExecutionResult {
95        /// The code execution result details
96        #[serde(rename = "codeExecutionResult")]
97        code_execution_result: super::tools::CodeExecutionResult,
98    },
99}
100
101/// Coordinates for a previously uploaded file.
102///
103/// This struct contains the coordinates needed to reference a file that was
104/// uploaded to the Gemini API. The file URI and MIME type are provided by
105/// the API when a file is successfully uploaded.
106///
107/// Implements `TryFrom` for [`FileHandle`] (or `&FileHandle` to be precise) for user convenience,
108/// as an uploaded file is represented via the [`File`] type but a [`FileHandle`] is required to
109/// upload a file or search for files.
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
111#[serde(rename_all = "camelCase")]
112pub struct FileData {
113    /// The IANA standard MIME type of the file
114    pub mime_type: String,
115    /// The URI of the uploaded file
116    pub file_uri: String,
117}
118
119impl TryFrom<&FileHandle> for FileData {
120    type Error = FilesError;
121
122    fn try_from(file_handle: &FileHandle) -> Result<Self, Self::Error> {
123        let File { mime_type, uri, .. } = file_handle.get_file_meta();
124
125        let none_fields: Vec<_> = [
126            mime_type.is_none().then_some("mime_type"),
127            uri.is_none().then_some("uri"),
128        ]
129        .into_iter()
130        .flatten()
131        .map(String::from)
132        .collect();
133
134        if !none_fields.is_empty() {
135            return Err(FilesError::Incomplete {
136                fields: none_fields,
137            });
138        }
139
140        Ok(Self {
141            mime_type: mime_type.as_ref().expect("Some-ness checked above").clone(),
142            file_uri: uri.as_ref().expect("Some-ness checked above").to_string(),
143        })
144    }
145}
146
147/// Blob for a message part
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
149#[serde(rename_all = "camelCase")]
150pub struct Blob {
151    /// The MIME type of the data
152    pub mime_type: String,
153    /// Base64 encoded data
154    pub data: String,
155}
156
157impl Blob {
158    /// Create a new blob with mime type and data
159    pub fn new(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
160        Self {
161            mime_type: mime_type.into(),
162            data: data.into(),
163        }
164    }
165}
166
167/// Content of a message
168#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
169#[serde(rename_all = "camelCase")]
170pub struct Content {
171    /// Parts of the content
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub parts: Option<Vec<Part>>,
174    /// Role of the content
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub role: Option<Role>,
177}
178
179impl Content {
180    /// Create a new text content
181    pub fn text(text: impl Into<String>) -> Self {
182        Self {
183            parts: Some(vec![Part::Text {
184                text: text.into(),
185                thought: None,
186                thought_signature: None,
187            }]),
188            role: None,
189        }
190    }
191
192    /// Create a new content with a function call
193    pub fn function_call(function_call: super::tools::FunctionCall) -> Self {
194        Self {
195            parts: Some(vec![Part::FunctionCall {
196                function_call,
197                thought_signature: None,
198            }]),
199            role: None,
200        }
201    }
202
203    /// Create a new content with a function call and thought signature
204    pub fn function_call_with_thought(
205        function_call: super::tools::FunctionCall,
206        thought_signature: impl Into<String>,
207    ) -> Self {
208        Self {
209            parts: Some(vec![Part::FunctionCall {
210                function_call,
211                thought_signature: Some(thought_signature.into()),
212            }]),
213            role: None,
214        }
215    }
216
217    /// Create a new text content with thought signature
218    pub fn text_with_thought_signature(
219        text: impl Into<String>,
220        thought_signature: impl Into<String>,
221    ) -> Self {
222        Self {
223            parts: Some(vec![Part::Text {
224                text: text.into(),
225                thought: None,
226                thought_signature: Some(thought_signature.into()),
227            }]),
228            role: None,
229        }
230    }
231
232    /// Create a new thought content with thought signature
233    pub fn thought_with_signature(
234        text: impl Into<String>,
235        thought_signature: impl Into<String>,
236    ) -> Self {
237        Self {
238            parts: Some(vec![Part::Text {
239                text: text.into(),
240                thought: Some(true),
241                thought_signature: Some(thought_signature.into()),
242            }]),
243            role: None,
244        }
245    }
246
247    /// Create a new content with a function response
248    pub fn function_response(function_response: super::tools::FunctionResponse) -> Self {
249        Self {
250            parts: Some(vec![Part::FunctionResponse { function_response }]),
251            role: None,
252        }
253    }
254
255    /// Create a new content with a function response from name and JSON value
256    pub fn function_response_json(name: impl Into<String>, response: serde_json::Value) -> Self {
257        Self {
258            parts: Some(vec![Part::FunctionResponse {
259                function_response: super::tools::FunctionResponse::new(name, response),
260            }]),
261            role: None,
262        }
263    }
264
265    /// Create a new content with inline data (blob data)
266    pub fn inline_data(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
267        Self {
268            parts: Some(vec![Part::InlineData {
269                inline_data: Blob::new(mime_type, data),
270                media_resolution: None,
271            }]),
272            role: None,
273        }
274    }
275
276    /// Create a new content with inline data and media resolution
277    pub fn inline_data_with_resolution(
278        mime_type: impl Into<String>,
279        data: impl Into<String>,
280        resolution: super::generation::model::MediaResolutionLevel,
281    ) -> Self {
282        Self {
283            parts: Some(vec![Part::InlineData {
284                inline_data: Blob::new(mime_type, data),
285                media_resolution: Some(super::generation::model::MediaResolution {
286                    level: resolution,
287                }),
288            }]),
289            role: None,
290        }
291    }
292
293    /// Create a new content with text and coordinates to a previously uploaded file
294    pub fn text_with_file(
295        text: impl Into<String>,
296        file_handle: &FileHandle,
297    ) -> Result<Self, FilesError> {
298        Ok(Self {
299            parts: Some(vec![
300                Part::Text {
301                    text: text.into(),
302                    thought: None,
303                    thought_signature: None,
304                },
305                Part::FileData {
306                    file_data: FileData::try_from(file_handle)?,
307                },
308            ]),
309            role: None,
310        })
311    }
312
313    /// Add a role to this content
314    pub fn with_role(mut self, role: Role) -> Self {
315        self.role = Some(role);
316        self
317    }
318}
319
320/// Message in a conversation
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct Message {
323    /// Content of the message
324    pub content: Content,
325    /// Role of the message
326    pub role: Role,
327}
328
329impl Message {
330    /// Create a new user message with text content
331    pub fn user(text: impl Into<String>) -> Self {
332        Self {
333            content: Content::text(text).with_role(Role::User),
334            role: Role::User,
335        }
336    }
337
338    /// Create a new model message with text content
339    pub fn model(text: impl Into<String>) -> Self {
340        Self {
341            content: Content::text(text).with_role(Role::Model),
342            role: Role::Model,
343        }
344    }
345
346    /// Create a new embedding message with text content
347    pub fn embed(text: impl Into<String>) -> Self {
348        Self {
349            content: Content::text(text),
350            role: Role::Model,
351        }
352    }
353
354    /// Create a new function message with function response content from JSON
355    pub fn function(name: impl Into<String>, response: serde_json::Value) -> Self {
356        Self {
357            content: Content::function_response_json(name, response).with_role(Role::Model),
358            role: Role::Model,
359        }
360    }
361
362    /// Create a new function message with function response from a JSON string
363    pub fn function_str(
364        name: impl Into<String>,
365        response: impl Into<String>,
366    ) -> Result<Self, serde_json::Error> {
367        let response_str = response.into();
368        let json = serde_json::from_str(&response_str)?;
369        Ok(Self {
370            content: Content::function_response_json(name, json).with_role(Role::Model),
371            role: Role::Model,
372        })
373    }
374}
375
376/// Content modality type - specifies the format of model output
377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
378#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
379pub enum Modality {
380    /// Default value.
381    ModalityUnspecified,
382    /// Indicates the model should return a (json) document.
383    Document,
384    /// Indicates the model should return text.
385    Text,
386    /// Indicates the model should return images.
387    Image,
388    /// Indicates the model should return audio.
389    Audio,
390    /// Indicates the model should return video.
391    Video,
392    #[serde(untagged)]
393    Other(String),
394}