1use std::collections::HashMap;
34use std::fmt;
35
36use serde::{Deserialize, Serialize};
37use serde_json::Value;
38
39use crate::usage::Usage;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[non_exhaustive]
48pub enum ChatRole {
49 System,
53 User,
55 Assistant,
57 Tool,
59}
60
61impl fmt::Display for ChatRole {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::System => f.write_str("system"),
65 Self::User => f.write_str("user"),
66 Self::Assistant => f.write_str("assistant"),
67 Self::Tool => f.write_str("tool"),
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct ChatMessage {
89 pub role: ChatRole,
91 pub content: Vec<ContentBlock>,
93}
94
95impl ChatMessage {
96 pub fn text(role: ChatRole, text: impl Into<String>) -> Self {
98 Self {
99 role,
100 content: vec![ContentBlock::Text(text.into())],
101 }
102 }
103
104 pub fn user(text: impl Into<String>) -> Self {
106 Self::text(ChatRole::User, text)
107 }
108
109 pub fn assistant(text: impl Into<String>) -> Self {
111 Self::text(ChatRole::Assistant, text)
112 }
113
114 pub fn system(text: impl Into<String>) -> Self {
116 Self::text(ChatRole::System, text)
117 }
118
119 pub fn tool_result(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
124 Self {
125 role: ChatRole::Tool,
126 content: vec![ContentBlock::ToolResult(ToolResult {
127 tool_call_id: tool_call_id.into(),
128 content: content.into(),
129 is_error: false,
130 })],
131 }
132 }
133
134 pub fn tool_error(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
137 Self {
138 role: ChatRole::Tool,
139 content: vec![ContentBlock::ToolResult(ToolResult {
140 tool_call_id: tool_call_id.into(),
141 content: content.into(),
142 is_error: true,
143 })],
144 }
145 }
146
147 pub fn is_empty(&self) -> bool {
153 self.content.is_empty()
154 }
155
156 pub fn to_json(&self) -> Result<Value, serde_json::Error> {
178 serde_json::to_value(self)
179 }
180
181 pub fn from_json(value: &Value) -> Result<Self, serde_json::Error> {
200 serde_json::from_value(value.clone())
201 }
202
203 pub fn from_json_owned(value: Value) -> Result<Self, serde_json::Error> {
222 serde_json::from_value(value)
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232#[serde(rename_all = "snake_case")]
233#[non_exhaustive]
234pub enum ContentBlock {
235 Text(String),
237 Image {
239 media_type: String,
241 data: ImageSource,
243 },
244 ToolCall(ToolCall),
246 ToolResult(ToolResult),
248 Reasoning {
251 content: String,
253 },
254}
255
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258#[non_exhaustive]
259pub enum ImageSource {
260 Base64(String),
262 Url(url::Url),
267}
268
269impl ImageSource {
270 pub fn from_url(url: impl AsRef<str>) -> Result<Self, url::ParseError> {
275 let parsed = url::Url::parse(url.as_ref())?;
276 Ok(Self::Url(parsed))
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286pub struct ToolCall {
287 pub id: String,
289 pub name: String,
291 pub arguments: Value,
293}
294
295impl std::fmt::Display for ToolCall {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 write!(f, "{}({})", self.name, self.id)
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
303pub struct ToolResult {
304 pub tool_call_id: String,
306 pub content: String,
308 pub is_error: bool,
310}
311
312impl std::fmt::Display for ToolResult {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 if self.is_error {
315 write!(f, "err:{} ({})", self.tool_call_id, self.content)
316 } else {
317 write!(f, "ok:{}", self.tool_call_id)
318 }
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328pub struct ChatResponse {
329 pub content: Vec<ContentBlock>,
331 pub usage: Usage,
333 pub stop_reason: StopReason,
335 pub model: String,
338 pub metadata: HashMap<String, Value>,
343}
344
345impl ChatResponse {
346 pub fn empty() -> Self {
350 Self {
351 content: Vec::new(),
352 usage: Usage::default(),
353 stop_reason: StopReason::EndTurn,
354 model: String::new(),
355 metadata: HashMap::new(),
356 }
357 }
358
359 pub fn text(&self) -> Option<&str> {
365 self.content.iter().find_map(|b| match b {
366 ContentBlock::Text(t) => Some(t.as_str()),
367 _ => None,
368 })
369 }
370
371 pub fn tool_calls(&self) -> Vec<&ToolCall> {
378 self.content
379 .iter()
380 .filter_map(|b| match b {
381 ContentBlock::ToolCall(tc) => Some(tc),
382 _ => None,
383 })
384 .collect()
385 }
386
387 pub fn tool_calls_iter(&self) -> impl Iterator<Item = &ToolCall> {
391 self.content.iter().filter_map(|b| match b {
392 ContentBlock::ToolCall(tc) => Some(tc),
393 _ => None,
394 })
395 }
396
397 pub fn into_tool_calls(self) -> Vec<ToolCall> {
428 self.content
429 .into_iter()
430 .filter_map(|b| match b {
431 ContentBlock::ToolCall(tc) => Some(tc),
432 _ => None,
433 })
434 .collect()
435 }
436
437 pub fn partition_content(self) -> (Vec<ToolCall>, Vec<ContentBlock>) {
446 let mut tool_calls = Vec::new();
447 let mut other = Vec::new();
448
449 for block in self.content {
450 match block {
451 ContentBlock::ToolCall(tc) => tool_calls.push(tc),
452 ContentBlock::ToolResult(_) => {}
454 other_block => other.push(other_block),
455 }
456 }
457
458 (tool_calls, other)
459 }
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
464#[non_exhaustive]
465pub enum StopReason {
466 EndTurn,
468 ToolUse,
470 MaxTokens,
472 StopSequence,
474}
475
476impl fmt::Display for StopReason {
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 match self {
479 Self::EndTurn => f.write_str("end_turn"),
480 Self::ToolUse => f.write_str("tool_use"),
481 Self::MaxTokens => f.write_str("max_tokens"),
482 Self::StopSequence => f.write_str("stop_sequence"),
483 }
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn test_chat_role_copy_hash() {
493 use std::collections::HashMap;
494 let mut map = HashMap::new();
495 let role = ChatRole::User;
496 let role_copy = role; map.insert(role, "user");
498 map.insert(role_copy, "user_copy");
499 assert_eq!(map.len(), 1);
500 }
501
502 #[test]
503 fn test_chat_role_all_variants() {
504 let variants = [
505 ChatRole::System,
506 ChatRole::User,
507 ChatRole::Assistant,
508 ChatRole::Tool,
509 ];
510 for v in &variants {
511 let debug = format!("{v:?}");
512 assert!(!debug.is_empty());
513 }
514 }
515
516 #[test]
517 fn test_chat_role_serde_roundtrip() {
518 let role = ChatRole::Assistant;
519 let json = serde_json::to_string(&role).unwrap();
520 let back: ChatRole = serde_json::from_str(&json).unwrap();
521 assert_eq!(role, back);
522 }
523
524 #[test]
527 fn test_user_constructor() {
528 let msg = ChatMessage::user("hello");
529 assert_eq!(msg.role, ChatRole::User);
530 assert_eq!(msg.content, vec![ContentBlock::Text("hello".into())]);
531 }
532
533 #[test]
534 fn test_assistant_constructor() {
535 let msg = ChatMessage::assistant("hi");
536 assert_eq!(msg.role, ChatRole::Assistant);
537 assert_eq!(msg.content, vec![ContentBlock::Text("hi".into())]);
538 }
539
540 #[test]
541 fn test_system_constructor() {
542 let msg = ChatMessage::system("be nice");
543 assert_eq!(msg.role, ChatRole::System);
544 }
545
546 #[test]
547 fn test_tool_result_constructor() {
548 let msg = ChatMessage::tool_result("tc_1", "42");
549 assert_eq!(msg.role, ChatRole::Tool);
550 assert!(matches!(
551 &msg.content[0],
552 ContentBlock::ToolResult(tr)
553 if tr.tool_call_id == "tc_1" && tr.content == "42" && !tr.is_error
554 ));
555 }
556
557 #[test]
558 fn test_tool_error_constructor() {
559 let msg = ChatMessage::tool_error("tc_1", "something broke");
560 assert!(matches!(
561 &msg.content[0],
562 ContentBlock::ToolResult(tr) if tr.is_error
563 ));
564 }
565
566 #[test]
569 fn test_message_text_clone_eq() {
570 let msg = ChatMessage::user("hello");
571 assert_eq!(msg, msg.clone());
572 }
573
574 #[test]
575 fn test_message_serde_roundtrip() {
576 let msg = ChatMessage::user("hello");
577 let json = serde_json::to_string(&msg).unwrap();
578 let back: ChatMessage = serde_json::from_str(&json).unwrap();
579 assert_eq!(msg, back);
580 }
581
582 #[test]
583 fn test_message_tool_use() {
584 let msg = ChatMessage {
585 role: ChatRole::Assistant,
586 content: vec![
587 ContentBlock::ToolCall(ToolCall {
588 id: "1".into(),
589 name: "calc".into(),
590 arguments: serde_json::json!({"a": 1}),
591 }),
592 ContentBlock::ToolCall(ToolCall {
593 id: "2".into(),
594 name: "search".into(),
595 arguments: serde_json::json!({"q": "rust"}),
596 }),
597 ],
598 };
599 assert_eq!(msg.content.len(), 2);
600 assert_eq!(msg, msg.clone());
601 }
602
603 #[test]
604 fn test_message_tool_result() {
605 let msg = ChatMessage::tool_result("1", "42");
606 assert!(matches!(
607 &msg.content[0],
608 ContentBlock::ToolResult(tr) if tr.content == "42" && !tr.is_error
609 ));
610 }
611
612 #[test]
613 fn test_message_mixed_content() {
614 let msg = ChatMessage {
615 role: ChatRole::User,
616 content: vec![
617 ContentBlock::Text("look at this".into()),
618 ContentBlock::Image {
619 media_type: "image/png".into(),
620 data: ImageSource::Base64("abc123".into()),
621 },
622 ContentBlock::ToolCall(ToolCall {
623 id: "1".into(),
624 name: "analyze".into(),
625 arguments: serde_json::json!({}),
626 }),
627 ],
628 };
629 assert_eq!(msg.content.len(), 3);
630 }
631
632 #[test]
635 fn test_content_block_image_base64() {
636 let block = ContentBlock::Image {
637 media_type: "image/jpeg".into(),
638 data: ImageSource::Base64("data...".into()),
639 };
640 assert_eq!(block, block.clone());
641 }
642
643 #[test]
644 fn test_content_block_image_url() {
645 let block = ContentBlock::Image {
646 media_type: "image/png".into(),
647 data: ImageSource::from_url("https://example.com/img.png").unwrap(),
648 };
649 assert_eq!(block, block.clone());
650 }
651
652 #[test]
653 fn test_image_source_from_url_valid() {
654 let src = ImageSource::from_url("https://example.com/img.png");
655 assert!(src.is_ok());
656 let url = url::Url::parse("https://example.com/img.png").unwrap();
657 assert_eq!(src.unwrap(), ImageSource::Url(url));
658 }
659
660 #[test]
661 fn test_image_source_from_url_normalizes() {
662 let src = ImageSource::from_url("HTTP://EXAMPLE.COM").unwrap();
664 assert!(matches!(
665 &src,
666 ImageSource::Url(u) if u.as_str() == "http://example.com/"
667 ));
668 }
669
670 #[test]
671 fn test_image_source_from_url_invalid() {
672 let err = ImageSource::from_url("not a url");
673 assert!(err.is_err());
674 let _parse_err: url::ParseError = err.unwrap_err();
675
676 assert!(ImageSource::from_url("").is_err());
677 }
678
679 #[test]
680 fn test_content_block_reasoning() {
681 let block = ContentBlock::Reasoning {
682 content: "thinking step by step".into(),
683 };
684 assert_eq!(block, block.clone());
685 }
686
687 #[test]
688 fn test_tool_call_json_arguments() {
689 let call = ToolCall {
690 id: "tc_1".into(),
691 name: "search".into(),
692 arguments: serde_json::json!({
693 "query": "rust async",
694 "filters": {"lang": "en", "limit": 10}
695 }),
696 };
697 assert_eq!(call, call.clone());
698 }
699
700 #[test]
701 fn test_tool_result_error_flag() {
702 let ok = ToolResult {
703 tool_call_id: "1".into(),
704 content: "result".into(),
705 is_error: false,
706 };
707 let err = ToolResult {
708 tool_call_id: "1".into(),
709 content: "result".into(),
710 is_error: true,
711 };
712 assert_ne!(ok, err);
713 }
714
715 #[test]
718 fn test_chat_response_metadata() {
719 let mut metadata = HashMap::new();
720 metadata.insert("cost".into(), serde_json::json!({"usd": 0.01}));
721 let resp = ChatResponse {
722 content: vec![ContentBlock::Text("hi".into())],
723 usage: Usage::default(),
724 stop_reason: StopReason::EndTurn,
725 model: "test-model".into(),
726 metadata,
727 };
728 assert!(resp.metadata.contains_key("cost"));
729 }
730
731 #[test]
732 fn test_chat_response_serde_roundtrip() {
733 let resp = ChatResponse {
734 content: vec![ContentBlock::Text("hi".into())],
735 usage: Usage::default(),
736 stop_reason: StopReason::EndTurn,
737 model: "test-model".into(),
738 metadata: HashMap::new(),
739 };
740 let json = serde_json::to_string(&resp).unwrap();
741 let back: ChatResponse = serde_json::from_str(&json).unwrap();
742 assert_eq!(resp, back);
743 }
744
745 #[test]
746 fn test_chat_response_empty_content() {
747 let resp = ChatResponse {
748 content: vec![],
749 usage: Usage::default(),
750 stop_reason: StopReason::EndTurn,
751 model: "test".into(),
752 metadata: HashMap::new(),
753 };
754 assert!(resp.content.is_empty());
755 }
756
757 #[test]
760 fn test_stop_reason_all_variants() {
761 let variants = [
762 StopReason::EndTurn,
763 StopReason::ToolUse,
764 StopReason::MaxTokens,
765 StopReason::StopSequence,
766 ];
767 for v in &variants {
768 assert_eq!(*v, *v);
769 }
770 }
771
772 #[test]
773 fn test_stop_reason_serde_roundtrip() {
774 let sr = StopReason::MaxTokens;
775 let json = serde_json::to_string(&sr).unwrap();
776 let back: StopReason = serde_json::from_str(&json).unwrap();
777 assert_eq!(sr, back);
778 }
779
780 #[test]
781 fn test_stop_reason_eq_hash() {
782 use std::collections::HashMap;
783 let mut map = HashMap::new();
784 map.insert(StopReason::EndTurn, "end");
785 map.insert(StopReason::ToolUse, "tool");
786 assert_eq!(map[&StopReason::EndTurn], "end");
787 assert_eq!(map[&StopReason::ToolUse], "tool");
788 }
789
790 #[test]
793 fn test_chat_role_display() {
794 assert_eq!(ChatRole::System.to_string(), "system");
795 assert_eq!(ChatRole::User.to_string(), "user");
796 assert_eq!(ChatRole::Assistant.to_string(), "assistant");
797 assert_eq!(ChatRole::Tool.to_string(), "tool");
798 }
799
800 #[test]
801 fn test_stop_reason_display() {
802 assert_eq!(StopReason::EndTurn.to_string(), "end_turn");
803 assert_eq!(StopReason::ToolUse.to_string(), "tool_use");
804 assert_eq!(StopReason::MaxTokens.to_string(), "max_tokens");
805 assert_eq!(StopReason::StopSequence.to_string(), "stop_sequence");
806 }
807
808 #[test]
811 fn test_chat_response_text_returns_first() {
812 let resp = ChatResponse {
813 content: vec![
814 ContentBlock::Reasoning {
815 content: "thinking...".into(),
816 },
817 ContentBlock::Text("first".into()),
818 ContentBlock::Text("second".into()),
819 ],
820 usage: Usage::default(),
821 stop_reason: StopReason::EndTurn,
822 model: "test".into(),
823 metadata: HashMap::new(),
824 };
825 assert_eq!(resp.text(), Some("first"));
826 }
827
828 #[test]
829 fn test_chat_response_text_none_when_no_text_blocks() {
830 let resp = ChatResponse {
831 content: vec![ContentBlock::Reasoning {
832 content: "thinking".into(),
833 }],
834 usage: Usage::default(),
835 stop_reason: StopReason::EndTurn,
836 model: "test".into(),
837 metadata: HashMap::new(),
838 };
839 assert_eq!(resp.text(), None);
840 }
841
842 #[test]
843 fn test_chat_response_text_none_when_empty() {
844 let resp = ChatResponse {
845 content: vec![],
846 usage: Usage::default(),
847 stop_reason: StopReason::EndTurn,
848 model: "test".into(),
849 metadata: HashMap::new(),
850 };
851 assert_eq!(resp.text(), None);
852 }
853
854 #[test]
857 fn test_chat_response_tool_calls() {
858 let resp = ChatResponse {
859 content: vec![
860 ContentBlock::Text("Let me search.".into()),
861 ContentBlock::ToolCall(ToolCall {
862 id: "1".into(),
863 name: "search".into(),
864 arguments: serde_json::json!({"q": "rust"}),
865 }),
866 ContentBlock::ToolCall(ToolCall {
867 id: "2".into(),
868 name: "calc".into(),
869 arguments: serde_json::json!({"expr": "2+2"}),
870 }),
871 ],
872 usage: Usage::default(),
873 stop_reason: StopReason::ToolUse,
874 model: "test".into(),
875 metadata: HashMap::new(),
876 };
877 let calls = resp.tool_calls();
878 assert_eq!(calls.len(), 2);
879 assert_eq!(calls[0].name, "search");
880 assert_eq!(calls[1].name, "calc");
881 }
882
883 #[test]
884 fn test_chat_response_tool_calls_empty_when_text_only() {
885 let resp = ChatResponse {
886 content: vec![ContentBlock::Text("hello".into())],
887 usage: Usage::default(),
888 stop_reason: StopReason::EndTurn,
889 model: "test".into(),
890 metadata: HashMap::new(),
891 };
892 assert!(resp.tool_calls().is_empty());
893 }
894
895 #[test]
898 fn test_message_is_empty() {
899 let empty = ChatMessage {
900 role: ChatRole::User,
901 content: vec![],
902 };
903 assert!(empty.is_empty());
904 assert!(!ChatMessage::user("hi").is_empty());
905 }
906
907 #[test]
910 fn test_content_block_serde_text() {
911 let block = ContentBlock::Text("hello".into());
912 let val = serde_json::to_value(&block).unwrap();
913 assert_eq!(val, serde_json::json!({"text": "hello"}));
914 let back: ContentBlock = serde_json::from_value(val).unwrap();
915 assert_eq!(back, block);
916 }
917
918 #[test]
919 fn test_content_block_serde_image() {
920 let block = ContentBlock::Image {
921 media_type: "image/png".into(),
922 data: ImageSource::Base64("abc".into()),
923 };
924 let val = serde_json::to_value(&block).unwrap();
925 assert_eq!(
926 val,
927 serde_json::json!({"image": {"media_type": "image/png", "data": {"Base64": "abc"}}})
928 );
929 let back: ContentBlock = serde_json::from_value(val).unwrap();
930 assert_eq!(back, block);
931 }
932
933 #[test]
934 fn test_content_block_serde_tool_call() {
935 let block = ContentBlock::ToolCall(ToolCall {
936 id: "tc_1".into(),
937 name: "search".into(),
938 arguments: serde_json::json!({"q": "rust"}),
939 });
940 let val = serde_json::to_value(&block).unwrap();
941 assert_eq!(
942 val,
943 serde_json::json!({"tool_call": {"id": "tc_1", "name": "search", "arguments": {"q": "rust"}}})
944 );
945 let back: ContentBlock = serde_json::from_value(val).unwrap();
946 assert_eq!(back, block);
947 }
948
949 #[test]
950 fn test_content_block_serde_tool_result() {
951 let block = ContentBlock::ToolResult(ToolResult {
952 tool_call_id: "tc_1".into(),
953 content: "42".into(),
954 is_error: false,
955 });
956 let val = serde_json::to_value(&block).unwrap();
957 assert_eq!(
958 val,
959 serde_json::json!({"tool_result": {"tool_call_id": "tc_1", "content": "42", "is_error": false}})
960 );
961 let back: ContentBlock = serde_json::from_value(val).unwrap();
962 assert_eq!(back, block);
963 }
964
965 #[test]
966 fn test_content_block_serde_reasoning() {
967 let block = ContentBlock::Reasoning {
968 content: "thinking".into(),
969 };
970 let val = serde_json::to_value(&block).unwrap();
971 assert_eq!(
972 val,
973 serde_json::json!({"reasoning": {"content": "thinking"}})
974 );
975 let back: ContentBlock = serde_json::from_value(val).unwrap();
976 assert_eq!(back, block);
977 }
978
979 #[test]
982 fn test_user_constructor_produces_text_only() {
983 let msg = ChatMessage::user("hello");
984 assert_eq!(msg.role, ChatRole::User);
985 assert!(
986 msg.content
987 .iter()
988 .all(|b| matches!(b, ContentBlock::Text(_)))
989 );
990 }
991
992 #[test]
993 fn test_assistant_constructor_produces_text_only() {
994 let msg = ChatMessage::assistant("hi");
995 assert_eq!(msg.role, ChatRole::Assistant);
996 assert!(
997 msg.content
998 .iter()
999 .all(|b| matches!(b, ContentBlock::Text(_)))
1000 );
1001 }
1002
1003 #[test]
1004 fn test_system_constructor_produces_text_only() {
1005 let msg = ChatMessage::system("be nice");
1006 assert_eq!(msg.role, ChatRole::System);
1007 assert!(
1008 msg.content
1009 .iter()
1010 .all(|b| matches!(b, ContentBlock::Text(_)))
1011 );
1012 }
1013
1014 #[test]
1015 fn test_tool_result_constructor_produces_tool_result_only() {
1016 let msg = ChatMessage::tool_result("tc_1", "42");
1017 assert_eq!(msg.role, ChatRole::Tool);
1018 assert!(
1019 msg.content
1020 .iter()
1021 .all(|b| matches!(b, ContentBlock::ToolResult(_)))
1022 );
1023 }
1024
1025 #[test]
1026 fn test_tool_error_constructor_produces_tool_result_only() {
1027 let msg = ChatMessage::tool_error("tc_1", "boom");
1028 assert_eq!(msg.role, ChatRole::Tool);
1029 assert!(
1030 msg.content
1031 .iter()
1032 .all(|b| matches!(b, ContentBlock::ToolResult(r) if r.is_error))
1033 );
1034 }
1035
1036 #[test]
1037 fn test_assistant_tool_calls_is_valid_combination() {
1038 let msg = ChatMessage {
1040 role: ChatRole::Assistant,
1041 content: vec![
1042 ContentBlock::Text("Let me search for that.".into()),
1043 ContentBlock::ToolCall(ToolCall {
1044 id: "1".into(),
1045 name: "search".into(),
1046 arguments: serde_json::json!({"q": "rust"}),
1047 }),
1048 ],
1049 };
1050 assert_eq!(msg.role, ChatRole::Assistant);
1051 assert_eq!(msg.content.len(), 2);
1052 }
1053
1054 #[test]
1055 fn test_user_with_image_is_valid_combination() {
1056 let msg = ChatMessage {
1058 role: ChatRole::User,
1059 content: vec![
1060 ContentBlock::Text("What's this?".into()),
1061 ContentBlock::Image {
1062 media_type: "image/png".into(),
1063 data: ImageSource::Base64("...".into()),
1064 },
1065 ],
1066 };
1067 assert_eq!(msg.role, ChatRole::User);
1068 assert_eq!(msg.content.len(), 2);
1069 }
1070
1071 #[test]
1072 fn test_assistant_with_reasoning_is_valid_combination() {
1073 let msg = ChatMessage {
1075 role: ChatRole::Assistant,
1076 content: vec![
1077 ContentBlock::Reasoning {
1078 content: "step 1: think about it".into(),
1079 },
1080 ContentBlock::Text("The answer is 42.".into()),
1081 ],
1082 };
1083 assert_eq!(msg.role, ChatRole::Assistant);
1084 assert_eq!(msg.content.len(), 2);
1085 }
1086
1087 #[test]
1090 fn test_chat_message_to_json() {
1091 let msg = ChatMessage::user("Hello, world!");
1092 let json = msg.to_json().unwrap();
1093 assert_eq!(json["role"], "User");
1094 assert_eq!(json["content"][0]["text"], "Hello, world!");
1095 }
1096
1097 #[test]
1098 fn test_chat_message_from_json() {
1099 let json = serde_json::json!({
1100 "role": "Assistant",
1101 "content": [{"text": "Hello!"}]
1102 });
1103 let msg = ChatMessage::from_json(&json).unwrap();
1104 assert_eq!(msg.role, ChatRole::Assistant);
1105 assert!(matches!(&msg.content[0], ContentBlock::Text(t) if t == "Hello!"));
1106 }
1107
1108 #[test]
1109 fn test_chat_message_json_roundtrip() {
1110 let original = ChatMessage {
1111 role: ChatRole::User,
1112 content: vec![
1113 ContentBlock::Text("What's this?".into()),
1114 ContentBlock::Image {
1115 media_type: "image/png".into(),
1116 data: ImageSource::Base64("abc123".into()),
1117 },
1118 ],
1119 };
1120 let json = original.to_json().unwrap();
1121 let restored = ChatMessage::from_json(&json).unwrap();
1122 assert_eq!(original, restored);
1123 }
1124
1125 #[test]
1126 fn test_chat_message_json_roundtrip_with_tool_result() {
1127 let original = ChatMessage::tool_result("tc_1", "success");
1128 let json = original.to_json().unwrap();
1129 let restored = ChatMessage::from_json(&json).unwrap();
1130 assert_eq!(original, restored);
1131 }
1132
1133 }