1use serde::{Deserialize, Deserializer, Serialize, Serializer};
15
16use crate::request::{ImageAspectRatio, ImageSize};
17
18#[derive(Clone, Debug, PartialEq)]
31#[non_exhaustive]
32pub enum ResponseDelivery {
33 Inline,
35 Uri,
37 Unknown {
39 delivery_type: String,
41 data: serde_json::Value,
43 },
44}
45
46impl ResponseDelivery {
47 #[must_use]
49 pub const fn is_unknown(&self) -> bool {
50 matches!(self, Self::Unknown { .. })
51 }
52
53 #[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 #[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#[derive(Clone, Debug, PartialEq)]
139#[non_exhaustive]
140pub enum ResponseFormat {
141 Text {
143 mime_type: Option<String>,
146 schema: Option<serde_json::Value>,
149 },
150 Audio {
158 mime_type: Option<String>,
162 delivery: Option<ResponseDelivery>,
165 sample_rate: Option<i32>,
167 bit_rate: Option<i32>,
170 },
171 Image {
178 mime_type: Option<String>,
181 delivery: Option<ResponseDelivery>,
184 aspect_ratio: Option<ImageAspectRatio>,
186 image_size: Option<ImageSize>,
188 },
189 Video {
191 delivery: Option<ResponseDelivery>,
193 gcs_uri: Option<String>,
198 aspect_ratio: Option<ImageAspectRatio>,
200 duration: Option<String>,
202 },
203 Unknown {
205 format_type: String,
207 data: serde_json::Value,
209 },
210}
211
212impl ResponseFormat {
213 #[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 #[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 #[must_use]
237 pub const fn is_unknown(&self) -> bool {
238 matches!(self, Self::Unknown { .. })
239 }
240
241 #[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 #[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
260impl 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 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 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#[derive(Clone, Debug, PartialEq)]
492#[non_exhaustive]
493pub enum ResponseFormatSpec {
494 Single(ResponseFormat),
496 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 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 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}