1use std::{convert::Infallible, str::FromStr};
2
3use crate::OneOrMany;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use super::CompletionError;
8
9#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
21#[serde(tag = "role", rename_all = "lowercase")]
22pub enum Message {
23 System { content: String },
25
26 User { content: OneOrMany<UserContent> },
28
29 Assistant {
31 id: Option<String>,
33 content: OneOrMany<AssistantContent>,
34 },
35}
36
37#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
41#[serde(tag = "type", rename_all = "lowercase")]
42pub enum UserContent {
43 Text(Text),
45 ToolResult(ToolResult),
47 Image(Image),
49 Audio(Audio),
51 Video(Video),
53 Document(Document),
55}
56
57#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
59#[serde(untagged)]
60pub enum AssistantContent {
61 Text(Text),
63 ToolCall(ToolCall),
65 Reasoning(Reasoning),
67 Image(Image),
69}
70
71#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
72#[serde(tag = "type", content = "content", rename_all = "snake_case")]
73#[non_exhaustive]
74pub enum ReasoningContent {
76 Text {
78 text: String,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 signature: Option<String>,
81 },
82 Encrypted(String),
84 Redacted { data: String },
86 Summary(String),
88}
89
90#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
91#[non_exhaustive]
92pub struct Reasoning {
94 pub id: Option<String>,
96 pub content: Vec<ReasoningContent>,
98}
99
100impl Reasoning {
101 pub fn new(input: &str) -> Self {
103 Self::new_with_signature(input, None)
104 }
105
106 pub fn new_with_signature(input: &str, signature: Option<String>) -> Self {
108 Self {
109 id: None,
110 content: vec![ReasoningContent::Text {
111 text: input.to_string(),
112 signature,
113 }],
114 }
115 }
116
117 pub fn optional_id(mut self, id: Option<String>) -> Self {
119 self.id = id;
120 self
121 }
122
123 pub fn with_id(mut self, id: String) -> Self {
125 self.id = Some(id);
126 self
127 }
128
129 pub fn multi(input: Vec<String>) -> Self {
131 Self {
132 id: None,
133 content: input
134 .into_iter()
135 .map(|text| ReasoningContent::Text {
136 text,
137 signature: None,
138 })
139 .collect(),
140 }
141 }
142
143 pub fn redacted(data: impl Into<String>) -> Self {
145 Self {
146 id: None,
147 content: vec![ReasoningContent::Redacted { data: data.into() }],
148 }
149 }
150
151 pub fn encrypted(data: impl Into<String>) -> Self {
153 Self {
154 id: None,
155 content: vec![ReasoningContent::Encrypted(data.into())],
156 }
157 }
158
159 pub fn summaries(input: Vec<String>) -> Self {
161 Self {
162 id: None,
163 content: input.into_iter().map(ReasoningContent::Summary).collect(),
164 }
165 }
166
167 pub fn display_text(&self) -> String {
169 self.content
170 .iter()
171 .filter_map(|content| match content {
172 ReasoningContent::Text { text, .. } => Some(text.as_str()),
173 ReasoningContent::Summary(summary) => Some(summary.as_str()),
174 ReasoningContent::Redacted { data } => Some(data.as_str()),
175 ReasoningContent::Encrypted(_) => None,
176 })
177 .collect::<Vec<_>>()
178 .join("\n")
179 }
180
181 pub fn first_text(&self) -> Option<&str> {
183 self.content.iter().find_map(|content| match content {
184 ReasoningContent::Text { text, .. } => Some(text.as_str()),
185 _ => None,
186 })
187 }
188
189 pub fn first_signature(&self) -> Option<&str> {
191 self.content.iter().find_map(|content| match content {
192 ReasoningContent::Text {
193 signature: Some(signature),
194 ..
195 } => Some(signature.as_str()),
196 _ => None,
197 })
198 }
199
200 pub fn encrypted_content(&self) -> Option<&str> {
202 self.content.iter().find_map(|content| match content {
203 ReasoningContent::Encrypted(data) => Some(data.as_str()),
204 _ => None,
205 })
206 }
207}
208
209#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
211pub struct ToolResult {
212 pub id: String,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub call_id: Option<String>,
217 pub content: OneOrMany<ToolResultContent>,
219}
220
221#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
223#[serde(tag = "type", rename_all = "lowercase")]
224pub enum ToolResultContent {
225 Text(Text),
227 Image(Image),
229 Json {
231 value: serde_json::Value,
233 },
234}
235
236impl ToolResultContent {
237 pub fn as_text(&self) -> Option<&str> {
239 match self {
240 Self::Text(text) => Some(&text.text),
241 Self::Image(_) | Self::Json { .. } => None,
242 }
243 }
244
245 pub fn as_json(&self) -> Option<&serde_json::Value> {
247 match self {
248 Self::Json { value } => Some(value),
249 Self::Text(_) | Self::Image(_) => None,
250 }
251 }
252
253 pub fn deserialize_json<T>(&self) -> Result<T, serde_json::Error>
260 where
261 T: serde::de::DeserializeOwned,
262 {
263 match self {
264 Self::Json { value } => serde_json::from_value(value.clone()),
265 Self::Text(text) => serde_json::from_str(&text.text),
266 Self::Image(_) => Err(<serde_json::Error as serde::de::Error>::custom(
267 "cannot decode image tool-result content as JSON",
268 )),
269 }
270 }
271}
272
273#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
275pub struct ToolCall {
276 pub id: String,
278 pub call_id: Option<String>,
280 pub function: ToolFunction,
282 pub signature: Option<String>,
292 pub additional_params: Option<serde_json::Value>,
294}
295
296impl ToolCall {
297 pub fn new(id: String, function: ToolFunction) -> Self {
298 Self {
299 id,
300 call_id: None,
301 function,
302 signature: None,
303 additional_params: None,
304 }
305 }
306
307 pub fn with_call_id(mut self, call_id: String) -> Self {
308 self.call_id = Some(call_id);
309 self
310 }
311
312 pub fn with_signature(mut self, signature: Option<String>) -> Self {
313 self.signature = signature;
314 self
315 }
316
317 pub fn with_additional_params(mut self, additional_params: Option<serde_json::Value>) -> Self {
318 self.additional_params = additional_params;
319 self
320 }
321}
322
323#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
325pub struct ToolFunction {
326 pub name: String,
328 pub arguments: serde_json::Value,
330}
331
332impl ToolFunction {
333 pub fn new(name: String, arguments: serde_json::Value) -> Self {
335 Self { name, arguments }
336 }
337}
338
339#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
350pub struct Text {
351 pub text: String,
353 #[serde(flatten, skip_serializing_if = "Option::is_none")]
355 pub additional_params: Option<serde_json::Value>,
356}
357
358impl Text {
359 pub fn new(text: impl Into<String>) -> Self {
361 Self {
362 text: text.into(),
363 additional_params: None,
364 }
365 }
366
367 pub fn text(&self) -> &str {
369 &self.text
370 }
371}
372
373impl std::fmt::Display for Text {
374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 let Self { text, .. } = self;
376 write!(f, "{text}")
377 }
378}
379
380#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
382pub struct Image {
383 pub data: DocumentSourceKind,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub media_type: Option<ImageMediaType>,
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub detail: Option<ImageDetail>,
391 #[serde(flatten, skip_serializing_if = "Option::is_none")]
393 pub additional_params: Option<serde_json::Value>,
394}
395
396impl Image {
397 pub fn try_into_url(self) -> Result<String, MessageError> {
398 match self.data {
399 DocumentSourceKind::Url(url) => Ok(url),
400 DocumentSourceKind::Base64(data) => {
401 let Some(media_type) = self.media_type else {
402 return Err(MessageError::ConversionError(
403 "A media type is required to create a valid base64-encoded image URL"
404 .to_string(),
405 ));
406 };
407
408 Ok(format!(
409 "data:image/{ty};base64,{data}",
410 ty = media_type.to_mime_type()
411 ))
412 }
413 unknown => Err(MessageError::ConversionError(format!(
414 "Tried to convert unknown type to a URL: {unknown:?}"
415 ))),
416 }
417 }
418}
419
420#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
422#[serde(tag = "type", content = "value", rename_all = "camelCase")]
423#[non_exhaustive]
424pub enum DocumentSourceKind {
425 Url(String),
427 Base64(String),
429 FileId(String),
431 Raw(Vec<u8>),
433 String(String),
435 #[default]
436 Unknown,
438}
439
440impl DocumentSourceKind {
441 pub fn url(url: &str) -> Self {
443 Self::Url(url.to_string())
444 }
445
446 pub fn base64(base64_string: &str) -> Self {
448 Self::Base64(base64_string.to_string())
449 }
450
451 pub fn file_id(file_id: &str) -> Self {
453 Self::FileId(file_id.to_string())
454 }
455
456 pub fn raw(bytes: impl Into<Vec<u8>>) -> Self {
458 Self::Raw(bytes.into())
459 }
460
461 pub fn string(input: &str) -> Self {
463 Self::String(input.into())
464 }
465
466 pub fn unknown() -> Self {
468 Self::Unknown
469 }
470
471 pub fn try_into_inner(self) -> Option<String> {
473 match self {
474 Self::Url(s) | Self::Base64(s) | Self::FileId(s) => Some(s),
475 _ => None,
476 }
477 }
478}
479
480impl std::fmt::Display for DocumentSourceKind {
481 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482 match self {
483 Self::Url(string) => write!(f, "{string}"),
484 Self::Base64(string) => write!(f, "{string}"),
485 Self::FileId(string) => write!(f, "{string}"),
486 Self::String(string) => write!(f, "{string}"),
487 Self::Raw(_) => write!(f, "<binary data>"),
488 Self::Unknown => write!(f, "<unknown>"),
489 }
490 }
491}
492
493#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
495pub struct Audio {
496 pub data: DocumentSourceKind,
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub media_type: Option<AudioMediaType>,
501 #[serde(flatten, skip_serializing_if = "Option::is_none")]
503 pub additional_params: Option<serde_json::Value>,
504}
505
506#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
508pub struct Video {
509 pub data: DocumentSourceKind,
511 #[serde(skip_serializing_if = "Option::is_none")]
513 pub media_type: Option<VideoMediaType>,
514 #[serde(flatten, skip_serializing_if = "Option::is_none")]
516 pub additional_params: Option<serde_json::Value>,
517}
518
519#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
521pub struct Document {
522 pub data: DocumentSourceKind,
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub media_type: Option<DocumentMediaType>,
527 #[serde(flatten, skip_serializing_if = "Option::is_none")]
529 pub additional_params: Option<serde_json::Value>,
530}
531
532#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
534#[serde(rename_all = "lowercase")]
535pub enum ContentFormat {
536 #[default]
537 Base64,
538 String,
539 Url,
540}
541
542#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
544pub enum MediaType {
545 Image(ImageMediaType),
546 Audio(AudioMediaType),
547 Document(DocumentMediaType),
548 Video(VideoMediaType),
549}
550
551#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
554#[serde(rename_all = "lowercase")]
555pub enum ImageMediaType {
556 JPEG,
557 PNG,
558 GIF,
559 WEBP,
560 HEIC,
561 HEIF,
562 SVG,
563}
564
565#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
569#[serde(rename_all = "lowercase")]
570pub enum DocumentMediaType {
571 PDF,
572 TXT,
573 RTF,
574 HTML,
575 CSS,
576 MARKDOWN,
577 CSV,
578 XML,
579 Javascript,
580 Python,
581}
582
583impl DocumentMediaType {
584 pub fn is_code(&self) -> bool {
585 matches!(self, Self::Javascript | Self::Python)
586 }
587}
588
589#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
592#[serde(rename_all = "lowercase")]
593pub enum AudioMediaType {
594 WAV,
595 MP3,
596 AIFF,
597 AAC,
598 OGG,
599 FLAC,
600 M4A,
601 PCM16,
602 PCM24,
603}
604
605#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
608#[serde(rename_all = "lowercase")]
609pub enum VideoMediaType {
610 AVI,
611 MP4,
612 MPEG,
613 MOV,
614 WEBM,
615}
616
617#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
619#[serde(rename_all = "lowercase")]
620pub enum ImageDetail {
621 Low,
622 High,
623 #[default]
624 Auto,
625}
626
627impl Message {
632 pub fn rag_text(&self) -> Option<String> {
635 match self {
636 Message::User { content } => {
637 for item in content.iter() {
638 if let UserContent::Text(Text { text, .. }) = item {
639 return Some(text.clone());
640 }
641 }
642 None
643 }
644 Message::System { .. } => None,
645 _ => None,
646 }
647 }
648
649 pub fn system(text: impl Into<String>) -> Self {
651 Message::System {
652 content: text.into(),
653 }
654 }
655
656 pub fn user(text: impl Into<String>) -> Self {
658 Message::User {
659 content: OneOrMany::one(UserContent::text(text)),
660 }
661 }
662
663 pub fn assistant(text: impl Into<String>) -> Self {
665 Message::Assistant {
666 id: None,
667 content: OneOrMany::one(AssistantContent::text(text)),
668 }
669 }
670
671 pub fn assistant_with_id(id: String, text: impl Into<String>) -> Self {
673 Message::Assistant {
674 id: Some(id),
675 content: OneOrMany::one(AssistantContent::text(text)),
676 }
677 }
678
679 pub fn tool_result(id: impl Into<String>, content: impl Into<String>) -> Self {
681 Message::User {
682 content: OneOrMany::one(UserContent::ToolResult(ToolResult {
683 id: id.into(),
684 call_id: None,
685 content: OneOrMany::one(ToolResultContent::text(content)),
686 })),
687 }
688 }
689
690 pub fn tool_result_with_call_id(
691 id: impl Into<String>,
692 call_id: Option<String>,
693 content: impl Into<String>,
694 ) -> Self {
695 Message::User {
696 content: OneOrMany::one(UserContent::ToolResult(ToolResult {
697 id: id.into(),
698 call_id,
699 content: OneOrMany::one(ToolResultContent::text(content)),
700 })),
701 }
702 }
703}
704
705impl UserContent {
706 pub fn text(text: impl Into<String>) -> Self {
708 UserContent::Text(text.into().into())
709 }
710
711 pub fn image_base64(
713 data: impl Into<String>,
714 media_type: Option<ImageMediaType>,
715 detail: Option<ImageDetail>,
716 ) -> Self {
717 UserContent::Image(Image {
718 data: DocumentSourceKind::Base64(data.into()),
719 media_type,
720 detail,
721 additional_params: None,
722 })
723 }
724
725 pub fn image_raw(
727 data: impl Into<Vec<u8>>,
728 media_type: Option<ImageMediaType>,
729 detail: Option<ImageDetail>,
730 ) -> Self {
731 UserContent::Image(Image {
732 data: DocumentSourceKind::Raw(data.into()),
733 media_type,
734 detail,
735 ..Default::default()
736 })
737 }
738
739 pub fn image_url(
741 url: impl Into<String>,
742 media_type: Option<ImageMediaType>,
743 detail: Option<ImageDetail>,
744 ) -> Self {
745 UserContent::Image(Image {
746 data: DocumentSourceKind::Url(url.into()),
747 media_type,
748 detail,
749 additional_params: None,
750 })
751 }
752
753 pub fn audio(data: impl Into<String>, media_type: Option<AudioMediaType>) -> Self {
755 UserContent::Audio(Audio {
756 data: DocumentSourceKind::Base64(data.into()),
757 media_type,
758 additional_params: None,
759 })
760 }
761
762 pub fn audio_raw(data: impl Into<Vec<u8>>, media_type: Option<AudioMediaType>) -> Self {
764 UserContent::Audio(Audio {
765 data: DocumentSourceKind::Raw(data.into()),
766 media_type,
767 ..Default::default()
768 })
769 }
770
771 pub fn audio_url(url: impl Into<String>, media_type: Option<AudioMediaType>) -> Self {
773 UserContent::Audio(Audio {
774 data: DocumentSourceKind::Url(url.into()),
775 media_type,
776 ..Default::default()
777 })
778 }
779
780 pub fn video(data: impl Into<String>, media_type: Option<VideoMediaType>) -> Self {
782 UserContent::Video(Video {
783 data: DocumentSourceKind::Base64(data.into()),
784 media_type,
785 additional_params: None,
786 })
787 }
788
789 pub fn video_raw(data: impl Into<Vec<u8>>, media_type: Option<VideoMediaType>) -> Self {
791 UserContent::Video(Video {
792 data: DocumentSourceKind::Raw(data.into()),
793 media_type,
794 ..Default::default()
795 })
796 }
797
798 pub fn video_url(url: impl Into<String>, media_type: Option<VideoMediaType>) -> Self {
800 UserContent::Video(Video {
801 data: DocumentSourceKind::Url(url.into()),
802 media_type,
803 ..Default::default()
804 })
805 }
806
807 pub fn document(data: impl Into<String>, media_type: Option<DocumentMediaType>) -> Self {
810 let data: String = data.into();
811 UserContent::Document(Document {
812 data: DocumentSourceKind::string(&data),
813 media_type,
814 additional_params: None,
815 })
816 }
817
818 pub fn document_raw(data: impl Into<Vec<u8>>, media_type: Option<DocumentMediaType>) -> Self {
820 UserContent::Document(Document {
821 data: DocumentSourceKind::Raw(data.into()),
822 media_type,
823 ..Default::default()
824 })
825 }
826
827 pub fn document_url(url: impl Into<String>, media_type: Option<DocumentMediaType>) -> Self {
829 UserContent::Document(Document {
830 data: DocumentSourceKind::Url(url.into()),
831 media_type,
832 ..Default::default()
833 })
834 }
835
836 pub fn tool_result(id: impl Into<String>, content: OneOrMany<ToolResultContent>) -> Self {
838 UserContent::ToolResult(ToolResult {
839 id: id.into(),
840 call_id: None,
841 content,
842 })
843 }
844
845 pub fn tool_result_with_call_id(
847 id: impl Into<String>,
848 call_id: String,
849 content: OneOrMany<ToolResultContent>,
850 ) -> Self {
851 UserContent::ToolResult(ToolResult {
852 id: id.into(),
853 call_id: Some(call_id),
854 content,
855 })
856 }
857}
858
859impl AssistantContent {
860 pub fn text(text: impl Into<String>) -> Self {
862 AssistantContent::Text(text.into().into())
863 }
864
865 pub fn image_base64(
867 data: impl Into<String>,
868 media_type: Option<ImageMediaType>,
869 detail: Option<ImageDetail>,
870 ) -> Self {
871 AssistantContent::Image(Image {
872 data: DocumentSourceKind::Base64(data.into()),
873 media_type,
874 detail,
875 additional_params: None,
876 })
877 }
878
879 pub fn tool_call(
881 id: impl Into<String>,
882 name: impl Into<String>,
883 arguments: serde_json::Value,
884 ) -> Self {
885 AssistantContent::ToolCall(ToolCall::new(
886 id.into(),
887 ToolFunction {
888 name: name.into(),
889 arguments,
890 },
891 ))
892 }
893
894 pub fn tool_call_with_call_id(
895 id: impl Into<String>,
896 call_id: String,
897 name: impl Into<String>,
898 arguments: serde_json::Value,
899 ) -> Self {
900 AssistantContent::ToolCall(
901 ToolCall::new(
902 id.into(),
903 ToolFunction {
904 name: name.into(),
905 arguments,
906 },
907 )
908 .with_call_id(call_id),
909 )
910 }
911
912 pub fn reasoning(reasoning: impl AsRef<str>) -> Self {
913 AssistantContent::Reasoning(Reasoning::new(reasoning.as_ref()))
914 }
915}
916
917impl ToolResultContent {
918 pub fn text(text: impl Into<String>) -> Self {
920 ToolResultContent::Text(text.into().into())
921 }
922
923 pub fn json(value: serde_json::Value) -> Self {
925 ToolResultContent::Json { value }
926 }
927
928 pub fn image_base64(
930 data: impl Into<String>,
931 media_type: Option<ImageMediaType>,
932 detail: Option<ImageDetail>,
933 ) -> Self {
934 ToolResultContent::Image(Image {
935 data: DocumentSourceKind::Base64(data.into()),
936 media_type,
937 detail,
938 additional_params: None,
939 })
940 }
941
942 pub fn image_raw(
944 data: impl Into<Vec<u8>>,
945 media_type: Option<ImageMediaType>,
946 detail: Option<ImageDetail>,
947 ) -> Self {
948 ToolResultContent::Image(Image {
949 data: DocumentSourceKind::Raw(data.into()),
950 media_type,
951 detail,
952 ..Default::default()
953 })
954 }
955
956 pub fn image_url(
958 url: impl Into<String>,
959 media_type: Option<ImageMediaType>,
960 detail: Option<ImageDetail>,
961 ) -> Self {
962 ToolResultContent::Image(Image {
963 data: DocumentSourceKind::Url(url.into()),
964 media_type,
965 detail,
966 additional_params: None,
967 })
968 }
969}
970
971pub trait MimeType {
973 fn from_mime_type(mime_type: &str) -> Option<Self>
974 where
975 Self: Sized;
976 fn to_mime_type(&self) -> &'static str;
977}
978
979impl MimeType for MediaType {
980 fn from_mime_type(mime_type: &str) -> Option<Self> {
981 ImageMediaType::from_mime_type(mime_type)
982 .map(MediaType::Image)
983 .or_else(|| {
984 DocumentMediaType::from_mime_type(mime_type)
985 .map(MediaType::Document)
986 .or_else(|| {
987 AudioMediaType::from_mime_type(mime_type)
988 .map(MediaType::Audio)
989 .or_else(|| {
990 VideoMediaType::from_mime_type(mime_type).map(MediaType::Video)
991 })
992 })
993 })
994 }
995
996 fn to_mime_type(&self) -> &'static str {
997 match self {
998 MediaType::Image(media_type) => media_type.to_mime_type(),
999 MediaType::Audio(media_type) => media_type.to_mime_type(),
1000 MediaType::Document(media_type) => media_type.to_mime_type(),
1001 MediaType::Video(media_type) => media_type.to_mime_type(),
1002 }
1003 }
1004}
1005
1006impl MimeType for ImageMediaType {
1007 fn from_mime_type(mime_type: &str) -> Option<Self> {
1008 match mime_type {
1009 "image/jpeg" => Some(ImageMediaType::JPEG),
1010 "image/png" => Some(ImageMediaType::PNG),
1011 "image/gif" => Some(ImageMediaType::GIF),
1012 "image/webp" => Some(ImageMediaType::WEBP),
1013 "image/heic" => Some(ImageMediaType::HEIC),
1014 "image/heif" => Some(ImageMediaType::HEIF),
1015 "image/svg+xml" => Some(ImageMediaType::SVG),
1016 _ => None,
1017 }
1018 }
1019
1020 fn to_mime_type(&self) -> &'static str {
1021 match self {
1022 ImageMediaType::JPEG => "image/jpeg",
1023 ImageMediaType::PNG => "image/png",
1024 ImageMediaType::GIF => "image/gif",
1025 ImageMediaType::WEBP => "image/webp",
1026 ImageMediaType::HEIC => "image/heic",
1027 ImageMediaType::HEIF => "image/heif",
1028 ImageMediaType::SVG => "image/svg+xml",
1029 }
1030 }
1031}
1032
1033impl MimeType for DocumentMediaType {
1034 fn from_mime_type(mime_type: &str) -> Option<Self> {
1035 match mime_type {
1036 "application/pdf" => Some(DocumentMediaType::PDF),
1037 "text/plain" => Some(DocumentMediaType::TXT),
1038 "text/rtf" => Some(DocumentMediaType::RTF),
1039 "text/html" => Some(DocumentMediaType::HTML),
1040 "text/css" => Some(DocumentMediaType::CSS),
1041 "text/md" | "text/markdown" => Some(DocumentMediaType::MARKDOWN),
1042 "text/csv" => Some(DocumentMediaType::CSV),
1043 "text/xml" => Some(DocumentMediaType::XML),
1044 "application/x-javascript" | "text/x-javascript" => Some(DocumentMediaType::Javascript),
1045 "application/x-python" | "text/x-python" => Some(DocumentMediaType::Python),
1046 _ => None,
1047 }
1048 }
1049
1050 fn to_mime_type(&self) -> &'static str {
1051 match self {
1052 DocumentMediaType::PDF => "application/pdf",
1053 DocumentMediaType::TXT => "text/plain",
1054 DocumentMediaType::RTF => "text/rtf",
1055 DocumentMediaType::HTML => "text/html",
1056 DocumentMediaType::CSS => "text/css",
1057 DocumentMediaType::MARKDOWN => "text/markdown",
1058 DocumentMediaType::CSV => "text/csv",
1059 DocumentMediaType::XML => "text/xml",
1060 DocumentMediaType::Javascript => "application/x-javascript",
1061 DocumentMediaType::Python => "application/x-python",
1062 }
1063 }
1064}
1065
1066impl MimeType for AudioMediaType {
1067 fn from_mime_type(mime_type: &str) -> Option<Self> {
1068 match mime_type {
1069 "audio/wav" => Some(AudioMediaType::WAV),
1070 "audio/mp3" => Some(AudioMediaType::MP3),
1071 "audio/aiff" => Some(AudioMediaType::AIFF),
1072 "audio/aac" => Some(AudioMediaType::AAC),
1073 "audio/ogg" => Some(AudioMediaType::OGG),
1074 "audio/flac" => Some(AudioMediaType::FLAC),
1075 "audio/m4a" => Some(AudioMediaType::M4A),
1076 "audio/pcm16" => Some(AudioMediaType::PCM16),
1077 "audio/pcm24" => Some(AudioMediaType::PCM24),
1078 _ => None,
1079 }
1080 }
1081
1082 fn to_mime_type(&self) -> &'static str {
1083 match self {
1084 AudioMediaType::WAV => "audio/wav",
1085 AudioMediaType::MP3 => "audio/mp3",
1086 AudioMediaType::AIFF => "audio/aiff",
1087 AudioMediaType::AAC => "audio/aac",
1088 AudioMediaType::OGG => "audio/ogg",
1089 AudioMediaType::FLAC => "audio/flac",
1090 AudioMediaType::M4A => "audio/m4a",
1091 AudioMediaType::PCM16 => "audio/pcm16",
1092 AudioMediaType::PCM24 => "audio/pcm24",
1093 }
1094 }
1095}
1096
1097impl MimeType for VideoMediaType {
1098 fn from_mime_type(mime_type: &str) -> Option<Self>
1099 where
1100 Self: Sized,
1101 {
1102 match mime_type {
1103 "video/avi" => Some(VideoMediaType::AVI),
1104 "video/mp4" => Some(VideoMediaType::MP4),
1105 "video/mpeg" => Some(VideoMediaType::MPEG),
1106 "video/mov" => Some(VideoMediaType::MOV),
1107 "video/webm" => Some(VideoMediaType::WEBM),
1108 &_ => None,
1109 }
1110 }
1111
1112 fn to_mime_type(&self) -> &'static str {
1113 match self {
1114 VideoMediaType::AVI => "video/avi",
1115 VideoMediaType::MP4 => "video/mp4",
1116 VideoMediaType::MPEG => "video/mpeg",
1117 VideoMediaType::MOV => "video/mov",
1118 VideoMediaType::WEBM => "video/webm",
1119 }
1120 }
1121}
1122
1123impl std::str::FromStr for ImageDetail {
1124 type Err = ();
1125
1126 fn from_str(s: &str) -> Result<Self, Self::Err> {
1127 match s.to_lowercase().as_str() {
1128 "low" => Ok(ImageDetail::Low),
1129 "high" => Ok(ImageDetail::High),
1130 "auto" => Ok(ImageDetail::Auto),
1131 _ => Err(()),
1132 }
1133 }
1134}
1135
1136impl From<String> for Text {
1141 fn from(text: String) -> Self {
1142 Text {
1143 text,
1144 additional_params: None,
1145 }
1146 }
1147}
1148
1149impl From<&String> for Text {
1150 fn from(text: &String) -> Self {
1151 text.to_owned().into()
1152 }
1153}
1154
1155impl From<&str> for Text {
1156 fn from(text: &str) -> Self {
1157 text.to_owned().into()
1158 }
1159}
1160
1161impl FromStr for Text {
1162 type Err = Infallible;
1163
1164 fn from_str(s: &str) -> Result<Self, Self::Err> {
1165 Ok(s.into())
1166 }
1167}
1168
1169impl From<&Message> for Message {
1170 fn from(msg: &Message) -> Self {
1171 msg.clone()
1172 }
1173}
1174
1175impl From<String> for Message {
1176 fn from(text: String) -> Self {
1177 Message::User {
1178 content: OneOrMany::one(UserContent::Text(text.into())),
1179 }
1180 }
1181}
1182
1183impl From<&str> for Message {
1184 fn from(text: &str) -> Self {
1185 Message::User {
1186 content: OneOrMany::one(UserContent::Text(text.into())),
1187 }
1188 }
1189}
1190
1191impl From<&String> for Message {
1192 fn from(text: &String) -> Self {
1193 Message::User {
1194 content: OneOrMany::one(UserContent::Text(text.into())),
1195 }
1196 }
1197}
1198
1199impl From<Text> for Message {
1200 fn from(text: Text) -> Self {
1201 Message::User {
1202 content: OneOrMany::one(UserContent::Text(text)),
1203 }
1204 }
1205}
1206
1207impl From<Image> for Message {
1208 fn from(image: Image) -> Self {
1209 Message::User {
1210 content: OneOrMany::one(UserContent::Image(image)),
1211 }
1212 }
1213}
1214
1215impl From<Audio> for Message {
1216 fn from(audio: Audio) -> Self {
1217 Message::User {
1218 content: OneOrMany::one(UserContent::Audio(audio)),
1219 }
1220 }
1221}
1222
1223impl From<Document> for Message {
1224 fn from(document: Document) -> Self {
1225 Message::User {
1226 content: OneOrMany::one(UserContent::Document(document)),
1227 }
1228 }
1229}
1230
1231impl From<String> for ToolResultContent {
1232 fn from(text: String) -> Self {
1233 ToolResultContent::text(text)
1234 }
1235}
1236
1237impl From<String> for AssistantContent {
1238 fn from(text: String) -> Self {
1239 AssistantContent::text(text)
1240 }
1241}
1242
1243impl From<String> for UserContent {
1244 fn from(text: String) -> Self {
1245 UserContent::text(text)
1246 }
1247}
1248
1249impl From<AssistantContent> for Message {
1250 fn from(content: AssistantContent) -> Self {
1251 Message::Assistant {
1252 id: None,
1253 content: OneOrMany::one(content),
1254 }
1255 }
1256}
1257
1258impl From<UserContent> for Message {
1259 fn from(content: UserContent) -> Self {
1260 Message::User {
1261 content: OneOrMany::one(content),
1262 }
1263 }
1264}
1265
1266impl From<OneOrMany<AssistantContent>> for Message {
1267 fn from(content: OneOrMany<AssistantContent>) -> Self {
1268 Message::Assistant { id: None, content }
1269 }
1270}
1271
1272impl From<OneOrMany<UserContent>> for Message {
1273 fn from(content: OneOrMany<UserContent>) -> Self {
1274 Message::User { content }
1275 }
1276}
1277
1278impl From<ToolCall> for Message {
1279 fn from(tool_call: ToolCall) -> Self {
1280 Message::Assistant {
1281 id: None,
1282 content: OneOrMany::one(AssistantContent::ToolCall(tool_call)),
1283 }
1284 }
1285}
1286
1287impl From<ToolResult> for Message {
1288 fn from(tool_result: ToolResult) -> Self {
1289 Message::User {
1290 content: OneOrMany::one(UserContent::ToolResult(tool_result)),
1291 }
1292 }
1293}
1294
1295impl From<ToolResultContent> for Message {
1296 fn from(tool_result_content: ToolResultContent) -> Self {
1297 Message::User {
1298 content: OneOrMany::one(UserContent::ToolResult(ToolResult {
1299 id: String::new(),
1300 call_id: None,
1301 content: OneOrMany::one(tool_result_content),
1302 })),
1303 }
1304 }
1305}
1306
1307#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
1308#[serde(rename_all = "snake_case")]
1309pub enum ToolChoice {
1310 #[default]
1311 Auto,
1312 None,
1313 Required,
1314 Specific {
1315 function_names: Vec<String>,
1316 },
1317}
1318
1319#[derive(Debug, Error)]
1325pub enum MessageError {
1326 #[error("Message conversion error: {0}")]
1327 ConversionError(String),
1328}
1329
1330impl From<MessageError> for CompletionError {
1331 fn from(error: MessageError) -> Self {
1332 CompletionError::RequestError(error.into())
1333 }
1334}
1335
1336#[cfg(test)]
1337mod tests {
1338 use serde::{Deserialize, Serialize};
1339
1340 use super::{Message, Reasoning, ReasoningContent, Text, ToolResultContent};
1341
1342 #[test]
1343 fn reasoning_constructors_and_accessors_work() {
1344 let single = Reasoning::new("think");
1345 assert_eq!(single.first_text(), Some("think"));
1346 assert_eq!(single.first_signature(), None);
1347
1348 let signed = Reasoning::new_with_signature("signed", Some("sig-1".to_string()));
1349 assert_eq!(signed.first_text(), Some("signed"));
1350 assert_eq!(signed.first_signature(), Some("sig-1"));
1351
1352 let multi = Reasoning::multi(vec!["a".to_string(), "b".to_string()]);
1353 assert_eq!(multi.display_text(), "a\nb");
1354 assert_eq!(multi.first_text(), Some("a"));
1355
1356 let redacted = Reasoning::redacted("redacted-value");
1357 assert_eq!(redacted.display_text(), "redacted-value");
1358 assert_eq!(redacted.first_text(), None);
1359
1360 let encrypted = Reasoning::encrypted("enc");
1361 assert_eq!(encrypted.encrypted_content(), Some("enc"));
1362 assert_eq!(encrypted.display_text(), "");
1363
1364 let summaries = Reasoning::summaries(vec!["s1".to_string(), "s2".to_string()]);
1365 assert_eq!(summaries.display_text(), "s1\ns2");
1366 assert_eq!(summaries.encrypted_content(), None);
1367 }
1368
1369 #[test]
1370 fn reasoning_content_serde_roundtrip() {
1371 let variants = vec![
1372 ReasoningContent::Text {
1373 text: "plain".to_string(),
1374 signature: Some("sig".to_string()),
1375 },
1376 ReasoningContent::Encrypted("opaque".to_string()),
1377 ReasoningContent::Redacted {
1378 data: "redacted".to_string(),
1379 },
1380 ReasoningContent::Summary("summary".to_string()),
1381 ];
1382
1383 for variant in variants {
1384 let json = serde_json::to_string(&variant).expect("serialize");
1385 let roundtrip: ReasoningContent = serde_json::from_str(&json).expect("deserialize");
1386 assert_eq!(roundtrip, variant);
1387 }
1388 }
1389
1390 #[test]
1391 fn system_message_constructor_and_serde_roundtrip() {
1392 let message = Message::system("You are concise.");
1393
1394 match &message {
1395 Message::System { content } => assert_eq!(content, "You are concise."),
1396 _ => panic!("Expected system message"),
1397 }
1398
1399 let json = serde_json::to_string(&message).expect("serialize");
1400 let roundtrip: Message = serde_json::from_str(&json).expect("deserialize");
1401 assert_eq!(roundtrip, message);
1402 }
1403
1404 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1405 struct ExecutorLikeResponse {
1406 output: serde_json::Value,
1407 logs: Vec<String>,
1408 execution_time_ms: u64,
1409 }
1410
1411 #[test]
1412 fn tool_result_content_decodes_structured_and_legacy_json() {
1413 let response = ExecutorLikeResponse {
1414 output: serde_json::json!({"answer": 42}),
1415 logs: vec!["computed".to_string()],
1416 execution_time_ms: 7,
1417 };
1418 let value = serde_json::to_value(&response).expect("serialize response");
1419
1420 let structured = ToolResultContent::json(value.clone());
1421 assert_eq!(structured.as_json(), Some(&value));
1422 assert_eq!(structured.as_text(), None);
1423 assert_eq!(
1424 structured
1425 .deserialize_json::<ExecutorLikeResponse>()
1426 .expect("decode structured response"),
1427 response
1428 );
1429
1430 let legacy_json = value.to_string();
1431 let legacy_text = ToolResultContent::Text(Text::new(legacy_json.clone()));
1432 assert_eq!(legacy_text.as_text(), Some(legacy_json.as_str()));
1433 assert_eq!(legacy_text.as_json(), None);
1434 assert_eq!(
1435 legacy_text
1436 .deserialize_json::<ExecutorLikeResponse>()
1437 .expect("decode legacy response"),
1438 response
1439 );
1440
1441 let image = ToolResultContent::image_url("https://example.com/result.png", None, None);
1442 let image_error = image.deserialize_json::<ExecutorLikeResponse>();
1443 assert!(image_error.is_err());
1444 if let Err(error) = image_error {
1445 assert_eq!(
1446 error.to_string(),
1447 "cannot decode image tool-result content as JSON"
1448 );
1449 }
1450 }
1451}