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
295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub struct ToolResult {
298 pub tool_call_id: String,
300 pub content: String,
302 pub is_error: bool,
304}
305
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
312pub struct ChatResponse {
313 pub content: Vec<ContentBlock>,
315 pub usage: Usage,
317 pub stop_reason: StopReason,
319 pub model: String,
322 pub metadata: HashMap<String, Value>,
324}
325
326impl ChatResponse {
327 pub fn empty() -> Self {
331 Self {
332 content: Vec::new(),
333 usage: Usage::default(),
334 stop_reason: StopReason::EndTurn,
335 model: String::new(),
336 metadata: HashMap::new(),
337 }
338 }
339
340 pub fn text(&self) -> Option<&str> {
346 self.content.iter().find_map(|b| match b {
347 ContentBlock::Text(t) => Some(t.as_str()),
348 _ => None,
349 })
350 }
351
352 pub fn tool_calls(&self) -> Vec<&ToolCall> {
359 self.content
360 .iter()
361 .filter_map(|b| match b {
362 ContentBlock::ToolCall(tc) => Some(tc),
363 _ => None,
364 })
365 .collect()
366 }
367
368 pub fn tool_calls_iter(&self) -> impl Iterator<Item = &ToolCall> {
372 self.content.iter().filter_map(|b| match b {
373 ContentBlock::ToolCall(tc) => Some(tc),
374 _ => None,
375 })
376 }
377
378 pub fn into_tool_calls(self) -> Vec<ToolCall> {
409 self.content
410 .into_iter()
411 .filter_map(|b| match b {
412 ContentBlock::ToolCall(tc) => Some(tc),
413 _ => None,
414 })
415 .collect()
416 }
417
418 pub fn partition_content(self) -> (Vec<ToolCall>, Vec<ContentBlock>) {
427 let mut tool_calls = Vec::new();
428 let mut other = Vec::new();
429
430 for block in self.content {
431 match block {
432 ContentBlock::ToolCall(tc) => tool_calls.push(tc),
433 ContentBlock::ToolResult(_) => {}
435 other_block => other.push(other_block),
436 }
437 }
438
439 (tool_calls, other)
440 }
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
445#[non_exhaustive]
446pub enum StopReason {
447 EndTurn,
449 ToolUse,
451 MaxTokens,
453 StopSequence,
455}
456
457impl fmt::Display for StopReason {
458 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459 match self {
460 Self::EndTurn => f.write_str("end_turn"),
461 Self::ToolUse => f.write_str("tool_use"),
462 Self::MaxTokens => f.write_str("max_tokens"),
463 Self::StopSequence => f.write_str("stop_sequence"),
464 }
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn test_chat_role_copy_hash() {
474 use std::collections::HashMap;
475 let mut map = HashMap::new();
476 let role = ChatRole::User;
477 let role_copy = role; map.insert(role, "user");
479 map.insert(role_copy, "user_copy");
480 assert_eq!(map.len(), 1);
481 }
482
483 #[test]
484 fn test_chat_role_all_variants() {
485 let variants = [
486 ChatRole::System,
487 ChatRole::User,
488 ChatRole::Assistant,
489 ChatRole::Tool,
490 ];
491 for v in &variants {
492 let debug = format!("{v:?}");
493 assert!(!debug.is_empty());
494 }
495 }
496
497 #[test]
498 fn test_chat_role_serde_roundtrip() {
499 let role = ChatRole::Assistant;
500 let json = serde_json::to_string(&role).unwrap();
501 let back: ChatRole = serde_json::from_str(&json).unwrap();
502 assert_eq!(role, back);
503 }
504
505 #[test]
508 fn test_user_constructor() {
509 let msg = ChatMessage::user("hello");
510 assert_eq!(msg.role, ChatRole::User);
511 assert_eq!(msg.content, vec![ContentBlock::Text("hello".into())]);
512 }
513
514 #[test]
515 fn test_assistant_constructor() {
516 let msg = ChatMessage::assistant("hi");
517 assert_eq!(msg.role, ChatRole::Assistant);
518 assert_eq!(msg.content, vec![ContentBlock::Text("hi".into())]);
519 }
520
521 #[test]
522 fn test_system_constructor() {
523 let msg = ChatMessage::system("be nice");
524 assert_eq!(msg.role, ChatRole::System);
525 }
526
527 #[test]
528 fn test_tool_result_constructor() {
529 let msg = ChatMessage::tool_result("tc_1", "42");
530 assert_eq!(msg.role, ChatRole::Tool);
531 assert!(matches!(
532 &msg.content[0],
533 ContentBlock::ToolResult(tr)
534 if tr.tool_call_id == "tc_1" && tr.content == "42" && !tr.is_error
535 ));
536 }
537
538 #[test]
539 fn test_tool_error_constructor() {
540 let msg = ChatMessage::tool_error("tc_1", "something broke");
541 assert!(matches!(
542 &msg.content[0],
543 ContentBlock::ToolResult(tr) if tr.is_error
544 ));
545 }
546
547 #[test]
550 fn test_message_text_clone_eq() {
551 let msg = ChatMessage::user("hello");
552 assert_eq!(msg, msg.clone());
553 }
554
555 #[test]
556 fn test_message_serde_roundtrip() {
557 let msg = ChatMessage::user("hello");
558 let json = serde_json::to_string(&msg).unwrap();
559 let back: ChatMessage = serde_json::from_str(&json).unwrap();
560 assert_eq!(msg, back);
561 }
562
563 #[test]
564 fn test_message_tool_use() {
565 let msg = ChatMessage {
566 role: ChatRole::Assistant,
567 content: vec![
568 ContentBlock::ToolCall(ToolCall {
569 id: "1".into(),
570 name: "calc".into(),
571 arguments: serde_json::json!({"a": 1}),
572 }),
573 ContentBlock::ToolCall(ToolCall {
574 id: "2".into(),
575 name: "search".into(),
576 arguments: serde_json::json!({"q": "rust"}),
577 }),
578 ],
579 };
580 assert_eq!(msg.content.len(), 2);
581 assert_eq!(msg, msg.clone());
582 }
583
584 #[test]
585 fn test_message_tool_result() {
586 let msg = ChatMessage::tool_result("1", "42");
587 assert!(matches!(
588 &msg.content[0],
589 ContentBlock::ToolResult(tr) if tr.content == "42" && !tr.is_error
590 ));
591 }
592
593 #[test]
594 fn test_message_mixed_content() {
595 let msg = ChatMessage {
596 role: ChatRole::User,
597 content: vec![
598 ContentBlock::Text("look at this".into()),
599 ContentBlock::Image {
600 media_type: "image/png".into(),
601 data: ImageSource::Base64("abc123".into()),
602 },
603 ContentBlock::ToolCall(ToolCall {
604 id: "1".into(),
605 name: "analyze".into(),
606 arguments: serde_json::json!({}),
607 }),
608 ],
609 };
610 assert_eq!(msg.content.len(), 3);
611 }
612
613 #[test]
616 fn test_content_block_image_base64() {
617 let block = ContentBlock::Image {
618 media_type: "image/jpeg".into(),
619 data: ImageSource::Base64("data...".into()),
620 };
621 assert_eq!(block, block.clone());
622 }
623
624 #[test]
625 fn test_content_block_image_url() {
626 let block = ContentBlock::Image {
627 media_type: "image/png".into(),
628 data: ImageSource::from_url("https://example.com/img.png").unwrap(),
629 };
630 assert_eq!(block, block.clone());
631 }
632
633 #[test]
634 fn test_image_source_from_url_valid() {
635 let src = ImageSource::from_url("https://example.com/img.png");
636 assert!(src.is_ok());
637 let url = url::Url::parse("https://example.com/img.png").unwrap();
638 assert_eq!(src.unwrap(), ImageSource::Url(url));
639 }
640
641 #[test]
642 fn test_image_source_from_url_normalizes() {
643 let src = ImageSource::from_url("HTTP://EXAMPLE.COM").unwrap();
645 assert!(matches!(
646 &src,
647 ImageSource::Url(u) if u.as_str() == "http://example.com/"
648 ));
649 }
650
651 #[test]
652 fn test_image_source_from_url_invalid() {
653 let err = ImageSource::from_url("not a url");
654 assert!(err.is_err());
655 let _parse_err: url::ParseError = err.unwrap_err();
656
657 assert!(ImageSource::from_url("").is_err());
658 }
659
660 #[test]
661 fn test_content_block_reasoning() {
662 let block = ContentBlock::Reasoning {
663 content: "thinking step by step".into(),
664 };
665 assert_eq!(block, block.clone());
666 }
667
668 #[test]
669 fn test_tool_call_json_arguments() {
670 let call = ToolCall {
671 id: "tc_1".into(),
672 name: "search".into(),
673 arguments: serde_json::json!({
674 "query": "rust async",
675 "filters": {"lang": "en", "limit": 10}
676 }),
677 };
678 assert_eq!(call, call.clone());
679 }
680
681 #[test]
682 fn test_tool_result_error_flag() {
683 let ok = ToolResult {
684 tool_call_id: "1".into(),
685 content: "result".into(),
686 is_error: false,
687 };
688 let err = ToolResult {
689 tool_call_id: "1".into(),
690 content: "result".into(),
691 is_error: true,
692 };
693 assert_ne!(ok, err);
694 }
695
696 #[test]
699 fn test_chat_response_metadata() {
700 let mut metadata = HashMap::new();
701 metadata.insert("cost".into(), serde_json::json!({"usd": 0.01}));
702 let resp = ChatResponse {
703 content: vec![ContentBlock::Text("hi".into())],
704 usage: Usage::default(),
705 stop_reason: StopReason::EndTurn,
706 model: "test-model".into(),
707 metadata,
708 };
709 assert!(resp.metadata.contains_key("cost"));
710 }
711
712 #[test]
713 fn test_chat_response_serde_roundtrip() {
714 let resp = ChatResponse {
715 content: vec![ContentBlock::Text("hi".into())],
716 usage: Usage::default(),
717 stop_reason: StopReason::EndTurn,
718 model: "test-model".into(),
719 metadata: HashMap::new(),
720 };
721 let json = serde_json::to_string(&resp).unwrap();
722 let back: ChatResponse = serde_json::from_str(&json).unwrap();
723 assert_eq!(resp, back);
724 }
725
726 #[test]
727 fn test_chat_response_empty_content() {
728 let resp = ChatResponse {
729 content: vec![],
730 usage: Usage::default(),
731 stop_reason: StopReason::EndTurn,
732 model: "test".into(),
733 metadata: HashMap::new(),
734 };
735 assert!(resp.content.is_empty());
736 }
737
738 #[test]
741 fn test_stop_reason_all_variants() {
742 let variants = [
743 StopReason::EndTurn,
744 StopReason::ToolUse,
745 StopReason::MaxTokens,
746 StopReason::StopSequence,
747 ];
748 for v in &variants {
749 assert_eq!(*v, *v);
750 }
751 }
752
753 #[test]
754 fn test_stop_reason_serde_roundtrip() {
755 let sr = StopReason::MaxTokens;
756 let json = serde_json::to_string(&sr).unwrap();
757 let back: StopReason = serde_json::from_str(&json).unwrap();
758 assert_eq!(sr, back);
759 }
760
761 #[test]
762 fn test_stop_reason_eq_hash() {
763 use std::collections::HashMap;
764 let mut map = HashMap::new();
765 map.insert(StopReason::EndTurn, "end");
766 map.insert(StopReason::ToolUse, "tool");
767 assert_eq!(map[&StopReason::EndTurn], "end");
768 assert_eq!(map[&StopReason::ToolUse], "tool");
769 }
770
771 #[test]
774 fn test_chat_role_display() {
775 assert_eq!(ChatRole::System.to_string(), "system");
776 assert_eq!(ChatRole::User.to_string(), "user");
777 assert_eq!(ChatRole::Assistant.to_string(), "assistant");
778 assert_eq!(ChatRole::Tool.to_string(), "tool");
779 }
780
781 #[test]
782 fn test_stop_reason_display() {
783 assert_eq!(StopReason::EndTurn.to_string(), "end_turn");
784 assert_eq!(StopReason::ToolUse.to_string(), "tool_use");
785 assert_eq!(StopReason::MaxTokens.to_string(), "max_tokens");
786 assert_eq!(StopReason::StopSequence.to_string(), "stop_sequence");
787 }
788
789 #[test]
792 fn test_chat_response_text_returns_first() {
793 let resp = ChatResponse {
794 content: vec![
795 ContentBlock::Reasoning {
796 content: "thinking...".into(),
797 },
798 ContentBlock::Text("first".into()),
799 ContentBlock::Text("second".into()),
800 ],
801 usage: Usage::default(),
802 stop_reason: StopReason::EndTurn,
803 model: "test".into(),
804 metadata: HashMap::new(),
805 };
806 assert_eq!(resp.text(), Some("first"));
807 }
808
809 #[test]
810 fn test_chat_response_text_none_when_no_text_blocks() {
811 let resp = ChatResponse {
812 content: vec![ContentBlock::Reasoning {
813 content: "thinking".into(),
814 }],
815 usage: Usage::default(),
816 stop_reason: StopReason::EndTurn,
817 model: "test".into(),
818 metadata: HashMap::new(),
819 };
820 assert_eq!(resp.text(), None);
821 }
822
823 #[test]
824 fn test_chat_response_text_none_when_empty() {
825 let resp = ChatResponse {
826 content: vec![],
827 usage: Usage::default(),
828 stop_reason: StopReason::EndTurn,
829 model: "test".into(),
830 metadata: HashMap::new(),
831 };
832 assert_eq!(resp.text(), None);
833 }
834
835 #[test]
838 fn test_chat_response_tool_calls() {
839 let resp = ChatResponse {
840 content: vec![
841 ContentBlock::Text("Let me search.".into()),
842 ContentBlock::ToolCall(ToolCall {
843 id: "1".into(),
844 name: "search".into(),
845 arguments: serde_json::json!({"q": "rust"}),
846 }),
847 ContentBlock::ToolCall(ToolCall {
848 id: "2".into(),
849 name: "calc".into(),
850 arguments: serde_json::json!({"expr": "2+2"}),
851 }),
852 ],
853 usage: Usage::default(),
854 stop_reason: StopReason::ToolUse,
855 model: "test".into(),
856 metadata: HashMap::new(),
857 };
858 let calls = resp.tool_calls();
859 assert_eq!(calls.len(), 2);
860 assert_eq!(calls[0].name, "search");
861 assert_eq!(calls[1].name, "calc");
862 }
863
864 #[test]
865 fn test_chat_response_tool_calls_empty_when_text_only() {
866 let resp = ChatResponse {
867 content: vec![ContentBlock::Text("hello".into())],
868 usage: Usage::default(),
869 stop_reason: StopReason::EndTurn,
870 model: "test".into(),
871 metadata: HashMap::new(),
872 };
873 assert!(resp.tool_calls().is_empty());
874 }
875
876 #[test]
879 fn test_message_is_empty() {
880 let empty = ChatMessage {
881 role: ChatRole::User,
882 content: vec![],
883 };
884 assert!(empty.is_empty());
885 assert!(!ChatMessage::user("hi").is_empty());
886 }
887
888 #[test]
891 fn test_content_block_serde_text() {
892 let block = ContentBlock::Text("hello".into());
893 let val = serde_json::to_value(&block).unwrap();
894 assert_eq!(val, serde_json::json!({"text": "hello"}));
895 let back: ContentBlock = serde_json::from_value(val).unwrap();
896 assert_eq!(back, block);
897 }
898
899 #[test]
900 fn test_content_block_serde_image() {
901 let block = ContentBlock::Image {
902 media_type: "image/png".into(),
903 data: ImageSource::Base64("abc".into()),
904 };
905 let val = serde_json::to_value(&block).unwrap();
906 assert_eq!(
907 val,
908 serde_json::json!({"image": {"media_type": "image/png", "data": {"Base64": "abc"}}})
909 );
910 let back: ContentBlock = serde_json::from_value(val).unwrap();
911 assert_eq!(back, block);
912 }
913
914 #[test]
915 fn test_content_block_serde_tool_call() {
916 let block = ContentBlock::ToolCall(ToolCall {
917 id: "tc_1".into(),
918 name: "search".into(),
919 arguments: serde_json::json!({"q": "rust"}),
920 });
921 let val = serde_json::to_value(&block).unwrap();
922 assert_eq!(
923 val,
924 serde_json::json!({"tool_call": {"id": "tc_1", "name": "search", "arguments": {"q": "rust"}}})
925 );
926 let back: ContentBlock = serde_json::from_value(val).unwrap();
927 assert_eq!(back, block);
928 }
929
930 #[test]
931 fn test_content_block_serde_tool_result() {
932 let block = ContentBlock::ToolResult(ToolResult {
933 tool_call_id: "tc_1".into(),
934 content: "42".into(),
935 is_error: false,
936 });
937 let val = serde_json::to_value(&block).unwrap();
938 assert_eq!(
939 val,
940 serde_json::json!({"tool_result": {"tool_call_id": "tc_1", "content": "42", "is_error": false}})
941 );
942 let back: ContentBlock = serde_json::from_value(val).unwrap();
943 assert_eq!(back, block);
944 }
945
946 #[test]
947 fn test_content_block_serde_reasoning() {
948 let block = ContentBlock::Reasoning {
949 content: "thinking".into(),
950 };
951 let val = serde_json::to_value(&block).unwrap();
952 assert_eq!(
953 val,
954 serde_json::json!({"reasoning": {"content": "thinking"}})
955 );
956 let back: ContentBlock = serde_json::from_value(val).unwrap();
957 assert_eq!(back, block);
958 }
959
960 #[test]
963 fn test_user_constructor_produces_text_only() {
964 let msg = ChatMessage::user("hello");
965 assert_eq!(msg.role, ChatRole::User);
966 assert!(
967 msg.content
968 .iter()
969 .all(|b| matches!(b, ContentBlock::Text(_)))
970 );
971 }
972
973 #[test]
974 fn test_assistant_constructor_produces_text_only() {
975 let msg = ChatMessage::assistant("hi");
976 assert_eq!(msg.role, ChatRole::Assistant);
977 assert!(
978 msg.content
979 .iter()
980 .all(|b| matches!(b, ContentBlock::Text(_)))
981 );
982 }
983
984 #[test]
985 fn test_system_constructor_produces_text_only() {
986 let msg = ChatMessage::system("be nice");
987 assert_eq!(msg.role, ChatRole::System);
988 assert!(
989 msg.content
990 .iter()
991 .all(|b| matches!(b, ContentBlock::Text(_)))
992 );
993 }
994
995 #[test]
996 fn test_tool_result_constructor_produces_tool_result_only() {
997 let msg = ChatMessage::tool_result("tc_1", "42");
998 assert_eq!(msg.role, ChatRole::Tool);
999 assert!(
1000 msg.content
1001 .iter()
1002 .all(|b| matches!(b, ContentBlock::ToolResult(_)))
1003 );
1004 }
1005
1006 #[test]
1007 fn test_tool_error_constructor_produces_tool_result_only() {
1008 let msg = ChatMessage::tool_error("tc_1", "boom");
1009 assert_eq!(msg.role, ChatRole::Tool);
1010 assert!(
1011 msg.content
1012 .iter()
1013 .all(|b| matches!(b, ContentBlock::ToolResult(r) if r.is_error))
1014 );
1015 }
1016
1017 #[test]
1018 fn test_assistant_tool_calls_is_valid_combination() {
1019 let msg = ChatMessage {
1021 role: ChatRole::Assistant,
1022 content: vec![
1023 ContentBlock::Text("Let me search for that.".into()),
1024 ContentBlock::ToolCall(ToolCall {
1025 id: "1".into(),
1026 name: "search".into(),
1027 arguments: serde_json::json!({"q": "rust"}),
1028 }),
1029 ],
1030 };
1031 assert_eq!(msg.role, ChatRole::Assistant);
1032 assert_eq!(msg.content.len(), 2);
1033 }
1034
1035 #[test]
1036 fn test_user_with_image_is_valid_combination() {
1037 let msg = ChatMessage {
1039 role: ChatRole::User,
1040 content: vec![
1041 ContentBlock::Text("What's this?".into()),
1042 ContentBlock::Image {
1043 media_type: "image/png".into(),
1044 data: ImageSource::Base64("...".into()),
1045 },
1046 ],
1047 };
1048 assert_eq!(msg.role, ChatRole::User);
1049 assert_eq!(msg.content.len(), 2);
1050 }
1051
1052 #[test]
1053 fn test_assistant_with_reasoning_is_valid_combination() {
1054 let msg = ChatMessage {
1056 role: ChatRole::Assistant,
1057 content: vec![
1058 ContentBlock::Reasoning {
1059 content: "step 1: think about it".into(),
1060 },
1061 ContentBlock::Text("The answer is 42.".into()),
1062 ],
1063 };
1064 assert_eq!(msg.role, ChatRole::Assistant);
1065 assert_eq!(msg.content.len(), 2);
1066 }
1067
1068 #[test]
1071 fn test_chat_message_to_json() {
1072 let msg = ChatMessage::user("Hello, world!");
1073 let json = msg.to_json().unwrap();
1074 assert_eq!(json["role"], "User");
1075 assert_eq!(json["content"][0]["text"], "Hello, world!");
1076 }
1077
1078 #[test]
1079 fn test_chat_message_from_json() {
1080 let json = serde_json::json!({
1081 "role": "Assistant",
1082 "content": [{"text": "Hello!"}]
1083 });
1084 let msg = ChatMessage::from_json(&json).unwrap();
1085 assert_eq!(msg.role, ChatRole::Assistant);
1086 assert!(matches!(&msg.content[0], ContentBlock::Text(t) if t == "Hello!"));
1087 }
1088
1089 #[test]
1090 fn test_chat_message_json_roundtrip() {
1091 let original = ChatMessage {
1092 role: ChatRole::User,
1093 content: vec![
1094 ContentBlock::Text("What's this?".into()),
1095 ContentBlock::Image {
1096 media_type: "image/png".into(),
1097 data: ImageSource::Base64("abc123".into()),
1098 },
1099 ],
1100 };
1101 let json = original.to_json().unwrap();
1102 let restored = ChatMessage::from_json(&json).unwrap();
1103 assert_eq!(original, restored);
1104 }
1105
1106 #[test]
1107 fn test_chat_message_json_roundtrip_with_tool_result() {
1108 let original = ChatMessage::tool_result("tc_1", "success");
1109 let json = original.to_json().unwrap();
1110 let restored = ChatMessage::from_json(&json).unwrap();
1111 assert_eq!(original, restored);
1112 }
1113
1114 }