Skip to main content

gemini_rust/generation/
model.rs

1use reqwest::Url;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4
5use crate::{
6    safety::{SafetyRating, SafetySetting},
7    Content, Modality, Part,
8};
9
10/// Reason why generation finished
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
13pub enum FinishReason {
14    /// Default value. This value is unused.
15    FinishReasonUnspecified,
16    /// Natural stop point of the model or provided stop sequence.
17    Stop,
18    /// The maximum number of tokens as specified in the request was reached.
19    MaxTokens,
20    /// The response candidate content was flagged for safety reasons.
21    Safety,
22    /// The response candidate content was flagged for recitation reasons.
23    Recitation,
24    /// The response candidate content was flagged for using an unsupported language.
25    Language,
26    /// Unknown reason.
27    Other,
28    /// Token generation stopped because the content contains forbidden terms.
29    Blocklist,
30    /// Token generation stopped for potentially containing prohibited content.
31    ProhibitedContent,
32    /// Token generation stopped because the content potentially contains Sensitive Personally Identifiable Information (SPII).
33    Spii,
34    /// The function call generated by the model is invalid.
35    MalformedFunctionCall,
36    /// Token generation stopped because generated images contain safety violations.
37    ImageSafety,
38    /// Model generated a tool call but no tools were enabled in the request.
39    UnexpectedToolCall,
40    /// Model called too many tools consecutively, thus the system exited execution.
41    TooManyToolCalls,
42}
43
44/// Citation metadata for content
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46#[serde(rename_all = "camelCase")]
47pub struct CitationMetadata {
48    /// The citation sources
49    pub citation_sources: Vec<CitationSource>,
50}
51
52/// Citation source
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54#[serde(rename_all = "camelCase")]
55pub struct CitationSource {
56    /// The URI of the citation source
57    pub uri: Option<String>,
58    /// The title of the citation source
59    pub title: Option<String>,
60    /// The start index of the citation in the response
61    pub start_index: Option<i32>,
62    /// The end index of the citation in the response
63    pub end_index: Option<i32>,
64    /// The license of the citation source
65    pub license: Option<String>,
66    /// The publication date of the citation source
67    #[serde(default, with = "time::serde::rfc3339::option")]
68    pub publication_date: Option<OffsetDateTime>,
69}
70
71/// A candidate response
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73#[serde(rename_all = "camelCase")]
74pub struct Candidate {
75    /// The content of the candidate
76    #[serde(default)]
77    pub content: Content,
78    /// The safety ratings for the candidate
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub safety_ratings: Option<Vec<SafetyRating>>,
81    /// The citation metadata for the candidate
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub citation_metadata: Option<CitationMetadata>,
84    /// The grounding metadata for the candidate
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub grounding_metadata: Option<GroundingMetadata>,
87    /// The finish reason for the candidate
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub finish_reason: Option<FinishReason>,
90    /// The index of the candidate
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub index: Option<i32>,
93}
94
95/// Metadata about token usage
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97#[serde(rename_all = "camelCase")]
98pub struct UsageMetadata {
99    /// The number of prompt tokens (null if request processing failed)
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub prompt_token_count: Option<i32>,
102    /// The number of response tokens (null if generation failed)
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub candidates_token_count: Option<i32>,
105    /// The total number of tokens (null if individual counts unavailable)
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub total_token_count: Option<i32>,
108    /// The number of thinking tokens (Gemini 2.5 series only)
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub thoughts_token_count: Option<i32>,
111    /// Detailed prompt token information
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub prompt_tokens_details: Option<Vec<PromptTokenDetails>>,
114    /// The number of cached content tokens (batch API)
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub cached_content_token_count: Option<i32>,
117    /// Detailed cache token information (batch API)
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub cache_tokens_details: Option<Vec<PromptTokenDetails>>,
120}
121
122/// Details about prompt tokens by modality
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124#[serde(rename_all = "camelCase")]
125pub struct PromptTokenDetails {
126    /// The modality (e.g., "TEXT")
127    pub modality: Modality,
128    /// Token count for this modality
129    pub token_count: i32,
130}
131
132/// Grounding metadata for responses that use grounding tools
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134#[serde(rename_all = "camelCase")]
135pub struct GroundingMetadata {
136    /// Grounding chunks containing source information
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub grounding_chunks: Option<Vec<GroundingChunk>>,
139    /// Grounding supports connecting response text to sources
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub grounding_supports: Option<Vec<GroundingSupport>>,
142    /// Web search queries used for grounding
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub web_search_queries: Option<Vec<String>>,
145    /// Google Maps widget context token
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub google_maps_widget_context_token: Option<String>,
148}
149
150/// A chunk of grounding information from a source
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152#[serde(rename_all = "camelCase")]
153pub struct GroundingChunk {
154    /// Maps-specific grounding information
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub maps: Option<MapsGroundingChunk>,
157    /// Web-specific grounding information
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub web: Option<WebGroundingChunk>,
160}
161
162/// Maps-specific grounding chunk information
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
164#[serde(rename_all = "camelCase")]
165pub struct MapsGroundingChunk {
166    /// The URI of the Maps source
167    pub uri: Url,
168    /// The title of the Maps source
169    pub title: String,
170    /// The place ID from Google Maps
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub place_id: Option<String>,
173}
174
175/// Web-specific grounding chunk information
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
177#[serde(rename_all = "camelCase")]
178pub struct WebGroundingChunk {
179    /// The URI of the web source
180    pub uri: Url,
181    /// The title of the web source
182    pub title: String,
183}
184
185/// Support information connecting response text to grounding sources
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
187#[serde(rename_all = "camelCase")]
188pub struct GroundingSupport {
189    /// Segment of the response text
190    pub segment: GroundingSegment,
191    /// Indices of grounding chunks that support this segment
192    pub grounding_chunk_indices: Vec<u32>,
193}
194
195/// A segment of response text
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
197#[serde(rename_all = "camelCase")]
198pub struct GroundingSegment {
199    /// Start index of the segment in the response text
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub start_index: Option<u32>,
202    /// End index of the segment in the response text
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub end_index: Option<u32>,
205    /// The text content of the segment
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub text: Option<String>,
208}
209
210/// Response from the Gemini API for content generation
211#[deprecated(
212    since = "1.8.0",
213    note = "Use crate::interactions::Interaction instead. See migration guide: interactions-api/migration-plan.md"
214)]
215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216#[serde(rename_all = "camelCase")]
217pub struct GenerationResponse {
218    /// The candidates generated
219    #[serde(default, skip_serializing_if = "Vec::is_empty")]
220    pub candidates: Vec<Candidate>,
221    /// The prompt feedback
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub prompt_feedback: Option<PromptFeedback>,
224    /// Usage metadata
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub usage_metadata: Option<UsageMetadata>,
227    /// Model version used
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub model_version: Option<String>,
230    /// Response ID
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub response_id: Option<String>,
233}
234
235/// Reason why content was blocked
236#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
237#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
238pub enum BlockReason {
239    /// Default value. This value is unused.
240    BlockReasonUnspecified,
241    /// Prompt was blocked due to safety reasons. Inspect safetyRatings to understand which safety category blocked it.
242    Safety,
243    /// Prompt was blocked due to unknown reasons.
244    Other,
245    /// Prompt was blocked due to the terms which are included from the terminology blocklist.
246    Blocklist,
247    /// Prompt was blocked due to prohibited content.
248    ProhibitedContent,
249    /// Candidates blocked due to unsafe image generation content.
250    ImageSafety,
251}
252
253/// Feedback about the prompt
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
255#[serde(rename_all = "camelCase")]
256pub struct PromptFeedback {
257    /// The safety ratings for the prompt
258    #[serde(default, skip_serializing_if = "Vec::is_empty")]
259    pub safety_ratings: Vec<SafetyRating>,
260    /// The block reason if the prompt was blocked
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub block_reason: Option<BlockReason>,
263}
264
265impl GenerationResponse {
266    /// Get the text of the first candidate
267    pub fn text(&self) -> String {
268        self.candidates
269            .first()
270            .and_then(|c| {
271                c.content.parts.as_ref().and_then(|parts| {
272                    parts.first().and_then(|p| match p {
273                        Part::Text {
274                            text,
275                            thought: _,
276                            thought_signature: _,
277                        } => Some(text.clone()),
278                        _ => None,
279                    })
280                })
281            })
282            .unwrap_or_default()
283    }
284
285    /// Get function calls from the response
286    pub fn function_calls(&self) -> Vec<&crate::tools::FunctionCall> {
287        self.candidates
288            .iter()
289            .flat_map(|c| {
290                c.content
291                    .parts
292                    .as_ref()
293                    .map(|parts| {
294                        parts
295                            .iter()
296                            .filter_map(|p| match p {
297                                Part::FunctionCall {
298                                    function_call,
299                                    thought_signature: _,
300                                } => Some(function_call),
301                                _ => None,
302                            })
303                            .collect::<Vec<_>>()
304                    })
305                    .unwrap_or_default()
306            })
307            .collect()
308    }
309
310    /// Get function calls with their thought signatures from the response
311    pub fn function_calls_with_thoughts(
312        &self,
313    ) -> Vec<(&crate::tools::FunctionCall, Option<&String>)> {
314        self.candidates
315            .iter()
316            .flat_map(|c| {
317                c.content
318                    .parts
319                    .as_ref()
320                    .map(|parts| {
321                        parts
322                            .iter()
323                            .filter_map(|p| match p {
324                                Part::FunctionCall {
325                                    function_call,
326                                    thought_signature,
327                                } => Some((function_call, thought_signature.as_ref())),
328                                _ => None,
329                            })
330                            .collect::<Vec<_>>()
331                    })
332                    .unwrap_or_default()
333            })
334            .collect()
335    }
336
337    /// Get thought summaries from the response
338    pub fn thoughts(&self) -> Vec<String> {
339        self.candidates
340            .iter()
341            .flat_map(|c| {
342                c.content
343                    .parts
344                    .as_ref()
345                    .map(|parts| {
346                        parts
347                            .iter()
348                            .filter_map(|p| match p {
349                                Part::Text {
350                                    text,
351                                    thought: Some(true),
352                                    thought_signature: _,
353                                } => Some(text.clone()),
354                                _ => None,
355                            })
356                            .collect::<Vec<_>>()
357                    })
358                    .unwrap_or_default()
359            })
360            .collect()
361    }
362
363    /// Get all text parts (both regular text and thoughts)
364    pub fn all_text(&self) -> Vec<(String, bool)> {
365        self.candidates
366            .iter()
367            .flat_map(|c| {
368                c.content
369                    .parts
370                    .as_ref()
371                    .map(|parts| {
372                        parts
373                            .iter()
374                            .filter_map(|p| match p {
375                                Part::Text {
376                                    text,
377                                    thought,
378                                    thought_signature: _,
379                                } => Some((text.clone(), thought.unwrap_or(false))),
380                                _ => None,
381                            })
382                            .collect::<Vec<_>>()
383                    })
384                    .unwrap_or_default()
385            })
386            .collect()
387    }
388
389    /// Get text parts with their thought signatures from the response
390    pub fn text_with_thoughts(&self) -> Vec<(String, bool, Option<&String>)> {
391        self.candidates
392            .iter()
393            .flat_map(|c| {
394                c.content
395                    .parts
396                    .as_ref()
397                    .map(|parts| {
398                        parts
399                            .iter()
400                            .filter_map(|p| match p {
401                                Part::Text {
402                                    text,
403                                    thought,
404                                    thought_signature,
405                                } => Some((
406                                    text.clone(),
407                                    thought.unwrap_or(false),
408                                    thought_signature.as_ref(),
409                                )),
410                                _ => None,
411                            })
412                            .collect::<Vec<_>>()
413                    })
414                    .unwrap_or_default()
415            })
416            .collect()
417    }
418}
419
420/// Request to generate content
421#[deprecated(
422    since = "1.8.0",
423    note = "Use crate::interactions::CreateInteractionRequest instead. See migration guide: interactions-api/migration-plan.md"
424)]
425#[derive(Debug, Clone, Serialize, Deserialize)]
426#[serde(rename_all = "camelCase")]
427pub struct GenerateContentRequest {
428    /// The contents to generate content from
429    pub contents: Vec<Content>,
430    /// The generation config
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub generation_config: Option<GenerationConfig>,
433    /// The safety settings
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub safety_settings: Option<Vec<SafetySetting>>,
436    /// The tools that the model can use
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub tools: Option<Vec<crate::tools::Tool>>,
439    /// The tool config
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub tool_config: Option<crate::tools::ToolConfig>,
442    /// The system instruction
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub system_instruction: Option<Content>,
445    /// The cached content to use
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub cached_content: Option<String>,
448}
449
450/// Thinking level for Gemini 3 series models
451///
452/// Controls the depth of reasoning and analysis that the model applies.
453#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
454#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
455pub enum ThinkingLevel {
456    /// Unspecified thinking level (uses model default)
457    ThinkingLevelUnspecified,
458    /// Minimal thinking level - fastest responses with minimal reasoning
459    Minimal,
460    /// Low thinking level - faster responses with less reasoning
461    Low,
462    /// Medium thinking level - balanced reasoning depth
463    Medium,
464    /// High thinking level - deeper analysis with more comprehensive reasoning
465    High,
466}
467
468/// Media resolution level for images and PDFs
469///
470/// Controls the resolution used when processing inline images and PDF documents,
471/// which affects both quality and token consumption.
472#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
473#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
474pub enum MediaResolutionLevel {
475    /// Unspecified resolution (uses model default)
476    MediaResolutionUnspecified,
477    /// Low resolution - uses fewer tokens, lower quality
478    MediaResolutionLow,
479    /// Medium resolution - balanced token usage and quality
480    MediaResolutionMedium,
481    /// High resolution - uses more tokens, higher quality
482    MediaResolutionHigh,
483}
484
485/// Wrapper struct for per-part media resolution.
486/// Allows fine-grained control over the resolution used for individual inline images and PDFs.
487#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
488pub struct MediaResolution {
489    /// The media resolution level to use
490    pub level: MediaResolutionLevel,
491}
492
493/// Configuration for thinking (Gemini 2.5 and Gemini 3 series)
494///
495/// - For Gemini 2.5 models, use `thinking_budget` and `include_thoughts`.
496/// - For Gemini 3 models, use `thinking_level` (mutually exclusive with `thinking_budget`).
497#[derive(Debug, Clone, Serialize, Deserialize)]
498#[serde(rename_all = "camelCase")]
499pub struct ThinkingConfig {
500    /// The thinking budget (number of thinking tokens)
501    ///
502    /// - Set to 0 to disable thinking
503    /// - Set to -1 for dynamic thinking (model decides)
504    /// - Set to a positive number for a specific token budget
505    ///
506    /// Model-specific ranges:
507    /// - 2.5 Pro: 128 to 32768 (cannot disable thinking)
508    /// - 2.5 Flash: 0 to 24576
509    /// - 2.5 Flash Lite: 512 to 24576
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub thinking_budget: Option<i32>,
512
513    /// Whether to include thought summaries in the response
514    ///
515    /// When enabled, the response will include synthesized versions of the model's
516    /// raw thoughts, providing insights into the reasoning process.
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub include_thoughts: Option<bool>,
519
520    /// The thinking level (Required for Gemini 3)
521    ///
522    /// Gemini 3 uses thinking_level (Low/High) which is mutually exclusive with thinking_budget
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub thinking_level: Option<ThinkingLevel>,
525}
526
527impl ThinkingConfig {
528    /// Create a new thinking config with default settings
529    pub fn new() -> Self {
530        Self {
531            thinking_budget: None,
532            include_thoughts: None,
533            thinking_level: None,
534        }
535    }
536
537    /// Set the thinking budget
538    pub fn with_thinking_budget(mut self, budget: i32) -> Self {
539        self.thinking_budget = Some(budget);
540        self
541    }
542
543    /// Enable dynamic thinking (model decides the budget)
544    pub fn with_dynamic_thinking(mut self) -> Self {
545        self.thinking_budget = Some(-1);
546        self
547    }
548
549    /// Include thought summaries in the response
550    pub fn with_thoughts_included(mut self, include: bool) -> Self {
551        self.include_thoughts = Some(include);
552        self
553    }
554
555    /// Set the thinking level (Required for Gemini 3)
556    pub fn with_thinking_level(mut self, level: ThinkingLevel) -> Self {
557        self.thinking_level = Some(level);
558        self
559    }
560
561    /// Create a thinking config that enables dynamic thinking with thoughts included
562    pub fn dynamic_thinking() -> Self {
563        Self {
564            thinking_budget: Some(-1),
565            include_thoughts: Some(true),
566            thinking_level: None,
567        }
568    }
569}
570
571impl Default for ThinkingConfig {
572    fn default() -> Self {
573        Self::new()
574    }
575}
576
577/// Configuration for generation
578#[deprecated(
579    since = "1.8.0",
580    note = "Use crate::interactions::InteractionGenerationConfig instead"
581)]
582#[derive(Debug, Default, Clone, Serialize, Deserialize)]
583#[serde(rename_all = "camelCase")]
584pub struct GenerationConfig {
585    /// The temperature for the model (0.0 to 1.0)
586    ///
587    /// Controls the randomness of the output. Higher values (e.g., 0.9) make output
588    /// more random, lower values (e.g., 0.1) make output more deterministic.
589    #[serde(skip_serializing_if = "Option::is_none")]
590    pub temperature: Option<f32>,
591
592    /// The top-p value for the model (0.0 to 1.0)
593    ///
594    /// For each token generation step, the model considers the top_p percentage of
595    /// probability mass for potential token choices. Lower values are more selective,
596    /// higher values allow more variety.
597    #[serde(skip_serializing_if = "Option::is_none")]
598    pub top_p: Option<f32>,
599
600    /// The top-k value for the model
601    ///
602    /// For each token generation step, the model considers the top_k most likely tokens.
603    /// Lower values are more selective, higher values allow more variety.
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub top_k: Option<i32>,
606
607    /// Seed used in decoding.
608    ///
609    /// By default, the model uses a random value for each request if a seed is not provided.
610    /// Setting a specific seed, along with consistent values for other parameters like temperature, can make the model return the same response for repeated requests with the same input.
611    /// Identical outputs are not guaranteed across all runs, due to backend infrastructure variations, but it provides a "best effort" for reproducibility.
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub seed: Option<i32>,
614
615    /// The maximum number of tokens to generate
616    ///
617    /// Limits the length of the generated content. One token is roughly 4 characters.
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub max_output_tokens: Option<i32>,
620
621    /// The candidate count
622    ///
623    /// Number of alternative responses to generate.
624    #[serde(skip_serializing_if = "Option::is_none")]
625    pub candidate_count: Option<i32>,
626
627    /// Whether to stop on specific sequences
628    ///
629    /// The model will stop generating content when it encounters any of these sequences.
630    #[serde(skip_serializing_if = "Option::is_none")]
631    pub stop_sequences: Option<Vec<String>>,
632
633    /// The response mime type
634    ///
635    /// Specifies the format of the model's response.
636    #[serde(skip_serializing_if = "Option::is_none")]
637    pub response_mime_type: Option<String>,
638    /// The response schema
639    ///
640    /// Specifies the JSON schema for structured responses.
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub response_schema: Option<serde_json::Value>,
643
644    /// The response JSON schema (strict mode).
645    ///
646    /// Prefer this field for modern Gemini models that support JSON Schema natively.
647    #[serde(skip_serializing_if = "Option::is_none")]
648    pub response_json_schema: Option<serde_json::Value>,
649
650    /// Response modalities (for TTS and other multimodal outputs)
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub response_modalities: Option<Vec<String>>,
653
654    /// Optional. Config for image generation. An error will be returned if this field is set for models
655    /// that don't support these config options.
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub image_config: Option<ImageConfig>,
658
659    /// Speech configuration for text-to-speech generation
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub speech_config: Option<SpeechConfig>,
662
663    /// The thinking configuration
664    ///
665    /// Configuration for the model's thinking process (Gemini 2.5 and Gemini 3 series).
666    #[serde(skip_serializing_if = "Option::is_none")]
667    pub thinking_config: Option<ThinkingConfig>,
668
669    /// Global media resolution for all images and PDFs.
670    /// Controls the resolution used for inline image and PDF data, affecting token usage.
671    /// Can be overridden per-part using the Part::InlineData media_resolution field.
672    #[serde(skip_serializing_if = "Option::is_none", rename = "media_resolution")]
673    pub media_resolution: Option<MediaResolutionLevel>,
674}
675
676/// Response from the Gemini API for token counting
677#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
678#[serde(rename_all = "camelCase")]
679pub struct CountTokensResponse {
680    /// The total number of tokens counted across all instances.
681    pub total_tokens: u32,
682    /// The total number of tokens in the cached content.
683    #[serde(skip_serializing_if = "Option::is_none")]
684    pub cached_content_token_count: Option<u32>,
685}
686
687/// Config for image generation features.
688#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
689#[serde(rename_all = "camelCase")]
690pub struct ImageConfig {
691    /// Optional. The aspect ratio of the image to generate. Supported aspect ratios: 1:1, 2:3, 3:2, 3:4,
692    /// 4:3, 9:16, 16:9, 21:9.
693    ///
694    /// If not specified, the model will choose a default aspect ratio based on any reference images
695    /// provided.
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub aspect_ratio: Option<String>,
698    /// Optional. Specifies the size of generated images. Supported values are `1K`, `2K`, `4K`. If not
699    /// specified, the model will use default value `1K`.
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub image_size: Option<String>,
702}
703
704/// Configuration for speech generation (text-to-speech)
705#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
706#[serde(rename_all = "camelCase")]
707pub struct SpeechConfig {
708    /// Single voice configuration
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub voice_config: Option<VoiceConfig>,
711    /// Multi-speaker voice configuration
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub multi_speaker_voice_config: Option<MultiSpeakerVoiceConfig>,
714}
715
716/// Voice configuration for text-to-speech
717#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
718#[serde(rename_all = "camelCase")]
719pub struct VoiceConfig {
720    /// Prebuilt voice configuration
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub prebuilt_voice_config: Option<PrebuiltVoiceConfig>,
723}
724
725/// Prebuilt voice configuration
726#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
727#[serde(rename_all = "camelCase")]
728pub struct PrebuiltVoiceConfig {
729    /// The name of the voice to use
730    pub voice_name: String,
731}
732
733/// Multi-speaker voice configuration
734#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
735#[serde(rename_all = "camelCase")]
736pub struct MultiSpeakerVoiceConfig {
737    /// Configuration for each speaker
738    pub speaker_voice_configs: Vec<SpeakerVoiceConfig>,
739}
740
741/// Configuration for a specific speaker in multi-speaker TTS
742#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
743#[serde(rename_all = "camelCase")]
744pub struct SpeakerVoiceConfig {
745    /// The name of the speaker (must match the name used in the prompt)
746    pub speaker: String,
747    /// Voice configuration for this speaker
748    pub voice_config: VoiceConfig,
749}
750
751impl SpeechConfig {
752    /// Create a new speech config with a single voice
753    pub fn single_voice(voice_name: impl Into<String>) -> Self {
754        Self {
755            voice_config: Some(VoiceConfig {
756                prebuilt_voice_config: Some(PrebuiltVoiceConfig {
757                    voice_name: voice_name.into(),
758                }),
759            }),
760            multi_speaker_voice_config: None,
761        }
762    }
763
764    /// Create a new speech config with multiple speakers
765    pub fn multi_speaker(speakers: Vec<SpeakerVoiceConfig>) -> Self {
766        Self {
767            voice_config: None,
768            multi_speaker_voice_config: Some(MultiSpeakerVoiceConfig {
769                speaker_voice_configs: speakers,
770            }),
771        }
772    }
773}
774
775impl SpeakerVoiceConfig {
776    /// Create a new speaker voice configuration
777    pub fn new(speaker: impl Into<String>, voice_name: impl Into<String>) -> Self {
778        Self {
779            speaker: speaker.into(),
780            voice_config: VoiceConfig {
781                prebuilt_voice_config: Some(PrebuiltVoiceConfig {
782                    voice_name: voice_name.into(),
783                }),
784            },
785        }
786    }
787}