1use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37
38#[cfg(feature = "specta")]
39use specta::Type;
40
41use crate::utils::uuid::ensure_id;
42
43#[cfg_attr(feature = "specta", derive(Type))]
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
55#[serde(rename_all = "lowercase")]
56pub enum ImageDetail {
57 Low,
58 High,
59 #[default]
60 Auto,
61}
62
63#[cfg_attr(feature = "specta", derive(Type))]
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66#[serde(tag = "type", rename_all = "snake_case")]
67pub enum ImageSource {
68 Url { url: String },
70 Base64 {
72 media_type: String,
74 data: String,
76 },
77}
78
79#[cfg_attr(feature = "specta", derive(Type))]
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
85#[serde(tag = "type", rename_all = "snake_case")]
86pub enum ContentPart {
87 Text { text: String },
89 Image {
91 source: ImageSource,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 detail: Option<ImageDetail>,
95 },
96}
97
98impl From<&str> for ContentPart {
99 fn from(text: &str) -> Self {
100 ContentPart::Text {
101 text: text.to_string(),
102 }
103 }
104}
105
106impl From<String> for ContentPart {
107 fn from(text: String) -> Self {
108 ContentPart::Text { text }
109 }
110}
111
112#[cfg_attr(feature = "specta", derive(Type))]
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
118#[serde(untagged)]
119pub enum MessageContent {
120 Text(String),
122 Parts(Vec<ContentPart>),
124}
125
126impl MessageContent {
127 pub fn as_text(&self) -> String {
129 match self {
130 MessageContent::Text(s) => s.clone(),
131 MessageContent::Parts(parts) => parts
132 .iter()
133 .filter_map(|p| match p {
134 ContentPart::Text { text } => Some(text.as_str()),
135 _ => None,
136 })
137 .collect::<Vec<_>>()
138 .join(" "),
139 }
140 }
141
142 pub fn has_images(&self) -> bool {
144 match self {
145 MessageContent::Text(_) => false,
146 MessageContent::Parts(parts) => {
147 parts.iter().any(|p| matches!(p, ContentPart::Image { .. }))
148 }
149 }
150 }
151
152 pub fn parts(&self) -> Vec<ContentPart> {
154 match self {
155 MessageContent::Text(s) => vec![ContentPart::Text { text: s.clone() }],
156 MessageContent::Parts(parts) => parts.clone(),
157 }
158 }
159}
160
161impl Default for MessageContent {
162 fn default() -> Self {
163 MessageContent::Text(String::new())
164 }
165}
166
167impl From<String> for MessageContent {
168 fn from(s: String) -> Self {
169 MessageContent::Text(s)
170 }
171}
172
173impl From<&str> for MessageContent {
174 fn from(s: &str) -> Self {
175 MessageContent::Text(s.to_string())
176 }
177}
178
179impl From<Vec<ContentPart>> for MessageContent {
180 fn from(parts: Vec<ContentPart>) -> Self {
181 MessageContent::Parts(parts)
182 }
183}
184
185#[cfg_attr(feature = "specta", derive(Type))]
192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
193#[serde(untagged)]
194pub enum BlockIndex {
195 Int(i64),
196 Str(String),
197}
198
199impl From<i64> for BlockIndex {
200 fn from(i: i64) -> Self {
201 BlockIndex::Int(i)
202 }
203}
204
205impl From<i32> for BlockIndex {
206 fn from(i: i32) -> Self {
207 BlockIndex::Int(i as i64)
208 }
209}
210
211impl From<usize> for BlockIndex {
212 fn from(i: usize) -> Self {
213 BlockIndex::Int(i as i64)
214 }
215}
216
217impl From<String> for BlockIndex {
218 fn from(s: String) -> Self {
219 BlockIndex::Str(s)
220 }
221}
222
223impl From<&str> for BlockIndex {
224 fn from(s: &str) -> Self {
225 BlockIndex::Str(s.to_string())
226 }
227}
228
229#[cfg_attr(feature = "specta", derive(Type))]
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
235pub struct Citation {
236 #[serde(rename = "type")]
238 pub block_type: String,
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub id: Option<String>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub url: Option<String>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub title: Option<String>,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub start_index: Option<i64>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub end_index: Option<i64>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub cited_text: Option<String>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub extras: Option<HashMap<String, serde_json::Value>>,
260}
261
262impl Citation {
263 pub fn new() -> Self {
265 Self {
266 block_type: "citation".to_string(),
267 id: None,
268 url: None,
269 title: None,
270 start_index: None,
271 end_index: None,
272 cited_text: None,
273 extras: None,
274 }
275 }
276}
277
278impl Default for Citation {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284#[cfg_attr(feature = "specta", derive(Type))]
286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
287pub struct NonStandardAnnotation {
288 #[serde(rename = "type")]
290 pub block_type: String,
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub id: Option<String>,
294 pub value: HashMap<String, serde_json::Value>,
296}
297
298impl NonStandardAnnotation {
299 pub fn new(value: HashMap<String, serde_json::Value>) -> Self {
301 Self {
302 block_type: "non_standard_annotation".to_string(),
303 id: None,
304 value,
305 }
306 }
307}
308
309#[cfg_attr(feature = "specta", derive(Type))]
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
312#[serde(tag = "type")]
313pub enum Annotation {
314 #[serde(rename = "citation")]
315 Citation(Citation),
316 #[serde(rename = "non_standard_annotation")]
317 NonStandardAnnotation(NonStandardAnnotation),
318}
319
320#[cfg_attr(feature = "specta", derive(Type))]
324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
325pub struct TextContentBlock {
326 #[serde(rename = "type")]
328 pub block_type: String,
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub id: Option<String>,
332 pub text: String,
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub annotations: Option<Vec<Annotation>>,
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub index: Option<BlockIndex>,
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub extras: Option<HashMap<String, serde_json::Value>>,
343}
344
345impl TextContentBlock {
346 pub fn new(text: impl Into<String>) -> Self {
348 Self {
349 block_type: "text".to_string(),
350 id: None,
351 text: text.into(),
352 annotations: None,
353 index: None,
354 extras: None,
355 }
356 }
357}
358
359#[cfg_attr(feature = "specta", derive(Type))]
364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
365pub struct ToolCallBlock {
366 #[serde(rename = "type")]
368 pub block_type: String,
369 pub id: Option<String>,
371 pub name: String,
373 pub args: HashMap<String, serde_json::Value>,
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub index: Option<BlockIndex>,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub extras: Option<HashMap<String, serde_json::Value>>,
381}
382
383impl ToolCallBlock {
384 pub fn new(name: impl Into<String>, args: HashMap<String, serde_json::Value>) -> Self {
386 Self {
387 block_type: "tool_call".to_string(),
388 id: None,
389 name: name.into(),
390 args,
391 index: None,
392 extras: None,
393 }
394 }
395}
396
397#[cfg_attr(feature = "specta", derive(Type))]
399#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
400pub struct ToolCallChunkBlock {
401 #[serde(rename = "type")]
403 pub block_type: String,
404 pub id: Option<String>,
406 pub name: Option<String>,
408 pub args: Option<String>,
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub index: Option<BlockIndex>,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub extras: Option<HashMap<String, serde_json::Value>>,
416}
417
418impl ToolCallChunkBlock {
419 pub fn new() -> Self {
421 Self {
422 block_type: "tool_call_chunk".to_string(),
423 id: None,
424 name: None,
425 args: None,
426 index: None,
427 extras: None,
428 }
429 }
430}
431
432impl Default for ToolCallChunkBlock {
433 fn default() -> Self {
434 Self::new()
435 }
436}
437
438#[cfg_attr(feature = "specta", derive(Type))]
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
443pub struct InvalidToolCallBlock {
444 #[serde(rename = "type")]
446 pub block_type: String,
447 pub id: Option<String>,
449 pub name: Option<String>,
451 pub args: Option<String>,
453 pub error: Option<String>,
455 #[serde(skip_serializing_if = "Option::is_none")]
457 pub index: Option<BlockIndex>,
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub extras: Option<HashMap<String, serde_json::Value>>,
461}
462
463impl InvalidToolCallBlock {
464 pub fn new() -> Self {
466 Self {
467 block_type: "invalid_tool_call".to_string(),
468 id: None,
469 name: None,
470 args: None,
471 error: None,
472 index: None,
473 extras: None,
474 }
475 }
476}
477
478impl Default for InvalidToolCallBlock {
479 fn default() -> Self {
480 Self::new()
481 }
482}
483
484#[cfg_attr(feature = "specta", derive(Type))]
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
489pub struct ServerToolCall {
490 #[serde(rename = "type")]
492 pub block_type: String,
493 pub id: String,
495 pub name: String,
497 pub args: HashMap<String, serde_json::Value>,
499 #[serde(skip_serializing_if = "Option::is_none")]
501 pub index: Option<BlockIndex>,
502 #[serde(skip_serializing_if = "Option::is_none")]
504 pub extras: Option<HashMap<String, serde_json::Value>>,
505}
506
507impl ServerToolCall {
508 pub fn new(
510 id: impl Into<String>,
511 name: impl Into<String>,
512 args: HashMap<String, serde_json::Value>,
513 ) -> Self {
514 Self {
515 block_type: "server_tool_call".to_string(),
516 id: id.into(),
517 name: name.into(),
518 args,
519 index: None,
520 extras: None,
521 }
522 }
523}
524
525#[cfg_attr(feature = "specta", derive(Type))]
527#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
528pub struct ServerToolCallChunk {
529 #[serde(rename = "type")]
531 pub block_type: String,
532 #[serde(skip_serializing_if = "Option::is_none")]
534 pub name: Option<String>,
535 #[serde(skip_serializing_if = "Option::is_none")]
537 pub args: Option<String>,
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub id: Option<String>,
541 #[serde(skip_serializing_if = "Option::is_none")]
543 pub index: Option<BlockIndex>,
544 #[serde(skip_serializing_if = "Option::is_none")]
546 pub extras: Option<HashMap<String, serde_json::Value>>,
547}
548
549impl ServerToolCallChunk {
550 pub fn new() -> Self {
552 Self {
553 block_type: "server_tool_call_chunk".to_string(),
554 name: None,
555 args: None,
556 id: None,
557 index: None,
558 extras: None,
559 }
560 }
561}
562
563impl Default for ServerToolCallChunk {
564 fn default() -> Self {
565 Self::new()
566 }
567}
568
569#[cfg_attr(feature = "specta", derive(Type))]
571#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
572#[serde(rename_all = "lowercase")]
573pub enum ServerToolStatus {
574 Success,
575 Error,
576}
577
578#[cfg_attr(feature = "specta", derive(Type))]
580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
581pub struct ServerToolResult {
582 #[serde(rename = "type")]
584 pub block_type: String,
585 #[serde(skip_serializing_if = "Option::is_none")]
587 pub id: Option<String>,
588 pub tool_call_id: String,
590 pub status: ServerToolStatus,
592 #[serde(skip_serializing_if = "Option::is_none")]
594 pub output: Option<serde_json::Value>,
595 #[serde(skip_serializing_if = "Option::is_none")]
597 pub index: Option<BlockIndex>,
598 #[serde(skip_serializing_if = "Option::is_none")]
600 pub extras: Option<HashMap<String, serde_json::Value>>,
601}
602
603impl ServerToolResult {
604 pub fn success(tool_call_id: impl Into<String>) -> Self {
606 Self {
607 block_type: "server_tool_result".to_string(),
608 id: None,
609 tool_call_id: tool_call_id.into(),
610 status: ServerToolStatus::Success,
611 output: None,
612 index: None,
613 extras: None,
614 }
615 }
616
617 pub fn error(tool_call_id: impl Into<String>) -> Self {
619 Self {
620 block_type: "server_tool_result".to_string(),
621 id: None,
622 tool_call_id: tool_call_id.into(),
623 status: ServerToolStatus::Error,
624 output: None,
625 index: None,
626 extras: None,
627 }
628 }
629}
630
631#[cfg_attr(feature = "specta", derive(Type))]
636#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
637pub struct ReasoningContentBlock {
638 #[serde(rename = "type")]
640 pub block_type: String,
641 #[serde(skip_serializing_if = "Option::is_none")]
643 pub id: Option<String>,
644 #[serde(skip_serializing_if = "Option::is_none")]
646 pub reasoning: Option<String>,
647 #[serde(skip_serializing_if = "Option::is_none")]
649 pub index: Option<BlockIndex>,
650 #[serde(skip_serializing_if = "Option::is_none")]
652 pub extras: Option<HashMap<String, serde_json::Value>>,
653}
654
655impl ReasoningContentBlock {
656 pub fn new(reasoning: impl Into<String>) -> Self {
658 Self {
659 block_type: "reasoning".to_string(),
660 id: None,
661 reasoning: Some(reasoning.into()),
662 index: None,
663 extras: None,
664 }
665 }
666
667 pub fn reasoning(&self) -> Option<&str> {
669 self.reasoning.as_deref()
670 }
671}
672
673impl Default for ReasoningContentBlock {
674 fn default() -> Self {
675 Self {
676 block_type: "reasoning".to_string(),
677 id: None,
678 reasoning: None,
679 index: None,
680 extras: None,
681 }
682 }
683}
684
685#[cfg_attr(feature = "specta", derive(Type))]
687#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
688pub struct ImageContentBlock {
689 #[serde(rename = "type")]
691 pub block_type: String,
692 #[serde(skip_serializing_if = "Option::is_none")]
694 pub id: Option<String>,
695 #[serde(skip_serializing_if = "Option::is_none")]
697 pub file_id: Option<String>,
698 #[serde(skip_serializing_if = "Option::is_none")]
700 pub mime_type: Option<String>,
701 #[serde(skip_serializing_if = "Option::is_none")]
703 pub index: Option<BlockIndex>,
704 #[serde(skip_serializing_if = "Option::is_none")]
706 pub url: Option<String>,
707 #[serde(skip_serializing_if = "Option::is_none")]
709 pub base64: Option<String>,
710 #[serde(skip_serializing_if = "Option::is_none")]
712 pub extras: Option<HashMap<String, serde_json::Value>>,
713}
714
715impl ImageContentBlock {
716 pub fn new() -> Self {
718 Self {
719 block_type: "image".to_string(),
720 id: None,
721 file_id: None,
722 mime_type: None,
723 index: None,
724 url: None,
725 base64: None,
726 extras: None,
727 }
728 }
729
730 pub fn from_url(url: impl Into<String>) -> Self {
732 Self {
733 block_type: "image".to_string(),
734 id: None,
735 file_id: None,
736 mime_type: None,
737 index: None,
738 url: Some(url.into()),
739 base64: None,
740 extras: None,
741 }
742 }
743
744 pub fn from_base64(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
746 Self {
747 block_type: "image".to_string(),
748 id: None,
749 file_id: None,
750 mime_type: Some(mime_type.into()),
751 index: None,
752 url: None,
753 base64: Some(data.into()),
754 extras: None,
755 }
756 }
757}
758
759impl Default for ImageContentBlock {
760 fn default() -> Self {
761 Self::new()
762 }
763}
764
765#[cfg_attr(feature = "specta", derive(Type))]
767#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
768pub struct VideoContentBlock {
769 #[serde(rename = "type")]
771 pub block_type: String,
772 #[serde(skip_serializing_if = "Option::is_none")]
774 pub id: Option<String>,
775 #[serde(skip_serializing_if = "Option::is_none")]
777 pub file_id: Option<String>,
778 #[serde(skip_serializing_if = "Option::is_none")]
780 pub mime_type: Option<String>,
781 #[serde(skip_serializing_if = "Option::is_none")]
783 pub index: Option<BlockIndex>,
784 #[serde(skip_serializing_if = "Option::is_none")]
786 pub url: Option<String>,
787 #[serde(skip_serializing_if = "Option::is_none")]
789 pub base64: Option<String>,
790 #[serde(skip_serializing_if = "Option::is_none")]
792 pub extras: Option<HashMap<String, serde_json::Value>>,
793}
794
795impl VideoContentBlock {
796 pub fn new() -> Self {
798 Self {
799 block_type: "video".to_string(),
800 id: None,
801 file_id: None,
802 mime_type: None,
803 index: None,
804 url: None,
805 base64: None,
806 extras: None,
807 }
808 }
809}
810
811impl Default for VideoContentBlock {
812 fn default() -> Self {
813 Self::new()
814 }
815}
816
817#[cfg_attr(feature = "specta", derive(Type))]
819#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
820pub struct AudioContentBlock {
821 #[serde(rename = "type")]
823 pub block_type: String,
824 #[serde(skip_serializing_if = "Option::is_none")]
826 pub id: Option<String>,
827 #[serde(skip_serializing_if = "Option::is_none")]
829 pub file_id: Option<String>,
830 #[serde(skip_serializing_if = "Option::is_none")]
832 pub mime_type: Option<String>,
833 #[serde(skip_serializing_if = "Option::is_none")]
835 pub index: Option<BlockIndex>,
836 #[serde(skip_serializing_if = "Option::is_none")]
838 pub url: Option<String>,
839 #[serde(skip_serializing_if = "Option::is_none")]
841 pub base64: Option<String>,
842 #[serde(skip_serializing_if = "Option::is_none")]
844 pub extras: Option<HashMap<String, serde_json::Value>>,
845}
846
847impl AudioContentBlock {
848 pub fn new() -> Self {
850 Self {
851 block_type: "audio".to_string(),
852 id: None,
853 file_id: None,
854 mime_type: None,
855 index: None,
856 url: None,
857 base64: None,
858 extras: None,
859 }
860 }
861}
862
863impl Default for AudioContentBlock {
864 fn default() -> Self {
865 Self::new()
866 }
867}
868
869#[cfg_attr(feature = "specta", derive(Type))]
871#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
872pub struct PlainTextContentBlock {
873 #[serde(rename = "type")]
875 pub block_type: String,
876 #[serde(skip_serializing_if = "Option::is_none")]
878 pub id: Option<String>,
879 #[serde(skip_serializing_if = "Option::is_none")]
881 pub file_id: Option<String>,
882 pub mime_type: String,
884 #[serde(skip_serializing_if = "Option::is_none")]
886 pub index: Option<BlockIndex>,
887 #[serde(skip_serializing_if = "Option::is_none")]
889 pub url: Option<String>,
890 #[serde(skip_serializing_if = "Option::is_none")]
892 pub base64: Option<String>,
893 #[serde(skip_serializing_if = "Option::is_none")]
895 pub text: Option<String>,
896 #[serde(skip_serializing_if = "Option::is_none")]
898 pub title: Option<String>,
899 #[serde(skip_serializing_if = "Option::is_none")]
901 pub context: Option<String>,
902 #[serde(skip_serializing_if = "Option::is_none")]
904 pub extras: Option<HashMap<String, serde_json::Value>>,
905}
906
907impl PlainTextContentBlock {
908 pub fn new() -> Self {
910 Self {
911 block_type: "text-plain".to_string(),
912 id: None,
913 file_id: None,
914 mime_type: "text/plain".to_string(),
915 index: None,
916 url: None,
917 base64: None,
918 text: None,
919 title: None,
920 context: None,
921 extras: None,
922 }
923 }
924}
925
926impl Default for PlainTextContentBlock {
927 fn default() -> Self {
928 Self::new()
929 }
930}
931
932#[cfg_attr(feature = "specta", derive(Type))]
937#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
938pub struct FileContentBlock {
939 #[serde(rename = "type")]
941 pub block_type: String,
942 #[serde(skip_serializing_if = "Option::is_none")]
944 pub id: Option<String>,
945 #[serde(skip_serializing_if = "Option::is_none")]
947 pub file_id: Option<String>,
948 #[serde(skip_serializing_if = "Option::is_none")]
950 pub mime_type: Option<String>,
951 #[serde(skip_serializing_if = "Option::is_none")]
953 pub index: Option<BlockIndex>,
954 #[serde(skip_serializing_if = "Option::is_none")]
956 pub url: Option<String>,
957 #[serde(skip_serializing_if = "Option::is_none")]
959 pub base64: Option<String>,
960 #[serde(skip_serializing_if = "Option::is_none")]
962 pub extras: Option<HashMap<String, serde_json::Value>>,
963}
964
965impl FileContentBlock {
966 pub fn new() -> Self {
968 Self {
969 block_type: "file".to_string(),
970 id: None,
971 file_id: None,
972 mime_type: None,
973 index: None,
974 url: None,
975 base64: None,
976 extras: None,
977 }
978 }
979}
980
981impl Default for FileContentBlock {
982 fn default() -> Self {
983 Self::new()
984 }
985}
986
987#[cfg_attr(feature = "specta", derive(Type))]
991#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
992pub struct NonStandardContentBlock {
993 #[serde(rename = "type")]
995 pub block_type: String,
996 #[serde(skip_serializing_if = "Option::is_none")]
998 pub id: Option<String>,
999 pub value: HashMap<String, serde_json::Value>,
1001 #[serde(skip_serializing_if = "Option::is_none")]
1003 pub index: Option<BlockIndex>,
1004}
1005
1006impl NonStandardContentBlock {
1007 pub fn new(value: HashMap<String, serde_json::Value>) -> Self {
1009 Self {
1010 block_type: "non_standard".to_string(),
1011 id: None,
1012 value,
1013 index: None,
1014 }
1015 }
1016}
1017
1018#[cfg_attr(feature = "specta", derive(Type))]
1024#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1025#[serde(tag = "type")]
1026pub enum DataContentBlock {
1027 #[serde(rename = "image")]
1028 Image(ImageContentBlock),
1029 #[serde(rename = "video")]
1030 Video(VideoContentBlock),
1031 #[serde(rename = "audio")]
1032 Audio(AudioContentBlock),
1033 #[serde(rename = "text-plain")]
1034 PlainText(PlainTextContentBlock),
1035 #[serde(rename = "file")]
1036 File(FileContentBlock),
1037}
1038
1039#[cfg_attr(feature = "specta", derive(Type))]
1041#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1042#[serde(tag = "type")]
1043pub enum ToolContentBlock {
1044 #[serde(rename = "tool_call")]
1045 ToolCall(ToolCallBlock),
1046 #[serde(rename = "tool_call_chunk")]
1047 ToolCallChunk(ToolCallChunkBlock),
1048 #[serde(rename = "server_tool_call")]
1049 ServerToolCall(ServerToolCall),
1050 #[serde(rename = "server_tool_call_chunk")]
1051 ServerToolCallChunk(ServerToolCallChunk),
1052 #[serde(rename = "server_tool_result")]
1053 ServerToolResult(ServerToolResult),
1054}
1055
1056#[cfg_attr(feature = "specta", derive(Type))]
1058#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1059#[serde(tag = "type")]
1060pub enum ContentBlock {
1061 #[serde(rename = "text")]
1062 Text(TextContentBlock),
1063 #[serde(rename = "invalid_tool_call")]
1064 InvalidToolCall(InvalidToolCallBlock),
1065 #[serde(rename = "reasoning")]
1066 Reasoning(ReasoningContentBlock),
1067 #[serde(rename = "non_standard")]
1068 NonStandard(NonStandardContentBlock),
1069 #[serde(rename = "image")]
1070 Image(ImageContentBlock),
1071 #[serde(rename = "video")]
1072 Video(VideoContentBlock),
1073 #[serde(rename = "audio")]
1074 Audio(AudioContentBlock),
1075 #[serde(rename = "text-plain")]
1076 PlainText(PlainTextContentBlock),
1077 #[serde(rename = "file")]
1078 File(FileContentBlock),
1079 #[serde(rename = "tool_call")]
1080 ToolCall(ToolCallBlock),
1081 #[serde(rename = "tool_call_chunk")]
1082 ToolCallChunk(ToolCallChunkBlock),
1083 #[serde(rename = "server_tool_call")]
1084 ServerToolCall(ServerToolCall),
1085 #[serde(rename = "server_tool_call_chunk")]
1086 ServerToolCallChunk(ServerToolCallChunk),
1087 #[serde(rename = "server_tool_result")]
1088 ServerToolResult(ServerToolResult),
1089}
1090
1091pub const KNOWN_BLOCK_TYPES: &[&str] = &[
1099 "text",
1101 "reasoning",
1102 "tool_call",
1104 "invalid_tool_call",
1105 "tool_call_chunk",
1106 "image",
1108 "audio",
1109 "file",
1110 "text-plain",
1111 "video",
1112 "server_tool_call",
1114 "server_tool_call_chunk",
1115 "server_tool_result",
1116 "non_standard",
1118 ];
1120
1121const DATA_CONTENT_BLOCK_TYPES: &[&str] = &["image", "video", "audio", "text-plain", "file"];
1123
1124pub fn is_data_content_block(block: &serde_json::Value) -> bool {
1132 let block_type = match block.get("type").and_then(|t| t.as_str()) {
1133 Some(t) => t,
1134 None => return false,
1135 };
1136
1137 if !DATA_CONTENT_BLOCK_TYPES.contains(&block_type) {
1138 return false;
1139 }
1140
1141 if block.get("url").is_some()
1143 || block.get("base64").is_some()
1144 || block.get("file_id").is_some()
1145 || block.get("text").is_some()
1146 {
1147 if block_type == "text" && block.get("source_type").is_none() {
1151 return false;
1152 }
1153 return true;
1154 }
1155
1156 if let Some(source_type) = block.get("source_type").and_then(|s| s.as_str()) {
1158 if (source_type == "url" && block.get("url").is_some())
1159 || (source_type == "base64" && block.get("data").is_some())
1160 {
1161 return true;
1162 }
1163 if (source_type == "id" && block.get("id").is_some())
1164 || (source_type == "text" && block.get("url").is_some())
1165 {
1166 return true;
1167 }
1168 }
1169
1170 false
1171}
1172
1173pub fn create_text_block(
1187 text: impl Into<String>,
1188 id: Option<String>,
1189 annotations: Option<Vec<Annotation>>,
1190 index: Option<BlockIndex>,
1191 extras: Option<HashMap<String, serde_json::Value>>,
1192) -> TextContentBlock {
1193 TextContentBlock {
1194 block_type: "text".to_string(),
1195 text: text.into(),
1196 id: Some(ensure_id(id)),
1197 annotations,
1198 index,
1199 extras,
1200 }
1201}
1202
1203pub fn create_image_block(
1219 url: Option<String>,
1220 base64: Option<String>,
1221 file_id: Option<String>,
1222 mime_type: Option<String>,
1223 id: Option<String>,
1224 index: Option<BlockIndex>,
1225 extras: Option<HashMap<String, serde_json::Value>>,
1226) -> Result<ImageContentBlock, &'static str> {
1227 if url.is_none() && base64.is_none() && file_id.is_none() {
1228 return Err("Must provide one of: url, base64, or file_id");
1229 }
1230
1231 Ok(ImageContentBlock {
1232 block_type: "image".to_string(),
1233 id: Some(ensure_id(id)),
1234 url,
1235 base64,
1236 file_id,
1237 mime_type,
1238 index,
1239 extras,
1240 })
1241}
1242
1243pub fn create_video_block(
1259 url: Option<String>,
1260 base64: Option<String>,
1261 file_id: Option<String>,
1262 mime_type: Option<String>,
1263 id: Option<String>,
1264 index: Option<BlockIndex>,
1265 extras: Option<HashMap<String, serde_json::Value>>,
1266) -> Result<VideoContentBlock, &'static str> {
1267 if url.is_none() && base64.is_none() && file_id.is_none() {
1268 return Err("Must provide one of: url, base64, or file_id");
1269 }
1270
1271 if base64.is_some() && mime_type.is_none() {
1272 return Err("mime_type is required when using base64 data");
1273 }
1274
1275 Ok(VideoContentBlock {
1276 block_type: "video".to_string(),
1277 id: Some(ensure_id(id)),
1278 url,
1279 base64,
1280 file_id,
1281 mime_type,
1282 index,
1283 extras,
1284 })
1285}
1286
1287pub fn create_audio_block(
1303 url: Option<String>,
1304 base64: Option<String>,
1305 file_id: Option<String>,
1306 mime_type: Option<String>,
1307 id: Option<String>,
1308 index: Option<BlockIndex>,
1309 extras: Option<HashMap<String, serde_json::Value>>,
1310) -> Result<AudioContentBlock, &'static str> {
1311 if url.is_none() && base64.is_none() && file_id.is_none() {
1312 return Err("Must provide one of: url, base64, or file_id");
1313 }
1314
1315 if base64.is_some() && mime_type.is_none() {
1316 return Err("mime_type is required when using base64 data");
1317 }
1318
1319 Ok(AudioContentBlock {
1320 block_type: "audio".to_string(),
1321 id: Some(ensure_id(id)),
1322 url,
1323 base64,
1324 file_id,
1325 mime_type,
1326 index,
1327 extras,
1328 })
1329}
1330
1331pub fn create_file_block(
1347 url: Option<String>,
1348 base64: Option<String>,
1349 file_id: Option<String>,
1350 mime_type: Option<String>,
1351 id: Option<String>,
1352 index: Option<BlockIndex>,
1353 extras: Option<HashMap<String, serde_json::Value>>,
1354) -> Result<FileContentBlock, &'static str> {
1355 if url.is_none() && base64.is_none() && file_id.is_none() {
1356 return Err("Must provide one of: url, base64, or file_id");
1357 }
1358
1359 if base64.is_some() && mime_type.is_none() {
1360 return Err("mime_type is required when using base64 data");
1361 }
1362
1363 Ok(FileContentBlock {
1364 block_type: "file".to_string(),
1365 id: Some(ensure_id(id)),
1366 url,
1367 base64,
1368 file_id,
1369 mime_type,
1370 index,
1371 extras,
1372 })
1373}
1374
1375#[derive(Debug, Clone, Default)]
1377pub struct PlainTextBlockConfig {
1378 pub text: Option<String>,
1380 pub url: Option<String>,
1382 pub base64: Option<String>,
1384 pub file_id: Option<String>,
1386 pub title: Option<String>,
1388 pub context: Option<String>,
1390 pub id: Option<String>,
1392 pub index: Option<BlockIndex>,
1394 pub extras: Option<HashMap<String, serde_json::Value>>,
1396}
1397
1398pub fn create_plaintext_block(config: PlainTextBlockConfig) -> PlainTextContentBlock {
1404 PlainTextContentBlock {
1405 block_type: "text-plain".to_string(),
1406 mime_type: "text/plain".to_string(),
1407 id: Some(ensure_id(config.id)),
1408 text: config.text,
1409 url: config.url,
1410 base64: config.base64,
1411 file_id: config.file_id,
1412 title: config.title,
1413 context: config.context,
1414 index: config.index,
1415 extras: config.extras,
1416 }
1417}
1418
1419pub fn create_tool_call_block(
1429 name: impl Into<String>,
1430 args: HashMap<String, serde_json::Value>,
1431 id: Option<String>,
1432 index: Option<BlockIndex>,
1433 extras: Option<HashMap<String, serde_json::Value>>,
1434) -> ToolCallBlock {
1435 ToolCallBlock {
1436 block_type: "tool_call".to_string(),
1437 name: name.into(),
1438 args,
1439 id: Some(ensure_id(id)),
1440 index,
1441 extras,
1442 }
1443}
1444
1445pub fn create_reasoning_block(
1454 reasoning: Option<String>,
1455 id: Option<String>,
1456 index: Option<BlockIndex>,
1457 extras: Option<HashMap<String, serde_json::Value>>,
1458) -> ReasoningContentBlock {
1459 ReasoningContentBlock {
1460 block_type: "reasoning".to_string(),
1461 reasoning,
1462 id: Some(ensure_id(id)),
1463 index,
1464 extras,
1465 }
1466}
1467
1468pub fn create_citation(
1480 url: Option<String>,
1481 title: Option<String>,
1482 start_index: Option<i64>,
1483 end_index: Option<i64>,
1484 cited_text: Option<String>,
1485 id: Option<String>,
1486 extras: Option<HashMap<String, serde_json::Value>>,
1487) -> Citation {
1488 Citation {
1489 block_type: "citation".to_string(),
1490 id: Some(ensure_id(id)),
1491 url,
1492 title,
1493 start_index,
1494 end_index,
1495 cited_text,
1496 extras,
1497 }
1498}
1499
1500pub fn create_non_standard_block(
1508 value: HashMap<String, serde_json::Value>,
1509 id: Option<String>,
1510 index: Option<BlockIndex>,
1511) -> NonStandardContentBlock {
1512 NonStandardContentBlock {
1513 block_type: "non_standard".to_string(),
1514 value,
1515 id: Some(ensure_id(id)),
1516 index,
1517 }
1518}
1519
1520#[cfg(test)]
1521mod tests {
1522 use super::*;
1523
1524 #[test]
1525 fn test_text_content_block_serialization() {
1526 let block = TextContentBlock::new("Hello, world!");
1527 let json = serde_json::to_string(&block).unwrap();
1528 assert!(json.contains("\"type\":\"text\""));
1529 assert!(json.contains("\"text\":\"Hello, world!\""));
1530 }
1531
1532 #[test]
1533 fn test_create_text_block() {
1534 let block = create_text_block("Test", None, None, None, None);
1535 assert_eq!(block.text, "Test");
1536 assert!(block.id.unwrap().starts_with("lc_"));
1537 }
1538
1539 #[test]
1540 fn test_create_image_block() {
1541 let block = create_image_block(
1542 Some("https://example.com/image.png".to_string()),
1543 None,
1544 None,
1545 Some("image/png".to_string()),
1546 None,
1547 None,
1548 None,
1549 )
1550 .unwrap();
1551 assert_eq!(block.url.as_ref().unwrap(), "https://example.com/image.png");
1552 assert_eq!(block.mime_type.as_ref().unwrap(), "image/png");
1553 }
1554
1555 #[test]
1556 fn test_create_image_block_error() {
1557 let result = create_image_block(None, None, None, None, None, None, None);
1558 assert!(result.is_err());
1559 assert_eq!(
1560 result.unwrap_err(),
1561 "Must provide one of: url, base64, or file_id"
1562 );
1563 }
1564
1565 #[test]
1566 fn test_reasoning_content_block() {
1567 let block = ReasoningContentBlock::new("Thinking...");
1568 assert_eq!(block.reasoning(), Some("Thinking..."));
1569 assert_eq!(block.block_type, "reasoning");
1570 }
1571
1572 #[test]
1573 fn test_known_block_types() {
1574 assert!(KNOWN_BLOCK_TYPES.contains(&"text"));
1575 assert!(KNOWN_BLOCK_TYPES.contains(&"reasoning"));
1576 assert!(KNOWN_BLOCK_TYPES.contains(&"image"));
1577 assert!(KNOWN_BLOCK_TYPES.contains(&"tool_call"));
1578 }
1579
1580 #[test]
1581 fn test_is_data_content_block() {
1582 let image_block = serde_json::json!({
1583 "type": "image",
1584 "url": "https://example.com/image.png"
1585 });
1586 assert!(is_data_content_block(&image_block));
1587
1588 let text_block = serde_json::json!({
1589 "type": "text",
1590 "text": "Hello"
1591 });
1592 assert!(!is_data_content_block(&text_block));
1593 }
1594
1595 #[test]
1596 fn test_content_block_enum_serialization() {
1597 let block = ContentBlock::Text(TextContentBlock::new("Hello"));
1598 let json = serde_json::to_string(&block).unwrap();
1599 assert!(json.contains("\"type\":\"text\""));
1600 }
1601
1602 #[test]
1603 fn test_legacy_message_content() {
1604 let content = MessageContent::Text("Hello".to_string());
1605 assert_eq!(content.as_text(), "Hello");
1606
1607 let content = MessageContent::Parts(vec![
1608 ContentPart::Text {
1609 text: "Hello".to_string(),
1610 },
1611 ContentPart::Text {
1612 text: "World".to_string(),
1613 },
1614 ]);
1615 assert_eq!(content.as_text(), "Hello World");
1616 }
1617}