1use crate::Api;
4use serde::{Deserialize, Serialize};
5use serde_json::Value as JsonValue;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct TextContent {
10 #[serde(rename = "type")]
12 pub content_type: TextContentType,
13 pub text: String,
15 #[serde(skip_serializing_if = "Option::is_none")]
17 pub text_signature: Option<String>,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename = "text")]
22pub enum TextContentType {
24 Text,
26}
27
28impl TextContent {
29 pub fn new(text: impl Into<String>) -> Self {
31 Self {
32 content_type: TextContentType::Text,
33 text: text.into(),
34 text_signature: None,
35 }
36 }
37
38 pub fn with_signature(text: impl Into<String>, signature: impl Into<String>) -> Self {
40 Self {
41 content_type: TextContentType::Text,
42 text: text.into(),
43 text_signature: Some(signature.into()),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ThinkingContent {
51 #[serde(rename = "type")]
53 pub content_type: ThinkingContentType,
54 pub thinking: String,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub thinking_signature: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub redacted: Option<bool>,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename = "thinking")]
66pub enum ThinkingContentType {
68 Thinking,
70}
71
72impl ThinkingContent {
73 pub fn new(thinking: impl Into<String>) -> Self {
75 Self {
76 content_type: ThinkingContentType::Thinking,
77 thinking: thinking.into(),
78 thinking_signature: None,
79 redacted: None,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ImageContent {
87 #[serde(rename = "type")]
89 pub content_type: ImageContentType,
90 pub data: String,
92 pub mime_type: String,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename = "image")]
98pub enum ImageContentType {
100 Image,
102}
103
104impl ImageContent {
105 pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
107 Self {
108 content_type: ImageContentType::Image,
109 data: data.into(),
110 mime_type: mime_type.into(),
111 }
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct ToolCall {
118 #[serde(rename = "type")]
120 pub content_type: ToolCallType,
121 pub id: String,
123 pub name: String,
125 pub arguments: JsonValue,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub thought_signature: Option<String>,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename = "toolCall")]
134pub enum ToolCallType {
136 ToolCall,
138}
139
140impl ToolCall {
141 pub fn new(id: impl Into<String>, name: impl Into<String>, arguments: JsonValue) -> Self {
143 Self {
144 content_type: ToolCallType::ToolCall,
145 id: id.into(),
146 name: name.into(),
147 arguments,
148 thought_signature: None,
149 }
150 }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(untagged)]
159pub enum ContentBlock {
160 Text(TextContent),
162 Thinking(ThinkingContent),
164 Image(ImageContent),
166 ToolCall(ToolCall),
168 Unknown(JsonValue),
170}
171
172impl ContentBlock {
173 pub fn as_text(&self) -> Option<&str> {
175 match self {
176 ContentBlock::Text(t) => Some(&t.text),
177 _ => None,
178 }
179 }
180
181 pub fn as_tool_call(&self) -> Option<&ToolCall> {
183 match self {
184 ContentBlock::ToolCall(t) => Some(t),
185 _ => None,
186 }
187 }
188
189 pub fn as_thinking(&self) -> Option<&ThinkingContent> {
191 match self {
192 ContentBlock::Thinking(t) => Some(t),
193 _ => None,
194 }
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct UserMessage {
201 pub role: UserRole,
203 pub content: MessageContent,
205 pub timestamp: i64,
207 #[serde(default = "default_visible")]
213 pub visible: bool,
214}
215
216fn default_visible() -> bool {
217 true
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename = "user")]
222pub enum UserRole {
224 #[serde(rename = "user")]
225 User,
227}
228
229impl UserMessage {
230 pub fn new(content: impl Into<MessageContent>) -> Self {
232 Self {
233 role: UserRole::User,
234 content: content.into(),
235 timestamp: chrono::Utc::now().timestamp_millis(),
236 visible: true,
237 }
238 }
239
240 pub fn hidden(content: impl Into<MessageContent>) -> Self {
242 Self {
243 role: UserRole::User,
244 content: content.into(),
245 timestamp: chrono::Utc::now().timestamp_millis(),
246 visible: false,
247 }
248 }
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct AssistantMessage {
254 pub role: AssistantRole,
256 pub content: Vec<ContentBlock>,
258 pub api: super::Api,
260 pub provider: String,
262 pub model: String,
264 pub usage: super::Usage,
266 pub stop_reason: super::StopReason,
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub error_message: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub response_id: Option<String>,
274 pub timestamp: i64,
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(rename = "assistant")]
280pub enum AssistantRole {
282 #[serde(rename = "assistant")]
283 Assistant,
285}
286
287impl AssistantMessage {
288 pub fn new(api: super::Api, provider: impl Into<String>, model: impl Into<String>) -> Self {
290 Self {
291 role: AssistantRole::Assistant,
292 content: Vec::new(),
293 api,
294 provider: provider.into(),
295 model: model.into(),
296 usage: super::Usage::default(),
297 stop_reason: super::StopReason::Stop,
298 error_message: None,
299 response_id: None,
300 timestamp: chrono::Utc::now().timestamp_millis(),
301 }
302 }
303
304 pub fn text_content(&self) -> String {
306 let estimated_len: usize = self
308 .content
309 .iter()
310 .map(|b| b.as_text().map(|t| t.len()).unwrap_or(0))
311 .sum();
312 let mut result = String::with_capacity(estimated_len);
313 for block in &self.content {
314 if let Some(text) = block.as_text() {
315 result.push_str(text);
316 }
317 }
318 result
319 }
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct ToolResultMessage {
325 pub role: ToolResultRole,
327 pub tool_call_id: String,
329 pub tool_name: String,
331 pub content: Vec<ContentBlock>,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub details: Option<JsonValue>,
336 #[serde(default)]
338 pub is_error: bool,
339 pub timestamp: i64,
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
344#[serde(rename = "toolResult")]
345pub enum ToolResultRole {
347 #[serde(rename = "toolResult")]
348 ToolResult,
350}
351
352impl ToolResultMessage {
353 pub fn new(
355 tool_call_id: impl Into<String>,
356 tool_name: impl Into<String>,
357 content: Vec<ContentBlock>,
358 ) -> Self {
359 Self {
360 role: ToolResultRole::ToolResult,
361 tool_call_id: tool_call_id.into(),
362 tool_name: tool_name.into(),
363 content,
364 details: None,
365 is_error: false,
366 timestamp: chrono::Utc::now().timestamp_millis(),
367 }
368 }
369
370 pub fn error(
372 tool_call_id: impl Into<String>,
373 tool_name: impl Into<String>,
374 error: impl Into<String>,
375 ) -> Self {
376 Self {
377 role: ToolResultRole::ToolResult,
378 tool_call_id: tool_call_id.into(),
379 tool_name: tool_name.into(),
380 content: vec![ContentBlock::Text(TextContent::new(error))],
381 details: None,
382 is_error: true,
383 timestamp: chrono::Utc::now().timestamp_millis(),
384 }
385 }
386
387 pub fn text_content(&self) -> Result<String, crate::error::ProviderError> {
389 let estimated_len: usize = self
391 .content
392 .iter()
393 .map(|b| match b {
394 ContentBlock::Text(t) => t.text.len() + 1,
395 ContentBlock::Image(_) => 7,
396 ContentBlock::Thinking(t) => t.thinking.len() + 12,
397 ContentBlock::ToolCall(tc) => tc.name.len() + 8,
398 ContentBlock::Unknown(_) => 0,
399 })
400 .sum();
401 let mut result = String::with_capacity(estimated_len);
402 for block in &self.content {
403 match block {
404 ContentBlock::Text(t) => {
405 result.push_str(&t.text);
406 result.push('\n');
407 }
408 ContentBlock::Image(_) => {
409 result.push_str("[Image]\n");
410 }
411 ContentBlock::Thinking(t) => {
412 result.push_str(&format!("[Thinking: {}]\n", t.thinking));
413 }
414 ContentBlock::ToolCall(tc) => {
415 result.push_str(&format!("[Tool: {}]\n", tc.name));
416 }
417 ContentBlock::Unknown(_) => {
418 }
420 }
421 }
422 Ok(result.trim().to_string())
423 }
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
431#[serde(tag = "role", rename_all = "camelCase")]
432pub enum Message {
433 User(UserMessage),
435 Assistant(AssistantMessage),
437 ToolResult(ToolResultMessage),
439}
440
441impl Message {
442 pub fn user(content: impl Into<MessageContent>) -> Self {
444 Message::User(UserMessage::new(content))
445 }
446
447 pub fn assistant(content: Vec<ContentBlock>) -> Self {
449 Message::Assistant(AssistantMessage {
450 role: AssistantRole::Assistant,
451 content,
452 api: Api::AnthropicMessages,
453 provider: "assistant".to_string(),
454 model: "assistant".to_string(),
455 usage: super::Usage::default(),
456 stop_reason: super::StopReason::Stop,
457 error_message: None,
458 response_id: None,
459 timestamp: chrono::Utc::now().timestamp_millis(),
460 })
461 }
462
463 pub fn tool_result(
465 tool_call_id: impl Into<String>,
466 tool_name: impl Into<String>,
467 content: Vec<ContentBlock>,
468 ) -> Self {
469 Message::ToolResult(ToolResultMessage::new(tool_call_id, tool_name, content))
470 }
471
472 pub fn timestamp(&self) -> i64 {
474 match self {
475 Message::User(m) => m.timestamp,
476 Message::Assistant(m) => m.timestamp,
477 Message::ToolResult(m) => m.timestamp,
478 }
479 }
480
481 pub fn text_content(&self) -> Result<String, crate::error::ProviderError> {
483 match self {
484 Message::User(m) => match &m.content {
485 MessageContent::Text(s) => Ok(s.clone()),
486 MessageContent::Blocks(blocks) => {
487 let estimated_len: usize = blocks
488 .iter()
489 .map(|b| match b {
490 ContentBlock::Text(t) => t.text.len() + 1,
491 ContentBlock::Image(_) => 8,
492 ContentBlock::Thinking(t) => t.thinking.len() + 1,
493 ContentBlock::ToolCall(_) => 12,
494 ContentBlock::Unknown(_) => 10,
495 })
496 .sum();
497 let mut result = String::with_capacity(estimated_len);
498 for block in blocks {
499 match block {
500 ContentBlock::Text(t) => {
501 result.push_str(&t.text);
502 result.push('\n');
503 }
504 ContentBlock::Image(_) => {
505 result.push_str("[Image]\n");
506 }
507 ContentBlock::Thinking(t) => {
508 result.push_str(&t.thinking);
509 result.push('\n');
510 }
511 ContentBlock::ToolCall(_) => {
512 result.push_str("[Tool Call]\n");
513 }
514 ContentBlock::Unknown(_) => {
515 result.push_str("[Unknown]\n");
516 }
517 }
518 }
519 Ok(result.trim().to_string())
520 }
521 },
522 Message::Assistant(m) => Ok(m.text_content()),
523 Message::ToolResult(m) => m.text_content(),
524 }
525 }
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize)]
530#[serde(untagged)]
531pub enum MessageContent {
532 Text(String),
534 Blocks(Vec<ContentBlock>),
536}
537
538impl MessageContent {
539 pub fn is_text(&self) -> bool {
541 matches!(self, MessageContent::Text(_))
542 }
543
544 pub fn as_str(&self) -> Option<&str> {
546 match self {
547 MessageContent::Text(s) => Some(s),
548 MessageContent::Blocks(_) => None,
549 }
550 }
551}
552
553impl From<String> for MessageContent {
555 fn from(text: String) -> Self {
556 MessageContent::Text(text)
557 }
558}
559
560impl From<&str> for MessageContent {
561 fn from(text: &str) -> Self {
562 MessageContent::Text(text.to_string())
563 }
564}
565
566impl From<Vec<ContentBlock>> for MessageContent {
567 fn from(blocks: Vec<ContentBlock>) -> Self {
568 MessageContent::Blocks(blocks)
569 }
570}
571
572impl From<TextContent> for MessageContent {
573 fn from(block: TextContent) -> Self {
574 MessageContent::Blocks(vec![ContentBlock::Text(block)])
575 }
576}
577
578impl From<ContentBlock> for MessageContent {
579 fn from(block: ContentBlock) -> Self {
580 MessageContent::Blocks(vec![block])
581 }
582}
583
584pub fn transform_for_provider(
596 messages: &[Message],
597 _from_api: &super::Api,
598 to_api: &super::Api,
599) -> Vec<Message> {
600 messages
601 .iter()
602 .map(|msg| match msg {
603 Message::Assistant(a) => {
604 let mut new_msg = AssistantMessage::new(*to_api, &a.provider, &a.model);
605 new_msg.content = transform_content_blocks(&a.content, to_api);
606 new_msg.usage = a.usage.clone();
607 new_msg.stop_reason = a.stop_reason;
608 new_msg.error_message = a.error_message.clone();
609 new_msg.response_id = a.response_id.clone();
610 new_msg.timestamp = a.timestamp;
611 Message::Assistant(new_msg)
612 }
613 Message::User(u) => Message::User(u.clone()),
614 Message::ToolResult(t) => Message::ToolResult(t.clone()),
615 })
616 .collect()
617}
618
619fn transform_content_blocks(blocks: &[ContentBlock], to_api: &super::Api) -> Vec<ContentBlock> {
624 match to_api {
625 super::Api::AnthropicMessages => blocks.to_vec(),
627
628 _ => {
630 let mut transformed = Vec::with_capacity(blocks.len());
631 for block in blocks {
632 match block {
633 ContentBlock::Thinking(t) => {
634 let text = format!("<thinking>\n{}\n</thinking>", t.thinking);
636 transformed.push(ContentBlock::Text(TextContent::new(text)));
637 }
638 ContentBlock::Text(t) => {
639 transformed.push(ContentBlock::Text(t.clone()));
640 }
641 ContentBlock::ToolCall(tc) => {
642 transformed.push(ContentBlock::ToolCall(tc.clone()));
643 }
644 ContentBlock::Image(img) => {
645 transformed.push(ContentBlock::Image(img.clone()));
646 }
647 ContentBlock::Unknown(v) => {
648 if let Some(text) = v.get("text").and_then(|t| t.as_str()) {
650 transformed.push(ContentBlock::Text(TextContent::new(text)));
651 }
652 }
654 }
655 }
656 merge_adjacent_text_blocks(transformed)
658 }
659 }
660}
661
662fn merge_adjacent_text_blocks(blocks: Vec<ContentBlock>) -> Vec<ContentBlock> {
664 let mut result = Vec::with_capacity(blocks.len());
665 let estimated_len = blocks
666 .iter()
667 .map(|b| match b {
668 ContentBlock::Text(t) => t.text.len() + 1,
669 _ => 0,
670 })
671 .sum::<usize>();
672 let mut pending_text = String::with_capacity(estimated_len.max(256));
673
674 for block in blocks {
675 match block {
676 ContentBlock::Text(t) => {
677 if !pending_text.is_empty() {
678 pending_text.push('\n');
679 }
680 pending_text.push_str(&t.text);
681 }
682 other => {
683 if !pending_text.is_empty() {
684 result.push(ContentBlock::Text(TextContent::new(std::mem::take(
685 &mut pending_text,
686 ))));
687 }
688 result.push(other);
689 }
690 }
691 }
692
693 if !pending_text.is_empty() {
694 result.push(ContentBlock::Text(TextContent::new(pending_text)));
695 }
696
697 result
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703 use crate::types::{Api, StopReason, Usage};
704
705 #[test]
706 fn user_message_new_defaults_visible_true() {
707 let m = UserMessage::new("hi");
708 assert!(m.visible);
709 }
710
711 #[test]
712 fn user_message_hidden_sets_visible_false() {
713 let m = UserMessage::hidden("system nudge");
714 assert!(!m.visible);
715 }
716
717 #[test]
718 fn user_message_deserializes_missing_visible_as_true() {
719 let json = r#"{"role":"user","content":"hi","timestamp":0}"#;
720 let m: UserMessage = serde_json::from_str(json).unwrap();
721 assert!(m.visible); }
723
724 #[test]
727 fn text_content_roundtrip() {
728 let block = ContentBlock::Text(TextContent::new("hello world"));
729 let json = serde_json::to_string(&block).unwrap();
730 let back: ContentBlock = serde_json::from_str(&json).unwrap();
731 assert_eq!(back.as_text(), Some("hello world"));
732 }
733
734 #[test]
735 fn thinking_content_roundtrip() {
736 let block = ContentBlock::Thinking(ThinkingContent::new("inner thoughts"));
737 let json = serde_json::to_string(&block).unwrap();
738 let back: ContentBlock = serde_json::from_str(&json).unwrap();
739 assert!(back.as_thinking().is_some());
740 assert_eq!(back.as_thinking().unwrap().thinking, "inner thoughts");
741 }
742
743 #[test]
744 fn image_content_roundtrip() {
745 let block = ContentBlock::Image(ImageContent::new("base64data==", "image/png"));
746 let json = serde_json::to_string(&block).unwrap();
747 let back: ContentBlock = serde_json::from_str(&json).unwrap();
748 match back {
749 ContentBlock::Image(img) => {
750 assert_eq!(img.data, "base64data==");
751 assert_eq!(img.mime_type, "image/png");
752 }
753 _ => panic!("Expected Image block"),
754 }
755 }
756
757 #[test]
758 fn tool_call_roundtrip() {
759 let block = ContentBlock::ToolCall(ToolCall::new(
760 "call_123",
761 "read_file",
762 serde_json::json!({"path": "/foo.rs"}),
763 ));
764 let json = serde_json::to_string(&block).unwrap();
765 let back: ContentBlock = serde_json::from_str(&json).unwrap();
766 let tc = back.as_tool_call().unwrap();
767 assert_eq!(tc.id, "call_123");
768 assert_eq!(tc.name, "read_file");
769 assert_eq!(tc.arguments["path"], "/foo.rs");
770 }
771
772 #[test]
775 fn user_message_inner_roundtrip() {
776 let msg = UserMessage::new("Hello, assistant!");
777 let json = serde_json::to_string(&msg).unwrap();
778 let back: UserMessage = serde_json::from_str(&json).unwrap();
779 assert!(matches!(&back.content, MessageContent::Text(s) if s == "Hello, assistant!"));
780 assert_eq!(back.role, UserRole::User);
781 }
782
783 #[test]
784 fn user_message_blocks_roundtrip() {
785 let blocks = vec![
786 ContentBlock::Text(TextContent::new("part one")),
787 ContentBlock::Text(TextContent::new("part two")),
788 ];
789 let msg = UserMessage::new(MessageContent::Blocks(blocks));
790 let json = serde_json::to_string(&msg).unwrap();
791 let back: UserMessage = serde_json::from_str(&json).unwrap();
792 match &back.content {
793 MessageContent::Blocks(blocks) => assert_eq!(blocks.len(), 2),
794 _ => panic!("Expected Blocks"),
795 }
796 }
797
798 #[test]
799 fn assistant_message_inner_roundtrip() {
800 let mut msg = AssistantMessage::new(Api::AnthropicMessages, "anthropic", "claude-3");
801 msg.content
802 .push(ContentBlock::Text(TextContent::new("Hi!")));
803 msg.content
804 .push(ContentBlock::Thinking(ThinkingContent::new("hmm")));
805 msg.usage = Usage {
806 input: 100,
807 output: 50,
808 ..Default::default()
809 };
810 msg.stop_reason = StopReason::Stop;
811 msg.response_id = Some("resp_abc".to_string());
812
813 let json = serde_json::to_string(&msg).unwrap();
814 let back: AssistantMessage = serde_json::from_str(&json).unwrap();
815
816 assert_eq!(back.content.len(), 2);
817 assert_eq!(back.usage.input, 100);
818 assert_eq!(back.response_id.as_deref(), Some("resp_abc"));
819 assert_eq!(back.role, AssistantRole::Assistant);
820 }
821
822 #[test]
823 fn tool_result_message_inner_roundtrip() {
824 let msg = ToolResultMessage::new(
825 "call_1",
826 "bash",
827 vec![ContentBlock::Text(TextContent::new("output"))],
828 );
829 let json = serde_json::to_string(&msg).unwrap();
830 let back: ToolResultMessage = serde_json::from_str(&json).unwrap();
831 assert_eq!(back.tool_call_id, "call_1");
832 assert_eq!(back.tool_name, "bash");
833 assert!(!back.is_error);
834 assert_eq!(back.role, ToolResultRole::ToolResult);
835 }
836
837 #[test]
838 fn message_construction_and_accessors() {
839 let user = Message::user("test");
840 assert!(matches!(user, Message::User(_)));
841
842 let ts = user.timestamp();
843 assert!(ts > 0);
844 }
845
846 #[test]
847 fn message_content_roundtrip() {
848 let mc = MessageContent::Text("hello".to_string());
850 let json = serde_json::to_string(&mc).unwrap();
851 let back: MessageContent = serde_json::from_str(&json).unwrap();
852 assert_eq!(back.as_str(), Some("hello"));
853
854 let mc = MessageContent::Blocks(vec![ContentBlock::Text(TextContent::new("block"))]);
856 let json = serde_json::to_string(&mc).unwrap();
857 let back: MessageContent = serde_json::from_str(&json).unwrap();
858 assert!(!back.is_text());
859 }
860
861 #[test]
864 fn user_text_content() {
865 let msg = Message::user("Hello!");
866 assert_eq!(msg.text_content().unwrap(), "Hello!");
867 }
868
869 #[test]
870 fn user_blocks_text_content() {
871 let blocks = vec![
872 ContentBlock::Text(TextContent::new("line 1")),
873 ContentBlock::Text(TextContent::new("line 2")),
874 ];
875 let msg = Message::User(UserMessage::new(MessageContent::Blocks(blocks)));
876 assert_eq!(msg.text_content().unwrap(), "line 1\nline 2");
877 }
878
879 #[test]
880 fn assistant_text_content() {
881 let mut a = AssistantMessage::new(Api::OpenAiCompletions, "openai", "gpt-4");
882 a.content
883 .push(ContentBlock::Text(TextContent::new("part A")));
884 a.content
885 .push(ContentBlock::Thinking(ThinkingContent::new("hidden")));
886 a.content
887 .push(ContentBlock::Text(TextContent::new("part B")));
888
889 let msg = Message::Assistant(a);
890 let text = msg.text_content().unwrap();
891 assert_eq!(text, "part Apart B");
893 }
894
895 #[test]
896 fn tool_result_text_content() {
897 let msg = ToolResultMessage::new(
898 "call_1",
899 "read",
900 vec![
901 ContentBlock::Text(TextContent::new("file contents")),
902 ContentBlock::Image(ImageContent::new("aaa", "image/png")),
903 ],
904 );
905 let text = msg.text_content().unwrap();
906 assert!(text.contains("file contents"));
907 assert!(text.contains("[Image]"));
908 }
909
910 #[test]
913 fn transform_openai_to_anthropic_keeps_thinking() {
914 let mut a = AssistantMessage::new(Api::OpenAiCompletions, "openai", "gpt-4");
915 a.content
916 .push(ContentBlock::Text(TextContent::new("Hello")));
917 a.content
918 .push(ContentBlock::Thinking(ThinkingContent::new("pondering")));
919 let messages = vec![Message::Assistant(a)];
920
921 let transformed =
922 transform_for_provider(&messages, &Api::OpenAiCompletions, &Api::AnthropicMessages);
923 match &transformed[0] {
924 Message::Assistant(a) => {
925 assert_eq!(a.content.len(), 2);
927 assert!(matches!(&a.content[1], ContentBlock::Thinking(_)));
928 }
929 _ => panic!("Expected Assistant"),
930 }
931 }
932
933 #[test]
934 fn transform_anthropic_to_openai_converts_thinking() {
935 let mut a = AssistantMessage::new(Api::AnthropicMessages, "anthropic", "claude-3");
936 a.content
937 .push(ContentBlock::Text(TextContent::new("Hello")));
938 a.content
939 .push(ContentBlock::Thinking(ThinkingContent::new("pondering")));
940 let messages = vec![Message::Assistant(a)];
941
942 let transformed =
943 transform_for_provider(&messages, &Api::AnthropicMessages, &Api::OpenAiCompletions);
944 match &transformed[0] {
945 Message::Assistant(a) => {
946 assert!(a.content.iter().all(|b| matches!(b, ContentBlock::Text(_))));
948 let full_text: String = a.content.iter().filter_map(|b| b.as_text()).collect();
949 assert!(full_text.contains("Hello"));
950 assert!(full_text.contains("<thinking>"));
951 assert!(full_text.contains("pondering"));
952 }
953 _ => panic!("Expected Assistant"),
954 }
955 }
956
957 #[test]
958 fn transform_roundtrip_openai_anthropic_openai() {
959 let mut a = AssistantMessage::new(Api::OpenAiCompletions, "openai", "gpt-4");
960 a.content
961 .push(ContentBlock::Text(TextContent::new("Hello")));
962 a.content
963 .push(ContentBlock::Thinking(ThinkingContent::new("pondering")));
964 a.content
965 .push(ContentBlock::Text(TextContent::new("World")));
966 let original = vec![Message::Assistant(a)];
967
968 let step1 =
970 transform_for_provider(&original, &Api::OpenAiCompletions, &Api::AnthropicMessages);
971 let step2 =
973 transform_for_provider(&step1, &Api::AnthropicMessages, &Api::OpenAiCompletions);
974
975 match &step2[0] {
976 Message::Assistant(a) => {
977 let full_text: String = a.content.iter().filter_map(|b| b.as_text()).collect();
978 assert!(full_text.contains("Hello"));
979 assert!(full_text.contains("World"));
980 assert!(full_text.contains("<thinking>"));
981 }
982 _ => panic!("Expected Assistant"),
983 }
984 }
985
986 #[test]
989 fn merge_adjacent_text_blocks_basic() {
990 let blocks = vec![
991 ContentBlock::Text(TextContent::new("a")),
992 ContentBlock::Text(TextContent::new("b")),
993 ContentBlock::Text(TextContent::new("c")),
994 ];
995 let merged = merge_adjacent_text_blocks(blocks);
996 assert_eq!(merged.len(), 1);
997 assert_eq!(merged[0].as_text(), Some("a\nb\nc"));
998 }
999
1000 #[test]
1001 fn merge_adjacent_text_blocks_with_intervening() {
1002 let blocks = vec![
1003 ContentBlock::Text(TextContent::new("a")),
1004 ContentBlock::Text(TextContent::new("b")),
1005 ContentBlock::ToolCall(ToolCall::new("1", "tool", serde_json::json!({}))),
1006 ContentBlock::Text(TextContent::new("c")),
1007 ];
1008 let merged = merge_adjacent_text_blocks(blocks);
1009 assert_eq!(merged.len(), 3); assert_eq!(merged[0].as_text(), Some("a\nb"));
1011 assert!(merged[1].as_tool_call().is_some());
1012 assert_eq!(merged[2].as_text(), Some("c"));
1013 }
1014
1015 #[test]
1016 fn merge_adjacent_text_blocks_empty() {
1017 let blocks: Vec<ContentBlock> = vec![];
1018 let merged = merge_adjacent_text_blocks(blocks);
1019 assert!(merged.is_empty());
1020 }
1021
1022 #[test]
1023 fn message_content_from_conversions() {
1024 let mc: MessageContent = "hello".into();
1025 assert!(mc.is_text());
1026 assert_eq!(mc.as_str(), Some("hello"));
1027
1028 let mc: MessageContent = "world".to_string().into();
1029 assert!(mc.is_text());
1030
1031 let mc: MessageContent = TextContent::new("block").into();
1032 assert!(!mc.is_text());
1033 }
1034}