1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
15use validator::Validate;
16
17#[derive(Clone, Serialize, Validate)]
26pub struct ChatCompletionResponse {
27 #[serde(
29 skip_serializing_if = "Option::is_none",
30 deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
31 )]
32 pub id: Option<String>,
33
34 #[serde(
36 skip_serializing_if = "Option::is_none",
37 deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
38 )]
39 pub request_id: Option<String>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub created: Option<u64>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub model: Option<String>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub choices: Option<Vec<Choice>>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub usage: Option<Usage>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub video_result: Option<Vec<VideoResultItem>>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
64 pub web_search: Option<Vec<WebSearchInfo>>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub content_filter: Option<Vec<ContentFilterInfo>>,
69 #[serde(skip_serializing_if = "Option::is_none")]
73 pub task_status: Option<TaskStatus>,
74}
75
76#[derive(Deserialize)]
77struct ChatCompletionResponseWire {
78 #[serde(
79 default,
80 deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
81 )]
82 id: Option<String>,
83 #[serde(
84 default,
85 deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
86 )]
87 request_id: Option<String>,
88 created: Option<u64>,
89 model: Option<String>,
90 choices: Option<Vec<Choice>>,
91 usage: Option<Usage>,
92 video_result: Option<Vec<VideoResultItem>>,
93 web_search: Option<Vec<WebSearchInfo>>,
94 content_filter: Option<Vec<ContentFilterInfo>>,
95 task_status: Option<TaskStatus>,
96}
97
98impl<'de> Deserialize<'de> for ChatCompletionResponse {
99 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
100 where
101 D: Deserializer<'de>,
102 {
103 let wire = ChatCompletionResponseWire::deserialize(deserializer)?;
104 let response = Self {
105 id: wire.id,
106 request_id: wire.request_id,
107 created: wire.created,
108 model: wire.model,
109 choices: wire.choices,
110 usage: wire.usage,
111 video_result: wire.video_result,
112 web_search: wire.web_search,
113 content_filter: wire.content_filter,
114 task_status: wire.task_status,
115 };
116 if response.id.is_none()
117 && response.request_id.is_none()
118 && response.created.is_none()
119 && response.model.is_none()
120 && response.choices.is_none()
121 && response.usage.is_none()
122 && response.video_result.is_none()
123 && response.web_search.is_none()
124 && response.content_filter.is_none()
125 && response.task_status.is_none()
126 {
127 return Err(D::Error::custom(
128 "chat completion response contained no documented fields",
129 ));
130 }
131 Ok(response)
132 }
133}
134
135impl std::fmt::Debug for ChatCompletionResponse {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 match serde_json::to_string_pretty(self) {
138 Ok(s) => f.write_str(&s),
139 Err(_) => f.debug_struct("ChatCompletionResponse").finish(),
140 }
141 }
142}
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
146#[non_exhaustive]
147pub enum TaskStatus {
148 #[serde(rename = "PROCESSING", alias = "processing")]
150 Processing,
151 #[serde(rename = "SUCCESS", alias = "success")]
153 Success,
154 #[serde(rename = "FAIL", alias = "fail")]
156 Fail,
157 #[serde(other)]
162 Unknown,
163}
164impl TaskStatus {
165 pub fn as_str(&self) -> &'static str {
168 match self {
169 TaskStatus::Processing => "PROCESSING",
170 TaskStatus::Success => "SUCCESS",
171 TaskStatus::Fail => "FAIL",
172 TaskStatus::Unknown => "UNKNOWN",
173 }
174 }
175}
176
177impl std::fmt::Display for TaskStatus {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 f.write_str(self.as_str())
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
185pub struct Choice {
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub index: Option<i32>,
189
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub message: Option<Message>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
197 pub finish_reason: Option<String>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
208pub struct Message {
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub role: Option<String>,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
218 pub content: Option<MessageContent>,
219
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub reasoning_content: Option<String>,
223
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub audio: Option<AudioContent>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub tool_calls: Option<Vec<ToolCallMessage>>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(untagged)]
236pub enum MessageContent {
237 Text(String),
239 Parts(Vec<MessageContentPart>),
241}
242
243impl MessageContent {
244 pub fn as_str(&self) -> Option<&str> {
246 match self {
247 Self::Text(text) => Some(text),
248 Self::Parts(_) => None,
249 }
250 }
251
252 pub fn as_parts(&self) -> Option<&[MessageContentPart]> {
254 match self {
255 Self::Text(_) => None,
256 Self::Parts(parts) => Some(parts),
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
263pub struct MessageContentPart {
264 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
266 pub type_: Option<MessageContentPartType>,
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub text: Option<String>,
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(rename_all = "lowercase")]
275pub enum MessageContentPartType {
276 Text,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
287pub struct ToolCallMessage {
288 #[serde(
290 default,
291 skip_serializing_if = "Option::is_none",
292 deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
293 )]
294 pub id: Option<String>,
295 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
297 pub type_: Option<String>,
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub function: Option<ToolFunction>,
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub mcp: Option<MCPMessage>,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
308pub struct ToolFunction {
309 pub name: String,
311 pub arguments: String,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
317pub struct MCPMessage {
318 #[serde(
320 default,
321 skip_serializing_if = "Option::is_none",
322 deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
323 )]
324 pub id: Option<String>,
325 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
327 pub type_: Option<MCPCallType>,
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub server_label: Option<String>,
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub error: Option<String>,
334
335 #[serde(skip_serializing_if = "Option::is_none")]
337 pub tools: Option<Vec<MCPTool>>,
338
339 #[serde(
342 default,
343 skip_serializing_if = "Option::is_none",
344 deserialize_with = "super::serde_helpers::optional_json_string"
345 )]
346 pub arguments: Option<String>,
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub name: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub output: Option<serde_json::Value>,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
358#[non_exhaustive]
359#[serde(rename_all = "snake_case")]
360pub enum MCPCallType {
361 McpListTools,
363 McpCall,
365 #[serde(other)]
369 Unknown,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
374pub struct MCPTool {
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub name: Option<String>,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub description: Option<String>,
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub annotations: Option<serde_json::Value>,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub input_schema: Option<MCPInputSchema>,
387}
388#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
390pub struct MCPInputSchema {
391 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
393 pub type_: Option<MCPInputType>,
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub properties: Option<serde_json::Value>,
397 #[serde(skip_serializing_if = "Option::is_none")]
399 pub required: Option<Vec<String>>,
400 #[serde(skip_serializing_if = "Option::is_none")]
402 pub additional_properties: Option<bool>,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
406#[non_exhaustive]
410#[serde(rename_all = "lowercase")]
411pub enum MCPInputType {
412 Object,
414 #[serde(other)]
417 Unknown,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
428pub struct AudioContent {
429 #[serde(
431 default,
432 skip_serializing_if = "Option::is_none",
433 deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
434 )]
435 pub id: Option<String>,
436 #[serde(skip_serializing_if = "Option::is_none")]
438 pub data: Option<String>,
439 #[serde(
441 default,
442 skip_serializing_if = "Option::is_none",
443 deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
444 )]
445 pub expires_at: Option<String>,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
457pub struct Usage {
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub prompt_tokens: Option<u32>,
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub completion_tokens: Option<u32>,
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub total_tokens: Option<u32>,
467 #[serde(skip_serializing_if = "Option::is_none")]
469 pub prompt_tokens_details: Option<PromptTokensDetails>,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
473pub struct PromptTokensDetails {
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub cached_tokens: Option<u32>,
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
488pub struct WebSearchInfo {
489 #[serde(skip_serializing_if = "Option::is_none")]
491 pub icon: Option<String>,
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub title: Option<String>,
495 #[serde(skip_serializing_if = "Option::is_none")]
497 #[validate(url)]
498 pub link: Option<String>,
499 #[serde(skip_serializing_if = "Option::is_none")]
501 pub media: Option<String>,
502 #[serde(skip_serializing_if = "Option::is_none")]
504 pub publish_date: Option<String>,
505 #[serde(skip_serializing_if = "Option::is_none")]
507 pub content: Option<String>,
508 #[serde(skip_serializing_if = "Option::is_none")]
510 pub refer: Option<String>,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
520pub struct VideoResultItem {
521 #[serde(skip_serializing_if = "Option::is_none")]
523 #[validate(url)]
524 pub url: Option<String>,
525 #[serde(skip_serializing_if = "Option::is_none")]
527 #[validate(url)]
528 pub cover_image_url: Option<String>,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
537pub struct ContentFilterInfo {
538 #[serde(skip_serializing_if = "Option::is_none")]
541 pub role: Option<String>,
542
543 #[serde(skip_serializing_if = "Option::is_none")]
545 #[validate(range(min = 0, max = 3))]
546 pub level: Option<i32>,
547}
548
549impl ChatCompletionResponse {
551 pub fn id(&self) -> Option<&str> {
553 self.id.as_deref()
554 }
555 pub fn request_id(&self) -> Option<&str> {
557 self.request_id.as_deref()
558 }
559 pub fn created(&self) -> Option<u64> {
561 self.created
562 }
563 pub fn model(&self) -> Option<&str> {
565 self.model.as_deref()
566 }
567 pub fn choices(&self) -> Option<&[Choice]> {
569 self.choices.as_deref()
570 }
571 pub fn usage(&self) -> Option<&Usage> {
573 self.usage.as_ref()
574 }
575 pub fn video_result(&self) -> Option<&[VideoResultItem]> {
577 self.video_result.as_deref()
578 }
579 pub fn web_search(&self) -> Option<&[WebSearchInfo]> {
581 self.web_search.as_deref()
582 }
583 pub fn content_filter(&self) -> Option<&[ContentFilterInfo]> {
585 self.content_filter.as_deref()
586 }
587 pub fn task_status(&self) -> Option<&TaskStatus> {
589 self.task_status.as_ref()
590 }
591}
592
593impl Choice {
594 pub fn index(&self) -> Option<i32> {
596 self.index
597 }
598 pub fn message(&self) -> Option<&Message> {
600 self.message.as_ref()
601 }
602 pub fn finish_reason(&self) -> Option<&str> {
604 self.finish_reason.as_deref()
605 }
606}
607
608impl Message {
609 pub fn role(&self) -> Option<&str> {
611 self.role.as_deref()
612 }
613 pub fn content(&self) -> Option<&MessageContent> {
615 self.content.as_ref()
616 }
617 pub fn content_str(&self) -> Option<&str> {
622 self.content.as_ref().and_then(MessageContent::as_str)
623 }
624 pub fn reasoning_content(&self) -> Option<&str> {
626 self.reasoning_content.as_deref()
627 }
628 pub fn audio(&self) -> Option<&AudioContent> {
630 self.audio.as_ref()
631 }
632 pub fn tool_calls(&self) -> Option<&[ToolCallMessage]> {
634 self.tool_calls.as_deref()
635 }
636}
637
638impl ToolCallMessage {
639 pub fn id(&self) -> Option<&str> {
641 self.id.as_deref()
642 }
643 pub fn type_(&self) -> Option<&str> {
645 self.type_.as_deref()
646 }
647 pub fn function(&self) -> Option<&ToolFunction> {
649 self.function.as_ref()
650 }
651 pub fn mcp(&self) -> Option<&MCPMessage> {
653 self.mcp.as_ref()
654 }
655}
656
657impl ToolFunction {
658 pub fn name(&self) -> &str {
660 &self.name
661 }
662 pub fn arguments(&self) -> &str {
664 &self.arguments
665 }
666}
667
668impl MCPMessage {
669 pub fn id(&self) -> Option<&str> {
671 self.id.as_deref()
672 }
673 pub fn type_(&self) -> Option<&MCPCallType> {
675 self.type_.as_ref()
676 }
677 pub fn server_label(&self) -> Option<&str> {
679 self.server_label.as_deref()
680 }
681 pub fn error(&self) -> Option<&str> {
683 self.error.as_deref()
684 }
685 pub fn tools(&self) -> Option<&[MCPTool]> {
687 self.tools.as_deref()
688 }
689 pub fn arguments(&self) -> Option<&str> {
691 self.arguments.as_deref()
692 }
693 pub fn name(&self) -> Option<&str> {
695 self.name.as_deref()
696 }
697 pub fn output(&self) -> Option<&serde_json::Value> {
699 self.output.as_ref()
700 }
701}
702
703impl MCPTool {
704 pub fn name(&self) -> Option<&str> {
706 self.name.as_deref()
707 }
708 pub fn description(&self) -> Option<&str> {
710 self.description.as_deref()
711 }
712 pub fn annotations(&self) -> Option<&serde_json::Value> {
714 self.annotations.as_ref()
715 }
716 pub fn input_schema(&self) -> Option<&MCPInputSchema> {
718 self.input_schema.as_ref()
719 }
720}
721
722impl MCPInputSchema {
723 pub fn type_(&self) -> Option<&MCPInputType> {
725 self.type_.as_ref()
726 }
727 pub fn properties(&self) -> Option<&serde_json::Value> {
729 self.properties.as_ref()
730 }
731 pub fn required(&self) -> Option<&[String]> {
733 self.required.as_deref()
734 }
735 pub fn additional_properties(&self) -> Option<bool> {
737 self.additional_properties
738 }
739}
740
741impl AudioContent {
742 pub fn id(&self) -> Option<&str> {
744 self.id.as_deref()
745 }
746 pub fn data(&self) -> Option<&str> {
748 self.data.as_deref()
749 }
750 pub fn expires_at(&self) -> Option<&str> {
752 self.expires_at.as_deref()
753 }
754}
755
756impl Usage {
757 pub fn prompt_tokens(&self) -> Option<u32> {
759 self.prompt_tokens
760 }
761 pub fn completion_tokens(&self) -> Option<u32> {
763 self.completion_tokens
764 }
765 pub fn total_tokens(&self) -> Option<u32> {
767 self.total_tokens
768 }
769 pub fn prompt_tokens_details(&self) -> Option<&PromptTokensDetails> {
771 self.prompt_tokens_details.as_ref()
772 }
773}
774
775impl PromptTokensDetails {
776 pub fn cached_tokens(&self) -> Option<u32> {
778 self.cached_tokens
779 }
780}
781
782impl WebSearchInfo {
783 pub fn icon(&self) -> Option<&str> {
785 self.icon.as_deref()
786 }
787 pub fn title(&self) -> Option<&str> {
789 self.title.as_deref()
790 }
791 pub fn link(&self) -> Option<&str> {
793 self.link.as_deref()
794 }
795 pub fn media(&self) -> Option<&str> {
797 self.media.as_deref()
798 }
799 pub fn publish_date(&self) -> Option<&str> {
801 self.publish_date.as_deref()
802 }
803 pub fn content(&self) -> Option<&str> {
805 self.content.as_deref()
806 }
807 pub fn refer(&self) -> Option<&str> {
809 self.refer.as_deref()
810 }
811}
812
813impl VideoResultItem {
814 pub fn url(&self) -> Option<&str> {
816 self.url.as_deref()
817 }
818 pub fn cover_image_url(&self) -> Option<&str> {
820 self.cover_image_url.as_deref()
821 }
822}
823
824impl ContentFilterInfo {
825 pub fn role(&self) -> Option<&str> {
827 self.role.as_deref()
828 }
829 pub fn level(&self) -> Option<i32> {
831 self.level
832 }
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838
839 #[test]
840 fn tool_function_requires_the_documented_string_fields() {
841 let tf: ToolFunction =
842 serde_json::from_str(r#"{"name":"f","arguments":"{\"a\":1}"}"#).unwrap();
843 assert_eq!(tf.name, "f");
844 assert_eq!(tf.arguments, r#"{"a":1}"#);
845 assert!(serde_json::from_str::<ToolFunction>(r#"{"arguments":"{}"}"#).is_err());
846 assert!(serde_json::from_str::<ToolFunction>(r#"{"name":"f"}"#).is_err());
847 assert!(serde_json::from_str::<ToolFunction>(r#"{"name":"f","arguments":{}}"#).is_err());
848 }
849
850 #[test]
851 fn mcp_message_arguments_lenient() {
852 let m: MCPMessage = serde_json::from_str(r#"{"id":"x","arguments":{}}"#).unwrap();
853 assert_eq!(m.arguments.as_deref(), Some("{}"));
854 let m: MCPMessage = serde_json::from_str(r#"{"id":"x","arguments":null}"#).unwrap();
855 assert!(m.arguments.is_none());
856 }
857
858 #[test]
859 fn optional_custom_deserialized_fields_may_be_omitted() {
860 let tool_call: ToolCallMessage = serde_json::from_str("{}").unwrap();
861 assert!(tool_call.id.is_none());
862 let mcp: MCPMessage = serde_json::from_str("{}").unwrap();
863 assert!(mcp.id.is_none());
864 assert!(mcp.arguments.is_none());
865 let audio: AudioContent = serde_json::from_str("{}").unwrap();
866 assert!(audio.id.is_none());
867 assert!(audio.expires_at.is_none());
868 }
869
870 #[test]
871 fn completion_response_rejects_empty_success_bodies() {
872 assert!(serde_json::from_str::<ChatCompletionResponse>("{}").is_err());
873 assert!(serde_json::from_str::<ChatCompletionResponse>(r#"{"id":null}"#).is_err());
874 assert!(serde_json::from_str::<ChatCompletionResponse>(r#"{"id":"task-1"}"#).is_ok());
875 assert!(serde_json::from_str::<ChatCompletionResponse>(r#"{"choices":[]}"#).is_ok());
876 }
877
878 #[test]
879 fn choice_fields_follow_their_optional_openapi_shape() {
880 let choice: Choice = serde_json::from_str("{}").unwrap();
881 assert_eq!(choice.index(), None);
882 assert!(choice.message().is_none());
883 }
884
885 #[test]
886 fn assistant_content_accepts_only_the_documented_union() {
887 let text: Message = serde_json::from_value(serde_json::json!({
888 "content": "hello"
889 }))
890 .unwrap();
891 assert_eq!(text.content_str(), Some("hello"));
892
893 let parts: Message = serde_json::from_value(serde_json::json!({
894 "content": [{"type": "text", "text": "hello"}]
895 }))
896 .unwrap();
897 assert_eq!(
898 parts
899 .content()
900 .and_then(MessageContent::as_parts)
901 .and_then(|parts| parts.first())
902 .and_then(|part| part.text.as_deref()),
903 Some("hello")
904 );
905
906 assert!(serde_json::from_value::<Message>(serde_json::json!({"content": 42})).is_err());
907 assert!(serde_json::from_value::<Message>(serde_json::json!({"content": {}})).is_err());
908 }
909
910 #[test]
911 fn mcp_call_type_known_values_round_trip() {
912 let m: MCPMessage = serde_json::from_str(r#"{"id":"x","type":"mcp_call"}"#).unwrap();
913 assert!(matches!(m.type_, Some(MCPCallType::McpCall)));
914 let m: MCPMessage = serde_json::from_str(r#"{"id":"x","type":"mcp_list_tools"}"#).unwrap();
915 assert!(matches!(m.type_, Some(MCPCallType::McpListTools)));
916 }
917
918 #[test]
919 fn mcp_call_type_unknown_value_falls_back_to_unknown() {
920 let m: MCPMessage =
923 serde_json::from_str(r#"{"id":"x","type":"mcp_future_transport"}"#).unwrap();
924 assert!(matches!(m.type_, Some(MCPCallType::Unknown)));
925 }
926
927 #[test]
928 fn mcp_input_type_unknown_value_falls_back_to_unknown() {
929 let s: MCPInputSchema = serde_json::from_str(r#"{"type":"array"}"#).unwrap();
930 assert!(matches!(s.type_, Some(MCPInputType::Unknown)));
931 let s: MCPInputSchema = serde_json::from_str(r#"{"type":"object"}"#).unwrap();
932 assert!(matches!(s.type_, Some(MCPInputType::Object)));
933 }
934}