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