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, 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}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(rename = "user")]
211pub enum UserRole {
213 #[serde(rename = "user")]
214 User,
216}
217
218impl UserMessage {
219 pub fn new(content: impl Into<MessageContent>) -> Self {
221 Self {
222 role: UserRole::User,
223 content: content.into(),
224 timestamp: chrono::Utc::now().timestamp_millis(),
225 }
226 }
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct AssistantMessage {
232 pub role: AssistantRole,
234 pub content: Vec<ContentBlock>,
236 pub api: super::Api,
238 pub provider: String,
240 pub model: String,
242 pub usage: super::Usage,
244 pub stop_reason: super::StopReason,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub error_message: Option<String>,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub response_id: Option<String>,
252 pub timestamp: i64,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(rename = "assistant")]
258pub enum AssistantRole {
260 #[serde(rename = "assistant")]
261 Assistant,
263}
264
265impl AssistantMessage {
266 pub fn new(api: super::Api, provider: impl Into<String>, model: impl Into<String>) -> Self {
268 Self {
269 role: AssistantRole::Assistant,
270 content: Vec::new(),
271 api,
272 provider: provider.into(),
273 model: model.into(),
274 usage: super::Usage::default(),
275 stop_reason: super::StopReason::Stop,
276 error_message: None,
277 response_id: None,
278 timestamp: chrono::Utc::now().timestamp_millis(),
279 }
280 }
281
282 pub fn text_content(&self) -> String {
284 let estimated_len: usize = self
286 .content
287 .iter()
288 .map(|b| b.as_text().map(|t| t.len()).unwrap_or(0))
289 .sum();
290 let mut result = String::with_capacity(estimated_len);
291 for block in &self.content {
292 if let Some(text) = block.as_text() {
293 result.push_str(text);
294 }
295 }
296 result
297 }
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct ToolResultMessage {
303 pub role: ToolResultRole,
305 pub tool_call_id: String,
307 pub tool_name: String,
309 pub content: Vec<ContentBlock>,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub details: Option<JsonValue>,
314 #[serde(default)]
316 pub is_error: bool,
317 pub timestamp: i64,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(rename = "toolResult")]
323pub enum ToolResultRole {
325 #[serde(rename = "toolResult")]
326 ToolResult,
328}
329
330impl ToolResultMessage {
331 pub fn new(
333 tool_call_id: impl Into<String>,
334 tool_name: impl Into<String>,
335 content: Vec<ContentBlock>,
336 ) -> Self {
337 Self {
338 role: ToolResultRole::ToolResult,
339 tool_call_id: tool_call_id.into(),
340 tool_name: tool_name.into(),
341 content,
342 details: None,
343 is_error: false,
344 timestamp: chrono::Utc::now().timestamp_millis(),
345 }
346 }
347
348 pub fn error(
350 tool_call_id: impl Into<String>,
351 tool_name: impl Into<String>,
352 error: impl Into<String>,
353 ) -> Self {
354 Self {
355 role: ToolResultRole::ToolResult,
356 tool_call_id: tool_call_id.into(),
357 tool_name: tool_name.into(),
358 content: vec![ContentBlock::Text(TextContent::new(error))],
359 details: None,
360 is_error: true,
361 timestamp: chrono::Utc::now().timestamp_millis(),
362 }
363 }
364
365 pub fn text_content(&self) -> Result<String, crate::error::ProviderError> {
367 let estimated_len: usize = self
369 .content
370 .iter()
371 .map(|b| match b {
372 ContentBlock::Text(t) => t.text.len() + 1,
373 ContentBlock::Image(_) => 7,
374 ContentBlock::Thinking(t) => t.thinking.len() + 12,
375 ContentBlock::ToolCall(tc) => tc.name.len() + 8,
376 ContentBlock::Unknown(_) => 0,
377 })
378 .sum();
379 let mut result = String::with_capacity(estimated_len);
380 for block in &self.content {
381 match block {
382 ContentBlock::Text(t) => {
383 result.push_str(&t.text);
384 result.push('\n');
385 }
386 ContentBlock::Image(_) => {
387 result.push_str("[Image]\n");
388 }
389 ContentBlock::Thinking(t) => {
390 result.push_str(&format!("[Thinking: {}]\n", t.thinking));
391 }
392 ContentBlock::ToolCall(tc) => {
393 result.push_str(&format!("[Tool: {}]\n", tc.name));
394 }
395 ContentBlock::Unknown(_) => {
396 }
398 }
399 }
400 Ok(result.trim().to_string())
401 }
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
409#[serde(tag = "role", rename_all = "camelCase")]
410pub enum Message {
411 User(UserMessage),
413 Assistant(AssistantMessage),
415 ToolResult(ToolResultMessage),
417}
418
419impl Message {
420 pub fn user(content: impl Into<MessageContent>) -> Self {
422 Message::User(UserMessage::new(content))
423 }
424
425 pub fn assistant(content: Vec<ContentBlock>) -> Self {
427 Message::Assistant(AssistantMessage {
428 role: AssistantRole::Assistant,
429 content,
430 api: Api::AnthropicMessages,
431 provider: "assistant".to_string(),
432 model: "assistant".to_string(),
433 usage: super::Usage::default(),
434 stop_reason: super::StopReason::Stop,
435 error_message: None,
436 response_id: None,
437 timestamp: chrono::Utc::now().timestamp_millis(),
438 })
439 }
440
441 pub fn tool_result(
443 tool_call_id: impl Into<String>,
444 tool_name: impl Into<String>,
445 content: Vec<ContentBlock>,
446 ) -> Self {
447 Message::ToolResult(ToolResultMessage::new(tool_call_id, tool_name, content))
448 }
449
450 pub fn timestamp(&self) -> i64 {
452 match self {
453 Message::User(m) => m.timestamp,
454 Message::Assistant(m) => m.timestamp,
455 Message::ToolResult(m) => m.timestamp,
456 }
457 }
458
459 pub fn text_content(&self) -> Result<String, crate::error::ProviderError> {
461 match self {
462 Message::User(m) => match &m.content {
463 MessageContent::Text(s) => Ok(s.clone()),
464 MessageContent::Blocks(blocks) => {
465 let estimated_len: usize = blocks
466 .iter()
467 .map(|b| match b {
468 ContentBlock::Text(t) => t.text.len() + 1,
469 ContentBlock::Image(_) => 8,
470 ContentBlock::Thinking(t) => t.thinking.len() + 1,
471 ContentBlock::ToolCall(_) => 12,
472 ContentBlock::Unknown(_) => 10,
473 })
474 .sum();
475 let mut result = String::with_capacity(estimated_len);
476 for block in blocks {
477 match block {
478 ContentBlock::Text(t) => {
479 result.push_str(&t.text);
480 result.push('\n');
481 }
482 ContentBlock::Image(_) => {
483 result.push_str("[Image]\n");
484 }
485 ContentBlock::Thinking(t) => {
486 result.push_str(&t.thinking);
487 result.push('\n');
488 }
489 ContentBlock::ToolCall(_) => {
490 result.push_str("[Tool Call]\n");
491 }
492 ContentBlock::Unknown(_) => {
493 result.push_str("[Unknown]\n");
494 }
495 }
496 }
497 Ok(result.trim().to_string())
498 }
499 },
500 Message::Assistant(m) => Ok(m.text_content()),
501 Message::ToolResult(m) => m.text_content(),
502 }
503 }
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
508#[serde(untagged)]
509pub enum MessageContent {
510 Text(String),
512 Blocks(Vec<ContentBlock>),
514}
515
516impl MessageContent {
517 pub fn is_text(&self) -> bool {
519 matches!(self, MessageContent::Text(_))
520 }
521
522 pub fn as_str(&self) -> Option<&str> {
524 match self {
525 MessageContent::Text(s) => Some(s),
526 MessageContent::Blocks(_) => None,
527 }
528 }
529}
530
531impl From<String> for MessageContent {
533 fn from(text: String) -> Self {
534 MessageContent::Text(text)
535 }
536}
537
538impl From<&str> for MessageContent {
539 fn from(text: &str) -> Self {
540 MessageContent::Text(text.to_string())
541 }
542}
543
544impl From<Vec<ContentBlock>> for MessageContent {
545 fn from(blocks: Vec<ContentBlock>) -> Self {
546 MessageContent::Blocks(blocks)
547 }
548}
549
550impl From<TextContent> for MessageContent {
551 fn from(block: TextContent) -> Self {
552 MessageContent::Blocks(vec![ContentBlock::Text(block)])
553 }
554}
555
556impl From<ContentBlock> for MessageContent {
557 fn from(block: ContentBlock) -> Self {
558 MessageContent::Blocks(vec![block])
559 }
560}
561
562pub fn transform_for_provider(
574 messages: &[Message],
575 _from_api: &super::Api,
576 to_api: &super::Api,
577) -> Vec<Message> {
578 messages
579 .iter()
580 .map(|msg| match msg {
581 Message::Assistant(a) => {
582 let mut new_msg = AssistantMessage::new(*to_api, &a.provider, &a.model);
583 new_msg.content = transform_content_blocks(&a.content, to_api);
584 new_msg.usage = a.usage.clone();
585 new_msg.stop_reason = a.stop_reason;
586 new_msg.error_message = a.error_message.clone();
587 new_msg.response_id = a.response_id.clone();
588 new_msg.timestamp = a.timestamp;
589 Message::Assistant(new_msg)
590 }
591 Message::User(u) => Message::User(u.clone()),
592 Message::ToolResult(t) => Message::ToolResult(t.clone()),
593 })
594 .collect()
595}
596
597fn transform_content_blocks(blocks: &[ContentBlock], to_api: &super::Api) -> Vec<ContentBlock> {
602 match to_api {
603 super::Api::AnthropicMessages => blocks.to_vec(),
605
606 _ => {
608 let mut transformed = Vec::with_capacity(blocks.len());
609 for block in blocks {
610 match block {
611 ContentBlock::Thinking(t) => {
612 let text = format!("<thinking>\n{}\n</thinking>", t.thinking);
614 transformed.push(ContentBlock::Text(TextContent::new(text)));
615 }
616 ContentBlock::Text(t) => {
617 transformed.push(ContentBlock::Text(t.clone()));
618 }
619 ContentBlock::ToolCall(tc) => {
620 transformed.push(ContentBlock::ToolCall(tc.clone()));
621 }
622 ContentBlock::Image(img) => {
623 transformed.push(ContentBlock::Image(img.clone()));
624 }
625 ContentBlock::Unknown(v) => {
626 if let Some(text) = v.get("text").and_then(|t| t.as_str()) {
628 transformed.push(ContentBlock::Text(TextContent::new(text)));
629 }
630 }
632 }
633 }
634 merge_adjacent_text_blocks(transformed)
636 }
637 }
638}
639
640fn merge_adjacent_text_blocks(blocks: Vec<ContentBlock>) -> Vec<ContentBlock> {
642 let mut result = Vec::with_capacity(blocks.len());
643 let estimated_len = blocks
644 .iter()
645 .map(|b| match b {
646 ContentBlock::Text(t) => t.text.len() + 1,
647 _ => 0,
648 })
649 .sum::<usize>();
650 let mut pending_text = String::with_capacity(estimated_len.max(256));
651
652 for block in blocks {
653 match block {
654 ContentBlock::Text(t) => {
655 if !pending_text.is_empty() {
656 pending_text.push('\n');
657 }
658 pending_text.push_str(&t.text);
659 }
660 other => {
661 if !pending_text.is_empty() {
662 result.push(ContentBlock::Text(TextContent::new(std::mem::take(
663 &mut pending_text,
664 ))));
665 }
666 result.push(other);
667 }
668 }
669 }
670
671 if !pending_text.is_empty() {
672 result.push(ContentBlock::Text(TextContent::new(pending_text)));
673 }
674
675 result
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681 use crate::types::{Api, StopReason, Usage};
682
683 #[test]
686 fn text_content_roundtrip() {
687 let block = ContentBlock::Text(TextContent::new("hello world"));
688 let json = serde_json::to_string(&block).unwrap();
689 let back: ContentBlock = serde_json::from_str(&json).unwrap();
690 assert_eq!(back.as_text(), Some("hello world"));
691 }
692
693 #[test]
694 fn thinking_content_roundtrip() {
695 let block = ContentBlock::Thinking(ThinkingContent::new("inner thoughts"));
696 let json = serde_json::to_string(&block).unwrap();
697 let back: ContentBlock = serde_json::from_str(&json).unwrap();
698 assert!(back.as_thinking().is_some());
699 assert_eq!(back.as_thinking().unwrap().thinking, "inner thoughts");
700 }
701
702 #[test]
703 fn image_content_roundtrip() {
704 let block = ContentBlock::Image(ImageContent::new("base64data==", "image/png"));
705 let json = serde_json::to_string(&block).unwrap();
706 let back: ContentBlock = serde_json::from_str(&json).unwrap();
707 match back {
708 ContentBlock::Image(img) => {
709 assert_eq!(img.data, "base64data==");
710 assert_eq!(img.mime_type, "image/png");
711 }
712 _ => panic!("Expected Image block"),
713 }
714 }
715
716 #[test]
717 fn tool_call_roundtrip() {
718 let block = ContentBlock::ToolCall(ToolCall::new(
719 "call_123",
720 "read_file",
721 serde_json::json!({"path": "/foo.rs"}),
722 ));
723 let json = serde_json::to_string(&block).unwrap();
724 let back: ContentBlock = serde_json::from_str(&json).unwrap();
725 let tc = back.as_tool_call().unwrap();
726 assert_eq!(tc.id, "call_123");
727 assert_eq!(tc.name, "read_file");
728 assert_eq!(tc.arguments["path"], "/foo.rs");
729 }
730
731 #[test]
734 fn user_message_inner_roundtrip() {
735 let msg = UserMessage::new("Hello, assistant!");
736 let json = serde_json::to_string(&msg).unwrap();
737 let back: UserMessage = serde_json::from_str(&json).unwrap();
738 assert!(matches!(&back.content, MessageContent::Text(s) if s == "Hello, assistant!"));
739 assert_eq!(back.role, UserRole::User);
740 }
741
742 #[test]
743 fn user_message_blocks_roundtrip() {
744 let blocks = vec![
745 ContentBlock::Text(TextContent::new("part one")),
746 ContentBlock::Text(TextContent::new("part two")),
747 ];
748 let msg = UserMessage::new(MessageContent::Blocks(blocks));
749 let json = serde_json::to_string(&msg).unwrap();
750 let back: UserMessage = serde_json::from_str(&json).unwrap();
751 match &back.content {
752 MessageContent::Blocks(blocks) => assert_eq!(blocks.len(), 2),
753 _ => panic!("Expected Blocks"),
754 }
755 }
756
757 #[test]
758 fn assistant_message_inner_roundtrip() {
759 let mut msg = AssistantMessage::new(Api::AnthropicMessages, "anthropic", "claude-3");
760 msg.content
761 .push(ContentBlock::Text(TextContent::new("Hi!")));
762 msg.content
763 .push(ContentBlock::Thinking(ThinkingContent::new("hmm")));
764 msg.usage = Usage {
765 input: 100,
766 output: 50,
767 ..Default::default()
768 };
769 msg.stop_reason = StopReason::Stop;
770 msg.response_id = Some("resp_abc".to_string());
771
772 let json = serde_json::to_string(&msg).unwrap();
773 let back: AssistantMessage = serde_json::from_str(&json).unwrap();
774
775 assert_eq!(back.content.len(), 2);
776 assert_eq!(back.usage.input, 100);
777 assert_eq!(back.response_id.as_deref(), Some("resp_abc"));
778 assert_eq!(back.role, AssistantRole::Assistant);
779 }
780
781 #[test]
782 fn tool_result_message_inner_roundtrip() {
783 let msg = ToolResultMessage::new(
784 "call_1",
785 "bash",
786 vec![ContentBlock::Text(TextContent::new("output"))],
787 );
788 let json = serde_json::to_string(&msg).unwrap();
789 let back: ToolResultMessage = serde_json::from_str(&json).unwrap();
790 assert_eq!(back.tool_call_id, "call_1");
791 assert_eq!(back.tool_name, "bash");
792 assert!(!back.is_error);
793 assert_eq!(back.role, ToolResultRole::ToolResult);
794 }
795
796 #[test]
797 fn message_construction_and_accessors() {
798 let user = Message::user("test");
799 assert!(matches!(user, Message::User(_)));
800
801 let ts = user.timestamp();
802 assert!(ts > 0);
803 }
804
805 #[test]
806 fn message_content_roundtrip() {
807 let mc = MessageContent::Text("hello".to_string());
809 let json = serde_json::to_string(&mc).unwrap();
810 let back: MessageContent = serde_json::from_str(&json).unwrap();
811 assert_eq!(back.as_str(), Some("hello"));
812
813 let mc = MessageContent::Blocks(vec![ContentBlock::Text(TextContent::new("block"))]);
815 let json = serde_json::to_string(&mc).unwrap();
816 let back: MessageContent = serde_json::from_str(&json).unwrap();
817 assert!(!back.is_text());
818 }
819
820 #[test]
823 fn user_text_content() {
824 let msg = Message::user("Hello!");
825 assert_eq!(msg.text_content().unwrap(), "Hello!");
826 }
827
828 #[test]
829 fn user_blocks_text_content() {
830 let blocks = vec![
831 ContentBlock::Text(TextContent::new("line 1")),
832 ContentBlock::Text(TextContent::new("line 2")),
833 ];
834 let msg = Message::User(UserMessage::new(MessageContent::Blocks(blocks)));
835 assert_eq!(msg.text_content().unwrap(), "line 1\nline 2");
836 }
837
838 #[test]
839 fn assistant_text_content() {
840 let mut a = AssistantMessage::new(Api::OpenAiCompletions, "openai", "gpt-4");
841 a.content
842 .push(ContentBlock::Text(TextContent::new("part A")));
843 a.content
844 .push(ContentBlock::Thinking(ThinkingContent::new("hidden")));
845 a.content
846 .push(ContentBlock::Text(TextContent::new("part B")));
847
848 let msg = Message::Assistant(a);
849 let text = msg.text_content().unwrap();
850 assert_eq!(text, "part Apart B");
852 }
853
854 #[test]
855 fn tool_result_text_content() {
856 let msg = ToolResultMessage::new(
857 "call_1",
858 "read",
859 vec![
860 ContentBlock::Text(TextContent::new("file contents")),
861 ContentBlock::Image(ImageContent::new("aaa", "image/png")),
862 ],
863 );
864 let text = msg.text_content().unwrap();
865 assert!(text.contains("file contents"));
866 assert!(text.contains("[Image]"));
867 }
868
869 #[test]
872 fn transform_openai_to_anthropic_keeps_thinking() {
873 let mut a = AssistantMessage::new(Api::OpenAiCompletions, "openai", "gpt-4");
874 a.content
875 .push(ContentBlock::Text(TextContent::new("Hello")));
876 a.content
877 .push(ContentBlock::Thinking(ThinkingContent::new("pondering")));
878 let messages = vec![Message::Assistant(a)];
879
880 let transformed =
881 transform_for_provider(&messages, &Api::OpenAiCompletions, &Api::AnthropicMessages);
882 match &transformed[0] {
883 Message::Assistant(a) => {
884 assert_eq!(a.content.len(), 2);
886 assert!(matches!(&a.content[1], ContentBlock::Thinking(_)));
887 }
888 _ => panic!("Expected Assistant"),
889 }
890 }
891
892 #[test]
893 fn transform_anthropic_to_openai_converts_thinking() {
894 let mut a = AssistantMessage::new(Api::AnthropicMessages, "anthropic", "claude-3");
895 a.content
896 .push(ContentBlock::Text(TextContent::new("Hello")));
897 a.content
898 .push(ContentBlock::Thinking(ThinkingContent::new("pondering")));
899 let messages = vec![Message::Assistant(a)];
900
901 let transformed =
902 transform_for_provider(&messages, &Api::AnthropicMessages, &Api::OpenAiCompletions);
903 match &transformed[0] {
904 Message::Assistant(a) => {
905 assert!(a.content.iter().all(|b| matches!(b, ContentBlock::Text(_))));
907 let full_text: String = a.content.iter().filter_map(|b| b.as_text()).collect();
908 assert!(full_text.contains("Hello"));
909 assert!(full_text.contains("<thinking>"));
910 assert!(full_text.contains("pondering"));
911 }
912 _ => panic!("Expected Assistant"),
913 }
914 }
915
916 #[test]
917 fn transform_roundtrip_openai_anthropic_openai() {
918 let mut a = AssistantMessage::new(Api::OpenAiCompletions, "openai", "gpt-4");
919 a.content
920 .push(ContentBlock::Text(TextContent::new("Hello")));
921 a.content
922 .push(ContentBlock::Thinking(ThinkingContent::new("pondering")));
923 a.content
924 .push(ContentBlock::Text(TextContent::new("World")));
925 let original = vec![Message::Assistant(a)];
926
927 let step1 =
929 transform_for_provider(&original, &Api::OpenAiCompletions, &Api::AnthropicMessages);
930 let step2 =
932 transform_for_provider(&step1, &Api::AnthropicMessages, &Api::OpenAiCompletions);
933
934 match &step2[0] {
935 Message::Assistant(a) => {
936 let full_text: String = a.content.iter().filter_map(|b| b.as_text()).collect();
937 assert!(full_text.contains("Hello"));
938 assert!(full_text.contains("World"));
939 assert!(full_text.contains("<thinking>"));
940 }
941 _ => panic!("Expected Assistant"),
942 }
943 }
944
945 #[test]
948 fn merge_adjacent_text_blocks_basic() {
949 let blocks = vec![
950 ContentBlock::Text(TextContent::new("a")),
951 ContentBlock::Text(TextContent::new("b")),
952 ContentBlock::Text(TextContent::new("c")),
953 ];
954 let merged = merge_adjacent_text_blocks(blocks);
955 assert_eq!(merged.len(), 1);
956 assert_eq!(merged[0].as_text(), Some("a\nb\nc"));
957 }
958
959 #[test]
960 fn merge_adjacent_text_blocks_with_intervening() {
961 let blocks = vec![
962 ContentBlock::Text(TextContent::new("a")),
963 ContentBlock::Text(TextContent::new("b")),
964 ContentBlock::ToolCall(ToolCall::new("1", "tool", serde_json::json!({}))),
965 ContentBlock::Text(TextContent::new("c")),
966 ];
967 let merged = merge_adjacent_text_blocks(blocks);
968 assert_eq!(merged.len(), 3); assert_eq!(merged[0].as_text(), Some("a\nb"));
970 assert!(merged[1].as_tool_call().is_some());
971 assert_eq!(merged[2].as_text(), Some("c"));
972 }
973
974 #[test]
975 fn merge_adjacent_text_blocks_empty() {
976 let blocks: Vec<ContentBlock> = vec![];
977 let merged = merge_adjacent_text_blocks(blocks);
978 assert!(merged.is_empty());
979 }
980
981 #[test]
982 fn message_content_from_conversions() {
983 let mc: MessageContent = "hello".into();
984 assert!(mc.is_text());
985 assert_eq!(mc.as_str(), Some("hello"));
986
987 let mc: MessageContent = "world".to_string().into();
988 assert!(mc.is_text());
989
990 let mc: MessageContent = TextContent::new("block").into();
991 assert!(!mc.is_text());
992 }
993}