use super::request::is_o_series_model;
use super::*;
use crate::{anthropic, openai};
use serde_json::json;
fn basic_request() -> anthropic::MessageCreateRequest {
anthropic::MessageCreateRequest {
model: "claude-3-5-sonnet-20241022".to_string(),
max_tokens: 1024,
messages: vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Text("Hello".to_string()),
}],
system: None,
temperature: None,
top_p: None,
top_k: None,
stop_sequences: None,
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
stream: None,
extra: serde_json::Map::new(),
}
}
fn basic_openai_response() -> openai::ChatCompletionResponse {
openai::ChatCompletionResponse {
id: "chatcmpl-abc123".to_string(),
object: "chat.completion".to_string(),
model: "gpt-4o".to_string(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: Some(openai::ChatContent::Text("Hi there!".to_string())),
name: None,
tool_calls: None,
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::Stop),
logprobs: None,
}],
usage: Some(openai::ChatUsage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
completion_tokens_details: None,
prompt_tokens_details: None,
}),
created: Some(1700000000),
system_fingerprint: None,
service_tier: None,
}
}
#[test]
fn basic_text_request() {
let req = basic_request();
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.model, "claude-3-5-sonnet-20241022");
assert_eq!(oai.max_tokens, Some(1024));
assert_eq!(oai.max_completion_tokens, Some(1024));
assert_eq!(oai.messages.len(), 1);
assert_eq!(oai.messages[0].role, openai::ChatRole::User);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t == "Hello"
));
assert!(oai.tools.is_none());
assert!(oai.tool_choice.is_none());
assert!(oai.stream_options.is_none());
}
#[test]
fn system_prompt_string_becomes_developer_message() {
let mut req = basic_request();
req.system = Some(anthropic::System::Text(
"You are a helpful assistant.".to_string(),
));
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages.len(), 2);
assert_eq!(oai.messages[0].role, openai::ChatRole::System);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t == "You are a helpful assistant."
));
}
#[test]
fn system_prompt_blocks_concatenated_into_developer_message() {
let mut req = basic_request();
req.system = Some(anthropic::System::Blocks(vec![
anthropic::messages::SystemBlock {
block_type: "text".to_string(),
text: "Be concise.".to_string(),
cache_control: None,
},
anthropic::messages::SystemBlock {
block_type: "text".to_string(),
text: "Respond in JSON.".to_string(),
cache_control: None,
},
]));
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages[0].role, openai::ChatRole::System);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t == "Be concise.\nRespond in JSON."
));
}
#[test]
fn tool_definitions_mapped() {
let schema = json!({
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"]
});
let mut req = basic_request();
req.tools = Some(vec![anthropic::Tool {
name: "get_weather".to_string(),
description: Some("Get weather for a location".to_string()),
input_schema: schema.clone(),
}]);
let oai = anthropic_to_openai_request(&req);
let tools = oai.tools.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].tool_type, "function");
assert_eq!(tools[0].function.name, "get_weather");
assert_eq!(
tools[0].function.description.as_deref(),
Some("Get weather for a location")
);
assert_eq!(tools[0].function.parameters, Some(schema));
}
#[test]
fn tool_choice_auto() {
let mut req = basic_request();
req.tool_choice = Some(anthropic::ToolChoice::Auto {
disable_parallel_tool_use: None,
});
let oai = anthropic_to_openai_request(&req);
assert!(matches!(
oai.tool_choice,
Some(openai::ChatToolChoice::Simple(ref s)) if s == "auto"
));
}
#[test]
fn tool_choice_any_becomes_required() {
let mut req = basic_request();
req.tool_choice = Some(anthropic::ToolChoice::Any {
disable_parallel_tool_use: None,
});
let oai = anthropic_to_openai_request(&req);
assert!(matches!(
oai.tool_choice,
Some(openai::ChatToolChoice::Simple(ref s)) if s == "required"
));
}
#[test]
fn tool_choice_none() {
let mut req = basic_request();
req.tool_choice = Some(anthropic::ToolChoice::None);
let oai = anthropic_to_openai_request(&req);
assert!(matches!(
oai.tool_choice,
Some(openai::ChatToolChoice::Simple(ref s)) if s == "none"
));
}
#[test]
fn tool_choice_specific_tool() {
let mut req = basic_request();
req.tool_choice = Some(anthropic::ToolChoice::Tool {
name: "get_weather".to_string(),
});
let oai = anthropic_to_openai_request(&req);
match oai.tool_choice {
Some(openai::ChatToolChoice::Named(ref n)) => {
assert_eq!(n.choice_type, "function");
assert_eq!(n.function.name, "get_weather");
}
other => panic!("expected Named tool choice, got {:?}", other),
}
}
#[test]
fn disable_parallel_tool_use_sets_parallel_tool_calls_false() {
let mut req = basic_request();
req.tool_choice = Some(anthropic::ToolChoice::Auto {
disable_parallel_tool_use: Some(true),
});
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.parallel_tool_calls, Some(false));
}
#[test]
fn disable_parallel_tool_use_false_leaves_parallel_tool_calls_none() {
let mut req = basic_request();
req.tool_choice = Some(anthropic::ToolChoice::Auto {
disable_parallel_tool_use: Some(false),
});
let oai = anthropic_to_openai_request(&req);
assert!(oai.parallel_tool_calls.is_none());
}
#[test]
fn no_tool_choice_leaves_parallel_tool_calls_none() {
let req = basic_request();
let oai = anthropic_to_openai_request(&req);
assert!(oai.parallel_tool_calls.is_none());
}
#[test]
fn stop_sequences_capped_at_four() {
let mut req = basic_request();
req.stop_sequences = Some(vec![
"a".into(),
"b".into(),
"c".into(),
"d".into(),
"e".into(),
]);
let oai = anthropic_to_openai_request(&req);
match oai.stop {
Some(openai::Stop::Multiple(ref v)) => assert_eq!(v.len(), 4),
other => panic!("expected Multiple stop, got {:?}", other),
}
}
#[test]
fn single_stop_sequence_is_single() {
let mut req = basic_request();
req.stop_sequences = Some(vec!["END".into()]);
let oai = anthropic_to_openai_request(&req);
assert!(matches!(
oai.stop,
Some(openai::Stop::Single(ref s)) if s == "END"
));
}
#[test]
fn empty_stop_sequences_becomes_none() {
let mut req = basic_request();
req.stop_sequences = Some(vec![]);
let oai = anthropic_to_openai_request(&req);
assert!(
oai.stop.is_none(),
"empty stop_sequences should map to None, not Stop::Multiple([])"
);
}
#[test]
fn streaming_sets_stream_options() {
let mut req = basic_request();
req.stream = Some(true);
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.stream, Some(true));
assert!(oai.stream_options.as_ref().unwrap().include_usage);
}
#[test]
fn conversation_with_tool_use_and_tool_result() {
let mut req = basic_request();
req.messages = vec![
anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Text("What is the weather in NYC?".to_string()),
},
anthropic::InputMessage {
role: anthropic::Role::Assistant,
content: anthropic::Content::Blocks(vec![
anthropic::ContentBlock::Text {
text: "Let me check.".to_string(),
},
anthropic::ContentBlock::ToolUse {
id: "call_001".to_string(),
name: "get_weather".to_string(),
input: json!({"location": "NYC"}),
},
]),
},
anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![anthropic::ContentBlock::ToolResult {
tool_use_id: "call_001".to_string(),
content: Some(anthropic::messages::ToolResultContent::Text(
"72F, sunny".to_string(),
)),
is_error: None,
}]),
},
];
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages[0].role, openai::ChatRole::User);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t == "What is the weather in NYC?"
));
assert_eq!(oai.messages[1].role, openai::ChatRole::Assistant);
assert!(matches!(
&oai.messages[1].content,
Some(openai::ChatContent::Text(t)) if t == "Let me check."
));
let tc = oai.messages[1].tool_calls.as_ref().unwrap();
assert_eq!(tc.len(), 1);
assert_eq!(tc[0].id, "call_001");
assert_eq!(tc[0].function.name, "get_weather");
assert_eq!(tc[0].function.arguments, r#"{"location":"NYC"}"#);
assert_eq!(oai.messages[2].role, openai::ChatRole::Tool);
assert_eq!(oai.messages[2].tool_call_id.as_deref(), Some("call_001"));
assert!(matches!(
&oai.messages[2].content,
Some(openai::ChatContent::Text(t)) if t == "72F, sunny"
));
}
#[test]
fn tool_result_error_prefixed() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![anthropic::ContentBlock::ToolResult {
tool_use_id: "call_err".to_string(),
content: Some(anthropic::messages::ToolResultContent::Text(
"not found".to_string(),
)),
is_error: Some(true),
}]),
}];
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages[0].role, openai::ChatRole::Tool);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t == "Error: not found"
));
}
#[test]
fn image_block_to_image_url_part() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![
anthropic::ContentBlock::Text {
text: "Describe this".to_string(),
},
anthropic::ContentBlock::Image {
source: anthropic::messages::ImageSource {
source_type: "base64".to_string(),
media_type: Some("image/jpeg".to_string()),
data: Some("abc123".to_string()),
url: None,
},
},
]),
}];
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages.len(), 1);
match &oai.messages[0].content {
Some(openai::ChatContent::Parts(parts)) => {
assert_eq!(parts.len(), 2);
assert!(matches!(
&parts[0],
openai::ChatContentPart::Text { text } if text == "Describe this"
));
match &parts[1] {
openai::ChatContentPart::ImageUrl { image_url } => {
assert_eq!(image_url.url, "data:image/jpeg;base64,abc123");
}
other => panic!("expected ImageUrl, got {:?}", other),
}
}
other => panic!("expected Parts, got {:?}", other),
}
}
#[test]
fn image_block_with_url_uses_url_directly() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![anthropic::ContentBlock::Image {
source: anthropic::messages::ImageSource {
source_type: "url".to_string(),
media_type: None,
data: None,
url: Some("https://example.com/img.png".to_string()),
},
}]),
}];
let oai = anthropic_to_openai_request(&req);
match &oai.messages[0].content {
Some(openai::ChatContent::Parts(parts)) => {
assert_eq!(parts.len(), 1);
match &parts[0] {
openai::ChatContentPart::ImageUrl { image_url } => {
assert_eq!(image_url.url, "https://example.com/img.png");
}
other => panic!("expected ImageUrl, got {:?}", other),
}
}
other => panic!("expected Parts, got {:?}", other),
}
}
#[test]
fn single_text_block_user_message_flattened() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![anthropic::ContentBlock::Text {
text: "just text".to_string(),
}]),
}];
let oai = anthropic_to_openai_request(&req);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t == "just text"
));
}
#[test]
fn openai_text_response_to_anthropic() {
let resp = basic_openai_response();
let anth = openai_to_anthropic_response(&resp, "claude-3-5-sonnet-20241022");
assert!(anth.id.starts_with("msg_"));
assert_eq!(anth.response_type, "message");
assert_eq!(anth.role, anthropic::Role::Assistant);
assert_eq!(anth.model, "claude-3-5-sonnet-20241022");
assert_eq!(anth.content.len(), 1);
assert!(matches!(
&anth.content[0],
anthropic::ContentBlock::Text { text } if text == "Hi there!"
));
assert_eq!(anth.stop_reason, Some(anthropic::StopReason::EndTurn));
assert!(anth.stop_sequence.is_none());
assert_eq!(anth.usage.input_tokens, 10);
assert_eq!(anth.usage.output_tokens, 5);
}
#[test]
fn openai_tool_calls_response_to_anthropic() {
let resp = openai::ChatCompletionResponse {
id: "chatcmpl-xyz".to_string(),
object: "chat.completion".to_string(),
model: "gpt-4o".to_string(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: None,
name: None,
tool_calls: Some(vec![openai::ToolCall {
id: "call_abc".to_string(),
call_type: "function".to_string(),
function: openai::FunctionCall {
name: "get_weather".to_string(),
arguments: r#"{"location":"NYC"}"#.to_string(),
},
}]),
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::ToolCalls),
logprobs: None,
}],
usage: Some(openai::ChatUsage {
prompt_tokens: 20,
completion_tokens: 10,
total_tokens: 30,
completion_tokens_details: None,
prompt_tokens_details: None,
}),
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "claude-3-5-sonnet-20241022");
assert_eq!(anth.stop_reason, Some(anthropic::StopReason::ToolUse));
assert_eq!(anth.content.len(), 1);
match &anth.content[0] {
anthropic::ContentBlock::ToolUse { id, name, input } => {
assert_eq!(id, "call_abc");
assert_eq!(name, "get_weather");
assert_eq!(input, &json!({"location": "NYC"}));
}
other => panic!("expected ToolUse, got {:?}", other),
}
}
#[test]
fn stop_reason_mapping() {
let cases = vec![
(openai::FinishReason::Stop, anthropic::StopReason::EndTurn),
(
openai::FinishReason::Length,
anthropic::StopReason::MaxTokens,
),
(
openai::FinishReason::ToolCalls,
anthropic::StopReason::ToolUse,
),
(
openai::FinishReason::ContentFilter,
anthropic::StopReason::EndTurn,
),
(
openai::FinishReason::FunctionCall,
anthropic::StopReason::ToolUse,
),
];
for (oai_reason, expected) in cases {
let resp = openai::ChatCompletionResponse {
id: "x".into(),
object: "chat.completion".into(),
model: "gpt-4o".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: Some(openai::ChatContent::Text("ok".into())),
name: None,
tool_calls: None,
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(oai_reason),
logprobs: None,
}],
usage: None,
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "m");
assert_eq!(anth.stop_reason, Some(expected));
}
}
#[test]
fn empty_content_response() {
let resp = openai::ChatCompletionResponse {
id: "x".into(),
object: "chat.completion".into(),
model: "gpt-4o".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: Some(openai::ChatContent::Text(String::new())),
name: None,
tool_calls: None,
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::Stop),
logprobs: None,
}],
usage: None,
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "m");
assert!(anth.content.is_empty());
}
#[test]
fn no_choices_produces_default_response() {
let resp = openai::ChatCompletionResponse {
id: "x".into(),
object: "chat.completion".into(),
model: "gpt-4o".into(),
choices: vec![],
usage: None,
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "m");
assert!(anth.content.is_empty());
assert_eq!(anth.stop_reason, Some(anthropic::StopReason::EndTurn));
}
#[test]
fn missing_usage_produces_defaults() {
let resp = openai::ChatCompletionResponse {
id: "x".into(),
object: "chat.completion".into(),
model: "gpt-4o".into(),
choices: vec![],
usage: None,
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "m");
assert_eq!(anth.usage.input_tokens, 0);
assert_eq!(anth.usage.output_tokens, 0);
}
#[test]
fn tool_result_blocks_content_concatenated() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![anthropic::ContentBlock::ToolResult {
tool_use_id: "call_1".to_string(),
content: Some(anthropic::messages::ToolResultContent::Blocks(vec![
anthropic::ContentBlock::Text {
text: "part1".to_string(),
},
anthropic::ContentBlock::Text {
text: "part2".to_string(),
},
])),
is_error: None,
}]),
}];
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages[0].role, openai::ChatRole::Tool);
match &oai.messages[0].content {
Some(openai::ChatContent::Parts(parts)) => {
assert_eq!(parts.len(), 2);
assert!(matches!(&parts[0], openai::ChatContentPart::Text { text } if text == "part1"));
assert!(matches!(&parts[1], openai::ChatContentPart::Text { text } if text == "part2"));
}
other => panic!("expected Parts with 2 text entries, got {:?}", other),
}
}
#[test]
fn tool_result_none_content_becomes_empty_string() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![anthropic::ContentBlock::ToolResult {
tool_use_id: "call_1".to_string(),
content: None,
is_error: None,
}]),
}];
let oai = anthropic_to_openai_request(&req);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t.is_empty()
));
}
#[test]
fn mixed_user_content_and_tool_results_produces_multiple_messages() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![
anthropic::ContentBlock::Text {
text: "Here are the results".to_string(),
},
anthropic::ContentBlock::ToolResult {
tool_use_id: "call_1".to_string(),
content: Some(anthropic::messages::ToolResultContent::Text(
"result1".to_string(),
)),
is_error: None,
},
]),
}];
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages.len(), 2);
assert_eq!(oai.messages[0].role, openai::ChatRole::Tool);
assert_eq!(oai.messages[1].role, openai::ChatRole::User);
}
#[test]
fn assistant_text_and_tool_use_combined() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::Assistant,
content: anthropic::Content::Blocks(vec![
anthropic::ContentBlock::Text {
text: "Thinking...".to_string(),
},
anthropic::ContentBlock::ToolUse {
id: "call_1".to_string(),
name: "search".to_string(),
input: json!({"q": "rust"}),
},
]),
}];
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages.len(), 1);
assert_eq!(oai.messages[0].role, openai::ChatRole::Assistant);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t == "Thinking..."
));
let tc = oai.messages[0].tool_calls.as_ref().unwrap();
assert_eq!(tc.len(), 1);
assert_eq!(tc[0].id, "call_1");
}
#[test]
fn openai_response_with_text_and_tool_calls() {
let resp = openai::ChatCompletionResponse {
id: "x".into(),
object: "chat.completion".into(),
model: "gpt-4o".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: Some(openai::ChatContent::Text("Let me check.".into())),
name: None,
tool_calls: Some(vec![openai::ToolCall {
id: "call_1".into(),
call_type: "function".into(),
function: openai::FunctionCall {
name: "lookup".into(),
arguments: "{}".into(),
},
}]),
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::ToolCalls),
logprobs: None,
}],
usage: Some(openai::ChatUsage {
prompt_tokens: 5,
completion_tokens: 3,
total_tokens: 8,
completion_tokens_details: None,
prompt_tokens_details: None,
}),
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "m");
assert_eq!(anth.content.len(), 2);
assert!(matches!(
&anth.content[0],
anthropic::ContentBlock::Text { text } if text == "Let me check."
));
assert!(matches!(
&anth.content[1],
anthropic::ContentBlock::ToolUse { id, name, .. } if id == "call_1" && name == "lookup"
));
}
#[test]
fn malformed_tool_arguments_handled() {
let resp = openai::ChatCompletionResponse {
id: "x".into(),
object: "chat.completion".into(),
model: "gpt-4o".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: None,
name: None,
tool_calls: Some(vec![openai::ToolCall {
id: "call_bad".into(),
call_type: "function".into(),
function: openai::FunctionCall {
name: "broken".into(),
arguments: "not json".into(),
},
}]),
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::ToolCalls),
logprobs: None,
}],
usage: None,
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "m");
match &anth.content[0] {
anthropic::ContentBlock::ToolUse { input, .. } => {
assert_eq!(input, &json!({"_raw_error": "not json"}));
}
other => panic!("expected ToolUse, got {:?}", other),
}
}
#[test]
fn document_block_converted_to_text_note() {
let req = anthropic::MessageCreateRequest {
model: "claude-opus-4-6".into(),
max_tokens: 1024,
messages: vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![
anthropic::ContentBlock::Text {
text: "Summarize this PDF".into(),
},
anthropic::ContentBlock::Document {
source: anthropic::messages::DocumentSource {
source_type: "base64".into(),
media_type: "application/pdf".into(),
data: "AAAA".into(),
},
title: Some("report.pdf".into()),
},
]),
}],
system: None,
temperature: None,
top_p: None,
top_k: None,
stop_sequences: None,
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
stream: None,
extra: serde_json::Map::new(),
};
let openai_req = anthropic_to_openai_request(&req);
assert_eq!(openai_req.messages.len(), 1);
match &openai_req.messages[0].content {
Some(openai::ChatContent::Parts(parts)) => {
assert_eq!(parts.len(), 2);
if let openai::ChatContentPart::Text { text } = &parts[1] {
assert!(text.contains("report.pdf"));
assert!(text.contains("application/pdf"));
} else {
panic!("expected text part for document");
}
}
other => panic!("expected Parts, got {:?}", other),
}
}
#[test]
fn created_timestamp_preserved_from_openai() {
let resp = basic_openai_response();
assert_eq!(resp.created, Some(1700000000));
let anth = openai_to_anthropic_response(&resp, "claude-sonnet-4-6");
assert_eq!(anth.created, Some(1700000000));
}
#[test]
fn created_timestamp_none_when_absent() {
let mut resp = basic_openai_response();
resp.created = None;
let anth = openai_to_anthropic_response(&resp, "claude-sonnet-4-6");
assert_eq!(anth.created, None);
let json = serde_json::to_string(&anth).unwrap();
assert!(!json.contains("\"created\""));
}
#[test]
fn thinking_config_stripped_in_translation() {
let mut req = basic_request();
req.thinking = Some(anthropic::ThinkingConfig::Enabled {
budget_tokens: 4096,
});
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.max_completion_tokens, Some(1024));
}
#[test]
fn thinking_block_mapped_to_reasoning_content_in_assistant_translation() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::Assistant,
content: anthropic::Content::Blocks(vec![
anthropic::ContentBlock::Thinking {
thinking: "Let me reason...".into(),
signature: Some("sig_abc".into()),
},
anthropic::ContentBlock::Text {
text: "Here is my answer.".into(),
},
]),
}];
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages.len(), 1);
assert_eq!(
oai.messages[0].reasoning_content.as_deref(),
Some("Let me reason...")
);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t == "Here is my answer."
));
}
#[test]
fn redacted_thinking_block_dropped_in_assistant_translation() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::Assistant,
content: anthropic::Content::Blocks(vec![
anthropic::ContentBlock::RedactedThinking {
data: "encrypted_data".into(),
},
anthropic::ContentBlock::Text {
text: "My answer.".into(),
},
]),
}];
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages.len(), 1);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t == "My answer."
));
}
#[test]
fn temperature_clamped_to_zero_one() {
let mut req = basic_request();
req.temperature = Some(1.5);
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.temperature, Some(1.0));
req.temperature = Some(0.5);
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.temperature, Some(0.5));
req.temperature = Some(-0.1);
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.temperature, Some(0.0));
req.temperature = None;
let oai = anthropic_to_openai_request(&req);
assert!(oai.temperature.is_none());
}
#[test]
fn metadata_user_id_maps_to_openai_user() {
let mut req = basic_request();
req.metadata = Some(anthropic::messages::Metadata {
user_id: Some("u-abc123".into()),
});
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.user.as_deref(), Some("u-abc123"));
req.metadata = None;
let oai = anthropic_to_openai_request(&req);
assert!(oai.user.is_none());
}
#[test]
fn claude_code_parallel_tool_use_request() {
let mut req = basic_request();
req.messages = vec![
anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Text("Read config and list tests.".into()),
},
anthropic::InputMessage {
role: anthropic::Role::Assistant,
content: anthropic::Content::Blocks(vec![
anthropic::ContentBlock::Text {
text: "I'll do both.".into(),
},
anthropic::ContentBlock::ToolUse {
id: "toolu_01A".into(),
name: "Read".into(),
input: json!({"file_path": "/config.toml"}),
},
anthropic::ContentBlock::ToolUse {
id: "toolu_01B".into(),
name: "Glob".into(),
input: json!({"pattern": "**/*test*"}),
},
]),
},
];
let oai = anthropic_to_openai_request(&req);
let assistant_msg = &oai.messages[1];
assert_eq!(assistant_msg.role, openai::ChatRole::Assistant);
match assistant_msg.content.as_ref().unwrap() {
openai::ChatContent::Text(t) => assert_eq!(t, "I'll do both."),
other => panic!("expected Text content, got {:?}", other),
}
let tool_calls = assistant_msg.tool_calls.as_ref().unwrap();
assert_eq!(tool_calls.len(), 2);
assert_eq!(tool_calls[0].id, "toolu_01A");
assert_eq!(tool_calls[0].function.name, "Read");
assert_eq!(tool_calls[1].id, "toolu_01B");
assert_eq!(tool_calls[1].function.name, "Glob");
}
#[test]
fn claude_code_tool_result_request() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![
anthropic::ContentBlock::ToolResult {
tool_use_id: "toolu_01A".into(),
content: Some(anthropic::messages::ToolResultContent::Text(
"file contents here".into(),
)),
is_error: Some(false),
},
anthropic::ContentBlock::ToolResult {
tool_use_id: "toolu_01B".into(),
content: Some(anthropic::messages::ToolResultContent::Text(
"test1.rs\ntest2.rs".into(),
)),
is_error: Some(false),
},
]),
}];
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages.len(), 2);
assert_eq!(oai.messages[0].role, openai::ChatRole::Tool);
assert_eq!(oai.messages[0].tool_call_id.as_deref(), Some("toolu_01A"));
assert_eq!(oai.messages[1].role, openai::ChatRole::Tool);
assert_eq!(oai.messages[1].tool_call_id.as_deref(), Some("toolu_01B"));
}
#[test]
fn claude_code_tool_response_roundtrip() {
let resp = openai::ChatCompletionResponse {
id: "chatcmpl-llama001".into(),
object: "chat.completion".into(),
model: "llama-3.3-70b".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: Some(openai::ChatContent::Text("Reading file.".into())),
name: None,
tool_calls: Some(vec![
openai::ToolCall {
id: "call_read_001".into(),
call_type: "function".into(),
function: openai::FunctionCall {
name: "Read".into(),
arguments: r#"{"file_path":"/config.toml"}"#.into(),
},
},
openai::ToolCall {
id: "call_glob_001".into(),
call_type: "function".into(),
function: openai::FunctionCall {
name: "Glob".into(),
arguments: r#"{"pattern":"**/*test*"}"#.into(),
},
},
]),
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::ToolCalls),
logprobs: None,
}],
usage: Some(openai::ChatUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
completion_tokens_details: None,
prompt_tokens_details: None,
}),
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "claude-sonnet-4-20250514");
assert_eq!(anth.stop_reason, Some(anthropic::StopReason::ToolUse));
match &anth.content[0] {
anthropic::ContentBlock::Text { text } => assert_eq!(text, "Reading file."),
other => panic!("expected Text, got {:?}", other),
}
match &anth.content[1] {
anthropic::ContentBlock::ToolUse { id, name, input } => {
assert_eq!(id, "call_read_001");
assert_eq!(name, "Read");
assert_eq!(input["file_path"], "/config.toml");
}
other => panic!("expected ToolUse, got {:?}", other),
}
match &anth.content[2] {
anthropic::ContentBlock::ToolUse { id, name, input } => {
assert_eq!(id, "call_glob_001");
assert_eq!(name, "Glob");
assert_eq!(input["pattern"], "**/*test*");
}
other => panic!("expected ToolUse, got {:?}", other),
}
}
#[test]
fn tool_call_empty_id_gets_synthetic_id() {
let resp = openai::ChatCompletionResponse {
id: "x".into(),
object: "chat.completion".into(),
model: "llama".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: None,
name: None,
tool_calls: Some(vec![openai::ToolCall {
id: "".into(), call_type: "function".into(),
function: openai::FunctionCall {
name: "Read".into(),
arguments: r#"{"file_path":"/tmp/x"}"#.into(),
},
}]),
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::ToolCalls),
logprobs: None,
}],
usage: None,
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "m");
match &anth.content[0] {
anthropic::ContentBlock::ToolUse { id, name, .. } => {
assert!(
id.starts_with("toolu_"),
"expected synthetic toolu_ ID, got: {}",
id
);
assert_eq!(name, "Read");
}
other => panic!("expected ToolUse, got {:?}", other),
}
}
#[test]
fn tool_call_empty_arguments_becomes_empty_object() {
let resp = openai::ChatCompletionResponse {
id: "x".into(),
object: "chat.completion".into(),
model: "llama".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: None,
name: None,
tool_calls: Some(vec![openai::ToolCall {
id: "call_1".into(),
call_type: "function".into(),
function: openai::FunctionCall {
name: "Bash".into(),
arguments: "".into(), },
}]),
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::ToolCalls),
logprobs: None,
}],
usage: None,
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "m");
match &anth.content[0] {
anthropic::ContentBlock::ToolUse { input, .. } => {
assert_eq!(input, &json!({}));
}
other => panic!("expected ToolUse, got {:?}", other),
}
}
#[test]
fn tool_call_missing_name_skipped() {
let resp = openai::ChatCompletionResponse {
id: "x".into(),
object: "chat.completion".into(),
model: "llama".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: Some(openai::ChatContent::Text("text".into())),
name: None,
tool_calls: Some(vec![
openai::ToolCall {
id: "call_1".into(),
call_type: "function".into(),
function: openai::FunctionCall {
name: "".into(), arguments: "{}".into(),
},
},
openai::ToolCall {
id: "call_2".into(),
call_type: "function".into(),
function: openai::FunctionCall {
name: "Read".into(),
arguments: r#"{"file_path":"/x"}"#.into(),
},
},
]),
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::ToolCalls),
logprobs: None,
}],
usage: None,
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "m");
assert_eq!(anth.content.len(), 2);
match &anth.content[0] {
anthropic::ContentBlock::Text { text } => assert_eq!(text, "text"),
other => panic!("expected Text, got {:?}", other),
}
match &anth.content[1] {
anthropic::ContentBlock::ToolUse { name, .. } => assert_eq!(name, "Read"),
other => panic!("expected ToolUse, got {:?}", other),
}
}
#[test]
fn refusal_mapped_to_text_block() {
let resp = openai::ChatCompletionResponse {
id: "chatcmpl-1".into(),
object: "chat.completion".into(),
model: "gpt-4o".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: None,
name: None,
tool_calls: None,
tool_call_id: None,
refusal: Some("I cannot help with that request.".into()),
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::ContentFilter),
logprobs: None,
}],
usage: None,
created: None,
system_fingerprint: None,
service_tier: None,
};
let anth = openai_to_anthropic_response(&resp, "m");
assert_eq!(anth.content.len(), 1);
match &anth.content[0] {
anthropic::ContentBlock::Text { text } => {
assert!(text.contains("Refusal"));
assert!(text.contains("I cannot help with that request."));
}
other => panic!("expected Text with refusal, got {:?}", other),
}
}
#[test]
fn extra_fields_forwarded_to_openai_request() {
let mut req = basic_request();
req.extra
.insert("seed".into(), serde_json::Value::Number(42.into()));
req.extra
.insert("logprobs".into(), serde_json::Value::Bool(true));
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.extra.get("seed"), Some(&json!(42)));
assert_eq!(oai.extra.get("logprobs"), Some(&json!(true)));
}
#[test]
fn n_parameter_stripped_from_extra() {
let mut req = basic_request();
req.extra.insert("n".into(), json!(4));
req.extra.insert("seed".into(), json!(42));
let oai = anthropic_to_openai_request(&req);
assert!(oai.extra.get("n").is_none());
assert_eq!(oai.extra.get("seed"), Some(&json!(42)));
}
#[test]
fn n_parameter_one_stripped_silently() {
let mut req = basic_request();
req.extra.insert("n".into(), json!(1));
let oai = anthropic_to_openai_request(&req);
assert!(oai.extra.get("n").is_none());
}
#[test]
fn reasoning_content_mapped_to_thinking_block_in_response() {
let oai_resp = openai::ChatCompletionResponse {
id: "chatcmpl-1".into(),
object: "chat.completion".into(),
model: "deepseek-reasoner".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: Some(openai::ChatContent::Text("The answer is 4.".into())),
name: None,
tool_calls: None,
tool_call_id: None,
refusal: None,
reasoning_content: Some("Let me think... 2+2=4".into()),
},
finish_reason: Some(openai::FinishReason::Stop),
logprobs: None,
}],
usage: Some(openai::ChatUsage {
prompt_tokens: 10,
completion_tokens: 20,
total_tokens: 30,
completion_tokens_details: None,
prompt_tokens_details: None,
}),
created: None,
system_fingerprint: None,
service_tier: None,
};
let resp = openai_to_anthropic_response(&oai_resp, "deepseek-reasoner");
assert_eq!(resp.content.len(), 2);
match &resp.content[0] {
anthropic::ContentBlock::Thinking {
thinking,
signature,
} => {
assert_eq!(thinking, "Let me think... 2+2=4");
assert!(signature.is_none());
}
other => panic!("expected Thinking block, got {:?}", other),
}
match &resp.content[1] {
anthropic::ContentBlock::Text { text } => {
assert_eq!(text, "The answer is 4.");
}
other => panic!("expected Text block, got {:?}", other),
}
}
#[test]
fn thinking_block_mapped_to_reasoning_content_in_request() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::Assistant,
content: anthropic::Content::Blocks(vec![
anthropic::ContentBlock::Thinking {
thinking: "Let me reason...".into(),
signature: Some("sig_abc".into()),
},
anthropic::ContentBlock::Text {
text: "Here is my answer.".into(),
},
]),
}];
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.messages.len(), 1);
assert_eq!(
oai.messages[0].reasoning_content.as_deref(),
Some("Let me reason...")
);
assert!(matches!(
&oai.messages[0].content,
Some(openai::ChatContent::Text(t)) if t == "Here is my answer."
));
}
#[test]
fn unknown_finish_reason_maps_to_end_turn() {
let oai_resp = openai::ChatCompletionResponse {
id: "chatcmpl-1".into(),
object: "chat.completion".into(),
model: "deepseek-chat".into(),
choices: vec![openai::Choice {
index: 0,
message: openai::ChatMessage {
role: openai::ChatRole::Assistant,
content: Some(openai::ChatContent::Text("Sorry".into())),
name: None,
tool_calls: None,
tool_call_id: None,
refusal: None,
reasoning_content: None,
},
finish_reason: Some(openai::FinishReason::Unknown),
logprobs: None,
}],
usage: None,
created: None,
system_fingerprint: None,
service_tier: None,
};
let resp = openai_to_anthropic_response(&oai_resp, "deepseek-chat");
assert_eq!(resp.stop_reason, Some(anthropic::StopReason::EndTurn));
}
#[test]
fn is_o_series_model_matches() {
assert!(is_o_series_model("o1"));
assert!(is_o_series_model("o3"));
assert!(is_o_series_model("o4"));
assert!(is_o_series_model("o1-mini"));
assert!(is_o_series_model("o1-preview"));
assert!(is_o_series_model("o3-mini"));
assert!(is_o_series_model("o4-mini"));
assert!(is_o_series_model("O1")); assert!(is_o_series_model("O3-Mini"));
}
#[test]
fn is_o_series_model_rejects() {
assert!(!is_o_series_model("gpt-4o"));
assert!(!is_o_series_model("gpt-4o-mini"));
assert!(!is_o_series_model("gpt-4"));
assert!(!is_o_series_model("claude-3-opus"));
}
#[test]
fn is_o_series_model_future_models() {
assert!(is_o_series_model("o2"), "o2 must match");
assert!(is_o_series_model("o5"), "o5 must match");
assert!(is_o_series_model("o10"), "o10 (multi-digit) must match");
assert!(is_o_series_model("o2-mini"), "o2-mini must match");
assert!(
!is_o_series_model("o-preview"),
"bare 'o' with no digit must not match"
);
assert!(
!is_o_series_model("openai-o1"),
"o1 not at start must not match"
);
}
fn make_request(model: &str, system: Option<&str>) -> anthropic::MessageCreateRequest {
anthropic::MessageCreateRequest {
model: model.into(),
max_tokens: 1024,
messages: vec![],
system: system.map(|s| anthropic::System::Text(s.into())),
temperature: None,
top_p: None,
top_k: None,
stop_sequences: None,
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
stream: None,
extra: serde_json::Map::new(),
}
}
#[test]
fn o_series_model_gets_only_max_completion_tokens() {
let req = make_request("o1-mini", Some("You are helpful."));
let oai = anthropic_to_openai_request(&req);
assert!(
oai.max_tokens.is_none(),
"o-series should not set max_tokens"
);
assert_eq!(oai.max_completion_tokens, Some(1024));
assert_eq!(oai.messages[0].role, openai::ChatRole::Developer);
}
#[test]
fn non_o_series_model_gets_both_max_tokens() {
let req = make_request("gpt-4o", Some("You are helpful."));
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.max_tokens, Some(1024));
assert_eq!(oai.max_completion_tokens, Some(1024));
assert_eq!(oai.messages[0].role, openai::ChatRole::System);
}
#[test]
fn o_series_ga_strips_temperature() {
let mut req = make_request("o3-mini", None);
req.temperature = Some(0.7);
let oai = anthropic_to_openai_request(&req);
assert!(
oai.temperature.is_none(),
"o3-mini should strip temperature (all o-series reject it)"
);
}
#[test]
fn o_series_preview_strips_temperature() {
let mut req = make_request("o1-preview", None);
req.temperature = Some(0.7);
let oai = anthropic_to_openai_request(&req);
assert!(
oai.temperature.is_none(),
"o1-preview should strip temperature"
);
}
#[test]
fn o_series_preview_strips_top_p() {
let mut req = make_request("o1-preview", None);
req.top_p = Some(0.9);
let oai = anthropic_to_openai_request(&req);
assert!(oai.top_p.is_none(), "o1-preview should strip top_p");
}
#[test]
fn o1_mini_strips_top_p() {
let mut req = make_request("o1-mini", None);
req.top_p = Some(0.9);
let oai = anthropic_to_openai_request(&req);
assert!(oai.top_p.is_none(), "o1-mini should strip top_p");
}
#[test]
fn non_o_series_preserves_temperature() {
let mut req = make_request("gpt-4o", None);
req.temperature = Some(0.7);
let oai = anthropic_to_openai_request(&req);
assert_eq!(oai.temperature, Some(0.7));
}
#[test]
fn warnings_empty_for_plain_request() {
let req = basic_request();
let w = compute_request_warnings(&req);
assert!(w.is_empty());
assert!(w.as_header_value().is_none());
}
#[test]
fn warnings_top_k() {
let mut req = basic_request();
req.top_k = Some(40);
let w = compute_request_warnings(&req);
assert_eq!(w.as_header_value().unwrap(), "top_k");
}
#[test]
fn warnings_thinking_config() {
let mut req = basic_request();
req.thinking = Some(anthropic::ThinkingConfig::Enabled {
budget_tokens: 5000,
});
let w = compute_request_warnings(&req);
assert_eq!(w.as_header_value().unwrap(), "thinking_config");
}
#[test]
fn warnings_stop_sequences_truncated_at_5() {
let mut req = basic_request();
req.stop_sequences = Some(vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
"e".to_string(),
]);
let w = compute_request_warnings(&req);
assert_eq!(w.as_header_value().unwrap(), "stop_sequences_truncated");
}
#[test]
fn warnings_stop_sequences_4_is_fine() {
let mut req = basic_request();
req.stop_sequences = Some(vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
]);
let w = compute_request_warnings(&req);
assert!(w.is_empty());
}
#[test]
fn warnings_cache_control_on_system() {
let mut req = basic_request();
req.system = Some(anthropic::System::Blocks(vec![anthropic::SystemBlock {
block_type: "text".to_string(),
text: "You are helpful.".to_string(),
cache_control: Some(anthropic::CacheControl {
cache_type: "ephemeral".to_string(),
}),
}]));
let w = compute_request_warnings(&req);
assert_eq!(w.as_header_value().unwrap(), "cache_control");
}
#[test]
fn warnings_document_blocks() {
let mut req = basic_request();
req.messages = vec![anthropic::InputMessage {
role: anthropic::Role::User,
content: anthropic::Content::Blocks(vec![anthropic::ContentBlock::Document {
source: anthropic::DocumentSource {
source_type: "base64".to_string(),
media_type: "application/pdf".to_string(),
data: "dGVzdA==".to_string(),
},
title: None,
}]),
}];
let w = compute_request_warnings(&req);
assert_eq!(w.as_header_value().unwrap(), "document_blocks");
}
#[test]
fn warnings_multiple_combined() {
let mut req = basic_request();
req.top_k = Some(10);
req.thinking = Some(anthropic::ThinkingConfig::Enabled {
budget_tokens: 1000,
});
let w = compute_request_warnings(&req);
let val = w.as_header_value().unwrap();
assert!(val.contains("top_k"), "missing top_k in: {val}");
assert!(
val.contains("thinking_config"),
"missing thinking_config in: {val}"
);
}
#[test]
fn forced_tool_choice_enables_strict_mode_in_openai_request() {
let anthropic_req: anthropic::MessageCreateRequest =
serde_json::from_value(serde_json::json!({
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 100,
"messages": [{"role": "user", "content": "Extract the data"}],
"tools": [{
"name": "extract_data",
"description": "Extract structured data",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"value": {"type": "integer"}
}
}
}],
"tool_choice": {"type": "tool", "name": "extract_data"}
}))
.unwrap();
let openai_req = anthropic_to_openai_request(&anthropic_req);
let tools = openai_req.tools.expect("tools should be present");
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].function.strict, Some(true));
let params = tools[0]
.function
.parameters
.as_ref()
.expect("parameters should be present");
assert_eq!(params["additionalProperties"], serde_json::json!(false));
let required = params["required"]
.as_array()
.expect("required should be present");
assert!(required.iter().any(|v| v == "name"));
assert!(required.iter().any(|v| v == "value"));
}