use crate::error::Result;
use crate::providers::anthropic::convert::to_anthropic_request;
use crate::providers::anthropic::types::AnthropicConfig;
use crate::types::{CacheWarning, GenerateRequest};
const BEDROCK_ANTHROPIC_VERSION: &str = "bedrock-2023-05-31";
pub struct BedrockConversionResult {
pub body: serde_json::Value,
pub model_id: String,
pub warnings: Vec<CacheWarning>,
}
pub fn to_bedrock_body(
req: &GenerateRequest,
config: &AnthropicConfig,
) -> Result<BedrockConversionResult> {
let conversion_result = to_anthropic_request(req, config, false)?;
let mut body = serde_json::to_value(&conversion_result.request).map_err(|e| {
crate::error::Error::invalid_response(format!("Failed to serialize request: {}", e))
})?;
let model_id = super::models::resolve_bedrock_model_id(&req.model.id);
if let serde_json::Value::Object(ref mut map) = body {
map.insert(
"anthropic_version".to_string(),
serde_json::Value::String(BEDROCK_ANTHROPIC_VERSION.to_string()),
);
map.remove("model");
map.remove("stream");
}
Ok(BedrockConversionResult {
body,
model_id,
warnings: conversion_result.warnings,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::providers::anthropic::types::AnthropicConfig;
use crate::types::{GenerateRequest, Message, Model, Role};
fn dummy_anthropic_config() -> AnthropicConfig {
AnthropicConfig::new("dummy-key-for-bedrock")
}
#[test]
fn test_bedrock_body_has_anthropic_version() {
let request = GenerateRequest::new(
Model::custom("anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
vec![Message::new(Role::User, "Hello")],
);
let result = to_bedrock_body(&request, &dummy_anthropic_config()).unwrap();
assert_eq!(result.body["anthropic_version"], "bedrock-2023-05-31");
}
#[test]
fn test_bedrock_body_removes_model() {
let request = GenerateRequest::new(
Model::custom("anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
vec![Message::new(Role::User, "Hello")],
);
let result = to_bedrock_body(&request, &dummy_anthropic_config()).unwrap();
assert!(result.body.get("model").is_none());
assert_eq!(result.model_id, "anthropic.claude-sonnet-4-5-20250929-v1:0");
}
#[test]
fn test_bedrock_body_removes_stream() {
let request = GenerateRequest::new(
Model::custom("anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
vec![Message::new(Role::User, "Hello")],
);
let result = to_bedrock_body(&request, &dummy_anthropic_config()).unwrap();
assert!(result.body.get("stream").is_none());
}
#[test]
fn test_bedrock_body_no_anthropic_beta() {
let request = GenerateRequest::new(
Model::custom("anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
vec![Message::new(Role::User, "Hello")],
);
let result = to_bedrock_body(&request, &dummy_anthropic_config()).unwrap();
assert!(
result.body.get("anthropic_beta").is_none(),
"anthropic_beta must NOT be present in Bedrock body — Bedrock rejects it"
);
}
#[test]
fn test_bedrock_body_cache_control_without_beta() {
let request = GenerateRequest::new(
Model::custom("anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
vec![Message::new(Role::User, "Hello")],
);
let result = to_bedrock_body(&request, &dummy_anthropic_config()).unwrap();
assert!(
result.body.get("anthropic_beta").is_none(),
"anthropic_beta must NOT be present even when cache_control is used"
);
}
#[test]
fn test_bedrock_body_preserves_messages() {
let request = GenerateRequest::new(
Model::custom("anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
vec![Message::new(Role::User, "What is Rust?")],
);
let result = to_bedrock_body(&request, &dummy_anthropic_config()).unwrap();
let messages = result.body["messages"].as_array().unwrap();
assert!(!messages.is_empty());
assert_eq!(messages[0]["role"], "user");
}
#[test]
fn test_bedrock_body_preserves_max_tokens() {
let request = GenerateRequest::new(
Model::custom("anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
vec![Message::new(Role::User, "Hello")],
);
let result = to_bedrock_body(&request, &dummy_anthropic_config()).unwrap();
assert!(result.body.get("max_tokens").is_some());
}
#[test]
fn test_bedrock_body_preserves_cache_control_in_messages_system_tools() {
use crate::types::{CacheControl, GenerateOptions, Tool};
use serde_json::json;
let system_msg = Message::new(Role::System, "You are a helpful assistant.")
.with_cache_control(CacheControl::ephemeral());
let user_msg =
Message::new(Role::User, "Hello").with_cache_control(CacheControl::ephemeral());
let tool = Tool::function("search", "Search documents")
.parameters(json!({"type": "object", "properties": {}}))
.with_cache_control(CacheControl::ephemeral());
let options = GenerateOptions::default().add_tool(tool);
let mut request = GenerateRequest::new(
Model::custom("anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
vec![system_msg, user_msg],
);
request.options = options;
let result = to_bedrock_body(&request, &dummy_anthropic_config()).unwrap();
let system = result.body.get("system").expect("system field must exist");
let system_arr = system.as_array().expect("system should be an array");
let has_system_cache = system_arr
.iter()
.any(|block| block.get("cache_control").is_some());
assert!(
has_system_cache,
"cache_control must be preserved on system message, got: {system_arr:?}"
);
let messages = result.body["messages"].as_array().expect("messages array");
let user_msg_body = &messages[0]; assert_eq!(user_msg_body["role"], "user");
let content = &user_msg_body["content"];
let has_msg_cache = if let Some(arr) = content.as_array() {
arr.iter().any(|block| block.get("cache_control").is_some())
} else {
false
};
assert!(
has_msg_cache,
"cache_control must be preserved on user message, got: {content:?}"
);
let tools = result.body.get("tools").expect("tools field must exist");
let tools_arr = tools.as_array().expect("tools should be an array");
let has_tool_cache = tools_arr
.iter()
.any(|tool| tool.get("cache_control").is_some());
assert!(
has_tool_cache,
"cache_control must be preserved on tool definitions, got: {tools_arr:?}"
);
assert!(
result.body.get("anthropic_beta").is_none(),
"anthropic_beta must NOT be present in Bedrock body"
);
}
#[test]
fn test_anthropic_model_id_maps_to_bedrock() {
let request = GenerateRequest::new(
Model::custom("claude-sonnet-4-5-20250929", "bedrock"),
vec![Message::new(Role::User, "Hello")],
);
let result = to_bedrock_body(&request, &dummy_anthropic_config()).unwrap();
assert_eq!(
result.model_id,
"us.anthropic.claude-sonnet-4-5-20250929-v1:0"
);
assert!(result.body.get("model").is_none());
}
#[test]
fn test_cross_region_model_id_passthrough() {
let request = GenerateRequest::new(
Model::custom("us.anthropic.claude-sonnet-4-5-20250929-v1:0", "bedrock"),
vec![Message::new(Role::User, "Hello")],
);
let result = to_bedrock_body(&request, &dummy_anthropic_config()).unwrap();
assert_eq!(
result.model_id,
"us.anthropic.claude-sonnet-4-5-20250929-v1:0"
);
}
}