Skip to main content

genai_rs/
response_format.rs

1//! Typed `response_format` union for interaction requests.
2//!
3//! The API accepts either a single response format object or a list of them
4//! (for multi-modality output). Each format is tagged by `type`:
5//! `text`, `audio`, `image`, or `video`.
6//!
7//! For the common JSON-schema case use [`ResponseFormat::json_schema()`], or
8//! keep passing a raw `serde_json::Value` schema to
9//! [`InteractionBuilder::with_response_format()`](crate::InteractionBuilder::with_response_format) —
10//! it converts into `ResponseFormat::Text { mime_type: "application/json", schema }`.
11//!
12//! See `docs/OUTPUT_MODALITIES.md` for delivery modes and per-modality options.
13
14use serde::{Deserialize, Deserializer, Serialize, Serializer};
15
16use crate::request::{ImageAspectRatio, ImageSize};
17
18/// Delivery mode for generated media output.
19///
20/// This enum is marked `#[non_exhaustive]` for forward compatibility.
21///
22/// # Wire Format
23///
24/// Serializes as lowercase strings: `"inline"`, `"uri"`.
25///
26/// # Evergreen Pattern
27///
28/// Unknown values from the API deserialize into the `Unknown` variant,
29/// preserving the original data for debugging and roundtrip serialization.
30#[derive(Clone, Debug, PartialEq)]
31#[non_exhaustive]
32pub enum ResponseDelivery {
33    /// Media bytes are returned inline (base64) in the response.
34    Inline,
35    /// Media is delivered by URI (e.g., a GCS object).
36    Uri,
37    /// Unknown variant for forward compatibility (Evergreen pattern)
38    Unknown {
39        /// The unrecognized delivery type from the API
40        delivery_type: String,
41        /// The raw JSON value, preserved for debugging and roundtrip
42        data: serde_json::Value,
43    },
44}
45
46impl ResponseDelivery {
47    /// Returns true if this is an unknown delivery mode.
48    #[must_use]
49    pub const fn is_unknown(&self) -> bool {
50        matches!(self, Self::Unknown { .. })
51    }
52
53    /// Returns the delivery type name if this is an unknown delivery mode.
54    #[must_use]
55    pub fn unknown_delivery_type(&self) -> Option<&str> {
56        match self {
57            Self::Unknown { delivery_type, .. } => Some(delivery_type),
58            _ => None,
59        }
60    }
61
62    /// Returns the preserved data if this is an unknown delivery mode.
63    #[must_use]
64    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
65        match self {
66            Self::Unknown { data, .. } => Some(data),
67            _ => None,
68        }
69    }
70}
71
72impl Serialize for ResponseDelivery {
73    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
74    where
75        S: Serializer,
76    {
77        match self {
78            Self::Inline => serializer.serialize_str("inline"),
79            Self::Uri => serializer.serialize_str("uri"),
80            Self::Unknown { delivery_type, .. } => serializer.serialize_str(delivery_type),
81        }
82    }
83}
84
85impl<'de> Deserialize<'de> for ResponseDelivery {
86    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
87    where
88        D: Deserializer<'de>,
89    {
90        let value = serde_json::Value::deserialize(deserializer)?;
91        match value.as_str() {
92            Some("inline") => Ok(Self::Inline),
93            Some("uri") => Ok(Self::Uri),
94            Some(other) => {
95                tracing::warn!(
96                    "Encountered unknown ResponseDelivery '{}' - using Unknown variant (Evergreen)",
97                    other
98                );
99                Ok(Self::Unknown {
100                    delivery_type: other.to_string(),
101                    data: value,
102                })
103            }
104            None => {
105                let delivery_type = format!("<non-string: {}>", value);
106                tracing::warn!(
107                    "ResponseDelivery received non-string value: {}. \
108                     Preserving in Unknown variant.",
109                    value
110                );
111                Ok(Self::Unknown {
112                    delivery_type,
113                    data: value,
114                })
115            }
116        }
117    }
118}
119
120/// A typed response format, tagged by `type` on the wire.
121///
122/// This enum is marked `#[non_exhaustive]` for forward compatibility.
123/// New output formats may be added in future API versions.
124///
125/// # Wire Format
126///
127/// ```json
128/// {"type": "text", "mime_type": "application/json", "schema": {...}}
129/// {"type": "audio", "mime_type": "audio/mp3", "delivery": "inline", "sample_rate": 24000}
130/// {"type": "image", "mime_type": "image/jpeg", "delivery": "uri", "aspect_ratio": "16:9"}
131/// {"type": "video", "delivery": "uri", "gcs_uri": "gs://bucket/out", "duration": "8s"}
132/// ```
133///
134/// # Evergreen Pattern
135///
136/// Unknown `type` tags from the API deserialize into the `Unknown` variant,
137/// preserving the original data for debugging and roundtrip serialization.
138#[derive(Clone, Debug, PartialEq)]
139#[non_exhaustive]
140pub enum ResponseFormat {
141    /// Text output configuration.
142    Text {
143        /// MIME type of the text output. Known values: `application/json`,
144        /// `text/plain`.
145        mime_type: Option<String>,
146        /// JSON schema the output must conform to. Only applicable when
147        /// `mime_type` is `application/json`.
148        schema: Option<serde_json::Value>,
149    },
150    /// Audio output configuration.
151    ///
152    /// Server-side constraints (verified live 2026-07 on the Gemini API):
153    /// `mime_type` and `delivery` are schema-valid but rejected
154    /// ("Audio mime_type is not supported in response_format." /
155    /// "Audio delivery mode is not supported."); `sample_rate` is accepted.
156    /// Output is returned inline as `audio/l16`.
157    Audio {
158        /// MIME type of the audio output. Known values: `audio/mp3`,
159        /// `audio/ogg_opus`, `audio/l16`, `audio/wav`, `audio/alaw`,
160        /// `audio/mulaw`. Rejected by the Gemini API as of 2026-07.
161        mime_type: Option<String>,
162        /// Delivery mode for the audio output. Rejected by the Gemini API
163        /// as of 2026-07 (inline-only).
164        delivery: Option<ResponseDelivery>,
165        /// Sample rate in Hz.
166        sample_rate: Option<i32>,
167        /// Bit rate in bits per second. Only applicable for compressed
168        /// formats (MP3, Opus).
169        bit_rate: Option<i32>,
170    },
171    /// Image output configuration.
172    ///
173    /// Server-side constraints (verified live 2026-07 on the Gemini API):
174    /// `mime_type` is accepted but only `image/jpeg` is supported;
175    /// `delivery` is rejected ("Image delivery mode is not supported.",
176    /// inline-only).
177    Image {
178        /// MIME type of the image output. Known value: `image/jpeg`
179        /// (the only value the Gemini API accepts as of 2026-07).
180        mime_type: Option<String>,
181        /// Delivery mode for the image output. Rejected by the Gemini API
182        /// as of 2026-07 (inline-only).
183        delivery: Option<ResponseDelivery>,
184        /// Aspect ratio for the image output.
185        aspect_ratio: Option<ImageAspectRatio>,
186        /// Size of the image output.
187        image_size: Option<ImageSize>,
188    },
189    /// Video output configuration.
190    Video {
191        /// Delivery mode for the video output.
192        delivery: Option<ResponseDelivery>,
193        /// GCS URI to store the video output. Required on Vertex when
194        /// `delivery` is `uri`. Rejected on the Gemini API (2026-07:
195        /// "not available on the Gemini API but it is available on the
196        /// Gemini Enterprise Agent Platform").
197        gcs_uri: Option<String>,
198        /// Aspect ratio for the video output. Known values: `16:9`, `9:16`.
199        aspect_ratio: Option<ImageAspectRatio>,
200        /// Duration for the video output (e.g., `"8s"`).
201        duration: Option<String>,
202    },
203    /// Unknown variant for forward compatibility (Evergreen pattern)
204    Unknown {
205        /// The unrecognized format type from the API
206        format_type: String,
207        /// The raw JSON value, preserved for debugging and roundtrip
208        data: serde_json::Value,
209    },
210}
211
212impl ResponseFormat {
213    /// Creates a `text` format enforcing a JSON schema
214    /// (`mime_type: "application/json"`).
215    ///
216    /// This is the common structured-output form used by
217    /// [`InteractionBuilder::with_response_format()`](crate::InteractionBuilder::with_response_format).
218    #[must_use]
219    pub fn json_schema(schema: serde_json::Value) -> Self {
220        Self::Text {
221            mime_type: Some("application/json".to_string()),
222            schema: Some(schema),
223        }
224    }
225
226    /// Creates a plain-text (`text/plain`) format.
227    #[must_use]
228    pub fn text_plain() -> Self {
229        Self::Text {
230            mime_type: Some("text/plain".to_string()),
231            schema: None,
232        }
233    }
234
235    /// Returns true if this is an unknown response format.
236    #[must_use]
237    pub const fn is_unknown(&self) -> bool {
238        matches!(self, Self::Unknown { .. })
239    }
240
241    /// Returns the format type name if this is an unknown response format.
242    #[must_use]
243    pub fn unknown_format_type(&self) -> Option<&str> {
244        match self {
245            Self::Unknown { format_type, .. } => Some(format_type),
246            _ => None,
247        }
248    }
249
250    /// Returns the preserved data if this is an unknown response format.
251    #[must_use]
252    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
253        match self {
254            Self::Unknown { data, .. } => Some(data),
255            _ => None,
256        }
257    }
258}
259
260/// Converts a raw JSON value into a [`ResponseFormat`].
261///
262/// - Objects tagged `{"type": "text" | "audio" | "image" | "video"}` parse
263///   as that typed variant.
264/// - Any other value is treated as a raw JSON schema and becomes
265///   `ResponseFormat::Text { mime_type: "application/json", schema }` —
266///   preserving the pre-0.8 `with_response_format(schema_json)` behavior.
267impl From<serde_json::Value> for ResponseFormat {
268    fn from(value: serde_json::Value) -> Self {
269        let tag = value.get("type").and_then(|t| t.as_str());
270        if matches!(tag, Some("text" | "audio" | "image" | "video")) {
271            // Deserialization only falls back to Unknown, never fails.
272            serde_json::from_value(value).expect("ResponseFormat deserialization is infallible")
273        } else {
274            Self::json_schema(value)
275        }
276    }
277}
278
279impl Serialize for ResponseFormat {
280    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
281    where
282        S: Serializer,
283    {
284        use serde::ser::SerializeMap;
285        match self {
286            Self::Text { mime_type, schema } => {
287                let mut map = serializer.serialize_map(None)?;
288                map.serialize_entry("type", "text")?;
289                if let Some(mime_type) = mime_type {
290                    map.serialize_entry("mime_type", mime_type)?;
291                }
292                if let Some(schema) = schema {
293                    map.serialize_entry("schema", schema)?;
294                }
295                map.end()
296            }
297            Self::Audio {
298                mime_type,
299                delivery,
300                sample_rate,
301                bit_rate,
302            } => {
303                let mut map = serializer.serialize_map(None)?;
304                map.serialize_entry("type", "audio")?;
305                if let Some(mime_type) = mime_type {
306                    map.serialize_entry("mime_type", mime_type)?;
307                }
308                if let Some(delivery) = delivery {
309                    map.serialize_entry("delivery", delivery)?;
310                }
311                if let Some(sample_rate) = sample_rate {
312                    map.serialize_entry("sample_rate", sample_rate)?;
313                }
314                if let Some(bit_rate) = bit_rate {
315                    map.serialize_entry("bit_rate", bit_rate)?;
316                }
317                map.end()
318            }
319            Self::Image {
320                mime_type,
321                delivery,
322                aspect_ratio,
323                image_size,
324            } => {
325                let mut map = serializer.serialize_map(None)?;
326                map.serialize_entry("type", "image")?;
327                if let Some(mime_type) = mime_type {
328                    map.serialize_entry("mime_type", mime_type)?;
329                }
330                if let Some(delivery) = delivery {
331                    map.serialize_entry("delivery", delivery)?;
332                }
333                if let Some(aspect_ratio) = aspect_ratio {
334                    map.serialize_entry("aspect_ratio", aspect_ratio)?;
335                }
336                if let Some(image_size) = image_size {
337                    map.serialize_entry("image_size", image_size)?;
338                }
339                map.end()
340            }
341            Self::Video {
342                delivery,
343                gcs_uri,
344                aspect_ratio,
345                duration,
346            } => {
347                let mut map = serializer.serialize_map(None)?;
348                map.serialize_entry("type", "video")?;
349                if let Some(delivery) = delivery {
350                    map.serialize_entry("delivery", delivery)?;
351                }
352                if let Some(gcs_uri) = gcs_uri {
353                    map.serialize_entry("gcs_uri", gcs_uri)?;
354                }
355                if let Some(aspect_ratio) = aspect_ratio {
356                    map.serialize_entry("aspect_ratio", aspect_ratio)?;
357                }
358                if let Some(duration) = duration {
359                    map.serialize_entry("duration", duration)?;
360                }
361                map.end()
362            }
363            Self::Unknown { data, .. } => data.serialize(serializer),
364        }
365    }
366}
367
368impl<'de> Deserialize<'de> for ResponseFormat {
369    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
370    where
371        D: Deserializer<'de>,
372    {
373        let value = serde_json::Value::deserialize(deserializer)?;
374
375        #[derive(Deserialize)]
376        #[serde(tag = "type")]
377        enum KnownFormat {
378            #[serde(rename = "text")]
379            Text {
380                #[serde(default)]
381                mime_type: Option<String>,
382                #[serde(default)]
383                schema: Option<serde_json::Value>,
384            },
385            #[serde(rename = "audio")]
386            Audio {
387                #[serde(default)]
388                mime_type: Option<String>,
389                #[serde(default)]
390                delivery: Option<ResponseDelivery>,
391                #[serde(default)]
392                sample_rate: Option<i32>,
393                #[serde(default)]
394                bit_rate: Option<i32>,
395            },
396            #[serde(rename = "image")]
397            Image {
398                #[serde(default)]
399                mime_type: Option<String>,
400                #[serde(default)]
401                delivery: Option<ResponseDelivery>,
402                #[serde(default)]
403                aspect_ratio: Option<ImageAspectRatio>,
404                #[serde(default)]
405                image_size: Option<ImageSize>,
406            },
407            #[serde(rename = "video")]
408            Video {
409                #[serde(default)]
410                delivery: Option<ResponseDelivery>,
411                #[serde(default)]
412                gcs_uri: Option<String>,
413                #[serde(default)]
414                aspect_ratio: Option<ImageAspectRatio>,
415                #[serde(default)]
416                duration: Option<String>,
417            },
418        }
419
420        match serde_json::from_value::<KnownFormat>(value.clone()) {
421            Ok(known) => Ok(match known {
422                KnownFormat::Text { mime_type, schema } => Self::Text { mime_type, schema },
423                KnownFormat::Audio {
424                    mime_type,
425                    delivery,
426                    sample_rate,
427                    bit_rate,
428                } => Self::Audio {
429                    mime_type,
430                    delivery,
431                    sample_rate,
432                    bit_rate,
433                },
434                KnownFormat::Image {
435                    mime_type,
436                    delivery,
437                    aspect_ratio,
438                    image_size,
439                } => Self::Image {
440                    mime_type,
441                    delivery,
442                    aspect_ratio,
443                    image_size,
444                },
445                KnownFormat::Video {
446                    delivery,
447                    gcs_uri,
448                    aspect_ratio,
449                    duration,
450                } => Self::Video {
451                    delivery,
452                    gcs_uri,
453                    aspect_ratio,
454                    duration,
455                },
456            }),
457            Err(parse_error) => {
458                // Unknown/raw shape (the spec also allows a raw schema dict
459                // here) - preserve the data intact.
460                let format_type = value
461                    .get("type")
462                    .and_then(|v| v.as_str())
463                    .unwrap_or("<missing type>")
464                    .to_string();
465                tracing::warn!(
466                    "Encountered unknown ResponseFormat type '{}'. \
467                     Parse error: {}. \
468                     The format will be preserved in the Unknown variant.",
469                    format_type,
470                    parse_error
471                );
472                Ok(Self::Unknown {
473                    format_type,
474                    data: value,
475                })
476            }
477        }
478    }
479}
480
481/// The `response_format` request field union: a single [`ResponseFormat`]
482/// or a list of them.
483///
484/// Built implicitly via
485/// [`InteractionBuilder::with_response_format()`](crate::InteractionBuilder::with_response_format)
486/// (single) and
487/// [`InteractionBuilder::with_response_formats()`](crate::InteractionBuilder::with_response_formats)
488/// (list).
489///
490/// This enum is marked `#[non_exhaustive]` for forward compatibility.
491#[derive(Clone, Debug, PartialEq)]
492#[non_exhaustive]
493pub enum ResponseFormatSpec {
494    /// A single response format (serialized as a bare object).
495    Single(ResponseFormat),
496    /// A list of response formats (serialized as an array).
497    List(Vec<ResponseFormat>),
498}
499
500impl From<ResponseFormat> for ResponseFormatSpec {
501    fn from(format: ResponseFormat) -> Self {
502        Self::Single(format)
503    }
504}
505
506impl From<Vec<ResponseFormat>> for ResponseFormatSpec {
507    fn from(formats: Vec<ResponseFormat>) -> Self {
508        Self::List(formats)
509    }
510}
511
512impl From<serde_json::Value> for ResponseFormatSpec {
513    fn from(value: serde_json::Value) -> Self {
514        Self::Single(value.into())
515    }
516}
517
518impl Serialize for ResponseFormatSpec {
519    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
520    where
521        S: Serializer,
522    {
523        match self {
524            Self::Single(format) => format.serialize(serializer),
525            Self::List(formats) => formats.serialize(serializer),
526        }
527    }
528}
529
530impl<'de> Deserialize<'de> for ResponseFormatSpec {
531    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
532    where
533        D: Deserializer<'de>,
534    {
535        let value = serde_json::Value::deserialize(deserializer)?;
536        match value {
537            serde_json::Value::Array(items) => {
538                let formats = items
539                    .into_iter()
540                    .map(|item| {
541                        serde_json::from_value::<ResponseFormat>(item)
542                            .expect("ResponseFormat deserialization is infallible")
543                    })
544                    .collect();
545                Ok(Self::List(formats))
546            }
547            other => {
548                let format = serde_json::from_value::<ResponseFormat>(other)
549                    .expect("ResponseFormat deserialization is infallible");
550                Ok(Self::Single(format))
551            }
552        }
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use serde_json::json;
560
561    #[test]
562    fn test_response_delivery_wire_roundtrip() {
563        for (delivery, wire) in [
564            (ResponseDelivery::Inline, "\"inline\""),
565            (ResponseDelivery::Uri, "\"uri\""),
566        ] {
567            assert_eq!(serde_json::to_string(&delivery).unwrap(), wire);
568            let parsed: ResponseDelivery = serde_json::from_str(wire).unwrap();
569            assert_eq!(parsed, delivery);
570        }
571    }
572
573    #[test]
574    fn test_response_delivery_unknown_roundtrip() {
575        let unknown: ResponseDelivery = serde_json::from_str("\"multipart\"").unwrap();
576        assert!(unknown.is_unknown());
577        assert_eq!(unknown.unknown_delivery_type(), Some("multipart"));
578        assert!(unknown.unknown_data().is_some());
579        assert_eq!(serde_json::to_string(&unknown).unwrap(), "\"multipart\"");
580    }
581
582    #[test]
583    fn test_text_format_wire_shape() {
584        let format = ResponseFormat::json_schema(json!({
585            "type": "object",
586            "properties": {"name": {"type": "string"}}
587        }));
588        let value = serde_json::to_value(&format).unwrap();
589        assert_eq!(
590            value,
591            json!({
592                "type": "text",
593                "mime_type": "application/json",
594                "schema": {
595                    "type": "object",
596                    "properties": {"name": {"type": "string"}}
597                }
598            })
599        );
600    }
601
602    #[test]
603    fn test_audio_format_wire_shape() {
604        let format = ResponseFormat::Audio {
605            mime_type: Some("audio/mp3".to_string()),
606            delivery: Some(ResponseDelivery::Inline),
607            sample_rate: Some(24000),
608            bit_rate: Some(128_000),
609        };
610        let value = serde_json::to_value(&format).unwrap();
611        assert_eq!(
612            value,
613            json!({
614                "type": "audio",
615                "mime_type": "audio/mp3",
616                "delivery": "inline",
617                "sample_rate": 24000,
618                "bit_rate": 128000
619            })
620        );
621    }
622
623    #[test]
624    fn test_image_format_wire_shape() {
625        let format = ResponseFormat::Image {
626            mime_type: Some("image/jpeg".to_string()),
627            delivery: Some(ResponseDelivery::Uri),
628            aspect_ratio: Some(ImageAspectRatio::Widescreen16x9),
629            image_size: Some(ImageSize::Hd2k),
630        };
631        let value = serde_json::to_value(&format).unwrap();
632        assert_eq!(
633            value,
634            json!({
635                "type": "image",
636                "mime_type": "image/jpeg",
637                "delivery": "uri",
638                "aspect_ratio": "16:9",
639                "image_size": "2K"
640            })
641        );
642    }
643
644    #[test]
645    fn test_video_format_wire_shape() {
646        let format = ResponseFormat::Video {
647            delivery: Some(ResponseDelivery::Uri),
648            gcs_uri: Some("gs://bucket/out".to_string()),
649            aspect_ratio: Some(ImageAspectRatio::Portrait9x16),
650            duration: Some("8s".to_string()),
651        };
652        let value = serde_json::to_value(&format).unwrap();
653        assert_eq!(
654            value,
655            json!({
656                "type": "video",
657                "delivery": "uri",
658                "gcs_uri": "gs://bucket/out",
659                "aspect_ratio": "9:16",
660                "duration": "8s"
661            })
662        );
663    }
664
665    #[test]
666    fn test_response_format_roundtrip_all_variants() {
667        let formats = vec![
668            ResponseFormat::json_schema(json!({"type": "object"})),
669            ResponseFormat::text_plain(),
670            ResponseFormat::Audio {
671                mime_type: Some("audio/wav".to_string()),
672                delivery: None,
673                sample_rate: Some(16000),
674                bit_rate: None,
675            },
676            ResponseFormat::Image {
677                mime_type: None,
678                delivery: Some(ResponseDelivery::Inline),
679                aspect_ratio: Some(ImageAspectRatio::Square),
680                image_size: None,
681            },
682            ResponseFormat::Video {
683                delivery: Some(ResponseDelivery::Inline),
684                gcs_uri: None,
685                aspect_ratio: None,
686                duration: Some("4s".to_string()),
687            },
688        ];
689        for format in formats {
690            let json = serde_json::to_string(&format).unwrap();
691            let parsed: ResponseFormat = serde_json::from_str(&json).unwrap();
692            assert_eq!(parsed, format, "roundtrip failed for {json}");
693        }
694    }
695
696    #[test]
697    fn test_response_format_unknown_roundtrip() {
698        let raw = json!({"type": "hologram", "dimensions": 3});
699        let parsed: ResponseFormat = serde_json::from_value(raw.clone()).unwrap();
700        assert!(parsed.is_unknown());
701        assert_eq!(parsed.unknown_format_type(), Some("hologram"));
702        assert_eq!(parsed.unknown_data(), Some(&raw));
703        assert_eq!(serde_json::to_value(&parsed).unwrap(), raw);
704    }
705
706    #[test]
707    fn test_raw_schema_dict_deserializes_to_unknown_and_roundtrips() {
708        // The spec also allows a raw JSON-schema dict as a response format;
709        // it has no recognized "type" tag, so it must be preserved intact.
710        let raw = json!({"type": "object", "properties": {"x": {"type": "number"}}});
711        let parsed: ResponseFormat = serde_json::from_value(raw.clone()).unwrap();
712        assert!(parsed.is_unknown());
713        assert_eq!(serde_json::to_value(&parsed).unwrap(), raw);
714    }
715
716    #[test]
717    fn test_from_value_raw_schema_maps_to_text() {
718        // Backward-compatible construction path: a raw schema Value becomes
719        // the typed text/application/json format.
720        let schema = json!({"type": "object", "properties": {"name": {"type": "string"}}});
721        let format: ResponseFormat = schema.clone().into();
722        match &format {
723            ResponseFormat::Text {
724                mime_type,
725                schema: s,
726            } => {
727                assert_eq!(mime_type.as_deref(), Some("application/json"));
728                assert_eq!(s.as_ref(), Some(&schema));
729            }
730            other => panic!("Expected Text variant, got {other:?}"),
731        }
732    }
733
734    #[test]
735    fn test_from_value_typed_object_parses_as_variant() {
736        let value = json!({"type": "audio", "mime_type": "audio/mp3"});
737        let format: ResponseFormat = value.into();
738        assert!(matches!(format, ResponseFormat::Audio { .. }));
739    }
740
741    #[test]
742    fn test_response_format_spec_single_vs_list() {
743        let single: ResponseFormatSpec = ResponseFormat::text_plain().into();
744        let value = serde_json::to_value(&single).unwrap();
745        assert!(value.is_object());
746
747        let list: ResponseFormatSpec = vec![
748            ResponseFormat::text_plain(),
749            ResponseFormat::json_schema(json!({})),
750        ]
751        .into();
752        let value = serde_json::to_value(&list).unwrap();
753        assert!(value.is_array());
754        assert_eq!(value.as_array().unwrap().len(), 2);
755    }
756
757    #[test]
758    fn test_response_format_spec_roundtrip() {
759        let single: ResponseFormatSpec = ResponseFormat::json_schema(json!({"a": 1})).into();
760        let json = serde_json::to_string(&single).unwrap();
761        let parsed: ResponseFormatSpec = serde_json::from_str(&json).unwrap();
762        assert_eq!(parsed, single);
763
764        let list: ResponseFormatSpec = vec![ResponseFormat::Video {
765            delivery: Some(ResponseDelivery::Uri),
766            gcs_uri: Some("gs://b/o".to_string()),
767            aspect_ratio: None,
768            duration: None,
769        }]
770        .into();
771        let json = serde_json::to_string(&list).unwrap();
772        let parsed: ResponseFormatSpec = serde_json::from_str(&json).unwrap();
773        assert_eq!(parsed, list);
774    }
775
776    #[test]
777    fn test_response_format_known_not_unknown() {
778        let format = ResponseFormat::text_plain();
779        assert!(!format.is_unknown());
780        assert_eq!(format.unknown_format_type(), None);
781        assert_eq!(format.unknown_data(), None);
782    }
783}