use crate::apis::amazon_bedrock::{ AnyChoice, AutoChoice, ContentBlock, ConversationRole, ConverseRequest, ImageBlock, ImageSource, InferenceConfiguration, Message as BedrockMessage, SystemContentBlock, Tool as BedrockTool, ToolChoice as BedrockToolChoice, ToolChoiceSpec, ToolConfiguration, ToolInputSchema, ToolResultBlock, ToolResultContentBlock, ToolResultStatus, ToolSpecDefinition, ToolUseBlock, }; use crate::apis::anthropic::{ MessagesMessage, MessagesMessageContent, MessagesRequest, MessagesRole, MessagesStopReason, MessagesSystemPrompt, MessagesTool, MessagesToolChoice, MessagesToolChoiceType, MessagesUsage, ToolResultContent, }; use crate::apis::openai::{ ChatCompletionsRequest, ContentPart, FinishReason, Function, FunctionChoice, Message, MessageContent, Role, Tool, ToolCall, ToolChoice, ToolChoiceType, Usage, }; use crate::clients::TransformError; use crate::transforms::lib::*; type AnthropicMessagesRequest = MessagesRequest; // Conversion from Anthropic MessagesRequest to OpenAI ChatCompletionsRequest impl TryFrom for ChatCompletionsRequest { type Error = TransformError; fn try_from(req: AnthropicMessagesRequest) -> Result { let mut openai_messages: Vec = Vec::new(); // Convert system prompt to system message if present if let Some(system) = req.system { openai_messages.push(system.into()); } // Convert messages for message in req.messages { let converted_messages: Vec = message.try_into()?; openai_messages.extend(converted_messages); } // Convert tools and tool choice let openai_tools = req.tools.map(|tools| convert_anthropic_tools(tools)); let (openai_tool_choice, parallel_tool_calls) = convert_anthropic_tool_choice(req.tool_choice); let mut _chat_completions_req: ChatCompletionsRequest = ChatCompletionsRequest { model: req.model, messages: openai_messages, temperature: req.temperature, top_p: req.top_p, max_completion_tokens: Some(req.max_tokens), stream: req.stream, stop: req.stop_sequences, tools: openai_tools, tool_choice: openai_tool_choice, parallel_tool_calls, ..Default::default() }; _chat_completions_req.suppress_max_tokens_if_o3(); _chat_completions_req.fix_temperature_if_gpt5(); Ok(_chat_completions_req) } } // Conversion from Anthropic MessagesRequest to Amazon Bedrock ConverseRequest impl TryFrom for ConverseRequest { type Error = TransformError; fn try_from(req: AnthropicMessagesRequest) -> Result { // Convert system prompt to SystemContentBlock if present let system: Option> = req.system.map(|system_prompt| { let text = match system_prompt { MessagesSystemPrompt::Single(text) => text, MessagesSystemPrompt::Blocks(blocks) => blocks.extract_text(), }; vec![SystemContentBlock::Text { text }] }); // Convert messages to Bedrock format let messages = if req.messages.is_empty() { None } else { let mut bedrock_messages = Vec::new(); for anthropic_message in req.messages { let bedrock_message: BedrockMessage = anthropic_message.try_into()?; bedrock_messages.push(bedrock_message); } Some(bedrock_messages) }; // Build inference configuration // Anthropic always requires max_tokens, so we should always include inferenceConfig let inference_config = Some(InferenceConfiguration { max_tokens: Some(req.max_tokens), temperature: req.temperature, top_p: req.top_p, stop_sequences: req.stop_sequences, }); // Convert tools and tool choice to ToolConfiguration // Only include toolConfig if we have actual tools (Bedrock requires at least 1 tool) let tool_config = req.tools.and_then(|anthropic_tools| { if anthropic_tools.is_empty() { return None; } let tools = anthropic_tools .into_iter() .map(|tool| BedrockTool::ToolSpec { tool_spec: ToolSpecDefinition { name: tool.name, description: tool.description, input_schema: ToolInputSchema { json: tool.input_schema, }, }, }) .collect(); let tool_choice = req.tool_choice.map(|choice| { match choice.kind { MessagesToolChoiceType::Auto => BedrockToolChoice::Auto { auto: AutoChoice {}, }, MessagesToolChoiceType::Any => BedrockToolChoice::Any { any: AnyChoice {} }, MessagesToolChoiceType::None => BedrockToolChoice::Auto { auto: AutoChoice {}, }, // Bedrock doesn't have explicit "none" MessagesToolChoiceType::Tool => { if let Some(name) = choice.name { BedrockToolChoice::Tool { tool: ToolChoiceSpec { name }, } } else { BedrockToolChoice::Auto { auto: AutoChoice {}, } } } } }); Some(ToolConfiguration { tools: Some(tools), tool_choice, }) }); Ok(ConverseRequest { model_id: req.model, messages, system, inference_config, tool_config, stream: req.stream.unwrap_or(false), guardrail_config: None, additional_model_request_fields: None, additional_model_response_field_paths: None, performance_config: None, prompt_variables: None, request_metadata: None, metadata: None, }) } } // Message Conversions impl TryFrom for Vec { type Error = TransformError; fn try_from(message: MessagesMessage) -> Result { let mut result = Vec::new(); match message.content { MessagesMessageContent::Single(text) => { result.push(Message { role: message.role.into(), content: MessageContent::Text(text), name: None, tool_calls: None, tool_call_id: None, }); } MessagesMessageContent::Blocks(blocks) => { let (content_parts, tool_calls, tool_results) = blocks.split_for_openai()?; // Add tool result messages for (tool_use_id, result_text, _is_error) in tool_results { result.push(Message { role: Role::Tool, content: MessageContent::Text(result_text), name: None, tool_calls: None, tool_call_id: Some(tool_use_id), }); } // Only create main message if there's actual content or tool calls // Skip creating empty content messages (e.g., when message only contains tool_result blocks) if !content_parts.is_empty() || !tool_calls.is_empty() { let content = build_openai_content(content_parts, &tool_calls); let main_message = Message { role: message.role.into(), content, name: None, tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) }, tool_call_id: None, }; result.push(main_message); } } } Ok(result) } } // Role Conversions impl Into for MessagesRole { fn into(self) -> Role { match self { MessagesRole::User => Role::User, MessagesRole::Assistant => Role::Assistant, } } } impl Into for FinishReason { fn into(self) -> MessagesStopReason { match self { FinishReason::Stop => MessagesStopReason::EndTurn, FinishReason::Length => MessagesStopReason::MaxTokens, FinishReason::ToolCalls => MessagesStopReason::ToolUse, FinishReason::ContentFilter => MessagesStopReason::Refusal, FinishReason::FunctionCall => MessagesStopReason::ToolUse, } } } impl Into for Usage { fn into(self) -> MessagesUsage { MessagesUsage { input_tokens: self.prompt_tokens, output_tokens: self.completion_tokens, cache_creation_input_tokens: None, cache_read_input_tokens: None, } } } // System Prompt Conversions impl Into for MessagesSystemPrompt { fn into(self) -> Message { let system_content = match self { MessagesSystemPrompt::Single(text) => MessageContent::Text(text), MessagesSystemPrompt::Blocks(blocks) => MessageContent::Text(blocks.extract_text()), }; Message { role: Role::System, content: system_content, name: None, tool_calls: None, tool_call_id: None, } } } //Utility Functions /// Convert Anthropic tools to OpenAI format fn convert_anthropic_tools(tools: Vec) -> Vec { tools .into_iter() .map(|tool| Tool { tool_type: "function".to_string(), function: Function { name: tool.name, description: tool.description, parameters: tool.input_schema, strict: None, }, }) .collect() } /// Convert Anthropic tool choice to OpenAI format fn convert_anthropic_tool_choice( tool_choice: Option, ) -> (Option, Option) { match tool_choice { Some(choice) => { let openai_choice = match choice.kind { MessagesToolChoiceType::Auto => ToolChoice::Type(ToolChoiceType::Auto), MessagesToolChoiceType::Any => ToolChoice::Type(ToolChoiceType::Required), MessagesToolChoiceType::None => ToolChoice::Type(ToolChoiceType::None), MessagesToolChoiceType::Tool => { if let Some(name) = choice.name { ToolChoice::Function { choice_type: "function".to_string(), function: FunctionChoice { name }, } } else { ToolChoice::Type(ToolChoiceType::Auto) } } }; let parallel = choice.disable_parallel_tool_use.map(|disable| !disable); (Some(openai_choice), parallel) } None => (None, None), } } /// Build OpenAI message content from parts and tool calls fn build_openai_content( content_parts: Vec, tool_calls: &[ToolCall], ) -> MessageContent { if content_parts.len() == 1 && tool_calls.is_empty() { match &content_parts[0] { ContentPart::Text { text } => MessageContent::Text(text.clone()), _ => MessageContent::Parts(content_parts), } } else if content_parts.is_empty() { MessageContent::Text("".to_string()) } else { MessageContent::Parts(content_parts) } } impl TryFrom for BedrockMessage { type Error = TransformError; fn try_from(message: MessagesMessage) -> Result { let role = match message.role { MessagesRole::User => ConversationRole::User, MessagesRole::Assistant => ConversationRole::Assistant, }; let mut content_blocks = Vec::new(); // Convert content blocks match message.content { MessagesMessageContent::Single(text) => { if !text.is_empty() { content_blocks.push(ContentBlock::Text { text }); } } MessagesMessageContent::Blocks(blocks) => { for block in blocks { match block { crate::apis::anthropic::MessagesContentBlock::Text { text, .. } => { if !text.is_empty() { content_blocks.push(ContentBlock::Text { text }); } } crate::apis::anthropic::MessagesContentBlock::ToolUse { id, name, input, .. } => { content_blocks.push(ContentBlock::ToolUse { tool_use: ToolUseBlock { tool_use_id: id, name, input, }, }); } crate::apis::anthropic::MessagesContentBlock::ToolResult { tool_use_id, is_error, content, .. } => { // Convert Anthropic ToolResultContent to Bedrock ToolResultContentBlock let tool_result_content = match content { ToolResultContent::Text(text) => { vec![ToolResultContentBlock::Text { text }] } ToolResultContent::Blocks(blocks) => { let mut result_blocks = Vec::new(); for result_block in blocks { match result_block { crate::apis::anthropic::MessagesContentBlock::Text { text, .. } => { result_blocks.push(ToolResultContentBlock::Text { text }); } // For now, skip other content types in tool results _ => {} } } result_blocks } }; // Ensure we have at least one content block let final_content = if tool_result_content.is_empty() { vec![ToolResultContentBlock::Text { text: " ".to_string(), }] } else { tool_result_content }; let status = if is_error.unwrap_or(false) { Some(ToolResultStatus::Error) } else { Some(ToolResultStatus::Success) }; content_blocks.push(ContentBlock::ToolResult { tool_result: ToolResultBlock { tool_use_id, content: final_content, status, }, }); } crate::apis::anthropic::MessagesContentBlock::Image { source } => { // Convert Anthropic image to Bedrock image format match source { crate::apis::anthropic::MessagesImageSource::Base64 { media_type, data, } => { content_blocks.push(ContentBlock::Image { image: ImageBlock { source: ImageSource::Base64 { media_type, data }, }, }); } crate::apis::anthropic::MessagesImageSource::Url { .. } => { // Bedrock doesn't support URL-based images, skip for now // Could potentially download and convert to base64, but not implemented } } } // Skip other content types for now (Thinking, Document, etc.) _ => {} } } } } // Ensure we have at least one content block if content_blocks.is_empty() { content_blocks.push(ContentBlock::Text { text: " ".to_string(), }); } Ok(BedrockMessage { role, content: content_blocks, }) } } #[cfg(test)] mod tests { use super::*; use crate::apis::amazon_bedrock::{ ContentBlock, ConversationRole, ConverseRequest, SystemContentBlock, ToolChoice as BedrockToolChoice, }; use crate::apis::anthropic::{ MessagesMessage, MessagesMessageContent, MessagesRequest, MessagesRole, MessagesSystemPrompt, MessagesTool, MessagesToolChoice, MessagesToolChoiceType, }; use serde_json::json; #[test] fn test_anthropic_to_bedrock_basic_request() { let anthropic_request = MessagesRequest { model: "claude-3-5-sonnet-20241022".to_string(), messages: vec![MessagesMessage { role: MessagesRole::User, content: MessagesMessageContent::Single("Hello, how are you?".to_string()), }], max_tokens: 1000, container: None, mcp_servers: None, system: Some(MessagesSystemPrompt::Single( "You are a helpful assistant.".to_string(), )), metadata: None, service_tier: None, thinking: None, temperature: Some(0.7), top_p: Some(0.9), top_k: None, stream: Some(false), stop_sequences: Some(vec!["STOP".to_string()]), tools: None, tool_choice: None, }; let bedrock_request: ConverseRequest = anthropic_request.try_into().unwrap(); assert_eq!(bedrock_request.model_id, "claude-3-5-sonnet-20241022"); assert!(bedrock_request.system.is_some()); assert_eq!(bedrock_request.system.as_ref().unwrap().len(), 1); assert!(bedrock_request.messages.is_some()); let messages = bedrock_request.messages.as_ref().unwrap(); assert_eq!(messages.len(), 1); assert_eq!(messages[0].role, ConversationRole::User); if let ContentBlock::Text { text } = &messages[0].content[0] { assert_eq!(text, "Hello, how are you?"); } else { panic!("Expected text content block"); } let inference_config = bedrock_request.inference_config.as_ref().unwrap(); assert_eq!(inference_config.temperature, Some(0.7)); assert_eq!(inference_config.top_p, Some(0.9)); assert_eq!(inference_config.max_tokens, Some(1000)); assert_eq!( inference_config.stop_sequences, Some(vec!["STOP".to_string()]) ); } #[test] fn test_anthropic_to_bedrock_with_tools() { let anthropic_request = MessagesRequest { model: "claude-3-5-sonnet-20241022".to_string(), messages: vec![MessagesMessage { role: MessagesRole::User, content: MessagesMessageContent::Single("What's the weather like?".to_string()), }], max_tokens: 1000, container: None, mcp_servers: None, system: None, metadata: None, service_tier: None, thinking: None, temperature: None, top_p: None, top_k: None, stream: None, stop_sequences: None, tools: Some(vec![MessagesTool { name: "get_weather".to_string(), description: Some("Get current weather information".to_string()), input_schema: json!({ "type": "object", "properties": { "location": { "type": "string", "description": "The city name" } }, "required": ["location"] }), }]), tool_choice: Some(MessagesToolChoice { kind: MessagesToolChoiceType::Tool, name: Some("get_weather".to_string()), disable_parallel_tool_use: None, }), }; let bedrock_request: ConverseRequest = anthropic_request.try_into().unwrap(); assert_eq!(bedrock_request.model_id, "claude-3-5-sonnet-20241022"); assert!(bedrock_request.tool_config.is_some()); let tool_config = bedrock_request.tool_config.as_ref().unwrap(); assert!(tool_config.tools.is_some()); let tools = tool_config.tools.as_ref().unwrap(); assert_eq!(tools.len(), 1); let crate::apis::amazon_bedrock::Tool::ToolSpec { tool_spec } = &tools[0]; assert_eq!(tool_spec.name, "get_weather"); assert_eq!( tool_spec.description, Some("Get current weather information".to_string()) ); if let Some(BedrockToolChoice::Tool { tool }) = &tool_config.tool_choice { assert_eq!(tool.name, "get_weather"); } else { panic!("Expected specific tool choice"); } } #[test] fn test_anthropic_to_bedrock_auto_tool_choice() { let anthropic_request = MessagesRequest { model: "claude-3-5-sonnet-20241022".to_string(), messages: vec![MessagesMessage { role: MessagesRole::User, content: MessagesMessageContent::Single("Help me with something".to_string()), }], max_tokens: 500, container: None, mcp_servers: None, system: None, metadata: None, service_tier: None, thinking: None, temperature: None, top_p: None, top_k: None, stream: None, stop_sequences: None, tools: Some(vec![MessagesTool { name: "help_tool".to_string(), description: Some("A helpful tool".to_string()), input_schema: json!({ "type": "object", "properties": {} }), }]), tool_choice: Some(MessagesToolChoice { kind: MessagesToolChoiceType::Auto, name: None, disable_parallel_tool_use: None, }), }; let bedrock_request: ConverseRequest = anthropic_request.try_into().unwrap(); assert!(bedrock_request.tool_config.is_some()); let tool_config = bedrock_request.tool_config.as_ref().unwrap(); assert!(matches!( tool_config.tool_choice, Some(BedrockToolChoice::Auto { .. }) )); } #[test] fn test_anthropic_to_bedrock_multi_message_conversation() { let anthropic_request = MessagesRequest { model: "claude-3-5-sonnet-20241022".to_string(), messages: vec![ MessagesMessage { role: MessagesRole::User, content: MessagesMessageContent::Single("Hello".to_string()), }, MessagesMessage { role: MessagesRole::Assistant, content: MessagesMessageContent::Single( "Hi there! How can I help you?".to_string(), ), }, MessagesMessage { role: MessagesRole::User, content: MessagesMessageContent::Single("What's 2+2?".to_string()), }, ], max_tokens: 100, container: None, mcp_servers: None, system: Some(MessagesSystemPrompt::Single("Be concise".to_string())), metadata: None, service_tier: None, thinking: None, temperature: Some(0.5), top_p: None, top_k: None, stream: None, stop_sequences: None, tools: None, tool_choice: None, }; let bedrock_request: ConverseRequest = anthropic_request.try_into().unwrap(); assert!(bedrock_request.messages.is_some()); let messages = bedrock_request.messages.as_ref().unwrap(); assert_eq!(messages.len(), 3); assert_eq!(messages[0].role, ConversationRole::User); assert_eq!(messages[1].role, ConversationRole::Assistant); assert_eq!(messages[2].role, ConversationRole::User); // Check system prompt assert!(bedrock_request.system.is_some()); if let SystemContentBlock::Text { text } = &bedrock_request.system.as_ref().unwrap()[0] { assert_eq!(text, "Be concise"); } else { panic!("Expected system text block"); } } #[test] fn test_anthropic_message_to_bedrock_conversion() { let anthropic_message = MessagesMessage { role: MessagesRole::User, content: MessagesMessageContent::Single("Test message".to_string()), }; let bedrock_message: BedrockMessage = anthropic_message.try_into().unwrap(); assert_eq!(bedrock_message.role, ConversationRole::User); assert_eq!(bedrock_message.content.len(), 1); if let ContentBlock::Text { text } = &bedrock_message.content[0] { assert_eq!(text, "Test message"); } else { panic!("Expected text content block"); } } }