1use serde_json::Value as JsonValue;
27
28use crate::{
29 Api, AssistantMessage, ContentBlock, ImageContent, ImageContentType, InputModality, Message,
30 MessageContent, Model, StopReason, TextContent, TextContentType, ThinkingContent, ToolCall,
31 ToolCallType, ToolResultMessage, Usage,
32};
33
34#[derive(Debug, Clone)]
40pub struct TransformOptions {
41 pub strip_thinking: bool,
43 pub convert_tools: bool,
45 pub convert_images: bool,
47 pub merge_text: bool,
49}
50
51impl Default for TransformOptions {
52 fn default() -> Self {
53 Self {
54 strip_thinking: false,
55 convert_tools: true,
56 convert_images: true,
57 merge_text: true,
58 }
59 }
60}
61
62pub fn transform_messages(
87 messages: &[Message],
88 from_api: Api,
89 to_api: Api,
90 opts: TransformOptions,
91) -> Vec<Message> {
92 if from_api == to_api {
93 return messages.to_vec();
94 }
95
96 let intermediate: Vec<IntermediateMessage> = messages
99 .iter()
100 .map(|m| to_intermediate(m, &from_api))
101 .collect();
102
103 intermediate
104 .into_iter()
105 .map(|im| from_intermediate(&im, &to_api, &opts))
106 .collect()
107}
108
109#[derive(Debug, Clone)]
116enum IntermediateMessage {
117 User {
118 content: IntermediateContent,
119 },
120 Assistant {
121 content: Vec<IntermediateBlock>,
122 model: String,
123 provider: String,
124 usage: Usage,
125 stop_reason: StopReason,
126 error_message: Option<String>,
127 response_id: Option<String>,
128 timestamp: i64,
129 },
130 ToolResult {
131 tool_call_id: String,
132 tool_name: String,
133 content: Vec<IntermediateBlock>,
134 is_error: bool,
135 },
136}
137
138#[derive(Debug, Clone)]
140enum IntermediateContent {
141 Text(String),
142 Blocks(Vec<IntermediateBlock>),
143}
144
145#[derive(Debug, Clone)]
147enum IntermediateBlock {
148 Text(String),
149 Thinking {
150 text: String,
151 signature: Option<String>,
152 },
153 Image {
154 data: String,
155 mime_type: String,
156 },
157 ToolCall {
158 id: String,
159 name: String,
160 arguments: JsonValue,
161 },
162}
163
164fn to_intermediate(msg: &Message, _from_api: &Api) -> IntermediateMessage {
170 match msg {
171 Message::User(u) => {
172 let content = match &u.content {
173 MessageContent::Text(s) => IntermediateContent::Text(s.clone()),
174 MessageContent::Blocks(blocks) => {
175 IntermediateContent::Blocks(blocks.iter().map(block_to_intermediate).collect())
176 }
177 };
178 IntermediateMessage::User { content }
179 }
180
181 Message::Assistant(a) => IntermediateMessage::Assistant {
182 content: a.content.iter().map(block_to_intermediate).collect(),
183 model: a.model.clone(),
184 provider: a.provider.clone(),
185 usage: a.usage.clone(),
186 stop_reason: a.stop_reason,
187 error_message: a.error_message.clone(),
188 response_id: a.response_id.clone(),
189 timestamp: a.timestamp,
190 },
191
192 Message::ToolResult(t) => IntermediateMessage::ToolResult {
193 tool_call_id: t.tool_call_id.clone(),
194 tool_name: t.tool_name.clone(),
195 content: t.content.iter().map(block_to_intermediate).collect(),
196 is_error: t.is_error,
197 },
198 }
199}
200
201fn block_to_intermediate(block: &ContentBlock) -> IntermediateBlock {
202 match block {
203 ContentBlock::Text(t) => IntermediateBlock::Text(t.text.clone()),
204 ContentBlock::Thinking(th) => IntermediateBlock::Thinking {
205 text: th.thinking.clone(),
206 signature: th.thinking_signature.clone(),
207 },
208 ContentBlock::Image(img) => IntermediateBlock::Image {
209 data: img.data.clone(),
210 mime_type: img.mime_type.clone(),
211 },
212 ContentBlock::ToolCall(tc) => IntermediateBlock::ToolCall {
213 id: tc.id.clone(),
214 name: tc.name.clone(),
215 arguments: tc.arguments.clone(),
216 },
217 ContentBlock::Unknown(val) => {
218 if let Some(text) = val.get("text").and_then(|v| v.as_str()) {
220 IntermediateBlock::Text(text.to_string())
221 } else {
222 IntermediateBlock::Text(format!("[unknown block: {}]", val))
223 }
224 }
225 }
226}
227
228fn from_intermediate(im: &IntermediateMessage, to_api: &Api, opts: &TransformOptions) -> Message {
234 match im {
235 IntermediateMessage::User { content } => {
236 let native_content = match content {
237 IntermediateContent::Text(s) => MessageContent::Text(s.clone()),
238 IntermediateContent::Blocks(blocks) => {
239 let native_blocks: Vec<ContentBlock> = blocks
240 .iter()
241 .flat_map(|b| intermediate_to_blocks(b, to_api, opts))
242 .collect();
243 let merged = if opts.merge_text {
244 merge_adjacent_text_blocks(native_blocks)
245 } else {
246 native_blocks
247 };
248 MessageContent::Blocks(merged)
249 }
250 };
251 Message::User(crate::UserMessage {
252 role: crate::UserRole::User,
253 content: native_content,
254 timestamp: chrono::Utc::now().timestamp_millis(),
255 })
256 }
257
258 IntermediateMessage::Assistant {
259 content,
260 model,
261 provider,
262 usage,
263 stop_reason,
264 error_message,
265 response_id,
266 timestamp,
267 } => {
268 let mut native_blocks: Vec<ContentBlock> = content
269 .iter()
270 .flat_map(|b| intermediate_to_blocks(b, to_api, opts))
271 .collect();
272 if opts.merge_text {
273 native_blocks = merge_adjacent_text_blocks(native_blocks);
274 }
275
276 let mut msg = AssistantMessage::new(*to_api, provider, model);
277 msg.content = native_blocks;
278 msg.usage = usage.clone();
279 msg.stop_reason = *stop_reason;
280 msg.error_message = error_message.clone();
281 msg.response_id = response_id.clone();
282 msg.timestamp = *timestamp;
283 Message::Assistant(msg)
284 }
285
286 IntermediateMessage::ToolResult {
287 tool_call_id,
288 tool_name,
289 content,
290 is_error,
291 } => {
292 let mut native_blocks: Vec<ContentBlock> = content
293 .iter()
294 .flat_map(|b| intermediate_to_blocks(b, to_api, opts))
295 .collect();
296 if opts.merge_text {
297 native_blocks = merge_adjacent_text_blocks(native_blocks);
298 }
299
300 let mut msg = ToolResultMessage::new(tool_call_id, tool_name, native_blocks);
301 msg.is_error = *is_error;
302 Message::ToolResult(msg)
303 }
304 }
305}
306
307fn intermediate_to_blocks(
310 ib: &IntermediateBlock,
311 to_api: &Api,
312 opts: &TransformOptions,
313) -> Vec<ContentBlock> {
314 match ib {
315 IntermediateBlock::Text(text) => {
316 vec![ContentBlock::Text(TextContent {
317 content_type: TextContentType::Text,
318 text: text.clone(),
319 text_signature: None,
320 })]
321 }
322
323 IntermediateBlock::Thinking { text, signature } => {
324 if opts.strip_thinking {
325 return vec![];
326 }
327 match to_api {
328 Api::AnthropicMessages => {
330 let th = ThinkingContent {
331 content_type: crate::ThinkingContentType::Thinking,
332 thinking: text.clone(),
333 thinking_signature: signature.clone(),
334 redacted: None,
335 };
336 vec![ContentBlock::Thinking(th)]
337 }
338 _ => {
340 let wrapped = format!("<thinking>\n{}\n</thinking>", text);
341 vec![ContentBlock::Text(TextContent {
342 content_type: TextContentType::Text,
343 text: wrapped,
344 text_signature: None,
345 })]
346 }
347 }
348 }
349
350 IntermediateBlock::Image { data, mime_type } => {
351 if !opts.convert_images {
352 return vec![];
353 }
354 vec![ContentBlock::Image(ImageContent {
355 content_type: ImageContentType::Image,
356 data: data.clone(),
357 mime_type: mime_type.clone(),
358 })]
359 }
360
361 IntermediateBlock::ToolCall {
362 id,
363 name,
364 arguments,
365 } => {
366 if !opts.convert_tools {
367 return vec![];
368 }
369 vec![ContentBlock::ToolCall(ToolCall {
370 content_type: ToolCallType::ToolCall,
371 id: id.clone(),
372 name: name.clone(),
373 arguments: arguments.clone(),
374 thought_signature: None,
375 })]
376 }
377 }
378}
379
380fn merge_adjacent_text_blocks(blocks: Vec<ContentBlock>) -> Vec<ContentBlock> {
386 let mut result = Vec::with_capacity(blocks.len());
387 let estimated_len = blocks
388 .iter()
389 .map(|b| match b {
390 ContentBlock::Text(t) => t.text.len() + 1,
391 _ => 0,
392 })
393 .sum::<usize>();
394 let mut pending = String::with_capacity(estimated_len.max(256));
395
396 for block in blocks {
397 match block {
398 ContentBlock::Text(t) => {
399 if !pending.is_empty() {
400 pending.push('\n');
401 }
402 pending.push_str(&t.text);
403 }
404 other => {
405 if !pending.is_empty() {
406 result.push(ContentBlock::Text(TextContent {
407 content_type: TextContentType::Text,
408 text: std::mem::take(&mut pending),
409 text_signature: None,
410 }));
411 }
412 result.push(other);
413 }
414 }
415 }
416
417 if !pending.is_empty() {
418 result.push(ContentBlock::Text(TextContent {
419 content_type: TextContentType::Text,
420 text: pending,
421 text_signature: None,
422 }));
423 }
424
425 result
426}
427
428const NON_VISION_USER_IMAGE_PLACEHOLDER: &str = "(image omitted: model does not support images)";
433const NON_VISION_TOOL_IMAGE_PLACEHOLDER: &str =
434 "(tool image omitted: model does not support images)";
435
436fn replace_images_with_placeholder(
439 blocks: &[ContentBlock],
440 placeholder: &str,
441) -> Vec<ContentBlock> {
442 let mut result = Vec::with_capacity(blocks.len());
443 let mut prev_was_placeholder = false;
444
445 for block in blocks {
446 if matches!(block, ContentBlock::Image(_)) {
447 if !prev_was_placeholder {
448 result.push(ContentBlock::Text(TextContent::new(placeholder)));
449 }
450 prev_was_placeholder = true;
451 continue;
452 }
453
454 result.push(block.clone());
455 prev_was_placeholder = matches!(block, ContentBlock::Text(t) if t.text == placeholder);
456 }
457
458 result
459}
460
461fn downgrade_unsupported_images(messages: &[Message], model: &Model) -> Vec<Message> {
463 if model.input.contains(&InputModality::Image) {
464 return messages.to_vec();
465 }
466
467 messages
468 .iter()
469 .map(|msg| match msg {
470 Message::User(u) => match &u.content {
471 MessageContent::Blocks(blocks) => {
472 let replaced =
473 replace_images_with_placeholder(blocks, NON_VISION_USER_IMAGE_PLACEHOLDER);
474 Message::User(crate::UserMessage {
475 role: u.role,
476 content: MessageContent::Blocks(replaced),
477 timestamp: u.timestamp,
478 })
479 }
480 _ => msg.clone(),
481 },
482 Message::ToolResult(t) => {
483 let replaced =
484 replace_images_with_placeholder(&t.content, NON_VISION_TOOL_IMAGE_PLACEHOLDER);
485 Message::ToolResult(ToolResultMessage {
486 role: t.role,
487 tool_call_id: t.tool_call_id.clone(),
488 tool_name: t.tool_name.clone(),
489 content: replaced,
490 details: t.details.clone(),
491 is_error: t.is_error,
492 timestamp: t.timestamp,
493 })
494 }
495 _ => msg.clone(),
496 })
497 .collect()
498}
499
500pub fn normalize_tool_call_id(id: &str) -> String {
504 crate::utils::normalize_tool_call_id(id)
505}
506
507pub fn transform_messages_for_model(messages: &[Message], model: &Model) -> Vec<Message> {
518 let image_aware = downgrade_unsupported_images(messages, model);
519
520 let mut tool_call_id_map: std::collections::HashMap<String, String> =
522 std::collections::HashMap::new();
523
524 let transformed: Vec<Message> = image_aware
526 .iter()
527 .map(|msg| match msg {
528 Message::User(_) => msg.clone(),
529
530 Message::ToolResult(t) => {
531 if let Some(normalized) = tool_call_id_map.get(&t.tool_call_id)
532 && normalized != &t.tool_call_id
533 {
534 return Message::ToolResult(ToolResultMessage {
535 tool_call_id: normalized.clone(),
536 ..t.clone()
537 });
538 }
539 msg.clone()
540 }
541
542 Message::Assistant(a) => {
543 let is_same_model =
544 a.provider == model.provider && a.api == model.api && a.model == model.id;
545
546 let new_content: Vec<ContentBlock> = a
547 .content
548 .iter()
549 .flat_map(|block| match block {
550 ContentBlock::Thinking(th) => {
551 if th.redacted == Some(true) && !is_same_model {
553 return vec![];
554 }
555 if is_same_model && th.thinking_signature.is_some() {
557 return vec![block.clone()];
558 }
559 if th.thinking.trim().is_empty() {
561 return vec![];
562 }
563 if is_same_model {
565 return vec![block.clone()];
566 }
567 vec![ContentBlock::Text(TextContent::new(&th.thinking))]
569 }
570
571 ContentBlock::Text(_) => vec![block.clone()],
572
573 ContentBlock::ToolCall(tc) => {
574 let mut new_tc = tc.clone();
575
576 if !is_same_model && tc.thought_signature.is_some() {
578 new_tc.thought_signature = None;
579 }
580
581 if !is_same_model {
583 let normalized = normalize_tool_call_id(&tc.id);
584 if normalized != tc.id {
585 tool_call_id_map.insert(tc.id.clone(), normalized.clone());
586 new_tc.id = normalized;
587 }
588 }
589
590 vec![ContentBlock::ToolCall(new_tc)]
591 }
592
593 _ => vec![block.clone()],
594 })
595 .collect();
596
597 Message::Assistant(AssistantMessage {
598 content: new_content,
599 ..a.clone()
600 })
601 }
602 })
603 .collect();
604
605 let mut result: Vec<Message> = Vec::with_capacity(transformed.len());
608 let mut pending_tool_calls: Vec<ToolCall> = Vec::new();
609 let mut existing_tool_result_ids: std::collections::HashSet<String> =
610 std::collections::HashSet::new();
611
612 let insert_synthetic_results = |pending: &mut Vec<ToolCall>,
613 existing: &mut std::collections::HashSet<String>,
614 out: &mut Vec<Message>| {
615 for tc in pending.drain(..) {
616 if !existing.contains(&tc.id) {
617 out.push(Message::ToolResult(ToolResultMessage {
618 role: crate::ToolResultRole::ToolResult,
619 tool_call_id: tc.id.clone(),
620 tool_name: tc.name.clone(),
621 content: vec![ContentBlock::Text(TextContent::new("No result provided"))],
622 details: None,
623 is_error: true,
624 timestamp: chrono::Utc::now().timestamp_millis(),
625 }));
626 }
627 }
628 existing.clear();
629 };
630
631 for msg in &transformed {
632 match msg {
633 Message::Assistant(a) => {
634 insert_synthetic_results(
636 &mut pending_tool_calls,
637 &mut existing_tool_result_ids,
638 &mut result,
639 );
640
641 if a.stop_reason == StopReason::Error || a.stop_reason == StopReason::Aborted {
643 continue;
644 }
645
646 let tool_calls: Vec<&ToolCall> =
648 a.content.iter().filter_map(|b| b.as_tool_call()).collect();
649 if !tool_calls.is_empty() {
650 pending_tool_calls = tool_calls.into_iter().cloned().collect();
651 existing_tool_result_ids.clear();
652 }
653
654 result.push(msg.clone());
655 }
656
657 Message::ToolResult(t) => {
658 existing_tool_result_ids.insert(t.tool_call_id.clone());
659 result.push(msg.clone());
660 }
661
662 Message::User(_) => {
663 insert_synthetic_results(
665 &mut pending_tool_calls,
666 &mut existing_tool_result_ids,
667 &mut result,
668 );
669 result.push(msg.clone());
670 }
671 }
672 }
673
674 insert_synthetic_results(
676 &mut pending_tool_calls,
677 &mut existing_tool_result_ids,
678 &mut result,
679 );
680
681 result
682}
683
684pub fn anthropic_to_openai(messages: &[Message]) -> Vec<Message> {
690 transform_messages(
691 messages,
692 Api::AnthropicMessages,
693 Api::OpenAiCompletions,
694 TransformOptions::default(),
695 )
696}
697
698pub fn openai_to_anthropic(messages: &[Message]) -> Vec<Message> {
700 transform_messages(
701 messages,
702 Api::OpenAiCompletions,
703 Api::AnthropicMessages,
704 TransformOptions::default(),
705 )
706}
707
708pub fn google_to_openai(messages: &[Message]) -> Vec<Message> {
710 transform_messages(
711 messages,
712 Api::GoogleGenerativeAi,
713 Api::OpenAiCompletions,
714 TransformOptions::default(),
715 )
716}
717
718pub fn anthropic_to_google(messages: &[Message]) -> Vec<Message> {
720 transform_messages(
721 messages,
722 Api::AnthropicMessages,
723 Api::GoogleGenerativeAi,
724 TransformOptions::default(),
725 )
726}
727
728#[cfg(test)]
733mod tests {
734 use super::*;
735 use crate::UserMessage;
736
737 fn user_msg(text: &str) -> Message {
739 Message::User(UserMessage::new(text))
740 }
741
742 fn assistant_msg(api: Api, provider: &str, model: &str, blocks: Vec<ContentBlock>) -> Message {
744 let mut msg = AssistantMessage::new(api, provider, model);
745 msg.content = blocks;
746 Message::Assistant(msg)
747 }
748
749 fn tool_result_msg(tool_call_id: &str, tool_name: &str, text: &str) -> Message {
751 Message::ToolResult(ToolResultMessage::new(
752 tool_call_id,
753 tool_name,
754 vec![ContentBlock::Text(TextContent::new(text))],
755 ))
756 }
757
758 #[test]
761 fn test_anthropic_to_openai_text() {
762 let msgs = vec![
763 user_msg("Hello"),
764 assistant_msg(
765 Api::AnthropicMessages,
766 "anthropic",
767 "claude-3.5-sonnet",
768 vec![ContentBlock::Text(TextContent::new("Hi there!"))],
769 ),
770 ];
771
772 let result = anthropic_to_openai(&msgs);
773 assert_eq!(result.len(), 2);
774
775 match &result[0] {
777 Message::User(u) => assert_eq!(u.content.as_str(), Some("Hello")),
778 _ => panic!("Expected User message"),
779 }
780
781 match &result[1] {
783 Message::Assistant(a) => {
784 assert_eq!(a.api, Api::OpenAiCompletions);
785 assert_eq!(a.text_content(), "Hi there!");
786 }
787 _ => panic!("Expected Assistant message"),
788 }
789 }
790
791 #[test]
794 fn test_thinking_block_anthropic_to_openai() {
795 let msgs = vec![assistant_msg(
796 Api::AnthropicMessages,
797 "anthropic",
798 "claude-3.5-sonnet",
799 vec![
800 ContentBlock::Thinking(ThinkingContent::new("Let me think...")),
801 ContentBlock::Text(TextContent::new("Here's the answer.")),
802 ],
803 )];
804
805 let result = anthropic_to_openai(&msgs);
806 match &result[0] {
807 Message::Assistant(a) => {
808 let text = a.text_content();
810 assert!(text.contains("<thinking>"));
811 assert!(text.contains("Let me think..."));
812 assert!(text.contains("Here's the answer."));
813 assert!(
815 !a.content
816 .iter()
817 .any(|b| matches!(b, ContentBlock::Thinking(_)))
818 );
819 }
820 _ => panic!("Expected Assistant"),
821 }
822 }
823
824 #[test]
827 fn test_thinking_block_stripped() {
828 let msgs = vec![assistant_msg(
829 Api::AnthropicMessages,
830 "anthropic",
831 "claude-3.5-sonnet",
832 vec![
833 ContentBlock::Thinking(ThinkingContent::new("Internal thought")),
834 ContentBlock::Text(TextContent::new("Final answer.")),
835 ],
836 )];
837
838 let opts = TransformOptions {
839 strip_thinking: true,
840 ..Default::default()
841 };
842 let result =
843 transform_messages(&msgs, Api::AnthropicMessages, Api::OpenAiCompletions, opts);
844
845 match &result[0] {
846 Message::Assistant(a) => {
847 assert_eq!(a.content.len(), 1);
849 assert_eq!(a.text_content(), "Final answer.");
850 }
851 _ => panic!("Expected Assistant"),
852 }
853 }
854
855 #[test]
858 fn test_tool_calls_preserved() {
859 let tool_call = ContentBlock::ToolCall(ToolCall::new(
860 "call_123",
861 "get_weather",
862 serde_json::json!({"city": "Tokyo"}),
863 ));
864
865 let msgs = vec![
866 assistant_msg(
867 Api::AnthropicMessages,
868 "anthropic",
869 "claude-3.5-sonnet",
870 vec![
871 ContentBlock::Text(TextContent::new("Let me check.")),
872 tool_call,
873 ],
874 ),
875 tool_result_msg("call_123", "get_weather", "Sunny, 22°C"),
876 ];
877
878 let result = anthropic_to_openai(&msgs);
879
880 match &result[0] {
882 Message::Assistant(a) => {
883 let tc = a.content.iter().find_map(|b| b.as_tool_call());
884 assert!(tc.is_some(), "Tool call should be preserved");
885 let tc = tc.unwrap();
886 assert_eq!(tc.id, "call_123");
887 assert_eq!(tc.name, "get_weather");
888 }
889 _ => panic!("Expected Assistant"),
890 }
891
892 match &result[1] {
894 Message::ToolResult(t) => {
895 assert_eq!(t.tool_call_id, "call_123");
896 assert_eq!(t.tool_name, "get_weather");
897 }
898 _ => panic!("Expected ToolResult"),
899 }
900 }
901
902 #[test]
905 fn test_tool_calls_dropped_with_option() {
906 let msgs = vec![assistant_msg(
907 Api::AnthropicMessages,
908 "anthropic",
909 "claude-3.5-sonnet",
910 vec![
911 ContentBlock::Text(TextContent::new("I will call a tool.")),
912 ContentBlock::ToolCall(ToolCall::new("tc_1", "search", serde_json::json!({}))),
913 ],
914 )];
915
916 let opts = TransformOptions {
917 convert_tools: false,
918 ..Default::default()
919 };
920 let result =
921 transform_messages(&msgs, Api::AnthropicMessages, Api::OpenAiCompletions, opts);
922
923 match &result[0] {
924 Message::Assistant(a) => {
925 assert_eq!(a.content.len(), 1);
926 assert_eq!(a.text_content(), "I will call a tool.");
927 }
928 _ => panic!("Expected Assistant"),
929 }
930 }
931
932 #[test]
935 fn test_image_block_conversion() {
936 let msgs = vec![assistant_msg(
937 Api::AnthropicMessages,
938 "anthropic",
939 "claude-3.5-sonnet",
940 vec![
941 ContentBlock::Text(TextContent::new("Here's the image:")),
942 ContentBlock::Image(ImageContent::new("iVBORw0KGgo=", "image/png")),
943 ],
944 )];
945
946 let result = anthropic_to_openai(&msgs);
947
948 match &result[0] {
949 Message::Assistant(a) => {
950 let has_text = a.content.iter().any(|b| matches!(b, ContentBlock::Text(_)));
951 let has_image = a
952 .content
953 .iter()
954 .any(|b| matches!(b, ContentBlock::Image(_)));
955 assert!(has_text, "Text block should be preserved");
956 assert!(has_image, "Image block should be preserved");
957 }
958 _ => panic!("Expected Assistant"),
959 }
960 }
961
962 #[test]
965 fn test_openai_to_anthropic_roundtrip() {
966 let original = vec![
967 user_msg("What is 2+2?"),
968 assistant_msg(
969 Api::OpenAiCompletions,
970 "openai",
971 "gpt-4o",
972 vec![ContentBlock::Text(TextContent::new("The answer is 4."))],
973 ),
974 ];
975
976 let to_anthropic = openai_to_anthropic(&original);
977 let back_to_openai = anthropic_to_openai(&to_anthropic);
978
979 match (&original[1], &back_to_openai[1]) {
981 (Message::Assistant(orig), Message::Assistant(rt)) => {
982 assert_eq!(orig.text_content(), rt.text_content());
983 }
984 _ => panic!("Expected Assistant messages"),
985 }
986 }
987
988 #[test]
991 fn test_google_to_openai() {
992 let msgs = vec![
993 user_msg("Summarize this"),
994 assistant_msg(
995 Api::GoogleGenerativeAi,
996 "google",
997 "gemini-2.0-flash",
998 vec![ContentBlock::Text(TextContent::new("Here's a summary."))],
999 ),
1000 ];
1001
1002 let result = google_to_openai(&msgs);
1003 assert_eq!(result.len(), 2);
1004
1005 match &result[1] {
1006 Message::Assistant(a) => {
1007 assert_eq!(a.api, Api::OpenAiCompletions);
1008 assert_eq!(a.text_content(), "Here's a summary.");
1009 }
1010 _ => panic!("Expected Assistant"),
1011 }
1012 }
1013
1014 #[test]
1017 fn test_same_api_noop() {
1018 let msgs = vec![user_msg("Hello")];
1019 let result = transform_messages(
1020 &msgs,
1021 Api::AnthropicMessages,
1022 Api::AnthropicMessages,
1023 TransformOptions::default(),
1024 );
1025 assert_eq!(result.len(), 1);
1026 match &result[0] {
1027 Message::User(u) => assert_eq!(u.content.as_str(), Some("Hello")),
1028 _ => panic!("Expected User"),
1029 }
1030 }
1031
1032 #[test]
1035 fn test_thinking_preserved_for_anthropic_target() {
1036 let msgs = vec![assistant_msg(
1037 Api::OpenAiCompletions,
1038 "openai",
1039 "gpt-4o",
1040 vec![
1041 ContentBlock::Thinking(ThinkingContent::new("Reasoning...")),
1042 ContentBlock::Text(TextContent::new("Answer.")),
1043 ],
1044 )];
1045
1046 let result = openai_to_anthropic(&msgs);
1047
1048 match &result[0] {
1049 Message::Assistant(a) => {
1050 let has_thinking = a
1052 .content
1053 .iter()
1054 .any(|b| matches!(b, ContentBlock::Thinking(_)));
1055 assert!(
1056 has_thinking,
1057 "Thinking block should be preserved for Anthropic"
1058 );
1059 }
1060 _ => panic!("Expected Assistant"),
1061 }
1062 }
1063
1064 #[test]
1067 fn test_anthropic_to_google_thinking() {
1068 let msgs = vec![assistant_msg(
1069 Api::AnthropicMessages,
1070 "anthropic",
1071 "claude-3.5-sonnet",
1072 vec![
1073 ContentBlock::Thinking(ThinkingContent::new("Deep thought")),
1074 ContentBlock::Text(TextContent::new("Result.")),
1075 ],
1076 )];
1077
1078 let result = anthropic_to_google(&msgs);
1079
1080 match &result[0] {
1081 Message::Assistant(a) => {
1082 let has_thinking = a
1084 .content
1085 .iter()
1086 .any(|b| matches!(b, ContentBlock::Thinking(_)));
1087 assert!(
1088 !has_thinking,
1089 "Google target should not have thinking blocks"
1090 );
1091 let text = a.text_content();
1093 assert!(text.contains("<thinking>"));
1094 assert!(text.contains("Deep thought"));
1095 assert!(text.contains("Result."));
1096 }
1097 _ => panic!("Expected Assistant"),
1098 }
1099 }
1100
1101 #[test]
1104 fn test_full_conversation_mixed_blocks() {
1105 let msgs = vec![
1106 user_msg("What's the weather in Paris?"),
1107 assistant_msg(
1108 Api::AnthropicMessages,
1109 "anthropic",
1110 "claude-3.5-sonnet",
1111 vec![
1112 ContentBlock::Thinking(ThinkingContent::new("User wants weather.")),
1113 ContentBlock::Text(TextContent::new("Let me check.")),
1114 ContentBlock::ToolCall(ToolCall::new(
1115 "tc_001",
1116 "get_weather",
1117 serde_json::json!({"location": "Paris"}),
1118 )),
1119 ],
1120 ),
1121 tool_result_msg("tc_001", "get_weather", "Rainy, 15°C"),
1122 assistant_msg(
1123 Api::AnthropicMessages,
1124 "anthropic",
1125 "claude-3.5-sonnet",
1126 vec![ContentBlock::Text(TextContent::new(
1127 "It's rainy and 15°C in Paris.",
1128 ))],
1129 ),
1130 ];
1131
1132 let result = anthropic_to_openai(&msgs);
1133 assert_eq!(result.len(), 4, "All 4 messages should be preserved");
1134
1135 match &result[1] {
1137 Message::Assistant(a) => {
1138 let has_tool = a
1139 .content
1140 .iter()
1141 .any(|b| matches!(b, ContentBlock::ToolCall(_)));
1142 assert!(has_tool, "Tool call should be preserved");
1143 let has_thinking = a
1144 .content
1145 .iter()
1146 .any(|b| matches!(b, ContentBlock::Thinking(_)));
1147 assert!(
1148 !has_thinking,
1149 "Thinking should be converted to text for OpenAI"
1150 );
1151 }
1152 _ => panic!("Expected Assistant"),
1153 }
1154
1155 match &result[2] {
1157 Message::ToolResult(t) => {
1158 assert_eq!(t.tool_call_id, "tc_001");
1159 }
1160 _ => panic!("Expected ToolResult"),
1161 }
1162
1163 match &result[3] {
1165 Message::Assistant(a) => {
1166 assert_eq!(a.text_content(), "It's rainy and 15°C in Paris.");
1167 }
1168 _ => panic!("Expected Assistant"),
1169 }
1170 }
1171
1172 #[test]
1175 fn test_images_dropped_with_option() {
1176 let msgs = vec![Message::User(UserMessage::new(vec![
1177 ContentBlock::Text(TextContent::new("Describe this:")),
1178 ContentBlock::Image(ImageContent::new("AAAA", "image/jpeg")),
1179 ]))];
1180
1181 let opts = TransformOptions {
1182 convert_images: false,
1183 ..Default::default()
1184 };
1185 let result =
1186 transform_messages(&msgs, Api::AnthropicMessages, Api::OpenAiCompletions, opts);
1187
1188 match &result[0] {
1189 Message::User(u) => match &u.content {
1190 MessageContent::Blocks(blocks) => {
1191 let has_image = blocks.iter().any(|b| matches!(b, ContentBlock::Image(_)));
1192 assert!(!has_image, "Image should be dropped");
1193 assert_eq!(blocks.len(), 1);
1194 }
1195 _ => panic!("Expected blocks"),
1196 },
1197 _ => panic!("Expected User"),
1198 }
1199 }
1200
1201 #[test]
1204 fn test_assistant_metadata_preserved() {
1205 let mut a = AssistantMessage::new(Api::AnthropicMessages, "anthropic", "claude-3.5-sonnet");
1206 a.content = vec![ContentBlock::Text(TextContent::new("Hi"))];
1207 a.usage = Usage {
1208 input: 100,
1209 output: 50,
1210 cache_read: 10,
1211 cache_write: 5,
1212 total_tokens: 165,
1213 cost: Default::default(),
1214 };
1215 a.stop_reason = StopReason::Stop;
1216 a.error_message = None;
1217 a.response_id = Some("msg_abc123".to_string());
1218 let original_ts = a.timestamp;
1219
1220 let msgs = vec![Message::Assistant(a)];
1221 let result = anthropic_to_openai(&msgs);
1222
1223 match &result[0] {
1224 Message::Assistant(a) => {
1225 assert_eq!(a.usage.input, 100);
1226 assert_eq!(a.usage.output, 50);
1227 assert_eq!(a.stop_reason, StopReason::Stop);
1228 assert_eq!(a.response_id, Some("msg_abc123".to_string()));
1229 assert_eq!(a.timestamp, original_ts);
1230 assert_eq!(a.api, Api::OpenAiCompletions);
1231 }
1232 _ => panic!("Expected Assistant"),
1233 }
1234 }
1235
1236 #[test]
1239 fn test_error_tool_result_preserved() {
1240 let err = ToolResultMessage::error("tc_err", "failing_tool", "Something went wrong");
1241 let msgs = vec![Message::ToolResult(err)];
1242
1243 let result = anthropic_to_openai(&msgs);
1244 match &result[0] {
1245 Message::ToolResult(t) => {
1246 assert!(t.is_error);
1247 assert_eq!(t.tool_call_id, "tc_err");
1248 assert_eq!(t.tool_name, "failing_tool");
1249 }
1250 _ => panic!("Expected ToolResult"),
1251 }
1252 }
1253}