use std::{collections::HashMap, env};
use bytes::Bytes;
use serde::Deserialize;
use serde_json::json;
use super::*;
use crate::model::{TransformError, TransformRequest, TransformResponse};
#[test]
fn test_anthropic_to_openai_basic() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"max_tokens": 256,
"system": "You are a concise assistant.",
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Say hello in one sentence."}]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input).unwrap();
assert_eq!(
result.headers.get("authorization"),
Some(&"Bearer test-key".to_string())
);
assert_eq!(result.path, "/v1/chat/completions");
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["model"], "claude-sonnet-4-20250514");
assert_eq!(out_body["messages"][0]["role"], "system");
assert_eq!(out_body["messages"][1]["role"], "user");
assert_eq!(
out_body["messages"][1]["content"],
"Say hello in one sentence."
);
}
#[test]
fn test_anthropic_to_openai_system_as_array() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"max_tokens": 256,
"system": [
{ "type": "text", "text": "You are a helpful assistant." },
{ "type": "text", "text": "Keep responses concise." }
],
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Hi"}]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input).unwrap();
assert_eq!(result.path, "/v1/chat/completions");
let body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(body["messages"][0]["role"], "system");
assert_eq!(
body["messages"][0]["content"],
"You are a helpful assistant.\nKeep responses concise."
);
}
#[test]
fn test_anthropic_to_openai_tool_use() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "assistant",
"content": [
{"type": "text", "text": "Let me check the weather."},
{
"type": "tool_use",
"id": "toolu_123",
"name": "get_weather",
"input": {"city": "Paris"}
}
]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input).unwrap();
assert_eq!(result.path, "/v1/chat/completions");
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["messages"][0]["role"], "assistant");
assert_eq!(
out_body["messages"][0]["content"],
"Let me check the weather."
);
assert_eq!(out_body["messages"][0]["tool_calls"][0]["id"], "toolu_123");
assert_eq!(out_body["messages"][0]["tool_calls"][0]["type"], "function");
assert_eq!(
out_body["messages"][0]["tool_calls"][0]["function"]["name"],
"get_weather"
);
}
#[test]
fn test_anthropic_to_openai_top_level_tools_and_tool_choice() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": "What's the weather in Paris?"
}
],
"tools": [
{
"name": "get_weather",
"description": "Get weather for a city",
"input_schema": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
],
"tool_choice": {"type": "any"}
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["tools"][0]["type"], "function");
assert_eq!(out_body["tools"][0]["function"]["name"], "get_weather");
assert_eq!(
out_body["tools"][0]["function"]["description"],
"Get weather for a city"
);
assert_eq!(
out_body["tools"][0]["function"]["parameters"]["properties"]["city"]["type"],
"string"
);
assert_eq!(out_body["tool_choice"], "required");
}
#[test]
fn test_anthropic_to_openai_specific_tool_choice() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "user",
"content": "Use the weather tool"
}
],
"tools": [
{
"name": "get_weather",
"input_schema": {
"type": "object",
"properties": { "city": { "type": "string" } }
}
}
],
"tool_choice": {"type": "tool", "name": "get_weather"}
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["tool_choice"]["type"], "function");
assert_eq!(out_body["tool_choice"]["function"]["name"], "get_weather");
}
#[test]
fn test_anthropic_to_openai_rejects_unknown_tool_choice() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "user",
"content": "Hello"
}
],
"tool_choice": {"type": "sometimes"}
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input);
assert!(matches!(result, Err(TransformError::InvalidFormat(_))));
}
#[test]
fn test_anthropic_to_openai_thinking_enabled_maps_to_enable_thinking() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "user",
"content": "Think carefully"
}
],
"thinking": {
"type": "enabled",
"budget_tokens": 2048,
"display": "summarized"
}
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["enable_thinking"], serde_json::Value::Bool(true));
}
#[test]
fn test_anthropic_to_openai_thinking_disabled_maps_to_enable_thinking_false() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "user",
"content": "Skip thinking"
}
],
"thinking": {
"type": "disabled"
}
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["enable_thinking"], serde_json::Value::Bool(false));
}
#[test]
fn test_anthropic_to_openai_lossy_downgrade_still_sets_content() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "internal reasoning"
}
]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["messages"][0]["role"], "assistant");
assert_eq!(out_body["messages"][0]["content"], "");
}
#[test]
fn test_anthropic_to_openai_tool_result() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_123",
"name": "get_weather",
"input": {"city": "Paris"}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_123",
"content": [{"type": "text", "text": "{\"temperature\":21}"}],
"is_error": false
}
]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
let messages = out_body["messages"].as_array().unwrap();
assert_eq!(messages.len(), 2);
assert_eq!(messages[0]["role"], "assistant");
assert_eq!(messages[0]["content"], "");
assert_eq!(messages[0]["tool_calls"][0]["id"], "toolu_123");
assert_eq!(messages[1]["role"], "tool");
assert_eq!(messages[1]["tool_call_id"], "toolu_123");
assert_eq!(messages[1]["content"], "{\"temperature\":21}");
}
#[test]
fn test_anthropic_to_openai_preserves_stream_flag() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"stream": true,
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Hello"}]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["stream"], serde_json::Value::Bool(true));
}
#[test]
fn test_anthropic_to_openai_invalid_json() {
let input = TransformRequest {
headers: HashMap::new(),
path: "/v1/messages".to_string(),
body: Bytes::from("not valid json"),
};
let result = anthropic_to_openai(&input);
assert!(matches!(result, Err(TransformError::InvalidFormat(_))));
}
#[test]
fn test_transform_headers_anthropic_to_openai() {
let input = HashMap::from([
("x-api-key".to_string(), "my-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]);
let result = transform_headers_anthropic_to_openai(&input);
assert_eq!(
result.get("authorization"),
Some(&"Bearer my-key".to_string())
);
assert_eq!(
result.get("content-type"),
Some(&"application/json".to_string())
);
}
#[test]
fn test_anthropic_to_openai_responses_basic() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"max_tokens": 256,
"system": "You are a concise assistant.",
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Say hello in one sentence."}]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai_responses(&input).unwrap();
assert_eq!(
result.headers.get("authorization"),
Some(&"Bearer test-key".to_string())
);
assert_eq!(result.path, "/v1/responses");
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["model"], "claude-sonnet-4-20250514");
assert_eq!(out_body["instructions"], "You are a concise assistant.");
assert_eq!(out_body["input"][0]["type"], "message");
assert_eq!(out_body["input"][0]["role"], "user");
assert_eq!(out_body["input"][0]["content"][0]["type"], "input_text");
assert_eq!(
out_body["input"][0]["content"][0]["text"],
"Say hello in one sentence."
);
assert_eq!(out_body["max_output_tokens"], 256);
}
#[test]
fn test_anthropic_to_openai_responses_system_as_array() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"max_tokens": 256,
"system": [
{ "type": "text", "text": "You are a helpful assistant." },
{ "type": "text", "text": "Keep responses concise." }
],
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Hi"}]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai_responses(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(
out_body["instructions"],
"You are a helpful assistant.\nKeep responses concise."
);
}
#[test]
fn test_anthropic_to_openai_responses_tool_use() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "assistant",
"content": [
{"type": "text", "text": "Let me check the weather."},
{
"type": "tool_use",
"id": "toolu_123",
"name": "get_weather",
"input": {"city": "Paris"}
}
]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai_responses(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
let input_items = out_body["input"].as_array().unwrap();
let has_function_call = input_items
.iter()
.any(|item| item.get("type").and_then(|t| t.as_str()) == Some("function_call"));
assert!(has_function_call, "expected function_call in input items");
}
#[test]
fn test_anthropic_to_openai_responses_tool_result() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_123",
"name": "get_weather",
"input": {"city": "Paris"}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_123",
"content": [{"type": "text", "text": "{\"temperature\":21}"}],
"is_error": false
}
]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai_responses(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
let input_items = out_body["input"].as_array().unwrap();
let has_function_call_output = input_items
.iter()
.any(|item| item.get("type").and_then(|t| t.as_str()) == Some("function_call_output"));
assert!(
has_function_call_output,
"expected function_call_output in input items"
);
}
#[test]
fn test_anthropic_to_openai_responses_top_level_tools_and_tool_choice() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": "What's the weather in Paris?"
}
],
"tools": [
{
"name": "get_weather",
"description": "Get weather for a city",
"input_schema": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
],
"tool_choice": {"type": "any"}
}))
.unwrap(),
),
};
let result = anthropic_to_openai_responses(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["tools"][0]["type"], "function");
assert_eq!(out_body["tools"][0]["name"], "get_weather");
assert_eq!(
out_body["tools"][0]["description"],
"Get weather for a city"
);
assert_eq!(
out_body["tools"][0]["parameters"]["properties"]["city"]["type"],
"string"
);
assert_eq!(out_body["tool_choice"], "required");
}
#[test]
fn test_anthropic_to_openai_responses_specific_tool_choice() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "user",
"content": "Use the weather tool"
}
],
"tools": [
{
"name": "get_weather",
"input_schema": {
"type": "object",
"properties": { "city": { "type": "string" } }
}
}
],
"tool_choice": {"type": "tool", "name": "get_weather"}
}))
.unwrap(),
),
};
let result = anthropic_to_openai_responses(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["tool_choice"]["type"], "function");
assert_eq!(out_body["tool_choice"]["name"], "get_weather");
}
#[test]
fn test_anthropic_to_openai_responses_thinking_lossy_downgrade() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"messages": [
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "internal reasoning"
}
]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai_responses(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
let input_items = out_body["input"].as_array().unwrap();
assert!(!input_items.is_empty());
}
#[test]
fn test_anthropic_to_openai_responses_preserves_stream_flag() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "test-key".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "claude-sonnet-4-20250514",
"stream": true,
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Hello"}]
}
]
}))
.unwrap(),
),
};
let result = anthropic_to_openai_responses(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["stream"], serde_json::Value::Bool(true));
}
#[test]
fn test_anthropic_to_openai_responses_invalid_json() {
let input = TransformRequest {
headers: HashMap::new(),
path: "/v1/messages".to_string(),
body: Bytes::from("not valid json"),
};
let result = anthropic_to_openai_responses(&input);
assert!(matches!(result, Err(TransformError::InvalidFormat(_))));
}
#[test]
fn test_openai_to_anthropic_basic() {
let input = TransformRequest {
headers: HashMap::from([
(
"authorization".to_string(),
"Bearer OPENAI_API_KEY_PLACEHOLDER".to_string(),
),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/chat/completions".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "gpt-4o",
"max_tokens": 256,
"temperature": 0.7,
"messages": [
{ "role": "system", "content": "You are a concise assistant." },
{ "role": "user", "content": "Say hello in one sentence." }
]
}))
.unwrap(),
),
};
let result = openai_to_anthropic(&input).unwrap();
assert_eq!(
result.headers.get("x-api-key"),
Some(&"OPENAI_API_KEY_PLACEHOLDER".to_string())
);
assert_eq!(result.path, "/v1/messages");
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["model"], "gpt-4o");
assert_eq!(out_body["max_tokens"], 256);
assert_eq!(out_body["system"], "You are a concise assistant.");
let messages = out_body["messages"].as_array().unwrap();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0]["role"], "user");
assert_eq!(
messages[0]["content"][0]["text"],
"Say hello in one sentence."
);
}
#[test]
fn test_openai_to_anthropic_tool_result() {
let input = TransformRequest {
headers: HashMap::from([
("authorization".to_string(), "Bearer TEST_KEY".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/chat/completions".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "gpt-4o",
"messages": [
{
"role": "assistant",
"content": "",
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"temperature\":21}"
}
]
}))
.unwrap(),
),
};
let result = openai_to_anthropic(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
let messages = out_body["messages"].as_array().unwrap();
assert_eq!(messages.len(), 2);
assert_eq!(messages[0]["role"], "assistant");
let assistant_content = messages[0]["content"].as_array().unwrap();
assert_eq!(assistant_content.len(), 1);
assert_eq!(assistant_content[0]["type"], "tool_use");
assert_eq!(assistant_content[0]["id"], "toolu_call_abc123");
assert_eq!(assistant_content[0]["name"], "get_weather");
assert_eq!(assistant_content[0]["input"]["city"], "Paris");
assert_eq!(messages[1]["role"], "user");
let user_content = messages[1]["content"].as_array().unwrap();
assert_eq!(user_content.len(), 1);
assert_eq!(user_content[0]["type"], "tool_result");
assert_eq!(user_content[0]["tool_use_id"], "toolu_call_abc123");
}
#[test]
fn test_openai_response_to_anthropic_message_with_thinking_and_tool_use() {
let input = TransformRequest {
headers: HashMap::from([
("authorization".to_string(), "Bearer TEST_KEY".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/chat/completions".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"id": "chatcmpl-thinking",
"model": "qwen3.6-plus",
"choices": [
{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"reasoning_content": "Inspecting the route table.",
"content": "I need to query the code graph.",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "codegraph_search",
"arguments": "{\"query\":\"sso google login\"}"
}
}
]
}
}
],
"usage": {
"prompt_tokens": 42,
"completion_tokens": 11
}
}))
.unwrap(),
),
};
let result = openai_response_to_anthropic_message(&input).unwrap();
assert_eq!(result.headers["x-api-key"], "TEST_KEY");
assert_eq!(result.path, "/v1/messages");
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
let content = out_body["content"].as_array().unwrap();
assert_eq!(content[0]["type"], "thinking");
assert_eq!(content[0]["thinking"], "Inspecting the route table.");
assert_eq!(content[0]["signature"], SYNTHETIC_THINKING_SIGNATURE);
assert_eq!(content[1]["type"], "text");
assert_eq!(content[1]["text"], "I need to query the code graph.");
assert_eq!(content[2]["type"], "tool_use");
assert_eq!(content[2]["id"], "call_abc123");
assert_eq!(content[2]["name"], "codegraph_search");
assert_eq!(content[2]["input"]["query"], "sso google login");
assert_eq!(out_body["stop_reason"], "tool_use");
assert_eq!(out_body["usage"]["input_tokens"], 42);
assert_eq!(out_body["usage"]["output_tokens"], 11);
}
#[test]
fn test_anthropic_response_to_openai_response_with_thinking_and_tool_use() {
let input = TransformRequest {
headers: HashMap::from([
("x-api-key".to_string(), "TEST_KEY".to_string()),
("content-type".to_string(), "application/json".to_string()),
]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"model": "qwen-plus-anthropic",
"content": [
{
"type": "thinking",
"thinking": "Inspecting the route table.",
"signature": SYNTHETIC_THINKING_SIGNATURE
},
{
"type": "text",
"text": "I need to query the code graph."
},
{
"type": "tool_use",
"id": "toolu_abc123",
"name": "codegraph_search",
"input": {
"query": "sso google login"
}
}
],
"stop_reason": "tool_use",
"stop_sequence": null,
"usage": {
"input_tokens": 42,
"output_tokens": 11
}
}))
.unwrap(),
),
};
let result = anthropic_response_to_openai_response(&input).unwrap();
assert_eq!(result.headers["authorization"], "Bearer TEST_KEY");
assert_eq!(result.path, "/v1/chat/completions");
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
let message = &out_body["choices"][0]["message"];
assert_eq!(out_body["id"], "msg_123");
assert_eq!(out_body["object"], "chat.completion");
assert_eq!(out_body["model"], "qwen-plus-anthropic");
assert_eq!(message["role"], "assistant");
assert_eq!(message["reasoning_content"], "Inspecting the route table.");
assert_eq!(message["content"], "I need to query the code graph.");
assert_eq!(message["tool_calls"][0]["id"], "toolu_abc123");
assert_eq!(
message["tool_calls"][0]["function"]["name"],
"codegraph_search"
);
assert_eq!(
message["tool_calls"][0]["function"]["arguments"],
"{\"query\":\"sso google login\"}"
);
assert_eq!(out_body["choices"][0]["finish_reason"], "tool_calls");
assert_eq!(out_body["usage"]["prompt_tokens"], 42);
assert_eq!(out_body["usage"]["completion_tokens"], 11);
assert_eq!(out_body["usage"]["total_tokens"], 53);
}
#[test]
fn test_responses_to_anthropic_basic_request() {
let input = TransformRequest {
headers: HashMap::from([("authorization".to_string(), "Bearer TEST_KEY".to_string())]),
path: "/v1/responses".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "qwen3.6-plus",
"instructions": "You are concise.",
"input": "Hello from Responses",
"max_output_tokens": 128,
"stream": true,
"tools": [{
"type": "function",
"name": "get_weather",
"description": "Get weather for a city",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}],
"tool_choice": "required"
}))
.unwrap(),
),
};
let result = responses_to_anthropic(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(result.path, "/v1/messages");
assert_eq!(result.headers["x-api-key"], "TEST_KEY");
assert_eq!(out_body["model"], "qwen3.6-plus");
assert_eq!(out_body["system"], "You are concise.");
assert_eq!(out_body["messages"][0]["role"], "user");
assert_eq!(
out_body["messages"][0]["content"][0]["text"],
"Hello from Responses"
);
assert_eq!(out_body["max_tokens"], 128);
assert_eq!(out_body["stream"], serde_json::Value::Bool(true));
assert_eq!(out_body["tools"][0]["name"], "get_weather");
assert_eq!(out_body["tool_choice"]["type"], "any");
}
#[test]
fn test_anthropic_response_to_responses_response_with_thinking_and_tool_use() {
let input = TransformRequest {
headers: HashMap::from([("x-api-key".to_string(), "TEST_KEY".to_string())]),
path: "/v1/messages".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"model": "qwen-plus-anthropic",
"content": [
{
"type": "thinking",
"thinking": "Inspecting the route table.",
"signature": SYNTHETIC_THINKING_SIGNATURE
},
{
"type": "text",
"text": "I need to query the code graph."
},
{
"type": "tool_use",
"id": "toolu_abc123",
"name": "codegraph_search",
"input": { "query": "sso google login" }
}
],
"stop_reason": "tool_use",
"usage": {
"input_tokens": 42,
"output_tokens": 11
}
}))
.unwrap(),
),
};
let result = anthropic_response_to_responses_response(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(result.path, "/v1/responses");
assert_eq!(out_body["object"], "response");
assert_eq!(out_body["id"], "msg_123");
assert_eq!(out_body["status"], "completed");
assert_eq!(out_body["output_text"], "I need to query the code graph.");
assert_eq!(out_body["output"][0]["type"], "message");
assert_eq!(
out_body["output"][0]["content"][0]["type"],
"reasoning_text"
);
assert_eq!(out_body["output"][1]["content"][0]["type"], "output_text");
assert_eq!(out_body["output"][2]["type"], "function_call");
assert_eq!(out_body["output"][2]["call_id"], "toolu_abc123");
assert_eq!(
out_body["output"][2]["arguments"],
"{\"query\":\"sso google login\"}"
);
assert_eq!(out_body["usage"]["total_tokens"], 53);
}
#[test]
fn test_openai_to_anthropic_preserves_stream_flag() {
let input = TransformRequest {
headers: HashMap::from([("authorization".to_string(), "Bearer TEST_KEY".to_string())]),
path: "/v1/chat/completions".to_string(),
body: Bytes::from(
serde_json::to_vec(&json!({
"model": "qwen3.6-plus",
"stream": true,
"messages": [
{
"role": "user",
"content": "Hello"
}
]
}))
.unwrap(),
),
};
let result = openai_to_anthropic(&input).unwrap();
let out_body: serde_json::Value = serde_json::from_slice(&result.body).unwrap();
assert_eq!(out_body["stream"], serde_json::Value::Bool(true));
}
#[test]
fn test_openai_to_anthropic_invalid_json() {
let input = TransformRequest {
headers: HashMap::new(),
path: "/v1/chat/completions".to_string(),
body: Bytes::from("not json"),
};
let result = openai_to_anthropic(&input);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TransformError::InvalidFormat(_)
));
}
#[derive(Debug, Deserialize)]
struct NonStreamFixture {
name: String,
#[allow(dead_code)]
mode: String,
input: NonStreamInput,
expected: NonStreamExpected,
}
#[derive(Debug, Deserialize)]
struct NonStreamInput {
headers: HashMap<String, String>,
path: String,
body: serde_json::Value,
}
#[derive(Debug, Deserialize)]
struct NonStreamExpected {
#[serde(default)]
headers: HashMap<String, String>,
#[serde(default)]
path: Option<String>,
body: serde_json::Value,
}
#[allow(clippy::disallowed_methods)] fn load_nonstream_fixture(path: &str) -> NonStreamFixture {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let full_path = format!("{manifest_dir}/../../{path}");
let content =
std::fs::read_to_string(&full_path).unwrap_or_else(|e| panic!("{full_path}: {e}"));
serde_json::from_str(&content).expect("fixture JSON parse")
}
fn run_nonstream_fixture(
path: &str,
transform_fn: fn(&TransformRequest) -> Result<TransformResponse, TransformError>,
) {
let fixture = load_nonstream_fixture(path);
let input = TransformRequest {
headers: fixture.input.headers.clone(),
path: fixture.input.path.clone(),
body: Bytes::from(serde_json::to_vec(&fixture.input.body).unwrap()),
};
let result = transform_fn(&input);
if fixture.expected.path.is_none() {
assert!(
result.is_err(),
"expected error for {} but got success",
fixture.name
);
return;
}
let result = result.unwrap_or_else(|e| panic!("transform failed for {}: {e}", fixture.name));
for (key, expected_val) in &fixture.expected.headers {
let actual = result.headers.get(key);
assert_eq!(
actual,
Some(expected_val),
"header mismatch for {}: key={key}, expected={expected_val}, actual={actual:?}",
fixture.name
);
}
let expected_path = fixture
.expected
.path
.as_ref()
.expect("expected path should be Some");
assert_eq!(
result.path, *expected_path,
"path mismatch for {}: expected={}, actual={}",
fixture.name, expected_path, result.path
);
let actual_body: serde_json::Value = serde_json::from_slice(&result.body)
.unwrap_or_else(|e| panic!("output body not valid JSON for {}: {e}", fixture.name));
assert_json_subset(&actual_body, &fixture.expected.body, &fixture.name);
}
fn assert_json_subset(actual: &serde_json::Value, expected: &serde_json::Value, name: &str) {
match (actual, expected) {
(serde_json::Value::Object(a), serde_json::Value::Object(e)) => {
for (k, ev) in e {
let av = a
.get(k)
.unwrap_or_else(|| panic!("missing key '{k}' in {name}"));
assert_json_subset(av, ev, name);
}
}
(serde_json::Value::Array(a), serde_json::Value::Array(ev)) => {
assert_eq!(
a.len(),
ev.len(),
"array length mismatch in {name}: expected={}, actual={}",
ev.len(),
a.len()
);
for (i, (av, ev)) in a.iter().zip(ev.iter()).enumerate() {
assert_json_subset(av, ev, &format!("{name}[{i}]"));
}
}
(a, e) => {
assert_eq!(a, e, "value mismatch in {name}: expected={e}, actual={a}");
}
}
}
#[test]
fn test_fixture_anthropic_to_openai_basic() {
run_nonstream_fixture(
"fixtures/protocol-transform/anthropic-to-openai/non-stream-basic.json",
anthropic_to_openai,
);
}
#[test]
fn test_fixture_anthropic_to_openai_tool_use() {
run_nonstream_fixture(
"fixtures/protocol-transform/anthropic-to-openai/non-stream-tool-use.json",
anthropic_to_openai,
);
}
#[test]
fn test_fixture_anthropic_to_openai_tool_result() {
run_nonstream_fixture(
"fixtures/protocol-transform/anthropic-to-openai/non-stream-tool-result.json",
anthropic_to_openai,
);
}
#[test]
fn test_fixture_anthropic_to_openai_top_level_tools() {
run_nonstream_fixture(
"fixtures/protocol-transform/anthropic-to-openai/non-stream-top-level-tools.json",
anthropic_to_openai,
);
}
#[test]
fn test_fixture_anthropic_response_to_openai_thinking_and_tool_use() {
run_nonstream_fixture(
"fixtures/protocol-transform/anthropic-to-openai/non-stream-response-thinking-tool-use.\
json",
anthropic_response_to_openai_response,
);
}
#[test]
fn test_fixture_anthropic_response_to_responses_thinking_and_tool_use() {
run_nonstream_fixture(
"fixtures/protocol-transform/anthropic-to-openai/\
non-stream-responses-response-thinking-tool-use.json",
anthropic_response_to_responses_response,
);
}
#[test]
fn test_fixture_openai_to_anthropic_tool_result() {
run_nonstream_fixture(
"fixtures/protocol-transform/openai-to-anthropic/non-stream-tool-result.json",
openai_to_anthropic,
);
}
#[test]
fn test_fixture_responses_to_anthropic_basic() {
run_nonstream_fixture(
"fixtures/protocol-transform/openai-to-anthropic/non-stream-responses-basic.json",
responses_to_anthropic,
);
}
#[test]
fn test_fixture_openai_response_to_anthropic_thinking() {
run_nonstream_fixture(
"fixtures/protocol-transform/openai-to-anthropic/non-stream-response-thinking.json",
openai_response_to_anthropic_message,
);
}
#[test]
fn test_fixture_anthropic_to_openai_responses_basic() {
run_nonstream_fixture(
"fixtures/protocol-transform/anthropic-to-openai/non-stream-responses-basic.json",
anthropic_to_openai_responses,
);
}