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
142                .as_ref()
143                .expect("Some-ness checked above")
144                .to_string(),
145            file_uri: uri.as_ref().expect("Some-ness checked above").to_string(),
146        })
147    }
148}
149
150/// Blob for a message part
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152#[serde(rename_all = "camelCase")]
153pub struct Blob {
154    /// The MIME type of the data
155    pub mime_type: String,
156    /// Base64 encoded data
157    pub data: String,
158}
159
160impl Blob {
161    /// Create a new blob with mime type and data
162    pub fn new(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
163        Self {
164            mime_type: mime_type.into(),
165            data: data.into(),
166        }
167    }
168}
169
170/// Content of a message
171#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
172#[serde(rename_all = "camelCase")]
173pub struct Content {
174    /// Parts of the content
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub parts: Option<Vec<Part>>,
177    /// Role of the content
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub role: Option<Role>,
180}
181
182impl Content {
183    /// Create a new text content
184    pub fn text(text: impl Into<String>) -> Self {
185        Self {
186            parts: Some(vec![Part::Text {
187                text: text.into(),
188                thought: None,
189                thought_signature: None,
190            }]),
191            role: None,
192        }
193    }
194
195    /// Create a new content with a function call
196    pub fn function_call(function_call: super::tools::FunctionCall) -> Self {
197        Self {
198            parts: Some(vec![Part::FunctionCall {
199                function_call,
200                thought_signature: None,
201            }]),
202            role: None,
203        }
204    }
205
206    /// Create a new content with a function call and thought signature
207    pub fn function_call_with_thought(
208        function_call: super::tools::FunctionCall,
209        thought_signature: impl Into<String>,
210    ) -> Self {
211        Self {
212            parts: Some(vec![Part::FunctionCall {
213                function_call,
214                thought_signature: Some(thought_signature.into()),
215            }]),
216            role: None,
217        }
218    }
219
220    /// Create a new text content with thought signature
221    pub fn text_with_thought_signature(
222        text: impl Into<String>,
223        thought_signature: impl Into<String>,
224    ) -> Self {
225        Self {
226            parts: Some(vec![Part::Text {
227                text: text.into(),
228                thought: None,
229                thought_signature: Some(thought_signature.into()),
230            }]),
231            role: None,
232        }
233    }
234
235    /// Create a new thought content with thought signature
236    pub fn thought_with_signature(
237        text: impl Into<String>,
238        thought_signature: impl Into<String>,
239    ) -> Self {
240        Self {
241            parts: Some(vec![Part::Text {
242                text: text.into(),
243                thought: Some(true),
244                thought_signature: Some(thought_signature.into()),
245            }]),
246            role: None,
247        }
248    }
249
250    /// Create a new content with a function response
251    pub fn function_response(function_response: super::tools::FunctionResponse) -> Self {
252        Self {
253            parts: Some(vec![Part::FunctionResponse { function_response }]),
254            role: None,
255        }
256    }
257
258    /// Create a new content with a function response from name and JSON value
259    pub fn function_response_json(name: impl Into<String>, response: serde_json::Value) -> Self {
260        Self {
261            parts: Some(vec![Part::FunctionResponse {
262                function_response: super::tools::FunctionResponse::new(name, response),
263            }]),
264            role: None,
265        }
266    }
267
268    /// Create a new content with inline data (blob data)
269    pub fn inline_data(mime_type: impl Into<String>, data: impl Into<String>) -> Self {
270        Self {
271            parts: Some(vec![Part::InlineData {
272                inline_data: Blob::new(mime_type, data),
273                media_resolution: None,
274            }]),
275            role: None,
276        }
277    }
278
279    /// Create a new content with inline data and media resolution
280    pub fn inline_data_with_resolution(
281        mime_type: impl Into<String>,
282        data: impl Into<String>,
283        resolution: super::generation::model::MediaResolutionLevel,
284    ) -> Self {
285        Self {
286            parts: Some(vec![Part::InlineData {
287                inline_data: Blob::new(mime_type, data),
288                media_resolution: Some(super::generation::model::MediaResolution {
289                    level: resolution,
290                }),
291            }]),
292            role: None,
293        }
294    }
295
296    /// Create a new content with text and coordinates to a previously uploaded file
297    pub fn text_with_file(
298        text: impl Into<String>,
299        file_handle: &FileHandle,
300    ) -> Result<Self, FilesError> {
301        Ok(Self {
302            parts: Some(vec![
303                Part::Text {
304                    text: text.into(),
305                    thought: None,
306                    thought_signature: None,
307                },
308                Part::FileData {
309                    file_data: FileData::try_from(file_handle)?,
310                },
311            ]),
312            role: None,
313        })
314    }
315
316    /// Add a role to this content
317    pub fn with_role(mut self, role: Role) -> Self {
318        self.role = Some(role);
319        self
320    }
321}
322
323/// Message in a conversation
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct Message {
326    /// Content of the message
327    pub content: Content,
328    /// Role of the message
329    pub role: Role,
330}
331
332impl Message {
333    /// Create a new user message with text content
334    pub fn user(text: impl Into<String>) -> Self {
335        Self {
336            content: Content::text(text).with_role(Role::User),
337            role: Role::User,
338        }
339    }
340
341    /// Create a new model message with text content
342    pub fn model(text: impl Into<String>) -> Self {
343        Self {
344            content: Content::text(text).with_role(Role::Model),
345            role: Role::Model,
346        }
347    }
348
349    /// Create a new embedding message with text content
350    pub fn embed(text: impl Into<String>) -> Self {
351        Self {
352            content: Content::text(text),
353            role: Role::Model,
354        }
355    }
356
357    /// Create a new function message with function response content from JSON
358    pub fn function(name: impl Into<String>, response: serde_json::Value) -> Self {
359        Self {
360            content: Content::function_response_json(name, response).with_role(Role::Model),
361            role: Role::Model,
362        }
363    }
364
365    /// Create a new function message with function response from a JSON string
366    pub fn function_str(
367        name: impl Into<String>,
368        response: impl Into<String>,
369    ) -> Result<Self, serde_json::Error> {
370        let response_str = response.into();
371        let json = serde_json::from_str(&response_str)?;
372        Ok(Self {
373            content: Content::function_response_json(name, json).with_role(Role::Model),
374            role: Role::Model,
375        })
376    }
377}
378
379/// Content modality type - specifies the format of model output
380#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
381#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
382pub enum Modality {
383    /// Default value.
384    ModalityUnspecified,
385    /// Indicates the model should return a (json) document.
386    Document,
387    /// Indicates the model should return text.
388    Text,
389    /// Indicates the model should return images.
390    Image,
391    /// Indicates the model should return audio.
392    Audio,
393    /// Indicates the model should return video.
394    Video,
395    #[serde(untagged)]
396    Other(String),
397}