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 visible answer text of the first candidate.
382    ///
383    /// Concatenates every non-thought text part, in order. This is deliberately
384    /// not "the first part": responses that use thinking or built-in tools
385    /// (Google Search, URL context, code execution) commonly lead with thought
386    /// or tool parts and place the answer text in a later part — so reading only
387    /// the first part would return an empty string. Thought parts are excluded;
388    /// use [`text_with_thoughts`](Self::text_with_thoughts) to inspect them.
389    pub fn text(&self) -> String {
390        self.candidates
391            .first()
392            .and_then(|c| c.content.parts.as_ref())
393            .map(|parts| {
394                parts
395                    .iter()
396                    .filter_map(|p| match p {
397                        Part::Text { text, thought, thought_signature: _ }
398                            if !thought.unwrap_or(false) =>
399                        {
400                            Some(text.as_str())
401                        }
402                        _ => None,
403                    })
404                    .collect::<String>()
405            })
406            .unwrap_or_default()
407    }
408
409    /// Get function calls from the response
410    pub fn function_calls(&self) -> Vec<&crate::tools::FunctionCall> {
411        self.candidates
412            .iter()
413            .flat_map(|c| {
414                c.content
415                    .parts
416                    .as_ref()
417                    .map(|parts| {
418                        parts
419                            .iter()
420                            .filter_map(|p| match p {
421                                Part::FunctionCall { function_call, thought_signature: _ } => {
422                                    Some(function_call)
423                                }
424                                _ => None,
425                            })
426                            .collect::<Vec<_>>()
427                    })
428                    .unwrap_or_default()
429            })
430            .collect()
431    }
432
433    /// Get function calls with their thought signatures from the response
434    pub fn function_calls_with_thoughts(
435        &self,
436    ) -> Vec<(&crate::tools::FunctionCall, Option<&String>)> {
437        self.candidates
438            .iter()
439            .flat_map(|c| {
440                c.content
441                    .parts
442                    .as_ref()
443                    .map(|parts| {
444                        parts
445                            .iter()
446                            .filter_map(|p| match p {
447                                Part::FunctionCall { function_call, thought_signature } => {
448                                    Some((function_call, thought_signature.as_ref()))
449                                }
450                                _ => None,
451                            })
452                            .collect::<Vec<_>>()
453                    })
454                    .unwrap_or_default()
455            })
456            .collect()
457    }
458
459    /// Get thought summaries from the response
460    pub fn thoughts(&self) -> Vec<String> {
461        self.candidates
462            .iter()
463            .flat_map(|c| {
464                c.content
465                    .parts
466                    .as_ref()
467                    .map(|parts| {
468                        parts
469                            .iter()
470                            .filter_map(|p| match p {
471                                Part::Text { text, thought: Some(true), thought_signature: _ } => {
472                                    Some(text.clone())
473                                }
474                                _ => None,
475                            })
476                            .collect::<Vec<_>>()
477                    })
478                    .unwrap_or_default()
479            })
480            .collect()
481    }
482
483    /// Get all text parts (both regular text and thoughts)
484    pub fn all_text(&self) -> Vec<(String, bool)> {
485        self.candidates
486            .iter()
487            .flat_map(|c| {
488                c.content
489                    .parts
490                    .as_ref()
491                    .map(|parts| {
492                        parts
493                            .iter()
494                            .filter_map(|p| match p {
495                                Part::Text { text, thought, thought_signature: _ } => {
496                                    Some((text.clone(), thought.unwrap_or(false)))
497                                }
498                                _ => None,
499                            })
500                            .collect::<Vec<_>>()
501                    })
502                    .unwrap_or_default()
503            })
504            .collect()
505    }
506
507    /// Get text parts with their thought signatures from the response
508    pub fn text_with_thoughts(&self) -> Vec<(String, bool, Option<&String>)> {
509        self.candidates
510            .iter()
511            .flat_map(|c| {
512                c.content
513                    .parts
514                    .as_ref()
515                    .map(|parts| {
516                        parts
517                            .iter()
518                            .filter_map(|p| match p {
519                                Part::Text { text, thought, thought_signature } => Some((
520                                    text.clone(),
521                                    thought.unwrap_or(false),
522                                    thought_signature.as_ref(),
523                                )),
524                                _ => None,
525                            })
526                            .collect::<Vec<_>>()
527                    })
528                    .unwrap_or_default()
529            })
530            .collect()
531    }
532}
533
534/// Request to generate content
535#[derive(Debug, Clone, Serialize, Deserialize)]
536#[serde(rename_all = "camelCase")]
537pub struct GenerateContentRequest {
538    /// The contents to generate content from
539    pub contents: Vec<Content>,
540    /// The generation config
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub generation_config: Option<GenerationConfig>,
543    /// The safety settings
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub safety_settings: Option<Vec<SafetySetting>>,
546    /// The tools that the model can use
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub tools: Option<Vec<crate::tools::Tool>>,
549    /// The tool config
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub tool_config: Option<crate::tools::ToolConfig>,
552    /// The system instruction
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub system_instruction: Option<Content>,
555    /// The cached content to use
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub cached_content: Option<String>,
558}
559
560impl GenerateContentRequest {
561    /// Strips fields that Vertex AI does not support.
562    ///
563    /// The Vertex AI surface (`aiplatform.googleapis.com`) rejects
564    /// `includeServerSideToolInvocations` with a 400 error. Vertex AI handles
565    /// built-in tools (Google Search, URL Context, etc.) natively without
566    /// needing this flag. This method clears the flag so the request can be
567    /// sent to Vertex AI without modification.
568    ///
569    /// AI Studio (`generativelanguage.googleapis.com`) requires the flag for
570    /// Gemini 3 models to return `toolCall`/`toolResponse` parts instead of
571    /// silently truncating the response.
572    pub fn strip_vertex_unsupported_fields(&mut self) {
573        if let Some(tc) = &mut self.tool_config {
574            tc.include_server_side_tool_invocations = None;
575        }
576    }
577}
578
579/// Native thinking level for Gemini 3 models.
580///
581/// Controls the amount of reasoning effort the model applies. This is the
582/// Gemini 3 native thinking control — for Gemini 2.5 budget-based thinking,
583/// use [`ThinkingConfig::with_thinking_budget`] instead.
584///
585/// Serializes as lowercase per the Gemini API contract
586/// (e.g., `"low"`, `"high"`).
587///
588/// Available levels (model support and defaults vary by model):
589/// - `Minimal` — matches "no thinking" for most queries; model may still
590///   think minimally for complex coding tasks. Not supported on Gemini 3.1 Pro.
591/// - `Low` — minimizes latency and cost; improved for code and agentic tasks
592///   that require fewer steps.
593/// - `Medium` — balanced thinking for most tasks. Default for Gemini 3.5 Flash.
594/// - `High` — maximizes reasoning depth. Default for Gemini 3 Flash Preview and
595///   Gemini 3.1 Pro.
596///
597/// Note: `temperature`, `top_p`, and `top_k` are no longer recommended for
598/// Gemini 3.x models — their reasoning is tuned for the default sampling
599/// settings. Use `thinking_level` to control reasoning effort instead.
600#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
601#[serde(rename_all = "lowercase")]
602pub enum ThinkingLevel {
603    /// Minimal reasoning effort. Matches "no thinking" for most queries.
604    /// Not supported on Gemini 3.1 Pro.
605    Minimal,
606    /// Low reasoning effort. Best for simple instruction following and chat.
607    Low,
608    /// Medium reasoning effort. Balanced thinking for most tasks.
609    /// Default for Gemini 3.5 Flash.
610    Medium,
611    /// High reasoning effort — maximizes reasoning depth. Default for
612    /// Gemini 3 Flash Preview and Gemini 3.1 Pro.
613    High,
614}
615
616/// Configuration for thinking (Gemini 2.5 and 3 series)
617#[derive(Debug, Clone, Serialize, Deserialize)]
618#[serde(rename_all = "camelCase")]
619pub struct ThinkingConfig {
620    /// The thinking budget (number of thinking tokens)
621    ///
622    /// This is the Gemini 2.5 budget-based thinking control.
623    ///
624    /// - Set to 0 to disable thinking
625    /// - Set to -1 for dynamic thinking (model decides)
626    /// - Set to a positive number for a specific token budget
627    ///
628    /// Model-specific ranges:
629    /// - 2.5 Pro: 128 to 32768 (cannot disable thinking)
630    /// - 2.5 Flash: 0 to 24576
631    /// - 2.5 Flash Lite: 512 to 24576
632    #[serde(skip_serializing_if = "Option::is_none")]
633    pub thinking_budget: Option<i32>,
634
635    /// Whether to include thought summaries in the response
636    ///
637    /// When enabled, the response will include synthesized versions of the model's
638    /// raw thoughts, providing insights into the reasoning process.
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub include_thoughts: Option<bool>,
641
642    /// Native thinking level for Gemini 3 models.
643    ///
644    /// When set, the model uses level-based reasoning instead of a token budget.
645    /// Do not combine with `thinking_budget` — use one or the other.
646    #[serde(skip_serializing_if = "Option::is_none")]
647    pub thinking_level: Option<ThinkingLevel>,
648}
649
650impl ThinkingConfig {
651    /// Validate the thinking configuration.
652    ///
653    /// Returns an error if both `thinking_budget` and `thinking_level` are set,
654    /// since they are mutually exclusive controls (budget for Gemini 2.5, level for Gemini 3).
655    pub fn validate(&self) -> Result<(), String> {
656        if self.thinking_budget.is_some() && self.thinking_level.is_some() {
657            return Err(
658                "thinking_budget and thinking_level are mutually exclusive; use one or the other"
659                    .to_string(),
660            );
661        }
662        Ok(())
663    }
664
665    /// Create a new thinking config with default settings
666    pub fn new() -> Self {
667        Self { thinking_budget: None, include_thoughts: None, thinking_level: None }
668    }
669
670    /// Set the thinking budget (Gemini 2.5 budget-based control)
671    pub fn with_thinking_budget(mut self, budget: i32) -> Self {
672        self.thinking_budget = Some(budget);
673        self
674    }
675
676    /// Enable dynamic thinking (model decides the budget)
677    pub fn with_dynamic_thinking(mut self) -> Self {
678        self.thinking_budget = Some(-1);
679        self
680    }
681
682    /// Include thought summaries in the response
683    pub fn with_thoughts_included(mut self, include: bool) -> Self {
684        self.include_thoughts = Some(include);
685        self
686    }
687
688    /// Set the thinking level (Gemini 3 native level-based control).
689    ///
690    /// This is the preferred control for Gemini 3 models. Do not combine
691    /// with `with_thinking_budget` — use one or the other.
692    pub fn with_thinking_level(mut self, level: ThinkingLevel) -> Self {
693        self.thinking_level = Some(level);
694        self
695    }
696
697    /// Create a thinking config that enables dynamic thinking with thoughts included
698    pub fn dynamic_thinking() -> Self {
699        Self { thinking_budget: Some(-1), include_thoughts: Some(true), thinking_level: None }
700    }
701}
702
703impl Default for ThinkingConfig {
704    fn default() -> Self {
705        Self::new()
706    }
707}
708
709/// Configuration for generation
710#[derive(Debug, Default, Clone, Serialize, Deserialize)]
711#[serde(rename_all = "camelCase")]
712pub struct GenerationConfig {
713    /// The temperature for the model (0.0 to 1.0)
714    ///
715    /// Controls the randomness of the output. Higher values (e.g., 0.9) make output
716    /// more random, lower values (e.g., 0.1) make output more deterministic.
717    #[serde(skip_serializing_if = "Option::is_none")]
718    pub temperature: Option<f32>,
719
720    /// The top-p value for the model (0.0 to 1.0)
721    ///
722    /// For each token generation step, the model considers the top_p percentage of
723    /// probability mass for potential token choices. Lower values are more selective,
724    /// higher values allow more variety.
725    #[serde(skip_serializing_if = "Option::is_none")]
726    pub top_p: Option<f32>,
727
728    /// The top-k value for the model
729    ///
730    /// For each token generation step, the model considers the top_k most likely tokens.
731    /// Lower values are more selective, higher values allow more variety.
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub top_k: Option<i32>,
734
735    /// The maximum number of tokens to generate
736    ///
737    /// Limits the length of the generated content. One token is roughly 4 characters.
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub max_output_tokens: Option<i32>,
740
741    /// The candidate count
742    ///
743    /// Number of alternative responses to generate.
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub candidate_count: Option<i32>,
746
747    /// Whether to stop on specific sequences
748    ///
749    /// The model will stop generating content when it encounters any of these sequences.
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub stop_sequences: Option<Vec<String>>,
752
753    /// The response mime type
754    ///
755    /// Specifies the format of the model's response.
756    #[serde(skip_serializing_if = "Option::is_none")]
757    pub response_mime_type: Option<String>,
758    /// The response schema
759    ///
760    /// Specifies the JSON schema for structured responses.
761    #[serde(skip_serializing_if = "Option::is_none")]
762    pub response_schema: Option<serde_json::Value>,
763
764    /// Response modalities (for TTS and other multimodal outputs)
765    #[serde(skip_serializing_if = "Option::is_none")]
766    pub response_modalities: Option<Vec<String>>,
767
768    /// Speech configuration for text-to-speech generation
769    #[serde(skip_serializing_if = "Option::is_none")]
770    pub speech_config: Option<SpeechConfig>,
771
772    /// The thinking configuration
773    ///
774    /// Configuration for the model's thinking process (Gemini 2.5 series only).
775    #[serde(skip_serializing_if = "Option::is_none")]
776    pub thinking_config: Option<ThinkingConfig>,
777}
778
779impl GenerationConfig {
780    /// Validate the generation configuration.
781    ///
782    /// Returns an error if any parameter is outside its valid range:
783    /// - `temperature`: must be between 0.0 and 2.0
784    /// - `top_p`: must be between 0.0 and 1.0
785    /// - `top_k`: must be positive
786    /// - `max_output_tokens`: must be positive
787    ///
788    /// If `thinking_config` is present, delegates to [`ThinkingConfig::validate`] as well.
789    /// All `None` fields are accepted without error.
790    pub fn validate(&self) -> Result<(), String> {
791        if let Some(t) = self.temperature
792            && !(0.0..=2.0).contains(&t)
793        {
794            return Err("temperature must be between 0.0 and 2.0".to_string());
795        }
796        if let Some(p) = self.top_p
797            && !(0.0..=1.0).contains(&p)
798        {
799            return Err("top_p must be between 0.0 and 1.0".to_string());
800        }
801        if let Some(k) = self.top_k
802            && k <= 0
803        {
804            return Err("top_k must be positive".to_string());
805        }
806        if let Some(m) = self.max_output_tokens
807            && m <= 0
808        {
809            return Err("max_output_tokens must be positive".to_string());
810        }
811        if let Some(ref tc) = self.thinking_config {
812            tc.validate()?;
813        }
814        Ok(())
815    }
816}
817
818/// Configuration for speech generation (text-to-speech)
819#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
820#[serde(rename_all = "camelCase")]
821pub struct SpeechConfig {
822    /// Single voice configuration
823    #[serde(skip_serializing_if = "Option::is_none")]
824    pub voice_config: Option<VoiceConfig>,
825    /// Multi-speaker voice configuration
826    #[serde(skip_serializing_if = "Option::is_none")]
827    pub multi_speaker_voice_config: Option<MultiSpeakerVoiceConfig>,
828}
829
830/// Voice configuration for text-to-speech
831#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
832#[serde(rename_all = "camelCase")]
833pub struct VoiceConfig {
834    /// Prebuilt voice configuration
835    #[serde(skip_serializing_if = "Option::is_none")]
836    pub prebuilt_voice_config: Option<PrebuiltVoiceConfig>,
837}
838
839/// Prebuilt voice configuration
840#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
841#[serde(rename_all = "camelCase")]
842pub struct PrebuiltVoiceConfig {
843    /// The name of the voice to use
844    pub voice_name: String,
845}
846
847/// Multi-speaker voice configuration
848#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
849#[serde(rename_all = "camelCase")]
850pub struct MultiSpeakerVoiceConfig {
851    /// Configuration for each speaker
852    pub speaker_voice_configs: Vec<SpeakerVoiceConfig>,
853}
854
855/// Configuration for a specific speaker in multi-speaker TTS
856#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
857#[serde(rename_all = "camelCase")]
858pub struct SpeakerVoiceConfig {
859    /// The name of the speaker (must match the name used in the prompt)
860    pub speaker: String,
861    /// Voice configuration for this speaker
862    pub voice_config: VoiceConfig,
863}
864
865impl SpeechConfig {
866    /// Create a new speech config with a single voice
867    pub fn single_voice(voice_name: impl Into<String>) -> Self {
868        Self {
869            voice_config: Some(VoiceConfig {
870                prebuilt_voice_config: Some(PrebuiltVoiceConfig { voice_name: voice_name.into() }),
871            }),
872            multi_speaker_voice_config: None,
873        }
874    }
875
876    /// Create a new speech config with multiple speakers
877    pub fn multi_speaker(speakers: Vec<SpeakerVoiceConfig>) -> Self {
878        Self {
879            voice_config: None,
880            multi_speaker_voice_config: Some(MultiSpeakerVoiceConfig {
881                speaker_voice_configs: speakers,
882            }),
883        }
884    }
885}
886
887impl SpeakerVoiceConfig {
888    /// Create a new speaker voice configuration
889    pub fn new(speaker: impl Into<String>, voice_name: impl Into<String>) -> Self {
890        Self {
891            speaker: speaker.into(),
892            voice_config: VoiceConfig {
893                prebuilt_voice_config: Some(PrebuiltVoiceConfig { voice_name: voice_name.into() }),
894            },
895        }
896    }
897}