Skip to main content

adk_gemini/generation/
model.rs

1use reqwest::Url;
2use serde::{Deserialize, Serialize, de};
3use time::OffsetDateTime;
4
5use crate::{
6    Content, Modality, Part,
7    safety::{SafetyRating, SafetySetting},
8};
9
10/// Reason why generation finished
11#[derive(Debug, Clone, Serialize, 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 the response was blocked by Model Armor.
37    ModelArmor,
38    /// Token generation stopped because generated images contain safety violations.
39    ImageSafety,
40    /// Model generated a tool call but no tools were enabled in the request.
41    UnexpectedToolCall,
42    /// Model called too many tools consecutively, thus the system exited execution.
43    TooManyToolCalls,
44}
45
46impl FinishReason {
47    fn from_wire_str(value: &str) -> Self {
48        match value {
49            "FINISH_REASON_UNSPECIFIED" => Self::FinishReasonUnspecified,
50            "STOP" => Self::Stop,
51            "MAX_TOKENS" => Self::MaxTokens,
52            "SAFETY" => Self::Safety,
53            "RECITATION" => Self::Recitation,
54            "LANGUAGE" => Self::Language,
55            "OTHER" => Self::Other,
56            "BLOCKLIST" => Self::Blocklist,
57            "PROHIBITED_CONTENT" => Self::ProhibitedContent,
58            "SPII" => Self::Spii,
59            "MALFORMED_FUNCTION_CALL" => Self::MalformedFunctionCall,
60            "MODEL_ARMOR" => Self::ModelArmor,
61            "IMAGE_SAFETY" => Self::ImageSafety,
62            "UNEXPECTED_TOOL_CALL" => Self::UnexpectedToolCall,
63            "TOO_MANY_TOOL_CALLS" => Self::TooManyToolCalls,
64            _ => Self::Other,
65        }
66    }
67
68    fn from_wire_number(value: i64) -> Self {
69        match value {
70            0 => Self::FinishReasonUnspecified,
71            1 => Self::Stop,
72            2 => Self::MaxTokens,
73            3 => Self::Safety,
74            4 => Self::Recitation,
75            5 => Self::Other,
76            6 => Self::Blocklist,
77            7 => Self::ProhibitedContent,
78            8 => Self::Spii,
79            9 => Self::MalformedFunctionCall,
80            10 => Self::ModelArmor,
81            _ => Self::Other,
82        }
83    }
84}
85
86impl<'de> Deserialize<'de> for FinishReason {
87    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
88    where
89        D: serde::Deserializer<'de>,
90    {
91        let value = serde_json::Value::deserialize(deserializer)?;
92        match value {
93            serde_json::Value::String(s) => Ok(Self::from_wire_str(&s)),
94            serde_json::Value::Number(n) => {
95                n.as_i64().map(Self::from_wire_number).ok_or_else(|| {
96                    de::Error::custom("finishReason must be an integer-compatible number")
97                })
98            }
99            _ => Err(de::Error::custom("finishReason must be a string or integer")),
100        }
101    }
102}
103
104/// Citation metadata for content
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
106#[serde(rename_all = "camelCase")]
107pub struct CitationMetadata {
108    /// The citation sources
109    #[serde(default)]
110    pub citation_sources: Vec<CitationSource>,
111}
112
113/// Citation source
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
115#[serde(rename_all = "camelCase")]
116pub struct CitationSource {
117    /// The URI of the citation source
118    pub uri: Option<String>,
119    /// The title of the citation source
120    pub title: Option<String>,
121    /// The start index of the citation in the response
122    pub start_index: Option<i32>,
123    /// The end index of the citation in the response
124    pub end_index: Option<i32>,
125    /// The license of the citation source
126    pub license: Option<String>,
127    /// The publication date of the citation source
128    #[serde(default, with = "time::serde::rfc3339::option")]
129    pub publication_date: Option<OffsetDateTime>,
130}
131
132/// A candidate response
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134#[serde(rename_all = "camelCase")]
135pub struct Candidate {
136    /// The content of the candidate
137    #[serde(default)]
138    pub content: Content,
139    /// The safety ratings for the candidate
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub safety_ratings: Option<Vec<SafetyRating>>,
142    /// The citation metadata for the candidate
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub citation_metadata: Option<CitationMetadata>,
145    /// The grounding metadata for the candidate
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub grounding_metadata: Option<GroundingMetadata>,
148    /// The finish reason for the candidate
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub finish_reason: Option<FinishReason>,
151    /// The index of the candidate
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub index: Option<i32>,
154}
155
156/// Metadata about token usage
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158#[serde(rename_all = "camelCase")]
159pub struct UsageMetadata {
160    /// The number of prompt tokens (null if request processing failed)
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub prompt_token_count: Option<i32>,
163    /// The number of response tokens (null if generation failed)
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub candidates_token_count: Option<i32>,
166    /// The total number of tokens (null if individual counts unavailable)
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub total_token_count: Option<i32>,
169    /// The number of thinking tokens (Gemini 2.5 series only)
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub thoughts_token_count: Option<i32>,
172    /// Detailed prompt token information
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub prompt_tokens_details: Option<Vec<PromptTokenDetails>>,
175    /// The number of cached content tokens (batch API)
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub cached_content_token_count: Option<i32>,
178    /// Detailed cache token information (batch API)
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub cache_tokens_details: Option<Vec<PromptTokenDetails>>,
181}
182
183/// Details about prompt tokens by modality
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
185#[serde(rename_all = "camelCase")]
186pub struct PromptTokenDetails {
187    /// The modality (e.g., "TEXT")
188    pub modality: Modality,
189    /// Token count for this modality
190    pub token_count: i32,
191}
192
193/// Grounding metadata for responses that use grounding tools
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
195#[serde(rename_all = "camelCase")]
196pub struct GroundingMetadata {
197    /// Grounding chunks containing source information
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub grounding_chunks: Option<Vec<GroundingChunk>>,
200    /// Grounding supports connecting response text to sources
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub grounding_supports: Option<Vec<GroundingSupport>>,
203    /// Web search queries used for grounding
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub web_search_queries: Option<Vec<String>>,
206    /// Google Maps widget context token
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub google_maps_widget_context_token: Option<String>,
209}
210
211/// A chunk of grounding information from a source
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
213#[serde(rename_all = "camelCase")]
214pub struct GroundingChunk {
215    /// Maps-specific grounding information
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub maps: Option<MapsGroundingChunk>,
218    /// Web-specific grounding information
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub web: Option<WebGroundingChunk>,
221}
222
223/// Maps-specific grounding chunk information
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
225#[serde(rename_all = "camelCase")]
226pub struct MapsGroundingChunk {
227    /// The URI of the Maps source
228    #[serde(default)]
229    pub uri: Option<Url>,
230    /// The title of the Maps source
231    #[serde(default)]
232    pub title: Option<String>,
233    /// The place ID from Google Maps
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub place_id: Option<String>,
236}
237
238/// Web-specific grounding chunk information
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
240#[serde(rename_all = "camelCase")]
241pub struct WebGroundingChunk {
242    /// The URI of the web source
243    #[serde(default)]
244    pub uri: Option<Url>,
245    /// The title of the web source
246    #[serde(default)]
247    pub title: Option<String>,
248}
249
250/// Support information connecting response text to grounding sources
251#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
252#[serde(rename_all = "camelCase")]
253pub struct GroundingSupport {
254    /// Segment of the response text
255    pub segment: GroundingSegment,
256    /// Indices of grounding chunks that support this segment
257    pub grounding_chunk_indices: Vec<u32>,
258}
259
260/// A segment of response text
261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
262#[serde(rename_all = "camelCase")]
263pub struct GroundingSegment {
264    /// Start index of the segment in the response text
265    #[serde(default)]
266    pub start_index: Option<u32>,
267    /// End index of the segment in the response text
268    #[serde(default)]
269    pub end_index: Option<u32>,
270    /// The text content of the segment
271    #[serde(default)]
272    pub text: Option<String>,
273}
274
275/// Response from the Gemini API for content generation
276#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
277#[serde(rename_all = "camelCase")]
278pub struct GenerationResponse {
279    /// The candidates generated
280    #[serde(default, skip_serializing_if = "Vec::is_empty")]
281    pub candidates: Vec<Candidate>,
282    /// The prompt feedback
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub prompt_feedback: Option<PromptFeedback>,
285    /// Usage metadata
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub usage_metadata: Option<UsageMetadata>,
288    /// Model version used
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub model_version: Option<String>,
291    /// Response ID
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub response_id: Option<String>,
294}
295
296/// Reason why content was blocked
297#[derive(Debug, Clone, Serialize, PartialEq)]
298#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
299pub enum BlockReason {
300    /// Default value. This value is unused.
301    BlockReasonUnspecified,
302    /// Prompt was blocked due to safety reasons. Inspect safetyRatings to understand which safety category blocked it.
303    Safety,
304    /// Prompt was blocked due to unknown reasons.
305    Other,
306    /// Prompt was blocked due to the terms which are included from the terminology blocklist.
307    Blocklist,
308    /// Prompt was blocked due to prohibited content.
309    ProhibitedContent,
310    /// Prompt was blocked by Model Armor.
311    ModelArmor,
312    /// Prompt was blocked due to jailbreak detection.
313    Jailbreak,
314    /// Candidates blocked due to unsafe image generation content.
315    ImageSafety,
316}
317
318impl BlockReason {
319    fn from_wire_str(value: &str) -> Self {
320        match value {
321            "BLOCK_REASON_UNSPECIFIED" | "BLOCKED_REASON_UNSPECIFIED" => {
322                Self::BlockReasonUnspecified
323            }
324            "SAFETY" => Self::Safety,
325            "OTHER" => Self::Other,
326            "BLOCKLIST" => Self::Blocklist,
327            "PROHIBITED_CONTENT" => Self::ProhibitedContent,
328            "MODEL_ARMOR" => Self::ModelArmor,
329            "JAILBREAK" => Self::Jailbreak,
330            "IMAGE_SAFETY" => Self::ImageSafety,
331            _ => Self::Other,
332        }
333    }
334
335    fn from_wire_number(value: i64) -> Self {
336        match value {
337            0 => Self::BlockReasonUnspecified,
338            1 => Self::Safety,
339            2 => Self::Other,
340            3 => Self::Blocklist,
341            4 => Self::ProhibitedContent,
342            5 => Self::ModelArmor,
343            6 => Self::Jailbreak,
344            7 => Self::ImageSafety,
345            _ => Self::Other,
346        }
347    }
348}
349
350impl<'de> Deserialize<'de> for BlockReason {
351    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
352    where
353        D: serde::Deserializer<'de>,
354    {
355        let value = serde_json::Value::deserialize(deserializer)?;
356        match value {
357            serde_json::Value::String(s) => Ok(Self::from_wire_str(&s)),
358            serde_json::Value::Number(n) => {
359                n.as_i64().map(Self::from_wire_number).ok_or_else(|| {
360                    de::Error::custom("blockReason must be an integer-compatible number")
361                })
362            }
363            _ => Err(de::Error::custom("blockReason must be a string or integer")),
364        }
365    }
366}
367
368/// Feedback about the prompt
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
370#[serde(rename_all = "camelCase")]
371pub struct PromptFeedback {
372    /// The safety ratings for the prompt
373    #[serde(default, skip_serializing_if = "Vec::is_empty")]
374    pub safety_ratings: Vec<SafetyRating>,
375    /// The block reason if the prompt was blocked
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub block_reason: Option<BlockReason>,
378}
379
380impl GenerationResponse {
381    /// Get the text of the first candidate
382    pub fn text(&self) -> String {
383        self.candidates
384            .first()
385            .and_then(|c| {
386                c.content.parts.as_ref().and_then(|parts| {
387                    parts.first().and_then(|p| match p {
388                        Part::Text { text, thought: _, thought_signature: _ } => Some(text.clone()),
389                        _ => None,
390                    })
391                })
392            })
393            .unwrap_or_default()
394    }
395
396    /// Get function calls from the response
397    pub fn function_calls(&self) -> Vec<&crate::tools::FunctionCall> {
398        self.candidates
399            .iter()
400            .flat_map(|c| {
401                c.content
402                    .parts
403                    .as_ref()
404                    .map(|parts| {
405                        parts
406                            .iter()
407                            .filter_map(|p| match p {
408                                Part::FunctionCall { function_call, thought_signature: _ } => {
409                                    Some(function_call)
410                                }
411                                _ => None,
412                            })
413                            .collect::<Vec<_>>()
414                    })
415                    .unwrap_or_default()
416            })
417            .collect()
418    }
419
420    /// Get function calls with their thought signatures from the response
421    pub fn function_calls_with_thoughts(
422        &self,
423    ) -> Vec<(&crate::tools::FunctionCall, Option<&String>)> {
424        self.candidates
425            .iter()
426            .flat_map(|c| {
427                c.content
428                    .parts
429                    .as_ref()
430                    .map(|parts| {
431                        parts
432                            .iter()
433                            .filter_map(|p| match p {
434                                Part::FunctionCall { function_call, thought_signature } => {
435                                    Some((function_call, thought_signature.as_ref()))
436                                }
437                                _ => None,
438                            })
439                            .collect::<Vec<_>>()
440                    })
441                    .unwrap_or_default()
442            })
443            .collect()
444    }
445
446    /// Get thought summaries from the response
447    pub fn thoughts(&self) -> Vec<String> {
448        self.candidates
449            .iter()
450            .flat_map(|c| {
451                c.content
452                    .parts
453                    .as_ref()
454                    .map(|parts| {
455                        parts
456                            .iter()
457                            .filter_map(|p| match p {
458                                Part::Text { text, thought: Some(true), thought_signature: _ } => {
459                                    Some(text.clone())
460                                }
461                                _ => None,
462                            })
463                            .collect::<Vec<_>>()
464                    })
465                    .unwrap_or_default()
466            })
467            .collect()
468    }
469
470    /// Get all text parts (both regular text and thoughts)
471    pub fn all_text(&self) -> Vec<(String, bool)> {
472        self.candidates
473            .iter()
474            .flat_map(|c| {
475                c.content
476                    .parts
477                    .as_ref()
478                    .map(|parts| {
479                        parts
480                            .iter()
481                            .filter_map(|p| match p {
482                                Part::Text { text, thought, thought_signature: _ } => {
483                                    Some((text.clone(), thought.unwrap_or(false)))
484                                }
485                                _ => None,
486                            })
487                            .collect::<Vec<_>>()
488                    })
489                    .unwrap_or_default()
490            })
491            .collect()
492    }
493
494    /// Get text parts with their thought signatures from the response
495    pub fn text_with_thoughts(&self) -> Vec<(String, bool, Option<&String>)> {
496        self.candidates
497            .iter()
498            .flat_map(|c| {
499                c.content
500                    .parts
501                    .as_ref()
502                    .map(|parts| {
503                        parts
504                            .iter()
505                            .filter_map(|p| match p {
506                                Part::Text { text, thought, thought_signature } => Some((
507                                    text.clone(),
508                                    thought.unwrap_or(false),
509                                    thought_signature.as_ref(),
510                                )),
511                                _ => None,
512                            })
513                            .collect::<Vec<_>>()
514                    })
515                    .unwrap_or_default()
516            })
517            .collect()
518    }
519}
520
521/// Request to generate content
522#[derive(Debug, Clone, Serialize, Deserialize)]
523#[serde(rename_all = "camelCase")]
524pub struct GenerateContentRequest {
525    /// The contents to generate content from
526    pub contents: Vec<Content>,
527    /// The generation config
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub generation_config: Option<GenerationConfig>,
530    /// The safety settings
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub safety_settings: Option<Vec<SafetySetting>>,
533    /// The tools that the model can use
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub tools: Option<Vec<crate::tools::Tool>>,
536    /// The tool config
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub tool_config: Option<crate::tools::ToolConfig>,
539    /// The system instruction
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub system_instruction: Option<Content>,
542    /// The cached content to use
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub cached_content: Option<String>,
545}
546
547impl GenerateContentRequest {
548    /// Strips fields that Vertex AI does not support.
549    ///
550    /// The Vertex AI surface (`aiplatform.googleapis.com`) rejects
551    /// `includeServerSideToolInvocations` with a 400 error. Vertex AI handles
552    /// built-in tools (Google Search, URL Context, etc.) natively without
553    /// needing this flag. This method clears the flag so the request can be
554    /// sent to Vertex AI without modification.
555    ///
556    /// AI Studio (`generativelanguage.googleapis.com`) requires the flag for
557    /// Gemini 3 models to return `toolCall`/`toolResponse` parts instead of
558    /// silently truncating the response.
559    pub fn strip_vertex_unsupported_fields(&mut self) {
560        if let Some(tc) = &mut self.tool_config {
561            tc.include_server_side_tool_invocations = None;
562        }
563    }
564}
565
566/// Native thinking level for Gemini 3 models.
567///
568/// Controls the amount of reasoning effort the model applies. This is the
569/// Gemini 3 native thinking control — for Gemini 2.5 budget-based thinking,
570/// use [`ThinkingConfig::with_thinking_budget`] instead.
571///
572/// Serializes as lowercase per the Gemini API contract
573/// (e.g., `"low"`, `"high"`).
574///
575/// Available levels (model support varies):
576/// - `Minimal` — matches "no thinking" for most queries; model may still
577///   think minimally for complex coding tasks. Not supported on Gemini 3.1 Pro.
578/// - `Low` — minimizes latency and cost; best for simple tasks.
579/// - `Medium` — balanced thinking for most tasks.
580/// - `High` — maximizes reasoning depth (default for Gemini 3 Flash and 3.1 Pro).
581#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
582#[serde(rename_all = "lowercase")]
583pub enum ThinkingLevel {
584    /// Minimal reasoning effort. Matches "no thinking" for most queries.
585    /// Not supported on Gemini 3.1 Pro.
586    Minimal,
587    /// Low reasoning effort. Best for simple instruction following and chat.
588    Low,
589    /// Medium reasoning effort. Balanced thinking for most tasks.
590    Medium,
591    /// High reasoning effort — maximizes reasoning depth (default).
592    High,
593}
594
595/// Configuration for thinking (Gemini 2.5 and 3 series)
596#[derive(Debug, Clone, Serialize, Deserialize)]
597#[serde(rename_all = "camelCase")]
598pub struct ThinkingConfig {
599    /// The thinking budget (number of thinking tokens)
600    ///
601    /// This is the Gemini 2.5 budget-based thinking control.
602    ///
603    /// - Set to 0 to disable thinking
604    /// - Set to -1 for dynamic thinking (model decides)
605    /// - Set to a positive number for a specific token budget
606    ///
607    /// Model-specific ranges:
608    /// - 2.5 Pro: 128 to 32768 (cannot disable thinking)
609    /// - 2.5 Flash: 0 to 24576
610    /// - 2.5 Flash Lite: 512 to 24576
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub thinking_budget: Option<i32>,
613
614    /// Whether to include thought summaries in the response
615    ///
616    /// When enabled, the response will include synthesized versions of the model's
617    /// raw thoughts, providing insights into the reasoning process.
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub include_thoughts: Option<bool>,
620
621    /// Native thinking level for Gemini 3 models.
622    ///
623    /// When set, the model uses level-based reasoning instead of a token budget.
624    /// Do not combine with `thinking_budget` — use one or the other.
625    #[serde(skip_serializing_if = "Option::is_none")]
626    pub thinking_level: Option<ThinkingLevel>,
627}
628
629impl ThinkingConfig {
630    /// Validate the thinking configuration.
631    ///
632    /// Returns an error if both `thinking_budget` and `thinking_level` are set,
633    /// since they are mutually exclusive controls (budget for Gemini 2.5, level for Gemini 3).
634    pub fn validate(&self) -> Result<(), String> {
635        if self.thinking_budget.is_some() && self.thinking_level.is_some() {
636            return Err(
637                "thinking_budget and thinking_level are mutually exclusive; use one or the other"
638                    .to_string(),
639            );
640        }
641        Ok(())
642    }
643
644    /// Create a new thinking config with default settings
645    pub fn new() -> Self {
646        Self { thinking_budget: None, include_thoughts: None, thinking_level: None }
647    }
648
649    /// Set the thinking budget (Gemini 2.5 budget-based control)
650    pub fn with_thinking_budget(mut self, budget: i32) -> Self {
651        self.thinking_budget = Some(budget);
652        self
653    }
654
655    /// Enable dynamic thinking (model decides the budget)
656    pub fn with_dynamic_thinking(mut self) -> Self {
657        self.thinking_budget = Some(-1);
658        self
659    }
660
661    /// Include thought summaries in the response
662    pub fn with_thoughts_included(mut self, include: bool) -> Self {
663        self.include_thoughts = Some(include);
664        self
665    }
666
667    /// Set the thinking level (Gemini 3 native level-based control).
668    ///
669    /// This is the preferred control for Gemini 3 models. Do not combine
670    /// with `with_thinking_budget` — use one or the other.
671    pub fn with_thinking_level(mut self, level: ThinkingLevel) -> Self {
672        self.thinking_level = Some(level);
673        self
674    }
675
676    /// Create a thinking config that enables dynamic thinking with thoughts included
677    pub fn dynamic_thinking() -> Self {
678        Self { thinking_budget: Some(-1), include_thoughts: Some(true), thinking_level: None }
679    }
680}
681
682impl Default for ThinkingConfig {
683    fn default() -> Self {
684        Self::new()
685    }
686}
687
688/// Configuration for generation
689#[derive(Debug, Default, Clone, Serialize, Deserialize)]
690#[serde(rename_all = "camelCase")]
691pub struct GenerationConfig {
692    /// The temperature for the model (0.0 to 1.0)
693    ///
694    /// Controls the randomness of the output. Higher values (e.g., 0.9) make output
695    /// more random, lower values (e.g., 0.1) make output more deterministic.
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub temperature: Option<f32>,
698
699    /// The top-p value for the model (0.0 to 1.0)
700    ///
701    /// For each token generation step, the model considers the top_p percentage of
702    /// probability mass for potential token choices. Lower values are more selective,
703    /// higher values allow more variety.
704    #[serde(skip_serializing_if = "Option::is_none")]
705    pub top_p: Option<f32>,
706
707    /// The top-k value for the model
708    ///
709    /// For each token generation step, the model considers the top_k most likely tokens.
710    /// Lower values are more selective, higher values allow more variety.
711    #[serde(skip_serializing_if = "Option::is_none")]
712    pub top_k: Option<i32>,
713
714    /// The maximum number of tokens to generate
715    ///
716    /// Limits the length of the generated content. One token is roughly 4 characters.
717    #[serde(skip_serializing_if = "Option::is_none")]
718    pub max_output_tokens: Option<i32>,
719
720    /// The candidate count
721    ///
722    /// Number of alternative responses to generate.
723    #[serde(skip_serializing_if = "Option::is_none")]
724    pub candidate_count: Option<i32>,
725
726    /// Whether to stop on specific sequences
727    ///
728    /// The model will stop generating content when it encounters any of these sequences.
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub stop_sequences: Option<Vec<String>>,
731
732    /// The response mime type
733    ///
734    /// Specifies the format of the model's response.
735    #[serde(skip_serializing_if = "Option::is_none")]
736    pub response_mime_type: Option<String>,
737    /// The response schema
738    ///
739    /// Specifies the JSON schema for structured responses.
740    #[serde(skip_serializing_if = "Option::is_none")]
741    pub response_schema: Option<serde_json::Value>,
742
743    /// Response modalities (for TTS and other multimodal outputs)
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub response_modalities: Option<Vec<String>>,
746
747    /// Speech configuration for text-to-speech generation
748    #[serde(skip_serializing_if = "Option::is_none")]
749    pub speech_config: Option<SpeechConfig>,
750
751    /// The thinking configuration
752    ///
753    /// Configuration for the model's thinking process (Gemini 2.5 series only).
754    #[serde(skip_serializing_if = "Option::is_none")]
755    pub thinking_config: Option<ThinkingConfig>,
756}
757
758impl GenerationConfig {
759    /// Validate the generation configuration.
760    ///
761    /// Returns an error if any parameter is outside its valid range:
762    /// - `temperature`: must be between 0.0 and 2.0
763    /// - `top_p`: must be between 0.0 and 1.0
764    /// - `top_k`: must be positive
765    /// - `max_output_tokens`: must be positive
766    ///
767    /// If `thinking_config` is present, delegates to [`ThinkingConfig::validate`] as well.
768    /// All `None` fields are accepted without error.
769    pub fn validate(&self) -> Result<(), String> {
770        if let Some(t) = self.temperature
771            && !(0.0..=2.0).contains(&t)
772        {
773            return Err("temperature must be between 0.0 and 2.0".to_string());
774        }
775        if let Some(p) = self.top_p
776            && !(0.0..=1.0).contains(&p)
777        {
778            return Err("top_p must be between 0.0 and 1.0".to_string());
779        }
780        if let Some(k) = self.top_k
781            && k <= 0
782        {
783            return Err("top_k must be positive".to_string());
784        }
785        if let Some(m) = self.max_output_tokens
786            && m <= 0
787        {
788            return Err("max_output_tokens must be positive".to_string());
789        }
790        if let Some(ref tc) = self.thinking_config {
791            tc.validate()?;
792        }
793        Ok(())
794    }
795}
796
797/// Configuration for speech generation (text-to-speech)
798#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
799#[serde(rename_all = "camelCase")]
800pub struct SpeechConfig {
801    /// Single voice configuration
802    #[serde(skip_serializing_if = "Option::is_none")]
803    pub voice_config: Option<VoiceConfig>,
804    /// Multi-speaker voice configuration
805    #[serde(skip_serializing_if = "Option::is_none")]
806    pub multi_speaker_voice_config: Option<MultiSpeakerVoiceConfig>,
807}
808
809/// Voice configuration for text-to-speech
810#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
811#[serde(rename_all = "camelCase")]
812pub struct VoiceConfig {
813    /// Prebuilt voice configuration
814    #[serde(skip_serializing_if = "Option::is_none")]
815    pub prebuilt_voice_config: Option<PrebuiltVoiceConfig>,
816}
817
818/// Prebuilt voice configuration
819#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
820#[serde(rename_all = "camelCase")]
821pub struct PrebuiltVoiceConfig {
822    /// The name of the voice to use
823    pub voice_name: String,
824}
825
826/// Multi-speaker voice configuration
827#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
828#[serde(rename_all = "camelCase")]
829pub struct MultiSpeakerVoiceConfig {
830    /// Configuration for each speaker
831    pub speaker_voice_configs: Vec<SpeakerVoiceConfig>,
832}
833
834/// Configuration for a specific speaker in multi-speaker TTS
835#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
836#[serde(rename_all = "camelCase")]
837pub struct SpeakerVoiceConfig {
838    /// The name of the speaker (must match the name used in the prompt)
839    pub speaker: String,
840    /// Voice configuration for this speaker
841    pub voice_config: VoiceConfig,
842}
843
844impl SpeechConfig {
845    /// Create a new speech config with a single voice
846    pub fn single_voice(voice_name: impl Into<String>) -> Self {
847        Self {
848            voice_config: Some(VoiceConfig {
849                prebuilt_voice_config: Some(PrebuiltVoiceConfig { voice_name: voice_name.into() }),
850            }),
851            multi_speaker_voice_config: None,
852        }
853    }
854
855    /// Create a new speech config with multiple speakers
856    pub fn multi_speaker(speakers: Vec<SpeakerVoiceConfig>) -> Self {
857        Self {
858            voice_config: None,
859            multi_speaker_voice_config: Some(MultiSpeakerVoiceConfig {
860                speaker_voice_configs: speakers,
861            }),
862        }
863    }
864}
865
866impl SpeakerVoiceConfig {
867    /// Create a new speaker voice configuration
868    pub fn new(speaker: impl Into<String>, voice_name: impl Into<String>) -> Self {
869        Self {
870            speaker: speaker.into(),
871            voice_config: VoiceConfig {
872                prebuilt_voice_config: Some(PrebuiltVoiceConfig { voice_name: voice_name.into() }),
873            },
874        }
875    }
876}