use super::*;
#[test]
fn chat_safety_identifier_maps_to_anthropic_metadata_and_rejects_bad_values() {
let request: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "answer"}],
"safety_identifier": "synthetic-user-42"
}))
.unwrap();
assert_eq!(
chat_completion_to_anthropic(&request)["metadata"]["user_id"],
"synthetic-user-42"
);
assert!(
crate::safety_identifier::validate_openai(request.safety_identifier.as_deref()).is_ok()
);
let too_long = "x".repeat(65);
assert!(crate::safety_identifier::validate_openai(Some(&too_long)).is_err());
assert!(
serde_json::from_value::<OpenAIChatCompletionRequest>(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "answer"}],
"safety_identifier": 42
}))
.is_err()
);
}
#[test]
fn legacy_user_maps_to_anthropic_with_safety_identifier_precedence() {
let legacy: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "answer"}],
"user": "legacy-user"
}))
.unwrap();
assert_eq!(
chat_completion_to_anthropic(&legacy)["metadata"]["user_id"],
"legacy-user"
);
let both: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "answer"}],
"user": "legacy-user",
"safety_identifier": "current-user"
}))
.unwrap();
assert_eq!(
chat_completion_to_anthropic(&both)["metadata"]["user_id"],
"current-user"
);
}
#[test]
fn reconciliation_never_guesses_parameter_support_from_model_digits() {
let mut body = json!({
"model": "claude-future-99",
"temperature": 0.7,
"top_p": 0.8
});
reconcile_subscription_parameters(crate::subscription::SubscriptionProvider::Claude, &mut body);
let temperature = body["temperature"].as_f64().unwrap();
assert!((temperature - 0.7).abs() < 1e-6);
assert_eq!(body["top_p"], 0.8);
}
#[test]
fn translated_chat_targets_reject_named_participants_without_erasing_identity() {
for (role, name) in [
("system", "policy-author"),
("developer", "application"),
("user", "alice"),
] {
let body = json!({
"model": "target-test",
"messages": [{"role": role, "name": name, "content": "hello"}]
});
let error = crate::bridge_controls::untranslatable_chat_participant_name(&body)
.expect("named participant must fail closed");
assert!(error.contains("message.name"), "{error}");
assert!(!error.contains(name), "identity must not enter diagnostics");
}
for body in [
json!({"messages": [{"role": "user", "content": "hello"}]}),
json!({"messages": [{"role": "user", "name": "", "content": "hello"}]}),
json!({"messages": [{"role": "user", "name": null, "content": "hello"}]}),
] {
assert_eq!(
crate::bridge_controls::untranslatable_chat_participant_name(&body),
None
);
}
}
#[test]
fn openai_service_tiers_are_preserved_or_rejected_by_target_contract() {
for tier in [json!("priority"), json!("flex"), json!("scale"), json!(42)] {
assert!(crate::bridge_controls::untranslatable_openai_service_tier(Some(&tier)).is_some());
}
for tier in [
None,
Some(&Value::Null),
Some(&json!("auto")),
Some(&json!("default")),
] {
assert_eq!(
crate::bridge_controls::untranslatable_openai_service_tier(tier),
None
);
}
let body = json!({
"model": "gpt-test", "messages": [{"role": "user", "content": "answer"}],
"service_tier": "priority"
});
assert_eq!(
crate::responses::chat_completion_to_responses(&body)["service_tier"],
"priority"
);
}
#[test]
fn translated_requests_fail_closed_for_future_moderation_and_cache_contracts() {
assert!(
crate::bridge_controls::unknown_chat_field(&json!({
"model": "x", "messages": [], "future_contract": true
}))
.is_some()
);
assert!(
crate::bridge_controls::unknown_responses_field(&json!({
"model": "x", "input": [], "future_contract": true
}))
.is_some()
);
assert!(
crate::bridge_controls::untranslatable_moderation(Some(
&json!({"model": "moderation", "policy": "strict"})
))
.is_some()
);
let supported = json!({
"prompt_cache_options": {"mode": "explicit"},
"messages": [{"role": "user", "content": [
{"type": "text", "text": "one", "prompt_cache_breakpoint": {"mode": "explicit"}},
{"type": "text", "text": "two", "prompt_cache_breakpoint": {"mode": "explicit"}}
]}]
});
assert!(crate::bridge_controls::validate_openai_prompt_cache(&supported, true).is_ok());
assert!(crate::bridge_controls::validate_openai_prompt_cache(&supported, false).is_err());
for unsupported in [
json!({"prompt_cache_key": "opaque"}),
json!({"prompt_cache_retention": "24h"}),
json!({"prompt_cache_options": {"mode": "explicit", "ttl": "30m"}}),
json!({"messages": [{"content": [{"prompt_cache_breakpoint": {"mode": "future"}}]}]}),
json!({"messages": [{"content": [
{"prompt_cache_breakpoint": {"mode": "explicit"}},
{"prompt_cache_breakpoint": {"mode": "explicit"}},
{"prompt_cache_breakpoint": {"mode": "explicit"}},
{"prompt_cache_breakpoint": {"mode": "explicit"}},
{"prompt_cache_breakpoint": {"mode": "explicit"}}
]}]}),
] {
assert!(crate::bridge_controls::validate_openai_prompt_cache(&unsupported, true).is_err());
}
let chat = json!({
"model": "gpt", "messages": [{"role": "user", "content": [{
"type": "text", "text": "hello",
"prompt_cache_breakpoint": {"mode": "explicit"}
}]}],
"prompt_cache_key": "opaque", "prompt_cache_options": {"mode": "explicit"},
"prompt_cache_retention": "24h", "moderation": {"policy": "strict"}
});
let responses = crate::responses::chat_completion_to_responses(&chat);
for field in [
"prompt_cache_key",
"prompt_cache_options",
"prompt_cache_retention",
"moderation",
] {
assert_eq!(responses[field], chat[field]);
}
assert_eq!(
responses.pointer("/input/0/content/0/prompt_cache_breakpoint"),
chat.pointer("/messages/0/content/0/prompt_cache_breakpoint")
);
}
#[test]
fn anthropic_bridge_maps_current_breakpoints_on_system_media_and_tool_output() {
let request: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model":"claude-test",
"messages":[
{"role":"system","content":[{
"type":"text","text":"policy",
"prompt_cache_breakpoint":{"mode":"explicit"}
}]},
{"role":"user","content":[{
"type":"image_url","image_url":{"url":"https://example.com/image.png"},
"prompt_cache_breakpoint":{"mode":"explicit"}
}]},
{"role":"tool","tool_call_id":"call_1","content":[{
"type":"text","text":"done",
"prompt_cache_breakpoint":{"mode":"explicit"}
}]}
]
}))
.unwrap();
let body = chat_completion_to_anthropic(&request);
assert_eq!(
body["system"][0]["cache_control"],
json!({"type":"ephemeral"})
);
assert_eq!(
body["messages"][0]["content"][0]["cache_control"],
json!({"type":"ephemeral"})
);
assert_eq!(
body["messages"][1]["content"][0]["content"][0]["cache_control"],
json!({"type":"ephemeral"})
);
}
#[test]
fn translated_resource_selectors_and_prediction_fail_closed() {
assert!(
crate::bridge_controls::untranslatable_chat_prediction(Some(&json!({
"type": "content",
"content": "expected replacement"
})))
.is_some()
);
assert_eq!(
crate::bridge_controls::untranslatable_chat_prediction(Some(&Value::Null)),
None
);
for body in [
json!({"prompt": {"id": "pmpt_test", "version": "7"}}),
json!({"include": ["message.output_text.logprobs"]}),
json!({"include": "message.output_text.logprobs"}),
] {
assert!(
crate::bridge_controls::validate_responses_resource_selectors(&body).is_err(),
"{body}"
);
}
for body in [
json!({}),
json!({"prompt": null, "include": null}),
json!({"include": []}),
] {
assert!(
crate::bridge_controls::validate_responses_resource_selectors(&body).is_ok(),
"{body}"
);
}
}
#[test]
fn chat_structured_output_and_parallel_tool_policy_reach_anthropic() {
for (response_format, expected_schema) in [
(
json!({"type": "json_schema", "json_schema": {
"name": "answer", "strict": true,
"schema": {"type": "object", "required": ["answer"]}
}}),
json!({"type": "object", "required": ["answer"]}),
),
(
json!({"type": "json_object"}),
json!({"type": "object", "additionalProperties": true}),
),
] {
let request: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "answer"}],
"response_format": response_format,
"parallel_tool_calls": false,
"tools": [{"type": "function", "function": {
"name": "lookup", "parameters": {"type": "object"}
}}],
"tool_choice": "required"
}))
.unwrap();
let translated = chat_completion_to_anthropic(&request);
assert_eq!(translated["output_config"]["format"]["type"], "json_schema");
assert_eq!(
translated["output_config"]["format"]["schema"],
expected_schema
);
assert_eq!(translated["tool_choice"]["type"], "any");
assert_eq!(translated["tool_choice"]["disable_parallel_tool_use"], true);
}
}
#[test]
fn anthropic_bridge_rejects_audio_content_in_every_chat_shape() {
for content in [
json!([{"type": "input_audio", "input_audio": {"data": "AAA", "format": "wav"}}]),
json!([{"type": "text", "text": "listen"}, {"type": "input_audio", "input_audio": {"data": "AAA", "format": "mp3"}}]),
json!([{"type": "input_audio", "input_audio": {}}]),
] {
let request: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": content}],
"stream": true
}))
.unwrap();
assert!(
untranslatable_chat_tool_history(&request.messages)
.is_some_and(|reason| reason.contains("input_audio"))
);
}
}
#[test]
fn chat_output_contract_fields_are_retained_and_fail_closed_on_bridge() {
let request: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "answer"}],
"n": 2,
"modalities": ["audio"],
"audio": {"format": "wav", "voice": "alloy"},
"logprobs": true,
"top_logprobs": 5
}))
.unwrap();
let retained = serde_json::to_value(&request).unwrap();
for field in ["n", "modalities", "audio", "logprobs", "top_logprobs"] {
assert!(retained.get(field).is_some(), "discarded {field}");
}
assert!(crate::structured_output::unsupported_chat_output_contract(&request).is_some());
let supported: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "answer"}],
"n": 1,
"modalities": ["text"],
"audio": null,
"logprobs": false,
"top_logprobs": 0
}))
.unwrap();
assert_eq!(
crate::structured_output::unsupported_chat_output_contract(&supported),
None
);
}
#[test]
fn chat_generation_controls_are_retained_and_only_neutral_values_bridge() {
let request: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "answer"}],
"frequency_penalty": 1.25,
"presence_penalty": -0.5,
"logit_bias": {"42": 10},
"seed": 1234
}))
.unwrap();
let retained = serde_json::to_value(&request).unwrap();
assert_eq!(retained["frequency_penalty"], 1.25);
assert_eq!(retained["presence_penalty"], -0.5);
assert_eq!(retained["logit_bias"], json!({"42": 10.0}));
assert_eq!(retained["seed"], 1234);
assert!(crate::structured_output::unsupported_chat_generation_control(&request).is_some());
let neutral: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "answer"}],
"frequency_penalty": 0,
"presence_penalty": -0.0,
"logit_bias": {}
}))
.unwrap();
assert_eq!(
crate::structured_output::unsupported_chat_generation_control(&neutral),
None
);
for value in [-2.1, 2.1] {
let invalid: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "answer"}],
"frequency_penalty": value
}))
.unwrap();
assert!(
crate::structured_output::unsupported_chat_generation_control(&invalid)
.is_some_and(|reason| reason.contains("between -2 and 2"))
);
}
}
#[test]
fn chat_function_tool_strictness_is_preserved_and_malformed_values_fail() {
let request: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "claude-test",
"messages": [{"role": "user", "content": "use tools"}],
"tools": [
{"type": "function", "function": {"name": "strict_tool", "strict": true, "parameters": {"type": "object"}}},
{"type": "function", "function": {"name": "loose_tool", "strict": false, "parameters": {"type": "object"}}},
{"type": "function", "function": {"name": "default_tool", "parameters": {"type": "object"}}}
]
}))
.unwrap();
let translated = chat_completion_to_anthropic(&request);
assert_eq!(translated["tools"][0]["strict"], true);
assert_eq!(translated["tools"][1]["strict"], false);
assert!(translated["tools"][2].get("strict").is_none());
for tools in [
json!([{"type": "function", "function": {"name": "bad", "strict": "yes", "parameters": {"type": "object"}}}]),
json!([{"type": "function", "function": {"name": "bad", "parameters": "object"}}]),
] {
assert!(invalid_anthropic_tool_definition(&tools).is_some());
}
}
#[test]
fn parallel_tool_policy_combines_with_every_chat_tool_choice() {
let cases = [
(None, None, Value::Null, Value::Null),
(Some(true), Some(json!("auto")), json!("auto"), Value::Null),
(
Some(false),
Some(json!("required")),
json!("any"),
json!(true),
),
(Some(false), Some(json!("none")), json!("none"), json!(true)),
(
Some(false),
Some(json!({"type": "function", "function": {"name": "lookup"}})),
json!("tool"),
json!(true),
),
];
for (parallel, choice, expected_type, expected_disabled) in cases {
let request = OpenAIChatCompletionRequest {
model: "claude-test".into(),
messages: vec![ChatMessage {
role: "user".into(),
content: json!("use tools"),
name: None,
tool_call_id: None,
tool_calls: None,
}],
max_tokens: None,
max_completion_tokens: None,
temperature: None,
top_p: None,
frequency_penalty: None,
presence_penalty: None,
logit_bias: None,
seed: None,
stream: None,
stop: None,
tools: Some(
json!([{"type": "function", "function": {"name": "lookup", "parameters": {"type": "object"}}}]),
),
tool_choice: choice,
reasoning_effort: None,
reasoning: None,
response_format: None,
parallel_tool_calls: parallel,
n: None,
modalities: None,
audio: None,
logprobs: None,
top_logprobs: None,
safety_identifier: None,
stream_options: None,
user: None,
};
let translated = chat_completion_to_anthropic(&request);
assert_eq!(translated["tool_choice"]["type"], expected_type);
assert_eq!(
translated["tool_choice"]["disable_parallel_tool_use"],
expected_disabled
);
}
}
#[test]
fn translates_anthropic_text_stream_to_openai_chat_chunks() {
let mut translator = OpenAIStreamTranslator::new(OpenAIStreamShape::ChatCompletion, "gpt-4o");
let frames = translator.push(
br#"event: message_start
data: {"type":"message_start","message":{"id":"msg_1","model":"claude-sonnet-4-5-20250929"}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hello"}}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"}}
event: message_stop
data: {"type":"message_stop"}
"#,
);
let joined = frames.join("");
assert!(joined.contains("\"object\":\"chat.completion.chunk\""));
assert!(joined.contains("\"content\":\"hello\""));
assert!(joined.contains("\"finish_reason\":\"stop\""));
assert!(joined.contains("\"model\":\"claude-sonnet-4-5-20250929\""));
assert!(joined.contains("data: [DONE]"));
}
#[test]
fn chat_stream_emits_usage_only_when_requested() {
let mut translator = OpenAIStreamTranslator::new(OpenAIStreamShape::ChatCompletion, "gpt-4o")
.with_include_usage(true);
let frames = translator.push(
br#"event: message_start
data: {"type":"message_start","message":{"usage":{"input_tokens":7,"output_tokens":0}}}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":3}}
event: message_stop
data: {"type":"message_stop"}
"#,
);
let usage = frames
.iter()
.filter_map(|frame| frame.strip_prefix("data: "))
.filter_map(|data| serde_json::from_str::<Value>(data.trim()).ok())
.find(|chunk| chunk["choices"].as_array().is_some_and(Vec::is_empty))
.expect("usage chunk");
assert_eq!(usage["usage"]["prompt_tokens"], 7);
assert_eq!(usage["usage"]["completion_tokens"], 3);
assert_eq!(usage["usage"]["total_tokens"], 10);
}
#[test]
fn translates_anthropic_text_stream_to_openai_response_events() {
let mut translator = OpenAIStreamTranslator::new(OpenAIStreamShape::Response, "gpt-4o");
let frames = translator.push(
br#"event: message_start
data: {"type":"message_start","message":{"id":"msg_1","model":"claude-sonnet-4-5-20250929"}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hello"}}
event: message_stop
data: {"type":"message_stop"}
"#,
);
let joined = frames.join("");
assert!(joined.contains("\"type\":\"response.created\""));
assert!(joined.contains("\"type\":\"response.output_text.delta\""));
assert!(joined.contains("\"type\":\"response.completed\""));
assert!(joined.contains("\"model\":\"claude-sonnet-4-5-20250929\""));
assert!(joined.contains("data: [DONE]"));
}
#[test]
fn translates_basic_chat_completion() {
let req = OpenAIChatCompletionRequest {
model: "gpt-4o".into(),
messages: vec![
ChatMessage {
role: "system".into(),
content: Value::String("You are helpful.".into()),
name: None,
tool_call_id: None,
tool_calls: None,
},
ChatMessage {
role: "user".into(),
content: Value::String("Hello".into()),
name: None,
tool_call_id: None,
tool_calls: None,
},
],
max_tokens: Some(100),
max_completion_tokens: None,
temperature: Some(0.5),
top_p: None,
frequency_penalty: None,
presence_penalty: None,
logit_bias: None,
seed: None,
stream: None,
stop: None,
tools: None,
tool_choice: None,
reasoning_effort: None,
reasoning: None,
response_format: None,
parallel_tool_calls: None,
n: None,
modalities: None,
audio: None,
logprobs: None,
top_logprobs: None,
safety_identifier: None,
stream_options: None,
user: None,
};
let body = chat_completion_to_anthropic(&req);
assert_eq!(body["model"], "gpt-4o");
assert_eq!(body["max_tokens"], 100);
assert_eq!(body["temperature"], 0.5);
assert_eq!(body["system"], "You are helpful.");
let msgs = body["messages"].as_array().unwrap();
assert_eq!(msgs.len(), 1);
assert_eq!(msgs[0]["role"], "user");
assert_eq!(msgs[0]["content"], "Hello");
}
#[test]
fn preserves_claude_native_model_id() {
let req = OpenAIChatCompletionRequest {
model: "claude-opus-4-7".into(),
messages: vec![ChatMessage {
role: "user".into(),
content: Value::String("hi".into()),
name: None,
tool_call_id: None,
tool_calls: None,
}],
max_tokens: None,
max_completion_tokens: None,
temperature: None,
top_p: None,
frequency_penalty: None,
presence_penalty: None,
logit_bias: None,
seed: None,
stream: None,
stop: None,
tools: None,
tool_choice: None,
reasoning_effort: None,
reasoning: None,
response_format: None,
parallel_tool_calls: None,
n: None,
modalities: None,
audio: None,
logprobs: None,
top_logprobs: None,
safety_identifier: None,
stream_options: None,
user: None,
};
let body = chat_completion_to_anthropic(&req);
assert_eq!(body["model"], "claude-opus-4-7");
assert_eq!(body["max_tokens"], 4096);
}
#[test]
fn preserves_temperature_for_live_claude_validation() {
let req = OpenAIChatCompletionRequest {
model: "claude-sonnet-5".into(),
messages: vec![ChatMessage {
role: "user".into(),
content: Value::String("hi".into()),
name: None,
tool_call_id: None,
tool_calls: None,
}],
max_tokens: None,
max_completion_tokens: None,
temperature: Some(0.7),
top_p: None,
frequency_penalty: None,
presence_penalty: None,
logit_bias: None,
seed: None,
stream: None,
stop: None,
tools: None,
tool_choice: None,
reasoning_effort: None,
reasoning: None,
response_format: None,
parallel_tool_calls: None,
n: None,
modalities: None,
audio: None,
logprobs: None,
top_logprobs: None,
safety_identifier: None,
stream_options: None,
user: None,
};
let body = chat_completion_to_anthropic(&req);
let temperature = body["temperature"].as_f64().unwrap();
assert!((temperature - 0.7).abs() < 1e-6);
}
#[test]
fn caller_reasoning_effort_uses_adaptive_thinking_and_preserves_explicit_limit() {
let req: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model":"claude-opus-5",
"messages":[{"role":"user","content":"hi"}],
"max_tokens":3000,
"reasoning_effort":"low"
}))
.unwrap();
let body = chat_completion_to_anthropic(&req);
assert_eq!(body["thinking"]["type"], "adaptive");
assert_eq!(body["output_config"]["effort"], "low");
assert_eq!(body["max_tokens"], 3000);
assert!(body.get("reasoning").is_none());
}
#[test]
fn omitted_limit_reserves_output_headroom_for_adaptive_thinking() {
let req: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model":"claude-opus-5",
"messages":[{"role":"user","content":"hi"}],
"reasoning_effort":"high"
}))
.unwrap();
let body = chat_completion_to_anthropic(&req);
assert_eq!(body["thinking"]["type"], "adaptive");
assert_eq!(body["output_config"]["effort"], "high");
assert_eq!(body["max_tokens"], 24_576);
}
#[test]
fn legacy_thinking_budget_keeps_visible_output_headroom() {
let req: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model":"claude-sonnet-4-5",
"messages":[{"role":"user","content":"hi"}],
"reasoning_effort":"high"
}))
.unwrap();
let body = chat_completion_to_anthropic(&req);
assert_eq!(body["thinking"]["type"], "enabled");
assert_eq!(body["thinking"]["budget_tokens"], 16_384);
assert_eq!(body["max_tokens"], 24_576);
}
#[test]
fn model_resolution_passes_a_named_model_through_without_a_catalog() {
assert_eq!(
resolve_model("aurora-2-base").as_deref(),
Some("aurora-2-base")
);
assert_eq!(resolve_model(""), None);
}
#[test]
fn model_resolution_is_bounded_by_the_live_catalog() {
use std::collections::BTreeMap;
let catalog = vec!["aurora-2-base".to_string(), "borealis-9-ultra".to_string()];
let mut aliases = BTreeMap::new();
aliases.insert("fast".to_string(), "aurora-2-base".to_string());
aliases.insert("stale".to_string(), "withdrawn-1".to_string());
assert_eq!(
resolve_model_with("aurora-2-base", &aliases, &catalog).as_deref(),
Some("aurora-2-base")
);
assert_eq!(
resolve_model_with("fast", &aliases, &catalog).as_deref(),
Some("aurora-2-base"),
"an operator alias resolves to a model the account advertises"
);
assert_eq!(
resolve_model_with("stale", &aliases, &catalog),
None,
"an alias pointing at a withdrawn model must not route anywhere"
);
assert_eq!(
resolve_model_with("never-advertised", &aliases, &catalog),
None
);
}
#[test]
fn translates_multipart_user_content() {
let req = OpenAIChatCompletionRequest {
model: "gpt-4o".into(),
messages: vec![ChatMessage {
role: "user".into(),
content: json!([
{"type": "text", "text": "describe"},
{"type": "image_url", "image_url": {"url": "https://example.com/x.png"}}
]),
name: None,
tool_call_id: None,
tool_calls: None,
}],
max_tokens: Some(50),
max_completion_tokens: None,
temperature: None,
top_p: None,
frequency_penalty: None,
presence_penalty: None,
logit_bias: None,
seed: None,
stream: None,
stop: None,
tools: None,
tool_choice: None,
reasoning_effort: None,
reasoning: None,
response_format: None,
parallel_tool_calls: None,
n: None,
modalities: None,
audio: None,
logprobs: None,
top_logprobs: None,
safety_identifier: None,
stream_options: None,
user: None,
};
let body = chat_completion_to_anthropic(&req);
let parts = body["messages"][0]["content"].as_array().unwrap();
assert_eq!(parts[0]["type"], "text");
assert_eq!(parts[0]["text"], "describe");
assert_eq!(parts[1]["type"], "image");
assert_eq!(parts[1]["source"]["url"], "https://example.com/x.png");
}
#[test]
fn chat_tool_loop_preserves_call_and_result_ids() {
let req: OpenAIChatCompletionRequest = serde_json::from_value(json!({
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "weather?"},
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "toolu_test123",
"type": "function",
"function": {"name": "weather", "arguments": "{\"city\":\"Paris\"}"}
}]
},
{"role": "tool", "tool_call_id": "toolu_test123", "content": "sunny"}
]
}))
.unwrap();
let body = chat_completion_to_anthropic(&req);
assert_eq!(body["messages"][1]["content"][0]["id"], "toolu_test123");
assert_eq!(body["messages"][1]["content"][0]["input"]["city"], "Paris");
assert_eq!(
body["messages"][2]["content"][0]["tool_use_id"],
"toolu_test123"
);
}
#[test]
fn responses_flat_tools_translate_without_silent_loss() {
let tools = json!([{
"type": "function",
"name": "get_weather",
"description": "Get weather",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
}]);
let translated = translate_tools(&tools);
assert_eq!(translated[0]["name"], "get_weather");
assert_eq!(
translated[0]["input_schema"]["properties"]["city"]["type"],
"string"
);
}
#[test]
fn responses_web_search_maps_to_anthropic_server_tool() {
let translated = translate_tools(&json!([{"type": "web_search", "max_uses": 2}]));
assert_eq!(translated[0]["type"], "web_search_20250305");
assert_eq!(translated[0]["name"], "web_search");
assert_eq!(translated[0]["max_uses"], 2);
}
#[test]
fn anthropic_never_receives_both_temperature_and_top_p() {
let sampling = |temperature: Option<f32>, top_p: Option<f32>| {
let req = OpenAIChatCompletionRequest {
model: "claude-haiku-4-5-20251001".into(),
messages: vec![ChatMessage {
role: "user".into(),
content: Value::String("hi".into()),
name: None,
tool_call_id: None,
tool_calls: None,
}],
max_tokens: Some(16),
max_completion_tokens: None,
temperature,
top_p,
frequency_penalty: None,
presence_penalty: None,
logit_bias: None,
seed: None,
stream: None,
stop: None,
tools: None,
tool_choice: None,
reasoning_effort: None,
reasoning: None,
response_format: None,
parallel_tool_calls: None,
n: None,
modalities: None,
audio: None,
logprobs: None,
top_logprobs: None,
safety_identifier: None,
stream_options: None,
user: None,
};
chat_completion_to_anthropic(&req)
};
let body = sampling(Some(1.0), Some(0.95));
assert_eq!(body["temperature"], 1.0);
assert!(body.get("top_p").is_none(), "{body}");
let body = sampling(None, Some(0.95));
assert!(
(body["top_p"].as_f64().expect("top_p is a number") - 0.95).abs() < 1e-6,
"{body}"
);
assert!(body.get("temperature").is_none(), "{body}");
let body = sampling(Some(0.5), None);
assert_eq!(body["temperature"], 0.5);
assert!(body.get("top_p").is_none(), "{body}");
let body = sampling(None, None);
assert!(body.get("temperature").is_none(), "{body}");
assert!(body.get("top_p").is_none(), "{body}");
}