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::tools::{FunctionCallingMode, Tool};
10
11/// Role in a conversation turn.
12///
13/// Indicates whether the content came from the user or the model.
14///
15/// This enum is marked `#[non_exhaustive]` for forward compatibility.
16/// New roles may be added in future API versions.
17///
18/// # Evergreen Pattern
19///
20/// Unknown values from the API deserialize into the `Unknown` variant, preserving
21/// the original data for debugging and roundtrip serialization.
22///
23/// # Example
24///
25/// ```
26/// use genai_rs::Role;
27///
28/// let role = Role::User;
29/// assert!(matches!(role, Role::User));
30/// ```
31#[derive(Clone, Debug, PartialEq)]
32#[non_exhaustive]
33pub enum Role {
34    /// Content from the user
35    User,
36    /// Content from the model
37    Model,
38    /// Unknown variant for forward compatibility (Evergreen pattern)
39    Unknown {
40        /// The unrecognized role type from the API
41        role_type: String,
42        /// The raw JSON value, preserved for debugging and roundtrip
43        data: serde_json::Value,
44    },
45}
46
47impl Role {
48    /// Returns true if this is an unknown role.
49    #[must_use]
50    pub const fn is_unknown(&self) -> bool {
51        matches!(self, Self::Unknown { .. })
52    }
53
54    /// Returns the role type name if this is an unknown role.
55    #[must_use]
56    pub fn unknown_role_type(&self) -> Option<&str> {
57        match self {
58            Self::Unknown { role_type, .. } => Some(role_type),
59            _ => None,
60        }
61    }
62
63    /// Returns the preserved data if this is an unknown role.
64    #[must_use]
65    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
66        match self {
67            Self::Unknown { data, .. } => Some(data),
68            _ => None,
69        }
70    }
71}
72
73impl fmt::Display for Role {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::User => write!(f, "user"),
77            Self::Model => write!(f, "model"),
78            Self::Unknown { role_type, .. } => write!(f, "{}", role_type),
79        }
80    }
81}
82
83impl Serialize for Role {
84    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85    where
86        S: Serializer,
87    {
88        match self {
89            Role::User => serializer.serialize_str("user"),
90            Role::Model => serializer.serialize_str("model"),
91            Role::Unknown { role_type, .. } => serializer.serialize_str(role_type),
92        }
93    }
94}
95
96impl<'de> Deserialize<'de> for Role {
97    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98    where
99        D: Deserializer<'de>,
100    {
101        let s = String::deserialize(deserializer)?;
102        match s.as_str() {
103            "user" => Ok(Role::User),
104            "model" => Ok(Role::Model),
105            other => {
106                tracing::warn!(
107                    "Encountered unknown Role '{}' - using Unknown variant (Evergreen)",
108                    other
109                );
110                Ok(Role::Unknown {
111                    role_type: other.to_string(),
112                    data: serde_json::Value::String(other.to_string()),
113                })
114            }
115        }
116    }
117}
118
119/// Content for a conversation turn.
120///
121/// Can be simple text or an array of content parts for multimodal turns.
122///
123/// # Example
124///
125/// ```
126/// use genai_rs::TurnContent;
127///
128/// // Simple text
129/// let content = TurnContent::Text("Hello!".to_string());
130///
131/// // From string reference
132/// let content: TurnContent = "Hello!".into();
133/// ```
134// Note: Unlike tagged enums (e.g., Content), this untagged enum cannot
135// have an Unknown variant. Untagged enums have no type discriminator field, so Serde
136// tries variants in order - there's no way to detect "unknown" content. The
137// #[non_exhaustive] attribute provides forward compatibility at the Rust level by
138// preventing exhaustive matches.
139#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
140#[serde(untagged)]
141#[non_exhaustive]
142pub enum TurnContent {
143    /// Simple text content
144    Text(String),
145    /// Array of content parts (for multimodal content)
146    Parts(Vec<Content>),
147}
148
149impl From<String> for TurnContent {
150    fn from(s: String) -> Self {
151        Self::Text(s)
152    }
153}
154
155impl From<&str> for TurnContent {
156    fn from(s: &str) -> Self {
157        Self::Text(s.to_string())
158    }
159}
160
161impl From<Vec<Content>> for TurnContent {
162    fn from(parts: Vec<Content>) -> Self {
163        Self::Parts(parts)
164    }
165}
166
167impl TurnContent {
168    /// Returns the text content if this is a `Text` variant.
169    #[must_use]
170    pub fn as_text(&self) -> Option<&str> {
171        match self {
172            Self::Text(t) => Some(t),
173            Self::Parts(_) => None,
174        }
175    }
176
177    /// Returns the content parts if this is a `Parts` variant.
178    #[must_use]
179    pub fn as_parts(&self) -> Option<&[Content]> {
180        match self {
181            Self::Parts(p) => Some(p),
182            Self::Text(_) => None,
183        }
184    }
185
186    /// Returns `true` if this is text content.
187    #[must_use]
188    pub const fn is_text(&self) -> bool {
189        matches!(self, Self::Text(_))
190    }
191
192    /// Returns `true` if this is parts content.
193    #[must_use]
194    pub const fn is_parts(&self) -> bool {
195        matches!(self, Self::Parts(_))
196    }
197}
198
199/// A single turn in a multi-turn conversation.
200///
201/// Represents one message in a conversation, containing the role (who sent it)
202/// and the content of the message.
203///
204/// # Example
205///
206/// ```
207/// use genai_rs::{Turn, Role, TurnContent};
208///
209/// // Create a user turn with text
210/// let user_turn = Turn::user("What is 2+2?");
211///
212/// // Create a model turn with text
213/// let model_turn = Turn::model("2+2 equals 4.");
214///
215/// // Create a turn with explicit role and content
216/// let turn = Turn::new(Role::User, "Hello!");
217///
218/// // Access via getters
219/// assert!(matches!(turn.role(), &Role::User));
220/// assert_eq!(turn.content().as_text(), Some("Hello!"));
221/// ```
222#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
223pub struct Turn {
224    role: Role,
225    content: TurnContent,
226}
227
228impl Turn {
229    /// Creates a new turn with the given role and content.
230    pub fn new(role: Role, content: impl Into<TurnContent>) -> Self {
231        Self {
232            role,
233            content: content.into(),
234        }
235    }
236
237    /// Returns a reference to the role of this turn.
238    #[must_use]
239    pub fn role(&self) -> &Role {
240        &self.role
241    }
242
243    /// Returns a reference to the content of this turn.
244    #[must_use]
245    pub fn content(&self) -> &TurnContent {
246        &self.content
247    }
248
249    /// Creates a user turn with the given content.
250    ///
251    /// # Example
252    ///
253    /// ```
254    /// use genai_rs::Turn;
255    ///
256    /// let turn = Turn::user("What is the capital of France?");
257    /// ```
258    pub fn user(content: impl Into<TurnContent>) -> Self {
259        Self::new(Role::User, content)
260    }
261
262    /// Creates a model turn with the given content.
263    ///
264    /// # Example
265    ///
266    /// ```
267    /// use genai_rs::Turn;
268    ///
269    /// let turn = Turn::model("The capital of France is Paris.");
270    /// ```
271    pub fn model(content: impl Into<TurnContent>) -> Self {
272        Self::new(Role::Model, content)
273    }
274
275    /// Returns `true` if this is a user turn.
276    #[must_use]
277    pub fn is_user(&self) -> bool {
278        *self.role() == Role::User
279    }
280
281    /// Returns `true` if this is a model turn.
282    #[must_use]
283    pub fn is_model(&self) -> bool {
284        *self.role() == Role::Model
285    }
286
287    /// Returns the text content if this turn contains text.
288    #[must_use]
289    pub fn as_text(&self) -> Option<&str> {
290        self.content().as_text()
291    }
292}
293
294/// Input for an interaction - can be a simple string, array of content, or turns.
295///
296/// This enum is marked `#[non_exhaustive]` for forward compatibility.
297/// New input types may be added in future versions.
298///
299/// # Variants
300///
301/// - `Text`: Simple text input for single-turn conversations
302/// - `Content`: Array of content objects for multimodal input
303/// - `Turns`: Array of turns for explicit multi-turn conversations
304///
305/// # Example
306///
307/// ```
308/// use genai_rs::{InteractionInput, Turn};
309///
310/// // Simple text
311/// let input = InteractionInput::Text("Hello!".to_string());
312///
313/// // Multi-turn conversation
314/// let turns = vec![
315///     Turn::user("What is 2+2?"),
316///     Turn::model("2+2 equals 4."),
317///     Turn::user("And what's that times 3?"),
318/// ];
319/// let input = InteractionInput::Turns(turns);
320/// ```
321#[derive(Clone, Serialize, Deserialize, Debug)]
322#[serde(untagged)]
323#[non_exhaustive]
324pub enum InteractionInput {
325    /// Simple text input
326    Text(String),
327    /// Array of content objects
328    Content(Vec<Content>),
329    /// Array of turns for multi-turn conversations
330    Turns(Vec<Turn>),
331}
332
333/// Thinking level for chain-of-thought reasoning.
334///
335/// Controls the depth of reasoning the model performs before generating a response.
336/// Higher levels produce more detailed reasoning but consume more tokens.
337///
338/// This enum is marked `#[non_exhaustive]` for forward compatibility.
339/// New thinking levels may be added in future versions.
340///
341/// # Evergreen Pattern
342///
343/// Unknown values from the API deserialize into the `Unknown` variant, preserving
344/// the original data for debugging and roundtrip serialization.
345#[derive(Clone, Debug, PartialEq, Eq)]
346#[non_exhaustive]
347pub enum ThinkingLevel {
348    /// Minimal reasoning, fastest responses
349    Minimal,
350    /// Light reasoning for simple problems
351    Low,
352    /// Balanced reasoning for moderate complexity
353    Medium,
354    /// Extensive reasoning for complex problems
355    High,
356    /// Unknown variant for forward compatibility (Evergreen pattern)
357    Unknown {
358        /// The unrecognized level type from the API
359        level_type: String,
360        /// The full JSON data, preserved for debugging and roundtrip serialization
361        data: serde_json::Value,
362    },
363}
364
365impl ThinkingLevel {
366    /// Returns true if this is an unknown thinking level.
367    #[must_use]
368    pub const fn is_unknown(&self) -> bool {
369        matches!(self, Self::Unknown { .. })
370    }
371
372    /// Returns the level type name if this is an unknown thinking level.
373    #[must_use]
374    pub fn unknown_level_type(&self) -> Option<&str> {
375        match self {
376            Self::Unknown { level_type, .. } => Some(level_type),
377            _ => None,
378        }
379    }
380
381    /// Returns the preserved data if this is an unknown thinking level.
382    #[must_use]
383    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
384        match self {
385            Self::Unknown { data, .. } => Some(data),
386            _ => None,
387        }
388    }
389}
390
391impl Serialize for ThinkingLevel {
392    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
393    where
394        S: Serializer,
395    {
396        match self {
397            ThinkingLevel::Minimal => serializer.serialize_str("minimal"),
398            ThinkingLevel::Low => serializer.serialize_str("low"),
399            ThinkingLevel::Medium => serializer.serialize_str("medium"),
400            ThinkingLevel::High => serializer.serialize_str("high"),
401            ThinkingLevel::Unknown { level_type, data } => {
402                // If data is a simple string, serialize just the level_type
403                if data.is_string() || data.is_null() {
404                    serializer.serialize_str(level_type)
405                } else {
406                    // For complex data, serialize as an object
407                    let mut map = serializer.serialize_map(None)?;
408                    map.serialize_entry("level", level_type)?;
409                    map.serialize_entry("data", data)?;
410                    map.end()
411                }
412            }
413        }
414    }
415}
416
417impl<'de> Deserialize<'de> for ThinkingLevel {
418    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
419    where
420        D: Deserializer<'de>,
421    {
422        deserializer.deserialize_any(ThinkingLevelVisitor)
423    }
424}
425
426struct ThinkingLevelVisitor;
427
428impl<'de> Visitor<'de> for ThinkingLevelVisitor {
429    type Value = ThinkingLevel;
430
431    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
432        formatter.write_str("a thinking level string or object")
433    }
434
435    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
436    where
437        E: de::Error,
438    {
439        match value {
440            "minimal" => Ok(ThinkingLevel::Minimal),
441            "low" => Ok(ThinkingLevel::Low),
442            "medium" => Ok(ThinkingLevel::Medium),
443            "high" => Ok(ThinkingLevel::High),
444            other => {
445                tracing::warn!(
446                    "Encountered unknown ThinkingLevel '{}' - using Unknown variant (Evergreen)",
447                    other
448                );
449                Ok(ThinkingLevel::Unknown {
450                    level_type: other.to_string(),
451                    data: serde_json::Value::String(other.to_string()),
452                })
453            }
454        }
455    }
456
457    fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
458    where
459        A: de::MapAccess<'de>,
460    {
461        // For object-based thinking levels (future API compatibility)
462        let value: serde_json::Value =
463            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;
464        let level_type = value
465            .get("level")
466            .and_then(|v| v.as_str())
467            .unwrap_or("unknown")
468            .to_string();
469
470        tracing::warn!(
471            "Encountered unknown ThinkingLevel object '{}' - using Unknown variant (Evergreen)",
472            level_type
473        );
474        Ok(ThinkingLevel::Unknown {
475            level_type,
476            data: value,
477        })
478    }
479}
480
481/// Generation configuration for model behavior
482#[derive(Clone, Serialize, Deserialize, Debug, Default)]
483#[serde(rename_all = "camelCase")]
484pub struct GenerationConfig {
485    #[serde(skip_serializing_if = "Option::is_none")]
486    pub temperature: Option<f32>,
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub max_output_tokens: Option<i32>,
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub top_p: Option<f32>,
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub top_k: Option<i32>,
493    /// Thinking level for chain-of-thought reasoning
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub thinking_level: Option<ThinkingLevel>,
496    /// Seed for deterministic output generation.
497    ///
498    /// Using the same seed with identical inputs will produce the same output,
499    /// useful for testing and debugging.
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub seed: Option<i64>,
502    /// Stop sequences that halt generation.
503    ///
504    /// When the model generates any of these sequences, generation stops immediately.
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub stop_sequences: Option<Vec<String>>,
507    /// Controls whether thinking summaries are included in output.
508    ///
509    /// Use with `thinking_level` to control reasoning output.
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub thinking_summaries: Option<ThinkingSummaries>,
512    /// Controls function calling behavior.
513    ///
514    /// This field determines how the model uses declared tools/functions:
515    /// - `Auto` (default): Model decides whether to call functions
516    /// - `Any`: Model must call a function
517    /// - `None`: Function calling is disabled
518    /// - `Validated`: Ensures schema adherence for both function calls and natural language
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub tool_choice: Option<FunctionCallingMode>,
521    /// Speech configuration for text-to-speech audio output.
522    ///
523    /// Required when using `AUDIO` response modality.
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub speech_config: Option<SpeechConfig>,
526}
527
528/// Speech configuration for text-to-speech audio output.
529///
530/// Configure voice, language, and speaker settings when using `AUDIO` response modality.
531///
532/// # Example
533///
534/// ```
535/// use genai_rs::SpeechConfig;
536///
537/// let config = SpeechConfig {
538///     voice: Some("Kore".to_string()),
539///     language: Some("en-US".to_string()),
540///     speaker: None,
541/// };
542/// ```
543///
544/// # Available Voices
545///
546/// Common voices include: Aoede, Charon, Fenrir, Kore, Puck, and others.
547/// See [Google's TTS documentation](https://ai.google.dev/gemini-api/docs/text-generation)
548/// for the full list of available voices.
549#[derive(Clone, Serialize, Deserialize, Debug, Default)]
550#[serde(rename_all = "camelCase")]
551pub struct SpeechConfig {
552    /// The voice to use for speech synthesis.
553    ///
554    /// Examples: "Kore", "Puck", "Charon", "Fenrir", "Aoede"
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub voice: Option<String>,
557
558    /// The language/locale for speech synthesis.
559    ///
560    /// Examples: "en-US", "es-ES", "fr-FR"
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub language: Option<String>,
563
564    /// The speaker name for multi-speaker scenarios.
565    ///
566    /// Should match a speaker name given in the prompt when using
567    /// multi-speaker text-to-speech.
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub speaker: Option<String>,
570}
571
572impl SpeechConfig {
573    /// Creates a new `SpeechConfig` with the specified voice.
574    #[must_use]
575    pub fn with_voice(voice: impl Into<String>) -> Self {
576        Self {
577            voice: Some(voice.into()),
578            ..Default::default()
579        }
580    }
581
582    /// Creates a new `SpeechConfig` with the specified voice and language.
583    #[must_use]
584    pub fn with_voice_and_language(voice: impl Into<String>, language: impl Into<String>) -> Self {
585        Self {
586            voice: Some(voice.into()),
587            language: Some(language.into()),
588            ..Default::default()
589        }
590    }
591}
592
593/// Request body for the Interactions API endpoint.
594///
595/// This type represents a fully-constructed interaction request that can be
596/// cloned, serialized, and executed via [`Client::execute()`](crate::Client::execute).
597///
598/// # Creating Requests
599///
600/// Use [`InteractionBuilder::build()`](crate::InteractionBuilder::build) to create requests:
601///
602/// ```no_run
603/// # use genai_rs::Client;
604/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
605/// let client = Client::new("api_key".to_string());
606///
607/// let request = client.interaction()
608///     .with_model("gemini-3-flash-preview")
609///     .with_text("Hello!")
610///     .build()?;
611///
612/// // Request can be cloned, serialized, inspected
613/// let backup = request.clone();
614/// println!("{}", serde_json::to_string_pretty(&request)?);
615/// # Ok(())
616/// # }
617/// ```
618///
619/// # Executing Requests
620///
621/// ```no_run
622/// # use genai_rs::Client;
623/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
624/// # let client = Client::new("api_key".to_string());
625/// # let request = client.interaction()
626/// #     .with_model("gemini-3-flash-preview")
627/// #     .with_text("Hello!")
628/// #     .build()?;
629/// let response = client.execute(request).await?;
630/// # Ok(())
631/// # }
632/// ```
633///
634/// # Retrying Requests
635///
636/// Since `InteractionRequest` is `Clone`, you can retry failed requests:
637///
638/// ```no_run
639/// # use genai_rs::{Client, GenaiError};
640/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
641/// # let client = Client::new("api_key".to_string());
642/// # let request = client.interaction()
643/// #     .with_model("gemini-3-flash-preview")
644/// #     .with_text("Hello!")
645/// #     .build()?;
646/// let response = loop {
647///     match client.execute(request.clone()).await {
648///         Ok(r) => break r,
649///         Err(e) if e.is_retryable() => continue,
650///         Err(e) => return Err(e.into()),
651///     }
652/// };
653/// # Ok(())
654/// # }
655/// ```
656#[derive(Clone, Serialize, Deserialize, Debug)]
657#[serde(rename_all = "camelCase")]
658pub struct InteractionRequest {
659    /// Model name (e.g., "gemini-3-flash-preview") - mutually exclusive with agent
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub model: Option<String>,
662
663    /// Agent name (e.g., "deep-research-pro-preview-12-2025") - mutually exclusive with model
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub agent: Option<String>,
666
667    /// Agent-specific configuration (e.g., Deep Research thinking summaries)
668    #[serde(rename = "agent_config", skip_serializing_if = "Option::is_none")]
669    pub agent_config: Option<AgentConfig>,
670
671    /// The input for this interaction
672    pub input: InteractionInput,
673
674    /// Reference to a previous interaction for stateful conversations
675    #[serde(skip_serializing_if = "Option::is_none")]
676    pub previous_interaction_id: Option<String>,
677
678    /// Tools available for function calling
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub tools: Option<Vec<Tool>>,
681
682    /// Response modalities (e.g., ["IMAGE"])
683    #[serde(skip_serializing_if = "Option::is_none")]
684    pub response_modalities: Option<Vec<String>>,
685
686    /// JSON schema for structured output
687    #[serde(skip_serializing_if = "Option::is_none")]
688    pub response_format: Option<serde_json::Value>,
689
690    /// Response MIME type for structured output.
691    ///
692    /// Required when using `response_format` with a JSON schema.
693    /// Typically "application/json".
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub response_mime_type: Option<String>,
696
697    /// Model configuration
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub generation_config: Option<GenerationConfig>,
700
701    /// Enable streaming responses
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub stream: Option<bool>,
704
705    /// Background execution mode (agents only)
706    #[serde(skip_serializing_if = "Option::is_none")]
707    pub background: Option<bool>,
708
709    /// Persist interaction data (default: true)
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub store: Option<bool>,
712
713    /// System instruction for the model
714    #[serde(skip_serializing_if = "Option::is_none")]
715    pub system_instruction: Option<InteractionInput>,
716}
717
718// =============================================================================
719// Agent Configuration Types
720// =============================================================================
721
722/// Thinking summaries configuration for agent output.
723///
724/// When using thinking mode (via `with_thinking_level`), you can control
725/// whether the model's reasoning process is summarized in the output.
726///
727/// This enum is marked `#[non_exhaustive]` for forward compatibility.
728/// New summary modes may be added in future versions.
729///
730/// # Evergreen Pattern
731///
732/// Unknown values from the API deserialize into the `Unknown` variant, preserving
733/// the original data for debugging and roundtrip serialization.
734#[derive(Clone, Debug, PartialEq, Eq)]
735#[non_exhaustive]
736pub enum ThinkingSummaries {
737    /// Automatically include thinking summaries (default when thinking is enabled)
738    Auto,
739    /// Do not include thinking summaries
740    None,
741    /// Unknown variant for forward compatibility (Evergreen pattern)
742    Unknown {
743        /// The unrecognized summaries type from the API
744        summaries_type: String,
745        /// The full JSON data, preserved for debugging and roundtrip serialization
746        data: serde_json::Value,
747    },
748}
749
750impl ThinkingSummaries {
751    /// Returns true if this is an unknown thinking summaries value.
752    #[must_use]
753    pub const fn is_unknown(&self) -> bool {
754        matches!(self, Self::Unknown { .. })
755    }
756
757    /// Returns the summaries type name if this is an unknown value.
758    #[must_use]
759    pub fn unknown_summaries_type(&self) -> Option<&str> {
760        match self {
761            Self::Unknown { summaries_type, .. } => Some(summaries_type),
762            _ => None,
763        }
764    }
765
766    /// Returns the preserved data if this is an unknown value.
767    #[must_use]
768    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
769        match self {
770            Self::Unknown { data, .. } => Some(data),
771            _ => None,
772        }
773    }
774
775    /// Convert to the agent_config wire format (THINKING_SUMMARIES_*).
776    ///
777    /// AgentConfig uses a different wire format than GenerationConfig:
778    /// - GenerationConfig: lowercase ("auto", "none")
779    /// - AgentConfig: SCREAMING_CASE ("THINKING_SUMMARIES_AUTO", "THINKING_SUMMARIES_NONE")
780    #[must_use]
781    pub fn to_agent_config_value(&self) -> serde_json::Value {
782        match self {
783            ThinkingSummaries::Auto => {
784                serde_json::Value::String("THINKING_SUMMARIES_AUTO".to_string())
785            }
786            ThinkingSummaries::None => {
787                serde_json::Value::String("THINKING_SUMMARIES_NONE".to_string())
788            }
789            ThinkingSummaries::Unknown { summaries_type, .. } => {
790                // For unknown values, preserve the original format
791                serde_json::Value::String(summaries_type.clone())
792            }
793        }
794    }
795}
796
797impl Serialize for ThinkingSummaries {
798    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
799    where
800        S: Serializer,
801    {
802        // Note: GenerationConfig uses lowercase ("auto"/"none")
803        // For AgentConfig, use to_agent_config_value() instead
804        match self {
805            ThinkingSummaries::Auto => serializer.serialize_str("auto"),
806            ThinkingSummaries::None => serializer.serialize_str("none"),
807            ThinkingSummaries::Unknown {
808                summaries_type,
809                data,
810            } => {
811                // If data is a simple string, serialize just the summaries_type
812                if data.is_string() || data.is_null() {
813                    serializer.serialize_str(summaries_type)
814                } else {
815                    // For complex data, serialize as an object
816                    let mut map = serializer.serialize_map(None)?;
817                    map.serialize_entry("summaries", summaries_type)?;
818                    map.serialize_entry("data", data)?;
819                    map.end()
820                }
821            }
822        }
823    }
824}
825
826impl<'de> Deserialize<'de> for ThinkingSummaries {
827    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
828    where
829        D: Deserializer<'de>,
830    {
831        deserializer.deserialize_any(ThinkingSummariesVisitor)
832    }
833}
834
835struct ThinkingSummariesVisitor;
836
837impl<'de> Visitor<'de> for ThinkingSummariesVisitor {
838    type Value = ThinkingSummaries;
839
840    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
841        formatter.write_str("a thinking summaries string or object")
842    }
843
844    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
845    where
846        E: de::Error,
847    {
848        match value {
849            // Wire format is THINKING_SUMMARIES_*, but also accept lowercase for flexibility
850            "THINKING_SUMMARIES_AUTO" | "auto" => Ok(ThinkingSummaries::Auto),
851            "THINKING_SUMMARIES_NONE" | "none" => Ok(ThinkingSummaries::None),
852            other => {
853                tracing::warn!(
854                    "Encountered unknown ThinkingSummaries '{}' - using Unknown variant (Evergreen)",
855                    other
856                );
857                Ok(ThinkingSummaries::Unknown {
858                    summaries_type: other.to_string(),
859                    data: serde_json::Value::String(other.to_string()),
860                })
861            }
862        }
863    }
864
865    fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
866    where
867        A: de::MapAccess<'de>,
868    {
869        // For object-based thinking summaries (future API compatibility)
870        let value: serde_json::Value =
871            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;
872        let summaries_type = value
873            .get("summaries")
874            .and_then(|v| v.as_str())
875            .unwrap_or("unknown")
876            .to_string();
877
878        tracing::warn!(
879            "Encountered unknown ThinkingSummaries object '{}' - using Unknown variant (Evergreen)",
880            summaries_type
881        );
882        Ok(ThinkingSummaries::Unknown {
883            summaries_type,
884            data: value,
885        })
886    }
887}
888
889/// Agent-specific configuration for specialized agents.
890///
891/// This is a thin wrapper around JSON that provides full forward compatibility.
892/// Use typed config structs like [`DeepResearchConfig`] for compile-time guidance,
893/// or construct directly from JSON for unknown/future agent types.
894///
895/// # Usage
896///
897/// ## Typed configs (recommended for known agents)
898/// ```
899/// use genai_rs::{AgentConfig, DeepResearchConfig, ThinkingSummaries};
900///
901/// let config: AgentConfig = DeepResearchConfig::new()
902///     .with_thinking_summaries(ThinkingSummaries::Auto)
903///     .into();
904/// ```
905///
906/// ## Raw JSON (for unknown/future agents)
907/// ```
908/// use genai_rs::AgentConfig;
909///
910/// let config = AgentConfig::from_value(serde_json::json!({
911///     "type": "future-agent",
912///     "newOption": true
913/// }));
914/// ```
915#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
916#[serde(transparent)]
917pub struct AgentConfig(serde_json::Value);
918
919impl AgentConfig {
920    /// Create an agent config from a raw JSON value.
921    ///
922    /// Use this for unknown or future agent types that don't have typed config structs.
923    #[must_use]
924    pub fn from_value(value: serde_json::Value) -> Self {
925        Self(value)
926    }
927
928    /// Access the underlying JSON value.
929    #[must_use]
930    pub fn as_value(&self) -> &serde_json::Value {
931        &self.0
932    }
933
934    /// Get the agent config type (e.g., "deep-research", "dynamic").
935    #[must_use]
936    pub fn config_type(&self) -> Option<&str> {
937        self.0.get("type").and_then(|v| v.as_str())
938    }
939}
940
941/// Configuration for Deep Research agent.
942///
943/// Deep Research agent performs comprehensive research tasks
944/// and can optionally include thinking summaries.
945///
946/// # Example
947///
948/// ```
949/// use genai_rs::{AgentConfig, DeepResearchConfig, ThinkingSummaries};
950///
951/// let config: AgentConfig = DeepResearchConfig::new()
952///     .with_thinking_summaries(ThinkingSummaries::Auto)
953///     .into();
954/// ```
955#[derive(Clone, Debug, Default)]
956pub struct DeepResearchConfig {
957    thinking_summaries: Option<ThinkingSummaries>,
958}
959
960impl DeepResearchConfig {
961    /// Create a new Deep Research configuration with default settings.
962    #[must_use]
963    pub fn new() -> Self {
964        Self::default()
965    }
966
967    /// Set thinking summaries mode.
968    ///
969    /// Controls whether the agent's reasoning process is summarized in output.
970    #[must_use]
971    pub fn with_thinking_summaries(mut self, summaries: ThinkingSummaries) -> Self {
972        self.thinking_summaries = Some(summaries);
973        self
974    }
975}
976
977impl From<DeepResearchConfig> for AgentConfig {
978    fn from(config: DeepResearchConfig) -> Self {
979        let mut map = serde_json::Map::new();
980        map.insert(
981            "type".into(),
982            serde_json::Value::String("deep-research".into()),
983        );
984        if let Some(ts) = config.thinking_summaries {
985            // Use agent_config format (THINKING_SUMMARIES_*), not generation_config format (auto/none)
986            map.insert("thinking_summaries".into(), ts.to_agent_config_value());
987        }
988        AgentConfig(serde_json::Value::Object(map))
989    }
990}
991
992/// Configuration for Dynamic agent.
993///
994/// Dynamic agents adapt their behavior based on the task.
995/// Currently has no configurable options.
996///
997/// # Example
998///
999/// ```
1000/// use genai_rs::{AgentConfig, DynamicConfig};
1001///
1002/// let config: AgentConfig = DynamicConfig::new().into();
1003/// ```
1004#[derive(Clone, Debug, Default)]
1005pub struct DynamicConfig;
1006
1007impl DynamicConfig {
1008    /// Create a new Dynamic agent configuration.
1009    #[must_use]
1010    pub fn new() -> Self {
1011        Self
1012    }
1013}
1014
1015impl From<DynamicConfig> for AgentConfig {
1016    fn from(_: DynamicConfig) -> Self {
1017        AgentConfig(serde_json::json!({"type": "dynamic"}))
1018    }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023    use super::*;
1024
1025    // =========================================================================
1026    // Agent Config Tests
1027    // =========================================================================
1028
1029    #[test]
1030    fn test_thinking_summaries_serialization() {
1031        // GenerationConfig wire format uses lowercase
1032        assert_eq!(
1033            serde_json::to_string(&ThinkingSummaries::Auto).unwrap(),
1034            "\"auto\""
1035        );
1036
1037        assert_eq!(
1038            serde_json::to_string(&ThinkingSummaries::None).unwrap(),
1039            "\"none\""
1040        );
1041    }
1042
1043    #[test]
1044    fn test_thinking_summaries_agent_config_format() {
1045        // AgentConfig uses THINKING_SUMMARIES_* format via to_agent_config_value()
1046        assert_eq!(
1047            ThinkingSummaries::Auto.to_agent_config_value(),
1048            serde_json::Value::String("THINKING_SUMMARIES_AUTO".to_string())
1049        );
1050
1051        assert_eq!(
1052            ThinkingSummaries::None.to_agent_config_value(),
1053            serde_json::Value::String("THINKING_SUMMARIES_NONE".to_string())
1054        );
1055    }
1056
1057    #[test]
1058    fn test_thinking_summaries_deserialization() {
1059        // Test wire format (THINKING_SUMMARIES_*)
1060        assert_eq!(
1061            serde_json::from_str::<ThinkingSummaries>("\"THINKING_SUMMARIES_AUTO\"").unwrap(),
1062            ThinkingSummaries::Auto
1063        );
1064        assert_eq!(
1065            serde_json::from_str::<ThinkingSummaries>("\"THINKING_SUMMARIES_NONE\"").unwrap(),
1066            ThinkingSummaries::None
1067        );
1068
1069        // Also accept lowercase for flexibility
1070        assert_eq!(
1071            serde_json::from_str::<ThinkingSummaries>("\"auto\"").unwrap(),
1072            ThinkingSummaries::Auto
1073        );
1074        assert_eq!(
1075            serde_json::from_str::<ThinkingSummaries>("\"none\"").unwrap(),
1076            ThinkingSummaries::None
1077        );
1078    }
1079
1080    #[test]
1081    fn test_thinking_summaries_unknown_roundtrip() {
1082        let unknown: ThinkingSummaries = serde_json::from_str("\"future_variant\"").unwrap();
1083        assert!(unknown.is_unknown());
1084        assert_eq!(unknown.unknown_summaries_type(), Some("future_variant"));
1085
1086        // Roundtrip preserves the unknown value
1087        let json = serde_json::to_string(&unknown).unwrap();
1088        assert_eq!(json, "\"future_variant\"");
1089    }
1090
1091    #[test]
1092    fn test_deep_research_config_serialization() {
1093        let config: AgentConfig = DeepResearchConfig::new()
1094            .with_thinking_summaries(ThinkingSummaries::Auto)
1095            .into();
1096
1097        let json = serde_json::to_string(&config).expect("Serialization failed");
1098        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1099
1100        assert_eq!(value["type"], "deep-research");
1101        assert_eq!(value["thinking_summaries"], "THINKING_SUMMARIES_AUTO");
1102    }
1103
1104    #[test]
1105    fn test_deep_research_config_without_thinking_summaries() {
1106        let config: AgentConfig = DeepResearchConfig::new().into();
1107
1108        let json = serde_json::to_string(&config).expect("Serialization failed");
1109        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1110
1111        assert_eq!(value["type"], "deep-research");
1112        assert!(value.get("thinking_summaries").is_none());
1113    }
1114
1115    #[test]
1116    fn test_dynamic_config_serialization() {
1117        let config: AgentConfig = DynamicConfig::new().into();
1118
1119        let json = serde_json::to_string(&config).expect("Serialization failed");
1120        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1121
1122        assert_eq!(value["type"], "dynamic");
1123    }
1124
1125    #[test]
1126    fn test_agent_config_from_raw_json() {
1127        let config = AgentConfig::from_value(serde_json::json!({
1128            "type": "custom-agent",
1129            "option1": true,
1130            "option2": "value"
1131        }));
1132
1133        assert_eq!(config.config_type(), Some("custom-agent"));
1134        assert_eq!(config.as_value()["option1"], true);
1135    }
1136
1137    #[test]
1138    fn test_agent_config_roundtrip() {
1139        let config: AgentConfig = DeepResearchConfig::new()
1140            .with_thinking_summaries(ThinkingSummaries::Auto)
1141            .into();
1142
1143        let json = serde_json::to_string(&config).expect("Serialization failed");
1144        let parsed: AgentConfig = serde_json::from_str(&json).expect("Deserialization failed");
1145
1146        assert_eq!(config, parsed);
1147    }
1148
1149    // =========================================================================
1150    // SpeechConfig Tests
1151    // =========================================================================
1152
1153    #[test]
1154    fn test_speech_config_with_voice() {
1155        let config = SpeechConfig::with_voice("Kore");
1156        assert_eq!(config.voice, Some("Kore".to_string()));
1157        assert_eq!(config.language, None);
1158        assert_eq!(config.speaker, None);
1159    }
1160
1161    #[test]
1162    fn test_speech_config_with_voice_and_language() {
1163        let config = SpeechConfig::with_voice_and_language("Puck", "en-GB");
1164        assert_eq!(config.voice, Some("Puck".to_string()));
1165        assert_eq!(config.language, Some("en-GB".to_string()));
1166        assert_eq!(config.speaker, None);
1167    }
1168
1169    #[test]
1170    fn test_speech_config_serialization() {
1171        let config = SpeechConfig {
1172            voice: Some("Fenrir".to_string()),
1173            language: Some("en-US".to_string()),
1174            speaker: None,
1175        };
1176
1177        let json = serde_json::to_string(&config).expect("Serialization failed");
1178        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1179
1180        // Verify flat format is produced (voice, language at top level)
1181        assert_eq!(value["voice"], "Fenrir");
1182        assert_eq!(value["language"], "en-US");
1183        assert!(value.get("speaker").is_none()); // None fields should be skipped
1184
1185        // Verify nested format is NOT produced
1186        // Google docs suggest voiceConfig.prebuiltVoiceConfig.voiceName but that returns 400.
1187        // See docs/ENUM_WIRE_FORMATS.md and docs/INTERACTIONS_API_FEEDBACK.md Issue #7.
1188        assert!(
1189            value.get("voiceConfig").is_none(),
1190            "Should use flat format, not nested voiceConfig"
1191        );
1192        assert!(
1193            value.get("prebuiltVoiceConfig").is_none(),
1194            "Should use flat format, not nested prebuiltVoiceConfig"
1195        );
1196    }
1197
1198    #[test]
1199    fn test_speech_config_roundtrip() {
1200        let config = SpeechConfig {
1201            voice: Some("Aoede".to_string()),
1202            language: Some("es-ES".to_string()),
1203            speaker: Some("narrator".to_string()),
1204        };
1205
1206        let json = serde_json::to_string(&config).expect("Serialization failed");
1207        let parsed: SpeechConfig = serde_json::from_str(&json).expect("Deserialization failed");
1208
1209        assert_eq!(config.voice, parsed.voice);
1210        assert_eq!(config.language, parsed.language);
1211        assert_eq!(config.speaker, parsed.speaker);
1212    }
1213
1214    #[test]
1215    fn test_speech_config_default() {
1216        let config = SpeechConfig::default();
1217        assert_eq!(config.voice, None);
1218        assert_eq!(config.language, None);
1219        assert_eq!(config.speaker, None);
1220    }
1221}