Skip to main content

genai_rs/
request.rs

1//! Request types for creating interactions.
2
3use serde::de::{self, Visitor};
4use serde::ser::SerializeMap;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use std::fmt;
7
8use crate::content::Content;
9use crate::environment::EnvironmentSpec;
10use crate::response_format::ResponseFormatSpec;
11use crate::safety::SafetySetting;
12use crate::steps::Step;
13use crate::tools::{Tool, ToolChoice};
14use crate::webhooks::WebhookConfig;
15
16/// Role in a conversation turn.
17///
18/// Indicates whether the content came from the user or the model.
19///
20/// This enum is marked `#[non_exhaustive]` for forward compatibility.
21/// New roles may be added in future API versions.
22///
23/// # Evergreen Pattern
24///
25/// Unknown values from the API deserialize into the `Unknown` variant, preserving
26/// the original data for debugging and roundtrip serialization.
27///
28/// # Example
29///
30/// ```
31/// use genai_rs::Role;
32///
33/// let role = Role::User;
34/// assert!(matches!(role, Role::User));
35/// ```
36#[derive(Clone, Debug, PartialEq)]
37#[non_exhaustive]
38pub enum Role {
39    /// Content from the user
40    User,
41    /// Content from the model
42    Model,
43    /// Unknown variant for forward compatibility (Evergreen pattern)
44    Unknown {
45        /// The unrecognized role type from the API
46        role_type: String,
47        /// The raw JSON value, preserved for debugging and roundtrip
48        data: serde_json::Value,
49    },
50}
51
52impl Role {
53    /// Returns true if this is an unknown role.
54    #[must_use]
55    pub const fn is_unknown(&self) -> bool {
56        matches!(self, Self::Unknown { .. })
57    }
58
59    /// Returns the role type name if this is an unknown role.
60    #[must_use]
61    pub fn unknown_role_type(&self) -> Option<&str> {
62        match self {
63            Self::Unknown { role_type, .. } => Some(role_type),
64            _ => None,
65        }
66    }
67
68    /// Returns the preserved data if this is an unknown role.
69    #[must_use]
70    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
71        match self {
72            Self::Unknown { data, .. } => Some(data),
73            _ => None,
74        }
75    }
76}
77
78impl fmt::Display for Role {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Self::User => write!(f, "user"),
82            Self::Model => write!(f, "model"),
83            Self::Unknown { role_type, .. } => write!(f, "{}", role_type),
84        }
85    }
86}
87
88impl Serialize for Role {
89    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
90    where
91        S: Serializer,
92    {
93        match self {
94            Role::User => serializer.serialize_str("user"),
95            Role::Model => serializer.serialize_str("model"),
96            Role::Unknown { role_type, .. } => serializer.serialize_str(role_type),
97        }
98    }
99}
100
101impl<'de> Deserialize<'de> for Role {
102    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103    where
104        D: Deserializer<'de>,
105    {
106        let s = String::deserialize(deserializer)?;
107        match s.as_str() {
108            "user" => Ok(Role::User),
109            "model" => Ok(Role::Model),
110            other => {
111                tracing::warn!(
112                    "Encountered unknown Role '{}' - using Unknown variant (Evergreen)",
113                    other
114                );
115                Ok(Role::Unknown {
116                    role_type: other.to_string(),
117                    data: serde_json::Value::String(other.to_string()),
118                })
119            }
120        }
121    }
122}
123
124/// Content for a conversation turn.
125///
126/// Can be simple text or an array of content parts for multimodal turns.
127///
128/// # Example
129///
130/// ```
131/// use genai_rs::TurnContent;
132///
133/// // Simple text
134/// let content = TurnContent::Text("Hello!".to_string());
135///
136/// // From string reference
137/// let content: TurnContent = "Hello!".into();
138/// ```
139// Note: Unlike tagged enums (e.g., Content), this untagged enum cannot
140// have an Unknown variant. Untagged enums have no type discriminator field, so Serde
141// tries variants in order - there's no way to detect "unknown" content. The
142// #[non_exhaustive] attribute provides forward compatibility at the Rust level by
143// preventing exhaustive matches.
144#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
145#[serde(untagged)]
146#[non_exhaustive]
147pub enum TurnContent {
148    /// Simple text content
149    Text(String),
150    /// Array of content parts (for multimodal content)
151    Parts(Vec<Content>),
152}
153
154impl From<String> for TurnContent {
155    fn from(s: String) -> Self {
156        Self::Text(s)
157    }
158}
159
160impl From<&str> for TurnContent {
161    fn from(s: &str) -> Self {
162        Self::Text(s.to_string())
163    }
164}
165
166impl From<Vec<Content>> for TurnContent {
167    fn from(parts: Vec<Content>) -> Self {
168        Self::Parts(parts)
169    }
170}
171
172impl TurnContent {
173    /// Returns the text content if this is a `Text` variant.
174    #[must_use]
175    pub fn as_text(&self) -> Option<&str> {
176        match self {
177            Self::Text(t) => Some(t),
178            Self::Parts(_) => None,
179        }
180    }
181
182    /// Returns the content parts if this is a `Parts` variant.
183    #[must_use]
184    pub fn as_parts(&self) -> Option<&[Content]> {
185        match self {
186            Self::Parts(p) => Some(p),
187            Self::Text(_) => None,
188        }
189    }
190
191    /// Returns `true` if this is text content.
192    #[must_use]
193    pub const fn is_text(&self) -> bool {
194        matches!(self, Self::Text(_))
195    }
196
197    /// Returns `true` if this is parts content.
198    #[must_use]
199    pub const fn is_parts(&self) -> bool {
200        matches!(self, Self::Parts(_))
201    }
202}
203
204/// Input for an interaction - a simple string, an array of content blocks,
205/// or an array of steps (conversation history).
206///
207/// This enum is marked `#[non_exhaustive]` for forward compatibility.
208/// New input types may be added in future versions.
209///
210/// # Variants
211///
212/// - `Text`: Simple text input for single-turn conversations
213/// - `Content`: Array of content blocks for multimodal input
214/// - `Steps`: Array of [`Step`]s — the canonical multi-turn/history form
215///   under API revision 2026-05-20 (replaces the deprecated `Turn` array)
216///
217/// # Example
218///
219/// ```
220/// use genai_rs::{InteractionInput, Step};
221///
222/// // Simple text
223/// let input = InteractionInput::Text("Hello!".to_string());
224///
225/// // Multi-turn conversation history as steps
226/// let steps = vec![
227///     Step::user_text("What is 2+2?"),
228///     Step::model_text("2+2 equals 4."),
229///     Step::user_text("And what's that times 3?"),
230/// ];
231/// let input = InteractionInput::Steps(steps);
232/// ```
233#[derive(Clone, Debug, PartialEq)]
234#[non_exhaustive]
235pub enum InteractionInput {
236    /// Simple text input
237    Text(String),
238    /// Array of content blocks (single-turn multimodal input)
239    Content(Vec<Content>),
240    /// Array of steps (multi-turn conversation history, function results,
241    /// thought signatures, ...)
242    Steps(Vec<Step>),
243}
244
245impl Default for InteractionInput {
246    /// An empty text input — the zero value for struct-literal
247    /// construction of [`InteractionRequest`]; replace it with real input
248    /// before sending.
249    fn default() -> Self {
250        Self::Text(String::new())
251    }
252}
253
254impl Serialize for InteractionInput {
255    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
256    where
257        S: Serializer,
258    {
259        match self {
260            Self::Text(t) => serializer.serialize_str(t),
261            Self::Content(c) => c.serialize(serializer),
262            Self::Steps(s) => s.serialize(serializer),
263        }
264    }
265}
266
267/// The set of `type` tags that identify a content block (as opposed to a step).
268const CONTENT_TYPE_TAGS: &[&str] = &["text", "image", "audio", "video", "document"];
269
270impl<'de> Deserialize<'de> for InteractionInput {
271    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
272    where
273        D: Deserializer<'de>,
274    {
275        let value = serde_json::Value::deserialize(deserializer)?;
276        input_from_value(value).map_err(serde::de::Error::custom)
277    }
278}
279
280/// The by-`Value` body of [`InteractionInput`]'s deserializer, shared with
281/// the lenient trigger-side probe so neither path buffers the tree twice.
282pub(crate) fn input_from_value(value: serde_json::Value) -> Result<InteractionInput, String> {
283    match value {
284        serde_json::Value::String(s) => Ok(InteractionInput::Text(s)),
285        serde_json::Value::Array(items) => {
286            // Decide between [Content] and [Step] by inspecting element
287            // type tags. Elements with content tags (text/image/...) are
288            // content blocks; everything else (user_input, function_call,
289            // unknown future types, ...) is treated as steps, the
290            // canonical revision 2026-05-20 form.
291            let is_content = items.iter().all(|item| {
292                item.get("type")
293                    .and_then(|t| t.as_str())
294                    .is_some_and(|t| CONTENT_TYPE_TAGS.contains(&t))
295            });
296            if is_content && !items.is_empty() {
297                let contents = serde_json::from_value(serde_json::Value::Array(items))
298                    .map_err(|e| e.to_string())?;
299                Ok(InteractionInput::Content(contents))
300            } else {
301                let steps = serde_json::from_value(serde_json::Value::Array(items))
302                    .map_err(|e| e.to_string())?;
303                Ok(InteractionInput::Steps(steps))
304            }
305        }
306        other @ serde_json::Value::Object(_) => {
307            // A single content or step object.
308            let is_content = other
309                .get("type")
310                .and_then(|t| t.as_str())
311                .is_some_and(|t| CONTENT_TYPE_TAGS.contains(&t));
312            if is_content {
313                let content: Content = serde_json::from_value(other).map_err(|e| e.to_string())?;
314                Ok(InteractionInput::Content(vec![content]))
315            } else {
316                let step: Step = serde_json::from_value(other).map_err(|e| e.to_string())?;
317                Ok(InteractionInput::Steps(vec![step]))
318            }
319        }
320        other => Err(format!(
321            "InteractionInput must be a string, array, or object; got {other}"
322        )),
323    }
324}
325
326/// Thinking level for chain-of-thought reasoning.
327///
328/// Controls the depth of reasoning the model performs before generating a response.
329/// Higher levels produce more detailed reasoning but consume more tokens.
330///
331/// This enum is marked `#[non_exhaustive]` for forward compatibility.
332/// New thinking levels may be added in future versions.
333///
334/// # Evergreen Pattern
335///
336/// Unknown values from the API deserialize into the `Unknown` variant, preserving
337/// the original data for debugging and roundtrip serialization.
338#[derive(Clone, Debug, PartialEq, Eq)]
339#[non_exhaustive]
340pub enum ThinkingLevel {
341    /// Minimal reasoning, fastest responses.
342    ///
343    /// Not supported by every model: [`DEFAULT_MODEL`](crate::DEFAULT_MODEL)
344    /// rejects it with a 400 (verified live 2026-08-15). Use
345    /// [`MINIMAL_THINKING_MODEL`](crate::MINIMAL_THINKING_MODEL), which is
346    /// pinned to a model that accepts it.
347    Minimal,
348    /// Light reasoning for simple problems
349    Low,
350    /// Balanced reasoning for moderate complexity
351    Medium,
352    /// Extensive reasoning for complex problems
353    High,
354    /// Unknown variant for forward compatibility (Evergreen pattern)
355    Unknown {
356        /// The unrecognized level type from the API
357        level_type: String,
358        /// The full JSON data, preserved for debugging and roundtrip serialization
359        data: serde_json::Value,
360    },
361}
362
363impl ThinkingLevel {
364    /// Returns true if this is an unknown thinking level.
365    #[must_use]
366    pub const fn is_unknown(&self) -> bool {
367        matches!(self, Self::Unknown { .. })
368    }
369
370    /// Returns the level type name if this is an unknown thinking level.
371    #[must_use]
372    pub fn unknown_level_type(&self) -> Option<&str> {
373        match self {
374            Self::Unknown { level_type, .. } => Some(level_type),
375            _ => None,
376        }
377    }
378
379    /// Returns the preserved data if this is an unknown thinking level.
380    #[must_use]
381    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
382        match self {
383            Self::Unknown { data, .. } => Some(data),
384            _ => None,
385        }
386    }
387}
388
389impl Serialize for ThinkingLevel {
390    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
391    where
392        S: Serializer,
393    {
394        match self {
395            ThinkingLevel::Minimal => serializer.serialize_str("minimal"),
396            ThinkingLevel::Low => serializer.serialize_str("low"),
397            ThinkingLevel::Medium => serializer.serialize_str("medium"),
398            ThinkingLevel::High => serializer.serialize_str("high"),
399            ThinkingLevel::Unknown { level_type, data } => {
400                // If data is a simple string, serialize just the level_type
401                if data.is_string() || data.is_null() {
402                    serializer.serialize_str(level_type)
403                } else {
404                    // For complex data, serialize as an object
405                    let mut map = serializer.serialize_map(None)?;
406                    map.serialize_entry("level", level_type)?;
407                    map.serialize_entry("data", data)?;
408                    map.end()
409                }
410            }
411        }
412    }
413}
414
415impl<'de> Deserialize<'de> for ThinkingLevel {
416    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
417    where
418        D: Deserializer<'de>,
419    {
420        deserializer.deserialize_any(ThinkingLevelVisitor)
421    }
422}
423
424struct ThinkingLevelVisitor;
425
426impl<'de> Visitor<'de> for ThinkingLevelVisitor {
427    type Value = ThinkingLevel;
428
429    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
430        formatter.write_str("a thinking level string or object")
431    }
432
433    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
434    where
435        E: de::Error,
436    {
437        match value {
438            "minimal" => Ok(ThinkingLevel::Minimal),
439            "low" => Ok(ThinkingLevel::Low),
440            "medium" => Ok(ThinkingLevel::Medium),
441            "high" => Ok(ThinkingLevel::High),
442            other => {
443                tracing::warn!(
444                    "Encountered unknown ThinkingLevel '{}' - using Unknown variant (Evergreen)",
445                    other
446                );
447                Ok(ThinkingLevel::Unknown {
448                    level_type: other.to_string(),
449                    data: serde_json::Value::String(other.to_string()),
450                })
451            }
452        }
453    }
454
455    fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
456    where
457        A: de::MapAccess<'de>,
458    {
459        // For object-based thinking levels (future API compatibility)
460        let value: serde_json::Value =
461            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;
462        let level_type = value
463            .get("level")
464            .and_then(|v| v.as_str())
465            .unwrap_or("unknown")
466            .to_string();
467
468        tracing::warn!(
469            "Encountered unknown ThinkingLevel object '{}' - using Unknown variant (Evergreen)",
470            level_type
471        );
472        Ok(ThinkingLevel::Unknown {
473            level_type,
474            data: value,
475        })
476    }
477}
478
479/// Generation configuration for model behavior
480#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq)]
481pub struct GenerationConfig {
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub temperature: Option<f32>,
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub max_output_tokens: Option<i32>,
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub top_p: Option<f32>,
488    /// Thinking level for chain-of-thought reasoning
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub thinking_level: Option<ThinkingLevel>,
491    /// Seed for deterministic output generation.
492    ///
493    /// Using the same seed with identical inputs will produce the same output,
494    /// useful for testing and debugging.
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub seed: Option<i64>,
497    /// Stop sequences that halt generation.
498    ///
499    /// When the model generates any of these sequences, generation stops immediately.
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub stop_sequences: Option<Vec<String>>,
502    /// Controls whether thinking summaries are included in output.
503    ///
504    /// Use with `thinking_level` to control reasoning output.
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub thinking_summaries: Option<ThinkingSummaries>,
507    /// Controls function calling behavior.
508    ///
509    /// Either a plain mode string (`auto|any|none|validated`) or an
510    /// `allowed_tools` restriction object. See [`ToolChoice`].
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub tool_choice: Option<ToolChoice>,
513    /// Positive values penalize tokens that already appeared in the text,
514    /// increasing the likelihood of new topics. Range: [-2.0, 2.0].
515    #[serde(skip_serializing_if = "Option::is_none")]
516    pub presence_penalty: Option<f32>,
517    /// Positive values penalize tokens proportionally to their frequency in
518    /// the text so far, reducing repetition. Range: [-2.0, 2.0].
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub frequency_penalty: Option<f32>,
521    /// Speech configuration for text-to-speech audio output.
522    ///
523    /// Required when using the `audio` response modality. The wire format is
524    /// a **list** of speaker configurations: a single entry for single-voice
525    /// TTS, multiple entries (each with a distinct `speaker` name matching
526    /// the prompt) for multi-speaker TTS.
527    ///
528    /// A legacy single-object wire form is still accepted on deserialize.
529    #[serde(
530        default,
531        skip_serializing_if = "Option::is_none",
532        deserialize_with = "deserialize_speech_configs"
533    )]
534    pub speech_config: Option<Vec<SpeechConfig>>,
535    /// Image generation configuration.
536    ///
537    /// Controls aspect ratio and size for image generation output.
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub image_config: Option<ImageConfig>,
540    /// Video generation configuration.
541    ///
542    /// Controls the video generation task mode when using the `video`
543    /// response modality.
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub video_config: Option<VideoConfig>,
546    /// Audio transcription configuration.
547    ///
548    /// Controls language hints, diarization, custom vocabulary and
549    /// timestamp granularity when transcribing audio input.
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub transcription_config: Option<TranscriptionConfig>,
552}
553
554/// Audio transcription configuration.
555///
556/// Set via
557/// [`GenerationConfig::transcription_config`] to control how audio input is
558/// transcribed.
559///
560/// # Example
561///
562/// ```
563/// use genai_rs::TranscriptionConfig;
564///
565/// // Prefer the builder over a struct literal: the struct is
566/// // constructible, so literals break when fields are added (see the
567/// // 0.9.0 CHANGELOG entry).
568/// let config = TranscriptionConfig::new()
569///     .with_language_codes(["en-US"])
570///     .with_diarization_mode("speaker");
571/// assert_eq!(
572///     serde_json::to_value(&config).unwrap(),
573///     serde_json::json!({
574///         "language_codes": ["en-US"],
575///         "diarization_mode": "speaker"
576///     })
577/// );
578/// ```
579#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq)]
580pub struct TranscriptionConfig {
581    /// Phrases to bias recognition toward (contextual adaptation).
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub adaptation_phrases: Option<Vec<String>>,
584    /// Domain-specific vocabulary to bias recognition toward.
585    #[serde(skip_serializing_if = "Option::is_none")]
586    pub custom_vocabulary: Option<Vec<String>>,
587    /// Speaker diarization mode. The SDK spec documents `"speaker"` as the
588    /// only supported value today; kept an open string (Evergreen) so new
589    /// modes work without a crate release.
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub diarization_mode: Option<String>,
592    /// BCP-47 language codes hinting the audio's language(s). Omitted or
593    /// empty means automatic language detection.
594    #[serde(skip_serializing_if = "Option::is_none")]
595    pub language_codes: Option<Vec<String>>,
596    /// Timestamp granularities to include. The SDK spec documents `"word"`
597    /// as the only supported value today (empty = no timestamps); kept an
598    /// open string list (Evergreen).
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub timestamp_granularities: Option<Vec<String>>,
601}
602
603impl TranscriptionConfig {
604    /// Create a new transcription configuration with default settings.
605    #[must_use]
606    pub fn new() -> Self {
607        Self::default()
608    }
609
610    /// Set the BCP-47 language hints (omitted means automatic detection).
611    #[must_use]
612    pub fn with_language_codes(
613        mut self,
614        language_codes: impl IntoIterator<Item = impl Into<String>>,
615    ) -> Self {
616        self.language_codes = Some(language_codes.into_iter().map(Into::into).collect());
617        self
618    }
619
620    /// Set the diarization mode (`"speaker"` is the only value the SDK
621    /// spec documents today; open string per the Evergreen posture).
622    #[must_use]
623    pub fn with_diarization_mode(mut self, mode: impl Into<String>) -> Self {
624        self.diarization_mode = Some(mode.into());
625        self
626    }
627
628    /// Set the timestamp granularities (`"word"` is the only value the SDK
629    /// spec documents today; open string list per the Evergreen posture).
630    #[must_use]
631    pub fn with_timestamp_granularities(
632        mut self,
633        granularities: impl IntoIterator<Item = impl Into<String>>,
634    ) -> Self {
635        self.timestamp_granularities = Some(granularities.into_iter().map(Into::into).collect());
636        self
637    }
638
639    /// Set the domain-specific vocabulary to bias recognition toward,
640    /// replacing any set earlier.
641    #[must_use]
642    pub fn with_custom_vocabulary(
643        mut self,
644        vocabulary: impl IntoIterator<Item = impl Into<String>>,
645    ) -> Self {
646        self.custom_vocabulary = Some(vocabulary.into_iter().map(Into::into).collect());
647        self
648    }
649
650    /// Set the contextual-adaptation phrases, replacing any set earlier.
651    ///
652    /// This is the one `Vec` field here with an accumulating `add_*`
653    /// companion — phrases are the incremental-assembly case; the other
654    /// list fields take a pre-built `Vec` via their `with_*` setter.
655    #[must_use]
656    pub fn with_adaptation_phrases(
657        mut self,
658        phrases: impl IntoIterator<Item = impl Into<String>>,
659    ) -> Self {
660        self.adaptation_phrases = Some(phrases.into_iter().map(Into::into).collect());
661        self
662    }
663
664    /// Add a single contextual-adaptation phrase, accumulating with any
665    /// added earlier.
666    #[must_use]
667    pub fn add_adaptation_phrase(mut self, phrase: impl Into<String>) -> Self {
668        self.adaptation_phrases
669            .get_or_insert_with(Vec::new)
670            .push(phrase.into());
671        self
672    }
673}
674
675/// Deserializes `speech_config` from either the spec list form or the legacy
676/// single-object form.
677fn deserialize_speech_configs<'de, D>(
678    deserializer: D,
679) -> Result<Option<Vec<SpeechConfig>>, D::Error>
680where
681    D: Deserializer<'de>,
682{
683    #[derive(Deserialize)]
684    #[serde(untagged)]
685    enum ListOrSingle {
686        List(Vec<SpeechConfig>),
687        Single(SpeechConfig),
688    }
689
690    Ok(
691        Option::<ListOrSingle>::deserialize(deserializer)?.map(|value| match value {
692            ListOrSingle::List(list) => list,
693            ListOrSingle::Single(single) => vec![single],
694        }),
695    )
696}
697
698/// Speech configuration for text-to-speech audio output.
699///
700/// Configure voice, language, and speaker settings when using the `audio` response modality.
701///
702/// # Example
703///
704/// ```
705/// use genai_rs::SpeechConfig;
706///
707/// let config = SpeechConfig {
708///     voice: Some("Kore".to_string()),
709///     language: Some("en-US".to_string()),
710///     speaker: None,
711/// };
712/// ```
713///
714/// # Available Voices
715///
716/// Common voices include: Aoede, Charon, Fenrir, Kore, Puck, and others.
717/// See [Google's TTS documentation](https://ai.google.dev/gemini-api/docs/text-generation)
718/// for the full list of available voices.
719#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq)]
720pub struct SpeechConfig {
721    /// The voice to use for speech synthesis.
722    ///
723    /// Examples: "Kore", "Puck", "Charon", "Fenrir", "Aoede"
724    #[serde(skip_serializing_if = "Option::is_none")]
725    pub voice: Option<String>,
726
727    /// The language/locale for speech synthesis.
728    ///
729    /// Examples: "en-US", "es-ES", "fr-FR"
730    #[serde(skip_serializing_if = "Option::is_none")]
731    pub language: Option<String>,
732
733    /// The speaker name for multi-speaker scenarios.
734    ///
735    /// Should match a speaker name given in the prompt when using
736    /// multi-speaker text-to-speech.
737    #[serde(skip_serializing_if = "Option::is_none")]
738    pub speaker: Option<String>,
739}
740
741impl SpeechConfig {
742    /// Creates a new `SpeechConfig` with the specified voice.
743    #[must_use]
744    pub fn with_voice(voice: impl Into<String>) -> Self {
745        Self {
746            voice: Some(voice.into()),
747            ..Default::default()
748        }
749    }
750
751    /// Creates a new `SpeechConfig` with the specified voice and language.
752    #[must_use]
753    pub fn with_voice_and_language(voice: impl Into<String>, language: impl Into<String>) -> Self {
754        Self {
755            voice: Some(voice.into()),
756            language: Some(language.into()),
757            ..Default::default()
758        }
759    }
760}
761
762/// Configuration for image generation output.
763///
764/// Controls aspect ratio and size when generating images.
765///
766/// # Example
767///
768/// ```
769/// use genai_rs::{ImageConfig, ImageAspectRatio, ImageSize};
770///
771/// let config = ImageConfig {
772///     aspect_ratio: Some(ImageAspectRatio::Square),
773///     image_size: Some(ImageSize::Hd1k),
774/// };
775/// ```
776#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
777#[serde(default)]
778pub struct ImageConfig {
779    /// The aspect ratio for generated images.
780    #[serde(skip_serializing_if = "Option::is_none")]
781    pub aspect_ratio: Option<ImageAspectRatio>,
782    /// The size/resolution for generated images.
783    #[serde(skip_serializing_if = "Option::is_none")]
784    pub image_size: Option<ImageSize>,
785}
786
787/// Aspect ratio for image generation output.
788///
789/// This enum is marked `#[non_exhaustive]` for forward compatibility.
790/// New aspect ratios may be added in future API versions.
791///
792/// # Evergreen Pattern
793///
794/// Unknown values from the API deserialize into the `Unknown` variant, preserving
795/// the original data for debugging and roundtrip serialization.
796///
797/// # Wire Format
798///
799/// Values serialize as string ratios: `"1:1"`, `"16:9"`, etc.
800#[derive(Clone, Debug, PartialEq, Eq)]
801#[non_exhaustive]
802pub enum ImageAspectRatio {
803    /// 1:1 square
804    Square,
805    /// 2:3 portrait
806    Portrait2x3,
807    /// 3:2 landscape
808    Landscape3x2,
809    /// 3:4 portrait
810    Portrait3x4,
811    /// 4:3 landscape
812    Landscape4x3,
813    /// 4:5 portrait
814    Portrait4x5,
815    /// 5:4 landscape
816    Landscape5x4,
817    /// 9:16 tall portrait
818    Portrait9x16,
819    /// 16:9 widescreen
820    Widescreen16x9,
821    /// 21:9 ultrawide
822    Ultrawide21x9,
823    /// 1:8 very tall
824    Tall1x8,
825    /// 8:1 very wide
826    Wide8x1,
827    /// 1:4 tall
828    Tall1x4,
829    /// 4:1 wide
830    Wide4x1,
831    /// Unknown variant for forward compatibility (Evergreen pattern)
832    Unknown {
833        /// The unrecognized ratio type from the API
834        ratio_type: String,
835        /// The full JSON data, preserved for debugging and roundtrip serialization
836        data: serde_json::Value,
837    },
838}
839
840impl ImageAspectRatio {
841    /// Returns true if this is an unknown aspect ratio.
842    #[must_use]
843    pub const fn is_unknown(&self) -> bool {
844        matches!(self, Self::Unknown { .. })
845    }
846
847    /// Returns the ratio type name if this is an unknown aspect ratio.
848    #[must_use]
849    pub fn unknown_ratio_type(&self) -> Option<&str> {
850        match self {
851            Self::Unknown { ratio_type, .. } => Some(ratio_type),
852            _ => None,
853        }
854    }
855
856    /// Returns the preserved data if this is an unknown aspect ratio.
857    #[must_use]
858    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
859        match self {
860            Self::Unknown { data, .. } => Some(data),
861            _ => None,
862        }
863    }
864}
865
866impl Serialize for ImageAspectRatio {
867    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
868    where
869        S: Serializer,
870    {
871        match self {
872            Self::Square => serializer.serialize_str("1:1"),
873            Self::Portrait2x3 => serializer.serialize_str("2:3"),
874            Self::Landscape3x2 => serializer.serialize_str("3:2"),
875            Self::Portrait3x4 => serializer.serialize_str("3:4"),
876            Self::Landscape4x3 => serializer.serialize_str("4:3"),
877            Self::Portrait4x5 => serializer.serialize_str("4:5"),
878            Self::Landscape5x4 => serializer.serialize_str("5:4"),
879            Self::Portrait9x16 => serializer.serialize_str("9:16"),
880            Self::Widescreen16x9 => serializer.serialize_str("16:9"),
881            Self::Ultrawide21x9 => serializer.serialize_str("21:9"),
882            Self::Tall1x8 => serializer.serialize_str("1:8"),
883            Self::Wide8x1 => serializer.serialize_str("8:1"),
884            Self::Tall1x4 => serializer.serialize_str("1:4"),
885            Self::Wide4x1 => serializer.serialize_str("4:1"),
886            Self::Unknown { ratio_type, .. } => serializer.serialize_str(ratio_type),
887        }
888    }
889}
890
891impl<'de> Deserialize<'de> for ImageAspectRatio {
892    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
893    where
894        D: Deserializer<'de>,
895    {
896        let value = serde_json::Value::deserialize(deserializer)?;
897        match value.as_str() {
898            Some("1:1") => Ok(Self::Square),
899            Some("2:3") => Ok(Self::Portrait2x3),
900            Some("3:2") => Ok(Self::Landscape3x2),
901            Some("3:4") => Ok(Self::Portrait3x4),
902            Some("4:3") => Ok(Self::Landscape4x3),
903            Some("4:5") => Ok(Self::Portrait4x5),
904            Some("5:4") => Ok(Self::Landscape5x4),
905            Some("9:16") => Ok(Self::Portrait9x16),
906            Some("16:9") => Ok(Self::Widescreen16x9),
907            Some("21:9") => Ok(Self::Ultrawide21x9),
908            Some("1:8") => Ok(Self::Tall1x8),
909            Some("8:1") => Ok(Self::Wide8x1),
910            Some("1:4") => Ok(Self::Tall1x4),
911            Some("4:1") => Ok(Self::Wide4x1),
912            Some(other) => {
913                tracing::warn!(
914                    "Encountered unknown ImageAspectRatio '{}'. \
915                     Preserving in Unknown variant.",
916                    other
917                );
918                Ok(Self::Unknown {
919                    ratio_type: other.to_string(),
920                    data: value,
921                })
922            }
923            None => {
924                let ratio_type = format!("<non-string: {}>", value);
925                tracing::warn!(
926                    "ImageAspectRatio received non-string value: {}. \
927                     Preserving in Unknown variant.",
928                    value
929                );
930                Ok(Self::Unknown {
931                    ratio_type,
932                    data: value,
933                })
934            }
935        }
936    }
937}
938
939/// Image size/resolution for image generation output.
940///
941/// This enum is marked `#[non_exhaustive]` for forward compatibility.
942/// New sizes may be added in future API versions.
943///
944/// # Evergreen Pattern
945///
946/// Unknown values from the API deserialize into the `Unknown` variant, preserving
947/// the original data for debugging and roundtrip serialization.
948///
949/// # Wire Format
950///
951/// Values serialize as strings: `"512"`, `"1K"`, `"2K"`, `"4K"`.
952#[derive(Clone, Debug, PartialEq, Eq)]
953#[non_exhaustive]
954pub enum ImageSize {
955    /// 512px resolution
956    Sd512,
957    /// 1K resolution
958    Hd1k,
959    /// 2K resolution
960    Hd2k,
961    /// 4K resolution
962    Uhd4k,
963    /// Unknown variant for forward compatibility (Evergreen pattern)
964    Unknown {
965        /// The unrecognized size type from the API
966        size_type: String,
967        /// The full JSON data, preserved for debugging and roundtrip serialization
968        data: serde_json::Value,
969    },
970}
971
972impl ImageSize {
973    /// Returns true if this is an unknown image size.
974    #[must_use]
975    pub const fn is_unknown(&self) -> bool {
976        matches!(self, Self::Unknown { .. })
977    }
978
979    /// Returns the size type name if this is an unknown image size.
980    #[must_use]
981    pub fn unknown_size_type(&self) -> Option<&str> {
982        match self {
983            Self::Unknown { size_type, .. } => Some(size_type),
984            _ => None,
985        }
986    }
987
988    /// Returns the preserved data if this is an unknown image size.
989    #[must_use]
990    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
991        match self {
992            Self::Unknown { data, .. } => Some(data),
993            _ => None,
994        }
995    }
996}
997
998impl Serialize for ImageSize {
999    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1000    where
1001        S: Serializer,
1002    {
1003        match self {
1004            Self::Sd512 => serializer.serialize_str("512"),
1005            Self::Hd1k => serializer.serialize_str("1K"),
1006            Self::Hd2k => serializer.serialize_str("2K"),
1007            Self::Uhd4k => serializer.serialize_str("4K"),
1008            Self::Unknown { size_type, .. } => serializer.serialize_str(size_type),
1009        }
1010    }
1011}
1012
1013impl<'de> Deserialize<'de> for ImageSize {
1014    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1015    where
1016        D: Deserializer<'de>,
1017    {
1018        let value = serde_json::Value::deserialize(deserializer)?;
1019        match value.as_str() {
1020            Some("512") => Ok(Self::Sd512),
1021            Some("1K") => Ok(Self::Hd1k),
1022            Some("2K") => Ok(Self::Hd2k),
1023            Some("4K") => Ok(Self::Uhd4k),
1024            Some(other) => {
1025                tracing::warn!(
1026                    "Encountered unknown ImageSize '{}'. \
1027                     Preserving in Unknown variant.",
1028                    other
1029                );
1030                Ok(Self::Unknown {
1031                    size_type: other.to_string(),
1032                    data: value,
1033                })
1034            }
1035            None => {
1036                let size_type = format!("<non-string: {}>", value);
1037                tracing::warn!(
1038                    "ImageSize received non-string value: {}. \
1039                     Preserving in Unknown variant.",
1040                    value
1041                );
1042                Ok(Self::Unknown {
1043                    size_type,
1044                    data: value,
1045                })
1046            }
1047        }
1048    }
1049}
1050
1051/// Task mode for video generation.
1052///
1053/// If not specified, the model automatically determines the appropriate mode
1054/// based on the provided text prompt and input media.
1055///
1056/// This enum is marked `#[non_exhaustive]` for forward compatibility.
1057///
1058/// # Wire Format
1059///
1060/// Serializes as lowercase snake_case strings: `"text_to_video"`,
1061/// `"image_to_video"`, `"reference_to_video"`, `"edit"`, `"extend"`.
1062/// The full value list was confirmed live (2026-07) via the API's own
1063/// validation error for `generation_config.video_config.task`.
1064///
1065/// # Evergreen Pattern
1066///
1067/// Unknown values from the API deserialize into the `Unknown` variant,
1068/// preserving the original data for debugging and roundtrip serialization.
1069#[derive(Clone, Debug, PartialEq)]
1070#[non_exhaustive]
1071pub enum VideoTask {
1072    /// Generate a video from a text prompt.
1073    TextToVideo,
1074    /// Generate a video from an input image.
1075    ImageToVideo,
1076    /// Generate a video from reference media.
1077    ReferenceToVideo,
1078    /// Edit an existing video.
1079    Edit,
1080    /// Extend an existing video.
1081    Extend,
1082    /// Unknown variant for forward compatibility (Evergreen pattern)
1083    Unknown {
1084        /// The unrecognized task type from the API
1085        task_type: String,
1086        /// The full JSON data, preserved for debugging and roundtrip serialization
1087        data: serde_json::Value,
1088    },
1089}
1090
1091impl VideoTask {
1092    /// Returns true if this is an unknown video task.
1093    #[must_use]
1094    pub const fn is_unknown(&self) -> bool {
1095        matches!(self, Self::Unknown { .. })
1096    }
1097
1098    /// Returns the task type name if this is an unknown video task.
1099    #[must_use]
1100    pub fn unknown_task_type(&self) -> Option<&str> {
1101        match self {
1102            Self::Unknown { task_type, .. } => Some(task_type),
1103            _ => None,
1104        }
1105    }
1106
1107    /// Returns the preserved data if this is an unknown video task.
1108    #[must_use]
1109    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
1110        match self {
1111            Self::Unknown { data, .. } => Some(data),
1112            _ => None,
1113        }
1114    }
1115}
1116
1117impl Serialize for VideoTask {
1118    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1119    where
1120        S: Serializer,
1121    {
1122        match self {
1123            Self::TextToVideo => serializer.serialize_str("text_to_video"),
1124            Self::ImageToVideo => serializer.serialize_str("image_to_video"),
1125            Self::ReferenceToVideo => serializer.serialize_str("reference_to_video"),
1126            Self::Edit => serializer.serialize_str("edit"),
1127            Self::Extend => serializer.serialize_str("extend"),
1128            Self::Unknown { task_type, .. } => serializer.serialize_str(task_type),
1129        }
1130    }
1131}
1132
1133impl<'de> Deserialize<'de> for VideoTask {
1134    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1135    where
1136        D: Deserializer<'de>,
1137    {
1138        let value = serde_json::Value::deserialize(deserializer)?;
1139        match value.as_str() {
1140            Some("text_to_video") => Ok(Self::TextToVideo),
1141            Some("image_to_video") => Ok(Self::ImageToVideo),
1142            Some("reference_to_video") => Ok(Self::ReferenceToVideo),
1143            Some("edit") => Ok(Self::Edit),
1144            Some("extend") => Ok(Self::Extend),
1145            Some(other) => {
1146                tracing::warn!(
1147                    "Encountered unknown VideoTask '{}' - using Unknown variant (Evergreen)",
1148                    other
1149                );
1150                Ok(Self::Unknown {
1151                    task_type: other.to_string(),
1152                    data: value,
1153                })
1154            }
1155            None => {
1156                let task_type = format!("<non-string: {}>", value);
1157                tracing::warn!(
1158                    "VideoTask received non-string value: {}. \
1159                     Preserving in Unknown variant.",
1160                    value
1161                );
1162                Ok(Self::Unknown {
1163                    task_type,
1164                    data: value,
1165                })
1166            }
1167        }
1168    }
1169}
1170
1171/// Configuration for video generation output
1172/// (`generation_config.video_config`).
1173///
1174/// # Example
1175///
1176/// ```
1177/// use genai_rs::{VideoConfig, VideoTask};
1178///
1179/// let config = VideoConfig::new().with_task(VideoTask::TextToVideo);
1180/// ```
1181#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1182#[serde(default)]
1183pub struct VideoConfig {
1184    /// Optional task mode for video generation. When unset, the model picks
1185    /// the mode based on the prompt and input media.
1186    #[serde(skip_serializing_if = "Option::is_none")]
1187    pub task: Option<VideoTask>,
1188}
1189
1190impl VideoConfig {
1191    /// Creates an empty video config (model chooses the task mode).
1192    #[must_use]
1193    pub fn new() -> Self {
1194        Self::default()
1195    }
1196
1197    /// Sets the video generation task mode.
1198    #[must_use]
1199    pub fn with_task(mut self, task: VideoTask) -> Self {
1200        self.task = Some(task);
1201        self
1202    }
1203}
1204
1205/// Request body for the Interactions API endpoint.
1206///
1207/// This type represents a fully-constructed interaction request that can be
1208/// cloned, serialized, and executed via [`Client::execute()`](crate::Client::execute).
1209///
1210/// # Creating Requests
1211///
1212/// Use [`InteractionBuilder::build()`](crate::InteractionBuilder::build) to create requests:
1213///
1214/// ```no_run
1215/// # use genai_rs::Client;
1216/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1217/// let client = Client::new("api_key".to_string());
1218///
1219/// let request = client.interaction()
1220///     .with_model(genai_rs::DEFAULT_MODEL)
1221///     .with_text("Hello!")
1222///     .build()?;
1223///
1224/// // Request can be cloned, serialized, inspected
1225/// let backup = request.clone();
1226/// println!("{}", serde_json::to_string_pretty(&request)?);
1227/// # Ok(())
1228/// # }
1229/// ```
1230///
1231/// # Executing Requests
1232///
1233/// ```no_run
1234/// # use genai_rs::Client;
1235/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1236/// # let client = Client::new("api_key".to_string());
1237/// # let request = client.interaction()
1238/// #     .with_model(genai_rs::DEFAULT_MODEL)
1239/// #     .with_text("Hello!")
1240/// #     .build()?;
1241/// let response = client.execute(request).await?;
1242/// # Ok(())
1243/// # }
1244/// ```
1245///
1246/// # Retrying Requests
1247///
1248/// Since `InteractionRequest` is `Clone`, you can retry failed requests:
1249///
1250/// ```no_run
1251/// # use genai_rs::{Client, GenaiError};
1252/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1253/// # let client = Client::new("api_key".to_string());
1254/// # let request = client.interaction()
1255/// #     .with_model(genai_rs::DEFAULT_MODEL)
1256/// #     .with_text("Hello!")
1257/// #     .build()?;
1258/// let response = loop {
1259///     match client.execute(request.clone()).await {
1260///         Ok(r) => break r,
1261///         Err(e) if e.is_retryable() => continue,
1262///         Err(e) => return Err(e.into()),
1263///     }
1264/// };
1265/// # Ok(())
1266/// # }
1267/// ```
1268#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq)]
1269pub struct InteractionRequest {
1270    /// Model name (e.g. [`DEFAULT_MODEL`](crate::DEFAULT_MODEL)) - mutually exclusive with agent
1271    #[serde(skip_serializing_if = "Option::is_none")]
1272    pub model: Option<String>,
1273
1274    /// Agent name (e.g., "deep-research-pro-preview-12-2025") - mutually exclusive with model
1275    #[serde(skip_serializing_if = "Option::is_none")]
1276    pub agent: Option<String>,
1277
1278    /// Agent-specific configuration (e.g., Deep Research thinking summaries)
1279    #[serde(rename = "agent_config", skip_serializing_if = "Option::is_none")]
1280    pub agent_config: Option<AgentConfig>,
1281
1282    /// The input for this interaction
1283    ///
1284    /// *Required* on deserialize, and strict: an absent, misspelled,
1285    /// malformed, or null `input` (e.g. a typo in a config file feeding
1286    /// [`TriggerCreateParams`](crate::TriggerCreateParams)) is a clean
1287    /// parse error, not a silently scheduled empty prompt. (The
1288    /// strictness covers `input` itself and the other modeled fields'
1289    /// shapes; a misspelled *optional sibling* key in a
1290    /// `TriggerCreateParams` config is absorbed by its flattened `extra`
1291    /// escape hatch — the documented cost of that hatch.) The
1292    /// *response*-side leniency lives on
1293    /// [`Trigger::interaction`](crate::Trigger) instead, where a sparse
1294    /// projection's absent `input` deserializes to empty text and an
1295    /// undeserializable one degrades the same way rather than failing a
1296    /// whole list response. (That path has a roundtrip asymmetry: absence
1297    /// re-serializes as a *present* `input` key — the one spot in the
1298    /// Evergreen surface where a sparse projection gains a field instead
1299    /// of preserving absence.)
1300    pub input: InteractionInput,
1301
1302    /// Reference to a previous interaction for stateful conversations
1303    #[serde(skip_serializing_if = "Option::is_none")]
1304    pub previous_interaction_id: Option<String>,
1305
1306    /// Tools available for function calling
1307    #[serde(skip_serializing_if = "Option::is_none")]
1308    pub tools: Option<Vec<Tool>>,
1309
1310    /// Response modalities (e.g., ["image"]; the API only accepts lowercase)
1311    ///
1312    /// Deprecation signal: the official SDK marks `response_modalities`
1313    /// (and `response_mime_type`, already removed from this crate) as
1314    /// deprecated in favor of the typed
1315    /// [`response_format`](Self::response_format) union — prefer
1316    /// [`ResponseFormatSpec`] for new code. Kept until the API removes it.
1317    #[serde(skip_serializing_if = "Option::is_none")]
1318    pub response_modalities: Option<Vec<String>>,
1319
1320    /// Typed response format(s) for structured/media output.
1321    ///
1322    /// A single [`ResponseFormat`](crate::ResponseFormat) or a list of them
1323    /// (see [`ResponseFormatSpec`]). For the common JSON-schema case use
1324    /// [`InteractionBuilder::with_response_format()`](crate::InteractionBuilder::with_response_format).
1325    #[serde(skip_serializing_if = "Option::is_none")]
1326    pub response_format: Option<ResponseFormatSpec>,
1327
1328    /// Model configuration
1329    #[serde(skip_serializing_if = "Option::is_none")]
1330    pub generation_config: Option<GenerationConfig>,
1331
1332    /// Enable streaming responses
1333    #[serde(skip_serializing_if = "Option::is_none")]
1334    pub stream: Option<bool>,
1335
1336    /// Background execution mode (agents only)
1337    #[serde(skip_serializing_if = "Option::is_none")]
1338    pub background: Option<bool>,
1339
1340    /// Persist interaction data (default: true)
1341    #[serde(skip_serializing_if = "Option::is_none")]
1342    pub store: Option<bool>,
1343
1344    /// System instruction for the model (plain string per the API spec)
1345    #[serde(skip_serializing_if = "Option::is_none")]
1346    pub system_instruction: Option<String>,
1347
1348    /// Latency/priority tier for processing this request.
1349    #[serde(skip_serializing_if = "Option::is_none")]
1350    pub service_tier: Option<ServiceTier>,
1351
1352    /// Name of an explicit context cache to use for this request
1353    /// (e.g., `cachedContents/xyz`).
1354    #[serde(skip_serializing_if = "Option::is_none")]
1355    pub cached_content: Option<String>,
1356
1357    /// Per-request webhook routing: deliver this request's events to the
1358    /// given URIs (instead of the registered webhooks) with optional
1359    /// user metadata echoed on each event.
1360    #[serde(skip_serializing_if = "Option::is_none")]
1361    pub webhook_config: Option<WebhookConfig>,
1362
1363    /// Environment for the interaction: a string environment ID or a typed
1364    /// remote environment (sources + network allowlist).
1365    #[serde(skip_serializing_if = "Option::is_none")]
1366    pub environment: Option<EnvironmentSpec>,
1367
1368    /// Safety settings for the interaction, one per harm category.
1369    ///
1370    /// Server-side constraint (verified live 2026-08-08): the Gemini API
1371    /// rejects `safety_settings` — "not available on the Gemini API but it
1372    /// is available on the Gemini Enterprise Agent Platform" (Vertex-only).
1373    /// The field is modeled for spec parity and forward compatibility.
1374    #[serde(skip_serializing_if = "Option::is_none")]
1375    pub safety_settings: Option<Vec<SafetySetting>>,
1376
1377    /// User-defined metadata labels for the request.
1378    ///
1379    /// Server-side constraint (verified live 2026-08-08): the Gemini API
1380    /// rejects `labels` — "not available on the Gemini API but it is
1381    /// available on the Gemini Enterprise Agent Platform" (Vertex-only).
1382    /// The field is modeled for spec parity and forward compatibility.
1383    ///
1384    /// `BTreeMap` (not `HashMap`) so the serialized key order is
1385    /// deterministic — wire captures and `LOUD_WIRE` diffs of the same
1386    /// logical request stay byte-identical across runs.
1387    #[serde(skip_serializing_if = "Option::is_none")]
1388    pub labels: Option<std::collections::BTreeMap<String, String>>,
1389}
1390
1391/// Latency/priority service tier for a request.
1392///
1393/// This enum is marked `#[non_exhaustive]` for forward compatibility.
1394///
1395/// # Wire Format
1396///
1397/// Serializes as lowercase strings: `"flex"`, `"standard"`, `"priority"`.
1398///
1399/// # Evergreen Pattern
1400///
1401/// Unknown values from the API deserialize into the `Unknown` variant,
1402/// preserving the original data for debugging and roundtrip serialization.
1403#[derive(Clone, Debug, PartialEq)]
1404#[non_exhaustive]
1405pub enum ServiceTier {
1406    /// Flexible latency, lower cost.
1407    Flex,
1408    /// Standard processing.
1409    Standard,
1410    /// Prioritized processing.
1411    Priority,
1412    /// Unknown variant for forward compatibility (Evergreen pattern)
1413    Unknown {
1414        /// The unrecognized tier type from the API
1415        tier_type: String,
1416        /// The raw JSON value, preserved for debugging and roundtrip
1417        data: serde_json::Value,
1418    },
1419}
1420
1421impl ServiceTier {
1422    /// Returns true if this is an unknown service tier.
1423    #[must_use]
1424    pub const fn is_unknown(&self) -> bool {
1425        matches!(self, Self::Unknown { .. })
1426    }
1427
1428    /// Returns the tier type name if this is an unknown service tier.
1429    #[must_use]
1430    pub fn unknown_tier_type(&self) -> Option<&str> {
1431        match self {
1432            Self::Unknown { tier_type, .. } => Some(tier_type),
1433            _ => None,
1434        }
1435    }
1436
1437    /// Returns the preserved data if this is an unknown service tier.
1438    #[must_use]
1439    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
1440        match self {
1441            Self::Unknown { data, .. } => Some(data),
1442            _ => None,
1443        }
1444    }
1445}
1446
1447impl fmt::Display for ServiceTier {
1448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1449        match self {
1450            Self::Flex => write!(f, "flex"),
1451            Self::Standard => write!(f, "standard"),
1452            Self::Priority => write!(f, "priority"),
1453            Self::Unknown { tier_type, .. } => write!(f, "{}", tier_type),
1454        }
1455    }
1456}
1457
1458impl Serialize for ServiceTier {
1459    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1460    where
1461        S: Serializer,
1462    {
1463        match self {
1464            Self::Flex => serializer.serialize_str("flex"),
1465            Self::Standard => serializer.serialize_str("standard"),
1466            Self::Priority => serializer.serialize_str("priority"),
1467            Self::Unknown { tier_type, .. } => serializer.serialize_str(tier_type),
1468        }
1469    }
1470}
1471
1472impl<'de> Deserialize<'de> for ServiceTier {
1473    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1474    where
1475        D: Deserializer<'de>,
1476    {
1477        let value = serde_json::Value::deserialize(deserializer)?;
1478        match value.as_str() {
1479            Some("flex") => Ok(Self::Flex),
1480            Some("standard") => Ok(Self::Standard),
1481            Some("priority") => Ok(Self::Priority),
1482            Some(other) => {
1483                tracing::warn!(
1484                    "Encountered unknown ServiceTier '{}' - using Unknown variant (Evergreen)",
1485                    other
1486                );
1487                Ok(Self::Unknown {
1488                    tier_type: other.to_string(),
1489                    data: value,
1490                })
1491            }
1492            None => {
1493                let tier_type = format!("<non-string: {}>", value);
1494                tracing::warn!(
1495                    "ServiceTier received non-string value: {}. \
1496                     Preserving in Unknown variant.",
1497                    value
1498                );
1499                Ok(Self::Unknown {
1500                    tier_type,
1501                    data: value,
1502                })
1503            }
1504        }
1505    }
1506}
1507
1508// =============================================================================
1509// Agent Configuration Types
1510// =============================================================================
1511
1512/// Thinking summaries configuration for agent output.
1513///
1514/// When using thinking mode (via `with_thinking_level`), you can control
1515/// whether the model's reasoning process is summarized in the output.
1516///
1517/// This enum is marked `#[non_exhaustive]` for forward compatibility.
1518/// New summary modes may be added in future versions.
1519///
1520/// # Evergreen Pattern
1521///
1522/// Unknown values from the API deserialize into the `Unknown` variant, preserving
1523/// the original data for debugging and roundtrip serialization.
1524#[derive(Clone, Debug, PartialEq, Eq)]
1525#[non_exhaustive]
1526pub enum ThinkingSummaries {
1527    /// Automatically include thinking summaries (default when thinking is enabled)
1528    Auto,
1529    /// Do not include thinking summaries
1530    None,
1531    /// Unknown variant for forward compatibility (Evergreen pattern)
1532    Unknown {
1533        /// The unrecognized summaries type from the API
1534        summaries_type: String,
1535        /// The full JSON data, preserved for debugging and roundtrip serialization
1536        data: serde_json::Value,
1537    },
1538}
1539
1540impl ThinkingSummaries {
1541    /// Returns true if this is an unknown thinking summaries value.
1542    #[must_use]
1543    pub const fn is_unknown(&self) -> bool {
1544        matches!(self, Self::Unknown { .. })
1545    }
1546
1547    /// Returns the summaries type name if this is an unknown value.
1548    #[must_use]
1549    pub fn unknown_summaries_type(&self) -> Option<&str> {
1550        match self {
1551            Self::Unknown { summaries_type, .. } => Some(summaries_type),
1552            _ => None,
1553        }
1554    }
1555
1556    /// Returns the preserved data if this is an unknown value.
1557    #[must_use]
1558    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
1559        match self {
1560            Self::Unknown { data, .. } => Some(data),
1561            _ => None,
1562        }
1563    }
1564
1565    /// Convert to the `agent_config` wire format (`"auto"` / `"none"`).
1566    ///
1567    /// This used to emit the SCREAMING_CASE `THINKING_SUMMARIES_*` form,
1568    /// which the API accepted when `DeepResearchConfig` was written.
1569    /// Verified live 2026-08-10, it no longer does:
1570    ///
1571    /// ```text
1572    /// The value 'THINKING_SUMMARIES_AUTO' is not supported for
1573    /// 'agent_config.thinking_summaries'. Supported values: 'auto', 'none'.
1574    /// ```
1575    ///
1576    /// So both contexts now take the lowercase spelling and this agrees
1577    /// with [`Serialize`]. The seam is kept rather than inlined because
1578    /// the two spellings diverged once and could again; deserialization
1579    /// still accepts either form (Evergreen).
1580    #[must_use]
1581    pub fn to_agent_config_value(&self) -> serde_json::Value {
1582        match self {
1583            ThinkingSummaries::Auto => serde_json::Value::String("auto".to_string()),
1584            ThinkingSummaries::None => serde_json::Value::String("none".to_string()),
1585            ThinkingSummaries::Unknown { summaries_type, .. } => {
1586                // For unknown values, preserve the original format
1587                serde_json::Value::String(summaries_type.clone())
1588            }
1589        }
1590    }
1591}
1592
1593impl Serialize for ThinkingSummaries {
1594    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1595    where
1596        S: Serializer,
1597    {
1598        // Note: GenerationConfig uses lowercase ("auto"/"none")
1599        // For AgentConfig, use to_agent_config_value() instead
1600        match self {
1601            ThinkingSummaries::Auto => serializer.serialize_str("auto"),
1602            ThinkingSummaries::None => serializer.serialize_str("none"),
1603            ThinkingSummaries::Unknown {
1604                summaries_type,
1605                data,
1606            } => {
1607                // If data is a simple string, serialize just the summaries_type
1608                if data.is_string() || data.is_null() {
1609                    serializer.serialize_str(summaries_type)
1610                } else {
1611                    // For complex data, serialize as an object
1612                    let mut map = serializer.serialize_map(None)?;
1613                    map.serialize_entry("summaries", summaries_type)?;
1614                    map.serialize_entry("data", data)?;
1615                    map.end()
1616                }
1617            }
1618        }
1619    }
1620}
1621
1622impl<'de> Deserialize<'de> for ThinkingSummaries {
1623    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1624    where
1625        D: Deserializer<'de>,
1626    {
1627        deserializer.deserialize_any(ThinkingSummariesVisitor)
1628    }
1629}
1630
1631struct ThinkingSummariesVisitor;
1632
1633impl<'de> Visitor<'de> for ThinkingSummariesVisitor {
1634    type Value = ThinkingSummaries;
1635
1636    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1637        formatter.write_str("a thinking summaries string or object")
1638    }
1639
1640    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1641    where
1642        E: de::Error,
1643    {
1644        match value {
1645            // Wire format is THINKING_SUMMARIES_*, but also accept lowercase for flexibility
1646            "THINKING_SUMMARIES_AUTO" | "auto" => Ok(ThinkingSummaries::Auto),
1647            "THINKING_SUMMARIES_NONE" | "none" => Ok(ThinkingSummaries::None),
1648            other => {
1649                tracing::warn!(
1650                    "Encountered unknown ThinkingSummaries '{}' - using Unknown variant (Evergreen)",
1651                    other
1652                );
1653                Ok(ThinkingSummaries::Unknown {
1654                    summaries_type: other.to_string(),
1655                    data: serde_json::Value::String(other.to_string()),
1656                })
1657            }
1658        }
1659    }
1660
1661    fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
1662    where
1663        A: de::MapAccess<'de>,
1664    {
1665        // For object-based thinking summaries (future API compatibility)
1666        let value: serde_json::Value =
1667            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;
1668        let summaries_type = value
1669            .get("summaries")
1670            .and_then(|v| v.as_str())
1671            .unwrap_or("unknown")
1672            .to_string();
1673
1674        tracing::warn!(
1675            "Encountered unknown ThinkingSummaries object '{}' - using Unknown variant (Evergreen)",
1676            summaries_type
1677        );
1678        Ok(ThinkingSummaries::Unknown {
1679            summaries_type,
1680            data: value,
1681        })
1682    }
1683}
1684
1685/// Agent-specific configuration for specialized agents.
1686///
1687/// This is a thin wrapper around JSON that provides full forward compatibility.
1688/// Use typed config structs like [`DeepResearchConfig`] for compile-time guidance,
1689/// or construct directly from JSON for unknown/future agent types.
1690///
1691/// # Usage
1692///
1693/// ## Typed configs (recommended for known agents)
1694/// ```
1695/// use genai_rs::{AgentConfig, DeepResearchConfig, ThinkingSummaries};
1696///
1697/// let config: AgentConfig = DeepResearchConfig::new()
1698///     .with_thinking_summaries(ThinkingSummaries::Auto)
1699///     .into();
1700/// ```
1701///
1702/// ## Raw JSON (for unknown/future agents)
1703/// ```
1704/// use genai_rs::AgentConfig;
1705///
1706/// let config = AgentConfig::from_value(serde_json::json!({
1707///     "type": "future-agent",
1708///     "newOption": true
1709/// }));
1710/// ```
1711#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1712#[serde(transparent)]
1713pub struct AgentConfig(serde_json::Value);
1714
1715impl AgentConfig {
1716    /// Create an agent config from a raw JSON value.
1717    ///
1718    /// Use this for unknown or future agent types that don't have typed config structs.
1719    #[must_use]
1720    pub fn from_value(value: serde_json::Value) -> Self {
1721        Self(value)
1722    }
1723
1724    /// Access the underlying JSON value.
1725    #[must_use]
1726    pub fn as_value(&self) -> &serde_json::Value {
1727        &self.0
1728    }
1729
1730    /// Get the agent config type (e.g., "deep-research", "dynamic").
1731    #[must_use]
1732    pub fn config_type(&self) -> Option<&str> {
1733        self.0.get("type").and_then(|v| v.as_str())
1734    }
1735}
1736
1737/// Visualization mode for the Deep Research agent.
1738///
1739/// Controls whether the agent includes visualizations in its response.
1740///
1741/// This enum is marked `#[non_exhaustive]` for forward compatibility.
1742///
1743/// # Wire Format
1744///
1745/// Serializes as lowercase strings: `"off"`, `"auto"`.
1746///
1747/// # Evergreen Pattern
1748///
1749/// Unknown values from the API deserialize into the `Unknown` variant,
1750/// preserving the original data for debugging and roundtrip serialization.
1751#[derive(Clone, Debug, PartialEq)]
1752#[non_exhaustive]
1753pub enum Visualization {
1754    /// No visualizations in the response.
1755    Off,
1756    /// The agent decides when to include visualizations.
1757    Auto,
1758    /// Unknown variant for forward compatibility (Evergreen pattern)
1759    Unknown {
1760        /// The unrecognized visualization type from the API
1761        visualization_type: String,
1762        /// The raw JSON value, preserved for debugging and roundtrip
1763        data: serde_json::Value,
1764    },
1765}
1766
1767impl Visualization {
1768    /// Returns true if this is an unknown visualization mode.
1769    #[must_use]
1770    pub const fn is_unknown(&self) -> bool {
1771        matches!(self, Self::Unknown { .. })
1772    }
1773
1774    /// Returns the visualization type name if this is an unknown mode.
1775    #[must_use]
1776    pub fn unknown_visualization_type(&self) -> Option<&str> {
1777        match self {
1778            Self::Unknown {
1779                visualization_type, ..
1780            } => Some(visualization_type),
1781            _ => None,
1782        }
1783    }
1784
1785    /// Returns the preserved data if this is an unknown mode.
1786    #[must_use]
1787    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
1788        match self {
1789            Self::Unknown { data, .. } => Some(data),
1790            _ => None,
1791        }
1792    }
1793}
1794
1795impl Serialize for Visualization {
1796    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1797    where
1798        S: Serializer,
1799    {
1800        match self {
1801            Self::Off => serializer.serialize_str("off"),
1802            Self::Auto => serializer.serialize_str("auto"),
1803            Self::Unknown {
1804                visualization_type, ..
1805            } => serializer.serialize_str(visualization_type),
1806        }
1807    }
1808}
1809
1810impl<'de> Deserialize<'de> for Visualization {
1811    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1812    where
1813        D: Deserializer<'de>,
1814    {
1815        let value = serde_json::Value::deserialize(deserializer)?;
1816        match value.as_str() {
1817            Some("off") => Ok(Self::Off),
1818            Some("auto") => Ok(Self::Auto),
1819            Some(other) => {
1820                tracing::warn!(
1821                    "Encountered unknown Visualization '{}' - using Unknown variant (Evergreen)",
1822                    other
1823                );
1824                Ok(Self::Unknown {
1825                    visualization_type: other.to_string(),
1826                    data: value,
1827                })
1828            }
1829            None => {
1830                let visualization_type = format!("<non-string: {}>", value);
1831                tracing::warn!(
1832                    "Visualization received non-string value: {}. \
1833                     Preserving in Unknown variant.",
1834                    value
1835                );
1836                Ok(Self::Unknown {
1837                    visualization_type,
1838                    data: value,
1839                })
1840            }
1841        }
1842    }
1843}
1844
1845/// Configuration for Deep Research agent.
1846///
1847/// Deep Research agent performs comprehensive research tasks
1848/// and can optionally include thinking summaries, visualizations,
1849/// collaborative planning, and BigQuery access.
1850///
1851/// Known Deep Research agent IDs: `deep-research-pro-preview-12-2025`,
1852/// `deep-research-preview-04-2026`, `deep-research-max-preview-04-2026`.
1853/// See `docs/AGENTS_AND_BACKGROUND.md`.
1854///
1855/// # Example
1856///
1857/// ```
1858/// use genai_rs::{AgentConfig, DeepResearchConfig, ThinkingSummaries, Visualization};
1859///
1860/// let config: AgentConfig = DeepResearchConfig::new()
1861///     .with_thinking_summaries(ThinkingSummaries::Auto)
1862///     .with_visualization(Visualization::Auto)
1863///     .with_collaborative_planning(true)
1864///     .with_bigquery_tool(true)
1865///     .into();
1866/// ```
1867#[derive(Clone, Debug, Default)]
1868pub struct DeepResearchConfig {
1869    thinking_summaries: Option<ThinkingSummaries>,
1870    visualization: Option<Visualization>,
1871    collaborative_planning: Option<bool>,
1872    enable_bigquery_tool: Option<bool>,
1873}
1874
1875impl DeepResearchConfig {
1876    /// Create a new Deep Research configuration with default settings.
1877    #[must_use]
1878    pub fn new() -> Self {
1879        Self::default()
1880    }
1881
1882    /// Set thinking summaries mode.
1883    ///
1884    /// Controls whether the agent's reasoning process is summarized in output.
1885    #[must_use]
1886    pub fn with_thinking_summaries(mut self, summaries: ThinkingSummaries) -> Self {
1887        self.thinking_summaries = Some(summaries);
1888        self
1889    }
1890
1891    /// Set the visualization mode (`off` | `auto`).
1892    #[must_use]
1893    pub fn with_visualization(mut self, visualization: Visualization) -> Self {
1894        self.visualization = Some(visualization);
1895        self
1896    }
1897
1898    /// Enable (or disable) human-in-the-loop planning.
1899    ///
1900    /// When `true`, the agent first returns a research plan and only proceeds
1901    /// after the user confirms the plan in the next turn.
1902    #[must_use]
1903    pub fn with_collaborative_planning(mut self, enabled: bool) -> Self {
1904        self.collaborative_planning = Some(enabled);
1905        self
1906    }
1907
1908    /// Enable (or disable) the BigQuery tool for the Deep Research agent.
1909    ///
1910    /// Server-side constraint (verified live 2026-07): the Gemini API
1911    /// rejects `agent_config.enable_bigquery_tool` — "not available on the
1912    /// Gemini API but it is available on the Gemini Enterprise Agent
1913    /// Platform" (Vertex-only).
1914    #[must_use]
1915    pub fn with_bigquery_tool(mut self, enabled: bool) -> Self {
1916        self.enable_bigquery_tool = Some(enabled);
1917        self
1918    }
1919}
1920
1921impl From<DeepResearchConfig> for AgentConfig {
1922    fn from(config: DeepResearchConfig) -> Self {
1923        let mut map = serde_json::Map::new();
1924        map.insert(
1925            "type".into(),
1926            serde_json::Value::String("deep-research".into()),
1927        );
1928        if let Some(ts) = config.thinking_summaries {
1929            // Use agent_config format (THINKING_SUMMARIES_*), not generation_config format (auto/none)
1930            map.insert("thinking_summaries".into(), ts.to_agent_config_value());
1931        }
1932        if let Some(visualization) = config.visualization {
1933            map.insert(
1934                "visualization".into(),
1935                serde_json::to_value(&visualization)
1936                    .expect("Visualization serialization is infallible"),
1937            );
1938        }
1939        if let Some(planning) = config.collaborative_planning {
1940            map.insert(
1941                "collaborative_planning".into(),
1942                serde_json::Value::Bool(planning),
1943            );
1944        }
1945        if let Some(bigquery) = config.enable_bigquery_tool {
1946            map.insert(
1947                "enable_bigquery_tool".into(),
1948                serde_json::Value::Bool(bigquery),
1949            );
1950        }
1951        AgentConfig(serde_json::Value::Object(map))
1952    }
1953}
1954
1955/// Configuration for Dynamic agent.
1956///
1957/// Dynamic agents adapt their behavior based on the task.
1958/// Currently has no configurable options.
1959///
1960/// # Example
1961///
1962/// ```
1963/// use genai_rs::{AgentConfig, DynamicConfig};
1964///
1965/// let config: AgentConfig = DynamicConfig::new().into();
1966/// ```
1967#[derive(Clone, Debug, Default)]
1968pub struct DynamicConfig;
1969
1970impl DynamicConfig {
1971    /// Create a new Dynamic agent configuration.
1972    #[must_use]
1973    pub fn new() -> Self {
1974        Self
1975    }
1976}
1977
1978impl From<DynamicConfig> for AgentConfig {
1979    fn from(_: DynamicConfig) -> Self {
1980        AgentConfig(serde_json::json!({"type": "dynamic"}))
1981    }
1982}
1983
1984/// Configuration for the server-side Antigravity coding agent.
1985///
1986/// This configures `agent("antigravity-preview-05-2026")` interactions that
1987/// run in Google's sandbox (an
1988/// [`environment`](InteractionRequest::environment) is **required** for that
1989/// agent) — distinct from the local-harness bridge in the `antigravity`
1990/// module (feature `antigravity`), which runs the agent on your
1991/// machine. The bare `antigravity` string is only the `agent_config` *type*
1992/// discriminant (which [`From`] sets), not an agent ID.
1993///
1994/// Probe notes (verified live 2026-08-09, standard API key):
1995/// `agent_config: {"type": "antigravity"}` and `max_total_tokens` are
1996/// **accepted** on `antigravity-preview-05-2026` (the server's validation
1997/// error enumerates the supported config types as `dynamic`,
1998/// `deep-research`, `code-mender`, `antigravity`). Setting `model` to a
1999/// value the agent doesn't offer returns 404 `not_found` — as
2000/// `gemini-3.6-flash` did when this was recorded, despite being a valid
2001/// model for ordinary interactions. The agent's model catalog is not
2002/// enumerable on a standard key, so leave `model` unset unless you know an
2003/// accepted value.
2004///
2005/// # Example
2006///
2007/// ```
2008/// use genai_rs::{AgentConfig, AntigravityConfig};
2009///
2010/// let config: AgentConfig = AntigravityConfig::new()
2011///     .with_max_total_tokens(200_000)
2012///     .into();
2013/// ```
2014#[derive(Clone, Debug, Default)]
2015pub struct AntigravityConfig {
2016    model: Option<String>,
2017    max_total_tokens: Option<i64>,
2018}
2019
2020impl AntigravityConfig {
2021    /// Create a new Antigravity agent configuration with default settings.
2022    #[must_use]
2023    pub fn new() -> Self {
2024        Self::default()
2025    }
2026
2027    /// Set the model the agent uses for its reasoning loop.
2028    ///
2029    /// A value the agent does not offer fails the interaction with 404
2030    /// `not_found` — see the [type docs](AntigravityConfig) for the probe
2031    /// notes; leave unset unless you know an accepted value.
2032    #[must_use]
2033    pub fn with_model(mut self, model: impl Into<String>) -> Self {
2034        self.model = Some(model.into());
2035        self
2036    }
2037
2038    /// Cap the total tokens the agent may consume across its whole run.
2039    #[must_use]
2040    pub fn with_max_total_tokens(mut self, max_total_tokens: i64) -> Self {
2041        self.max_total_tokens = Some(max_total_tokens);
2042        self
2043    }
2044}
2045
2046impl From<AntigravityConfig> for AgentConfig {
2047    fn from(config: AntigravityConfig) -> Self {
2048        let mut map = serde_json::Map::new();
2049        map.insert(
2050            "type".into(),
2051            serde_json::Value::String("antigravity".into()),
2052        );
2053        if let Some(model) = config.model {
2054            map.insert("model".into(), serde_json::Value::String(model));
2055        }
2056        if let Some(max) = config.max_total_tokens {
2057            map.insert("max_total_tokens".into(), serde_json::Value::from(max));
2058        }
2059        AgentConfig(serde_json::Value::Object(map))
2060    }
2061}
2062
2063#[cfg(test)]
2064mod tests {
2065    use super::*;
2066
2067    // =========================================================================
2068    // Agent Config Tests
2069    // =========================================================================
2070
2071    #[test]
2072    fn test_thinking_summaries_serialization() {
2073        // GenerationConfig wire format uses lowercase
2074        assert_eq!(
2075            serde_json::to_string(&ThinkingSummaries::Auto).unwrap(),
2076            "\"auto\""
2077        );
2078
2079        assert_eq!(
2080            serde_json::to_string(&ThinkingSummaries::None).unwrap(),
2081            "\"none\""
2082        );
2083    }
2084
2085    #[test]
2086    fn test_thinking_summaries_agent_config_format() {
2087        // AgentConfig uses THINKING_SUMMARIES_* format via to_agent_config_value()
2088        assert_eq!(
2089            ThinkingSummaries::Auto.to_agent_config_value(),
2090            serde_json::Value::String("auto".to_string())
2091        );
2092
2093        assert_eq!(
2094            ThinkingSummaries::None.to_agent_config_value(),
2095            serde_json::Value::String("none".to_string())
2096        );
2097    }
2098
2099    #[test]
2100    fn test_thinking_summaries_deserialization() {
2101        // Test wire format (THINKING_SUMMARIES_*)
2102        assert_eq!(
2103            serde_json::from_str::<ThinkingSummaries>("\"THINKING_SUMMARIES_AUTO\"").unwrap(),
2104            ThinkingSummaries::Auto
2105        );
2106        assert_eq!(
2107            serde_json::from_str::<ThinkingSummaries>("\"THINKING_SUMMARIES_NONE\"").unwrap(),
2108            ThinkingSummaries::None
2109        );
2110
2111        // Also accept lowercase for flexibility
2112        assert_eq!(
2113            serde_json::from_str::<ThinkingSummaries>("\"auto\"").unwrap(),
2114            ThinkingSummaries::Auto
2115        );
2116        assert_eq!(
2117            serde_json::from_str::<ThinkingSummaries>("\"none\"").unwrap(),
2118            ThinkingSummaries::None
2119        );
2120    }
2121
2122    #[test]
2123    fn test_thinking_summaries_unknown_roundtrip() {
2124        let unknown: ThinkingSummaries = serde_json::from_str("\"future_variant\"").unwrap();
2125        assert!(unknown.is_unknown());
2126        assert_eq!(unknown.unknown_summaries_type(), Some("future_variant"));
2127
2128        // Roundtrip preserves the unknown value
2129        let json = serde_json::to_string(&unknown).unwrap();
2130        assert_eq!(json, "\"future_variant\"");
2131    }
2132
2133    #[test]
2134    fn test_deep_research_config_serialization() {
2135        let config: AgentConfig = DeepResearchConfig::new()
2136            .with_thinking_summaries(ThinkingSummaries::Auto)
2137            .into();
2138
2139        let json = serde_json::to_string(&config).expect("Serialization failed");
2140        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
2141
2142        assert_eq!(value["type"], "deep-research");
2143        assert_eq!(value["thinking_summaries"], "auto");
2144    }
2145
2146    #[test]
2147    fn test_deep_research_config_without_thinking_summaries() {
2148        let config: AgentConfig = DeepResearchConfig::new().into();
2149
2150        let json = serde_json::to_string(&config).expect("Serialization failed");
2151        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
2152
2153        assert_eq!(value["type"], "deep-research");
2154        assert!(value.get("thinking_summaries").is_none());
2155    }
2156
2157    #[test]
2158    fn test_dynamic_config_serialization() {
2159        let config: AgentConfig = DynamicConfig::new().into();
2160
2161        let json = serde_json::to_string(&config).expect("Serialization failed");
2162        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
2163
2164        assert_eq!(value["type"], "dynamic");
2165    }
2166
2167    #[test]
2168    fn test_agent_config_from_raw_json() {
2169        let config = AgentConfig::from_value(serde_json::json!({
2170            "type": "custom-agent",
2171            "option1": true,
2172            "option2": "value"
2173        }));
2174
2175        assert_eq!(config.config_type(), Some("custom-agent"));
2176        assert_eq!(config.as_value()["option1"], true);
2177    }
2178
2179    #[test]
2180    fn test_agent_config_roundtrip() {
2181        let config: AgentConfig = DeepResearchConfig::new()
2182            .with_thinking_summaries(ThinkingSummaries::Auto)
2183            .into();
2184
2185        let json = serde_json::to_string(&config).expect("Serialization failed");
2186        let parsed: AgentConfig = serde_json::from_str(&json).expect("Deserialization failed");
2187
2188        assert_eq!(config, parsed);
2189    }
2190
2191    // =========================================================================
2192    // SpeechConfig Tests
2193    // =========================================================================
2194
2195    #[test]
2196    fn test_speech_config_with_voice() {
2197        let config = SpeechConfig::with_voice("Kore");
2198        assert_eq!(config.voice, Some("Kore".to_string()));
2199        assert_eq!(config.language, None);
2200        assert_eq!(config.speaker, None);
2201    }
2202
2203    #[test]
2204    fn test_speech_config_with_voice_and_language() {
2205        let config = SpeechConfig::with_voice_and_language("Puck", "en-GB");
2206        assert_eq!(config.voice, Some("Puck".to_string()));
2207        assert_eq!(config.language, Some("en-GB".to_string()));
2208        assert_eq!(config.speaker, None);
2209    }
2210
2211    #[test]
2212    fn test_speech_config_serialization() {
2213        let config = SpeechConfig {
2214            voice: Some("Fenrir".to_string()),
2215            language: Some("en-US".to_string()),
2216            speaker: None,
2217        };
2218
2219        let json = serde_json::to_string(&config).expect("Serialization failed");
2220        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
2221
2222        // Verify flat format is produced (voice, language at top level)
2223        assert_eq!(value["voice"], "Fenrir");
2224        assert_eq!(value["language"], "en-US");
2225        assert!(value.get("speaker").is_none()); // None fields should be skipped
2226
2227        // Verify nested format is NOT produced
2228        // Google docs suggest voiceConfig.prebuiltVoiceConfig.voiceName but that returns 400.
2229        // See docs/ENUM_WIRE_FORMATS.md and docs/INTERACTIONS_API_FEEDBACK.md Issue #7.
2230        assert!(
2231            value.get("voiceConfig").is_none(),
2232            "Should use flat format, not nested voiceConfig"
2233        );
2234        assert!(
2235            value.get("prebuiltVoiceConfig").is_none(),
2236            "Should use flat format, not nested prebuiltVoiceConfig"
2237        );
2238    }
2239
2240    #[test]
2241    fn test_speech_config_roundtrip() {
2242        let config = SpeechConfig {
2243            voice: Some("Aoede".to_string()),
2244            language: Some("es-ES".to_string()),
2245            speaker: Some("narrator".to_string()),
2246        };
2247
2248        let json = serde_json::to_string(&config).expect("Serialization failed");
2249        let parsed: SpeechConfig = serde_json::from_str(&json).expect("Deserialization failed");
2250
2251        assert_eq!(config.voice, parsed.voice);
2252        assert_eq!(config.language, parsed.language);
2253        assert_eq!(config.speaker, parsed.speaker);
2254    }
2255
2256    #[test]
2257    fn test_speech_config_default() {
2258        let config = SpeechConfig::default();
2259        assert_eq!(config.voice, None);
2260        assert_eq!(config.language, None);
2261        assert_eq!(config.speaker, None);
2262    }
2263
2264    // =========================================================================
2265    // ImageAspectRatio Tests
2266    // =========================================================================
2267
2268    #[test]
2269    fn test_image_aspect_ratio_serialization() {
2270        assert_eq!(
2271            serde_json::to_string(&ImageAspectRatio::Square).unwrap(),
2272            "\"1:1\""
2273        );
2274        assert_eq!(
2275            serde_json::to_string(&ImageAspectRatio::Portrait2x3).unwrap(),
2276            "\"2:3\""
2277        );
2278        assert_eq!(
2279            serde_json::to_string(&ImageAspectRatio::Landscape3x2).unwrap(),
2280            "\"3:2\""
2281        );
2282        assert_eq!(
2283            serde_json::to_string(&ImageAspectRatio::Portrait3x4).unwrap(),
2284            "\"3:4\""
2285        );
2286        assert_eq!(
2287            serde_json::to_string(&ImageAspectRatio::Landscape4x3).unwrap(),
2288            "\"4:3\""
2289        );
2290        assert_eq!(
2291            serde_json::to_string(&ImageAspectRatio::Portrait4x5).unwrap(),
2292            "\"4:5\""
2293        );
2294        assert_eq!(
2295            serde_json::to_string(&ImageAspectRatio::Landscape5x4).unwrap(),
2296            "\"5:4\""
2297        );
2298        assert_eq!(
2299            serde_json::to_string(&ImageAspectRatio::Portrait9x16).unwrap(),
2300            "\"9:16\""
2301        );
2302        assert_eq!(
2303            serde_json::to_string(&ImageAspectRatio::Widescreen16x9).unwrap(),
2304            "\"16:9\""
2305        );
2306        assert_eq!(
2307            serde_json::to_string(&ImageAspectRatio::Ultrawide21x9).unwrap(),
2308            "\"21:9\""
2309        );
2310        assert_eq!(
2311            serde_json::to_string(&ImageAspectRatio::Tall1x8).unwrap(),
2312            "\"1:8\""
2313        );
2314        assert_eq!(
2315            serde_json::to_string(&ImageAspectRatio::Wide8x1).unwrap(),
2316            "\"8:1\""
2317        );
2318        assert_eq!(
2319            serde_json::to_string(&ImageAspectRatio::Tall1x4).unwrap(),
2320            "\"1:4\""
2321        );
2322        assert_eq!(
2323            serde_json::to_string(&ImageAspectRatio::Wide4x1).unwrap(),
2324            "\"4:1\""
2325        );
2326    }
2327
2328    #[test]
2329    fn test_image_aspect_ratio_deserialization_roundtrip() {
2330        let ratios = vec![
2331            ("\"1:1\"", ImageAspectRatio::Square),
2332            ("\"2:3\"", ImageAspectRatio::Portrait2x3),
2333            ("\"3:2\"", ImageAspectRatio::Landscape3x2),
2334            ("\"3:4\"", ImageAspectRatio::Portrait3x4),
2335            ("\"4:3\"", ImageAspectRatio::Landscape4x3),
2336            ("\"4:5\"", ImageAspectRatio::Portrait4x5),
2337            ("\"5:4\"", ImageAspectRatio::Landscape5x4),
2338            ("\"9:16\"", ImageAspectRatio::Portrait9x16),
2339            ("\"16:9\"", ImageAspectRatio::Widescreen16x9),
2340            ("\"21:9\"", ImageAspectRatio::Ultrawide21x9),
2341            ("\"1:8\"", ImageAspectRatio::Tall1x8),
2342            ("\"8:1\"", ImageAspectRatio::Wide8x1),
2343            ("\"1:4\"", ImageAspectRatio::Tall1x4),
2344            ("\"4:1\"", ImageAspectRatio::Wide4x1),
2345        ];
2346
2347        for (json, expected) in ratios {
2348            let parsed: ImageAspectRatio = serde_json::from_str(json).unwrap();
2349            assert_eq!(parsed, expected);
2350
2351            // Roundtrip
2352            let serialized = serde_json::to_string(&parsed).unwrap();
2353            assert_eq!(serialized, json);
2354        }
2355    }
2356
2357    #[test]
2358    fn test_image_aspect_ratio_unknown_roundtrip() {
2359        let unknown: ImageAspectRatio = serde_json::from_str("\"7:3\"").unwrap();
2360        assert!(unknown.is_unknown());
2361        assert_eq!(unknown.unknown_ratio_type(), Some("7:3"));
2362        assert!(unknown.unknown_data().is_some());
2363
2364        // Roundtrip preserves the unknown value
2365        let json = serde_json::to_string(&unknown).unwrap();
2366        assert_eq!(json, "\"7:3\"");
2367    }
2368
2369    #[test]
2370    fn test_image_aspect_ratio_known_not_unknown() {
2371        assert!(!ImageAspectRatio::Square.is_unknown());
2372        assert_eq!(ImageAspectRatio::Widescreen16x9.unknown_ratio_type(), None);
2373        assert_eq!(ImageAspectRatio::Portrait2x3.unknown_data(), None);
2374    }
2375
2376    // =========================================================================
2377    // ImageSize Tests
2378    // =========================================================================
2379
2380    #[test]
2381    fn test_image_size_serialization() {
2382        assert_eq!(serde_json::to_string(&ImageSize::Sd512).unwrap(), "\"512\"");
2383        assert_eq!(serde_json::to_string(&ImageSize::Hd1k).unwrap(), "\"1K\"");
2384        assert_eq!(serde_json::to_string(&ImageSize::Hd2k).unwrap(), "\"2K\"");
2385        assert_eq!(serde_json::to_string(&ImageSize::Uhd4k).unwrap(), "\"4K\"");
2386    }
2387
2388    #[test]
2389    fn test_image_size_deserialization_roundtrip() {
2390        let sizes = vec![
2391            ("\"512\"", ImageSize::Sd512),
2392            ("\"1K\"", ImageSize::Hd1k),
2393            ("\"2K\"", ImageSize::Hd2k),
2394            ("\"4K\"", ImageSize::Uhd4k),
2395        ];
2396
2397        for (json, expected) in sizes {
2398            let parsed: ImageSize = serde_json::from_str(json).unwrap();
2399            assert_eq!(parsed, expected);
2400
2401            let serialized = serde_json::to_string(&parsed).unwrap();
2402            assert_eq!(serialized, json);
2403        }
2404    }
2405
2406    #[test]
2407    fn test_image_size_unknown_roundtrip() {
2408        let unknown: ImageSize = serde_json::from_str("\"8K\"").unwrap();
2409        assert!(unknown.is_unknown());
2410        assert_eq!(unknown.unknown_size_type(), Some("8K"));
2411        assert!(unknown.unknown_data().is_some());
2412
2413        let json = serde_json::to_string(&unknown).unwrap();
2414        assert_eq!(json, "\"8K\"");
2415    }
2416
2417    #[test]
2418    fn test_image_size_known_not_unknown() {
2419        assert!(!ImageSize::Sd512.is_unknown());
2420        assert_eq!(ImageSize::Hd1k.unknown_size_type(), None);
2421        assert_eq!(ImageSize::Uhd4k.unknown_data(), None);
2422    }
2423
2424    // =========================================================================
2425    // ImageConfig Tests
2426    // =========================================================================
2427
2428    #[test]
2429    fn test_image_config_serialization_roundtrip() {
2430        let config = ImageConfig {
2431            aspect_ratio: Some(ImageAspectRatio::Widescreen16x9),
2432            image_size: Some(ImageSize::Hd2k),
2433        };
2434
2435        let json = serde_json::to_string(&config).expect("Serialization failed");
2436        let parsed: ImageConfig = serde_json::from_str(&json).expect("Deserialization failed");
2437
2438        assert_eq!(config, parsed);
2439    }
2440
2441    #[test]
2442    fn test_image_config_default() {
2443        let config = ImageConfig::default();
2444        assert_eq!(config.aspect_ratio, None);
2445        assert_eq!(config.image_size, None);
2446    }
2447
2448    #[test]
2449    fn test_image_config_partial_fields() {
2450        let config = ImageConfig {
2451            aspect_ratio: Some(ImageAspectRatio::Square),
2452            image_size: None,
2453        };
2454
2455        let json = serde_json::to_string(&config).unwrap();
2456        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
2457
2458        assert_eq!(value["aspect_ratio"], "1:1");
2459        assert!(value.get("image_size").is_none());
2460    }
2461
2462    #[test]
2463    fn test_image_config_skip_serializing_none() {
2464        let config = ImageConfig::default();
2465        let json = serde_json::to_string(&config).unwrap();
2466        assert_eq!(json, "{}");
2467    }
2468
2469    #[test]
2470    fn test_generation_config_with_image_config() {
2471        let config = GenerationConfig {
2472            image_config: Some(ImageConfig {
2473                aspect_ratio: Some(ImageAspectRatio::Portrait9x16),
2474                image_size: Some(ImageSize::Uhd4k),
2475            }),
2476            ..Default::default()
2477        };
2478
2479        let json = serde_json::to_string(&config).expect("Serialization failed");
2480        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
2481
2482        assert_eq!(value["image_config"]["aspect_ratio"], "9:16");
2483        assert_eq!(value["image_config"]["image_size"], "4K");
2484    }
2485
2486    // =========================================================================
2487    // GenerationConfig tool_choice / penalty Tests
2488    // =========================================================================
2489
2490    #[test]
2491    fn test_generation_config_tool_choice_mode_serializes_lowercase() {
2492        let config = GenerationConfig {
2493            tool_choice: Some(ToolChoice::Mode(crate::tools::FunctionCallingMode::Any)),
2494            ..Default::default()
2495        };
2496
2497        let value = serde_json::to_value(&config).unwrap();
2498        assert_eq!(value["tool_choice"], "any");
2499    }
2500
2501    #[test]
2502    fn test_generation_config_tool_choice_allowed_tools_object() {
2503        let config = GenerationConfig {
2504            tool_choice: Some(ToolChoice::allowed_tools(
2505                Some(crate::tools::FunctionCallingMode::Any),
2506                vec!["get_weather".to_string(), "get_time".to_string()],
2507            )),
2508            ..Default::default()
2509        };
2510
2511        let value = serde_json::to_value(&config).unwrap();
2512        assert_eq!(value["tool_choice"]["allowed_tools"]["mode"], "any");
2513        assert_eq!(
2514            value["tool_choice"]["allowed_tools"]["tools"][0],
2515            "get_weather"
2516        );
2517        assert!(
2518            value.get("allowed_tools").is_none(),
2519            "top-level allowed_tools was removed from generation_config"
2520        );
2521    }
2522
2523    #[test]
2524    fn test_generation_config_penalties_serialize() {
2525        let config = GenerationConfig {
2526            presence_penalty: Some(0.5),
2527            frequency_penalty: Some(-0.5),
2528            ..Default::default()
2529        };
2530        let value = serde_json::to_value(&config).unwrap();
2531        assert_eq!(value["presence_penalty"], 0.5);
2532        assert_eq!(value["frequency_penalty"], -0.5);
2533    }
2534
2535    #[test]
2536    fn test_generation_config_has_no_top_k() {
2537        // top_k was dropped from the 2026-05-20 spec; ensure it never serializes.
2538        let config = GenerationConfig {
2539            temperature: Some(0.3),
2540            ..Default::default()
2541        };
2542        let value = serde_json::to_value(&config).unwrap();
2543        assert!(value.get("top_k").is_none());
2544    }
2545
2546    // =========================================================================
2547    // ServiceTier Tests
2548    // =========================================================================
2549
2550    #[test]
2551    fn test_service_tier_roundtrip() {
2552        for (tier, wire) in [
2553            (ServiceTier::Flex, "\"flex\""),
2554            (ServiceTier::Standard, "\"standard\""),
2555            (ServiceTier::Priority, "\"priority\""),
2556        ] {
2557            assert_eq!(serde_json::to_string(&tier).unwrap(), wire);
2558            let parsed: ServiceTier = serde_json::from_str(wire).unwrap();
2559            assert_eq!(parsed, tier);
2560        }
2561    }
2562
2563    #[test]
2564    fn test_service_tier_unknown_roundtrip() {
2565        let unknown: ServiceTier = serde_json::from_str("\"turbo\"").unwrap();
2566        assert!(unknown.is_unknown());
2567        assert_eq!(unknown.unknown_tier_type(), Some("turbo"));
2568        assert!(unknown.unknown_data().is_some());
2569        assert_eq!(serde_json::to_string(&unknown).unwrap(), "\"turbo\"");
2570    }
2571
2572    // =========================================================================
2573    // InteractionInput Tests
2574    // =========================================================================
2575
2576    #[test]
2577    fn test_interaction_input_text_roundtrip() {
2578        let input = InteractionInput::Text("Hello".into());
2579        let json = serde_json::to_string(&input).unwrap();
2580        assert_eq!(json, "\"Hello\"");
2581        let back: InteractionInput = serde_json::from_str(&json).unwrap();
2582        assert!(matches!(back, InteractionInput::Text(t) if t == "Hello"));
2583    }
2584
2585    #[test]
2586    fn test_interaction_input_content_array_roundtrip() {
2587        let json = r#"[{"type":"text","text":"hi"},{"type":"image","uri":"files/x","mime_type":"image/png"}]"#;
2588        let input: InteractionInput = serde_json::from_str(json).unwrap();
2589        match &input {
2590            InteractionInput::Content(c) => assert_eq!(c.len(), 2),
2591            other => panic!("Expected Content, got {other:?}"),
2592        }
2593    }
2594
2595    #[test]
2596    fn test_interaction_input_steps_array_roundtrip() {
2597        let json = r#"[
2598            {"type":"user_input","content":[{"type":"text","text":"hi"}]},
2599            {"type":"model_output","content":[{"type":"text","text":"hello"}]},
2600            {"type":"function_result","call_id":"c1","result":"done"}
2601        ]"#;
2602        let input: InteractionInput = serde_json::from_str(json).unwrap();
2603        match &input {
2604            InteractionInput::Steps(s) => assert_eq!(s.len(), 3),
2605            other => panic!("Expected Steps, got {other:?}"),
2606        }
2607    }
2608
2609    #[test]
2610    fn test_interaction_input_single_content_object() {
2611        let json = r#"{"type":"text","text":"hi"}"#;
2612        let input: InteractionInput = serde_json::from_str(json).unwrap();
2613        match &input {
2614            InteractionInput::Content(c) => assert_eq!(c.len(), 1),
2615            other => panic!("Expected Content, got {other:?}"),
2616        }
2617    }
2618}