#![allow(dead_code, unused_imports)]
use std::collections::HashMap;
use std::sync::Arc;
use crate::{
BridgeCustomization, BridgeLossAction, BridgeLossPolicy, BridgeMode, BridgeOptions,
BridgePrimitiveContext, BridgePrimitiveRemapper, BridgeTarget, BridgeWarning,
BridgeWarningKind, RequestBridgeContext, RequestBridgeHook, RequestBridgePhase,
ResponseBridgeContext, StreamBridgeContext,
};
use serde_json::json;
use siumai_core::types::{
CacheControl, ChatMessage, ChatRequest, ContentPart, ProviderOptionsMap, ResponseFormat,
};
#[cfg(any(feature = "openai", feature = "anthropic"))]
use siumai_core::types::Tool;
#[cfg(all(feature = "anthropic", feature = "openai"))]
use siumai_core::types::ProviderDefinedTool;
#[cfg(all(feature = "anthropic", feature = "openai"))]
use crate::{ProviderToolRewriteCustomization, ProviderToolRewriteRule};
#[cfg(all(feature = "anthropic", feature = "openai"))]
use super::bridge_chat_request_to_openai_responses_json_with_options;
#[cfg(feature = "anthropic")]
use super::{
bridge_anthropic_messages_json_to_chat_request, bridge_chat_request_to_anthropic_messages_json,
bridge_chat_request_to_anthropic_messages_json_with_options,
};
#[cfg(feature = "google")]
use super::{
bridge_chat_request_to_gemini_generate_content_json,
bridge_gemini_generate_content_json_to_chat_request,
};
#[cfg(feature = "openai")]
use super::{
bridge_chat_request_to_openai_chat_completions_json,
bridge_chat_request_to_openai_chat_completions_json_with_options,
bridge_chat_request_to_openai_responses_json,
bridge_openai_chat_completions_json_to_chat_request,
bridge_openai_responses_json_to_chat_request,
bridge_openai_responses_json_to_chat_request_with_options,
};
#[cfg(all(feature = "anthropic", feature = "openai"))]
fn anthropic_cache_control_message(
mut message: ChatMessage,
cache_control: CacheControl,
) -> ChatMessage {
message.metadata.cache_control = Some(cache_control);
message
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
fn anthropic_cache_control_part(part: ContentPart, cache_control: CacheControl) -> ContentPart {
part.with_provider_option(
"anthropic",
match cache_control {
CacheControl::Ephemeral => json!({
"cacheControl": { "type": "ephemeral" }
}),
CacheControl::Persistent { ttl } => {
let mut cache_control = json!({ "type": "ephemeral" });
if let Some(ttl) = ttl {
cache_control["ttl"] = json!(ttl.as_secs());
}
json!({ "cacheControl": cache_control })
}
},
)
}
struct PrefixRemapper;
impl BridgePrimitiveRemapper for PrefixRemapper {
fn remap_tool_name(&self, _ctx: &BridgePrimitiveContext, name: &str) -> Option<String> {
Some(format!("gw_{name}"))
}
}
struct RejectLossyPolicy;
impl BridgeLossPolicy for RejectLossyPolicy {
fn request_action(
&self,
_ctx: &RequestBridgeContext,
report: &crate::BridgeReport,
) -> BridgeLossAction {
if report.is_lossy() || report.is_rejected() {
BridgeLossAction::Reject
} else {
BridgeLossAction::Continue
}
}
fn response_action(
&self,
_ctx: &ResponseBridgeContext,
report: &crate::BridgeReport,
) -> BridgeLossAction {
if report.is_lossy() || report.is_rejected() {
BridgeLossAction::Reject
} else {
BridgeLossAction::Continue
}
}
fn stream_action(
&self,
_ctx: &StreamBridgeContext,
report: &crate::BridgeReport,
) -> BridgeLossAction {
if report.is_lossy() || report.is_rejected() {
BridgeLossAction::Reject
} else {
BridgeLossAction::Continue
}
}
}
struct RequestAuditHook;
impl RequestBridgeHook for RequestAuditHook {
fn transform_request(
&self,
ctx: &RequestBridgeContext,
request: &mut ChatRequest,
report: &mut crate::BridgeReport,
) -> Result<(), siumai_core::LlmError> {
assert_eq!(ctx.phase, RequestBridgePhase::SerializeTarget);
assert_eq!(ctx.route_label.as_deref(), Some("tests.request.hook"));
request.common_params.max_tokens = Some(77);
report.add_warning(BridgeWarning::new(
BridgeWarningKind::Custom,
"request hook transformed normalized request",
));
Ok(())
}
fn transform_json(
&self,
_ctx: &RequestBridgeContext,
body: &mut serde_json::Value,
_report: &mut crate::BridgeReport,
) -> Result<(), siumai_core::LlmError> {
body["metadata"] = json!({
"hooked": true,
});
Ok(())
}
fn validate_json(
&self,
_ctx: &RequestBridgeContext,
body: &serde_json::Value,
report: &mut crate::BridgeReport,
) -> Result<(), siumai_core::LlmError> {
assert_eq!(body["metadata"]["hooked"], json!(true));
report.add_warning(BridgeWarning::new(
BridgeWarningKind::Custom,
"request hook validated target json",
));
Ok(())
}
}
struct NormalizeAuditHook;
impl RequestBridgeHook for NormalizeAuditHook {
fn transform_request(
&self,
ctx: &RequestBridgeContext,
request: &mut ChatRequest,
report: &mut crate::BridgeReport,
) -> Result<(), siumai_core::LlmError> {
assert_eq!(ctx.phase, RequestBridgePhase::NormalizeSource);
assert_eq!(ctx.source, Some(BridgeTarget::OpenAiResponses));
assert_eq!(ctx.target, BridgeTarget::OpenAiResponses);
assert_eq!(ctx.route_label.as_deref(), Some("tests.normalize.hook"));
assert_eq!(ctx.path_label.as_deref(), Some("source-normalize"));
request.common_params.max_tokens = Some(55);
report.record_lossy_field(
"normalize.custom",
"custom normalize hook rewrote the normalized request",
);
Ok(())
}
}
struct CompositeRequestCustomization;
impl BridgeCustomization for CompositeRequestCustomization {
fn transform_request(
&self,
ctx: &RequestBridgeContext,
request: &mut ChatRequest,
report: &mut crate::BridgeReport,
) -> Result<(), siumai_core::LlmError> {
assert_eq!(ctx.phase, RequestBridgePhase::SerializeTarget);
assert_eq!(ctx.source, Some(BridgeTarget::AnthropicMessages));
assert_eq!(ctx.target, BridgeTarget::OpenAiChatCompletions);
assert_eq!(
ctx.route_label.as_deref(),
Some("tests.request.customization")
);
assert_eq!(ctx.path_label.as_deref(), Some("via-normalized"));
request.common_params.max_tokens = Some(88);
report.add_warning(BridgeWarning::new(
BridgeWarningKind::Custom,
"bundled customization rewrote normalized request",
));
Ok(())
}
fn transform_request_json(
&self,
ctx: &RequestBridgeContext,
body: &mut serde_json::Value,
report: &mut crate::BridgeReport,
) -> Result<(), siumai_core::LlmError> {
assert_eq!(ctx.target, BridgeTarget::OpenAiChatCompletions);
body["metadata"] = json!({
"customized": true,
"target": ctx.target.as_str()
});
report.add_warning(BridgeWarning::new(
BridgeWarningKind::Custom,
"bundled customization rewrote target json",
));
Ok(())
}
fn validate_request_json(
&self,
_ctx: &RequestBridgeContext,
body: &serde_json::Value,
report: &mut crate::BridgeReport,
) -> Result<(), siumai_core::LlmError> {
assert_eq!(body["metadata"]["customized"], json!(true));
report.add_warning(BridgeWarning::new(
BridgeWarningKind::Custom,
"bundled customization validated target json",
));
Ok(())
}
fn remap_tool_name(&self, ctx: &BridgePrimitiveContext, name: &str) -> Option<String> {
assert_eq!(ctx.source, Some(BridgeTarget::AnthropicMessages));
assert_eq!(ctx.target, BridgeTarget::OpenAiChatCompletions);
Some(format!("bundle_{name}"))
}
}
#[cfg(feature = "openai")]
#[test]
fn strict_openai_chat_bridge_rejects_reasoning_loss() {
let mut request = ChatRequest::new(vec![
ChatMessage::assistant_with_content(vec![
ContentPart::reasoning("step by step"),
ContentPart::text("final answer"),
])
.build(),
]);
request.common_params.model = "gpt-4o-mini".to_string();
let bridged = bridge_chat_request_to_openai_chat_completions_json(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeMode::Strict,
)
.expect("bridge");
assert!(bridged.is_rejected());
assert!(bridged.report.is_rejected());
assert_eq!(bridged.report.lossy_fields.len(), 1);
}
#[cfg(feature = "openai")]
#[test]
fn best_effort_openai_chat_bridge_allows_reasoning_loss() {
let mut request = ChatRequest::new(vec![
ChatMessage::assistant_with_content(vec![
ContentPart::reasoning("step by step"),
ContentPart::text("final answer"),
])
.build(),
]);
request.common_params.model = "gpt-4o-mini".to_string();
let bridged = bridge_chat_request_to_openai_chat_completions_json(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeMode::BestEffort,
)
.expect("bridge");
assert!(!bridged.is_rejected());
assert!(bridged.report.is_lossy());
assert_eq!(bridged.report.lossy_fields.len(), 1);
assert!(bridged.value.is_some());
}
#[cfg(feature = "openai")]
#[test]
fn custom_request_loss_policy_can_reject_lossy_bridge_in_best_effort_mode() {
let mut request = ChatRequest::new(vec![
ChatMessage::assistant_with_content(vec![
ContentPart::reasoning("step by step"),
ContentPart::text("final answer"),
])
.build(),
]);
request.common_params.model = "gpt-4o-mini".to_string();
let bridged = bridge_chat_request_to_openai_chat_completions_json_with_options(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeOptions::new(BridgeMode::BestEffort).with_loss_policy(Arc::new(RejectLossyPolicy)),
)
.expect("bridge");
assert!(bridged.is_rejected());
assert!(bridged.report.is_rejected());
assert!(
bridged.report.warnings.iter().any(|warning| warning
.message
.contains("bridge policy rejected request conversion")),
"expected custom loss policy rejection"
);
}
#[cfg(feature = "openai")]
#[test]
fn best_effort_openai_responses_bridge_returns_json_and_report() {
let mut request = ChatRequest::new(vec![
ChatMessage::assistant_with_content(vec![
ContentPart::Reasoning {
text: "hidden chain of thought".to_string(),
provider_options: ProviderOptionsMap(std::collections::BTreeMap::from([(
"openai".to_string(),
json!({ "itemId": "rs_1" }),
)])),
provider_metadata: None,
},
ContentPart::text("visible answer"),
])
.build(),
]);
request.common_params.model = "gpt-4o-mini".to_string();
let bridged = bridge_chat_request_to_openai_responses_json(
&request,
Some(BridgeTarget::GeminiGenerateContent),
BridgeMode::BestEffort,
)
.expect("bridge");
assert!(!bridged.is_rejected());
assert!(bridged.report.is_lossy());
let value = bridged.value.expect("json body");
let input = value
.get("input")
.and_then(|value| value.as_array())
.expect("responses input array");
assert!(
input.iter().any(|item| {
item.get("type")
.and_then(|value| value.as_str())
.is_some_and(|kind| kind == "item_reference")
&& item
.get("id")
.and_then(|value| value.as_str())
.is_some_and(|id| id == "rs_1")
}),
"expected reasoning item reference in responses input"
);
}
#[cfg(feature = "openai")]
#[test]
fn strict_openai_responses_bridge_rejects_non_provider_executed_tool_approval_response() {
let request = ChatRequest::builder()
.message(ChatMessage {
role: siumai_core::types::MessageRole::Tool,
content: siumai_core::types::MessageContent::MultiModal(vec![
ContentPart::tool_approval_response("approval_local", true),
]),
provider_options: ProviderOptionsMap::default(),
metadata: Default::default(),
})
.model("gpt-4.1")
.build();
let bridged = bridge_chat_request_to_openai_responses_json(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeMode::Strict,
)
.expect("bridge");
assert!(bridged.is_rejected());
assert!(bridged.report.is_rejected());
assert!(
bridged.report.warnings.iter().any(|warning| {
warning
.message
.contains("provider-executed approval responses")
}),
"expected provider-executed approval warning, got {:?}",
bridged.report.warnings
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn anthropic_direct_pair_bridge_flattens_system_and_forces_responses_store_policy() {
let request = ChatRequest::builder()
.message(ChatMessage::system("sys-1").build())
.message(ChatMessage::developer("sys-2").build())
.message(ChatMessage::user("hi").build())
.model("gpt-4.1-mini")
.build();
let bridged = bridge_chat_request_to_openai_responses_json(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
assert_eq!(value["store"], serde_json::json!(false));
let include = value
.get("include")
.and_then(|value| value.as_array())
.expect("responses include array");
assert!(
include
.iter()
.any(|value| value.as_str() == Some("reasoning.encrypted_content")),
"expected reasoning.encrypted_content include"
);
let input = value
.get("input")
.and_then(|value| value.as_array())
.expect("responses input array");
assert_eq!(input[0]["role"], serde_json::json!("system"));
assert_eq!(input[0]["content"], serde_json::json!("sys-1\n\nsys-2"));
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn anthropic_direct_pair_bridge_translates_web_search_tool_and_required_choice() {
let request = ChatRequest::builder()
.message(ChatMessage::user("search rust").build())
.tools(vec![
siumai_protocol_anthropic::tool_catalog::anthropic::web_search_20250305().with_args(
json!({
"allowedDomains": ["example.com"],
"maxUses": 2
}),
),
])
.tool_choice(siumai_core::types::ToolChoice::Required)
.model("gpt-4.1-mini")
.build();
let bridged = bridge_chat_request_to_openai_responses_json(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
let tools = value
.get("tools")
.and_then(|value| value.as_array())
.expect("responses tools array");
assert_eq!(tools[0]["type"], serde_json::json!("web_search"));
assert_eq!(
tools[0]["filters"]["allowed_domains"],
serde_json::json!(["example.com"])
);
assert_eq!(value["tool_choice"], serde_json::json!("required"));
assert!(
bridged
.report
.dropped_fields
.iter()
.any(|field| field == "tools[0].args.maxUses"),
"expected dropped maxUses warning in bridge report"
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn anthropic_direct_pair_customization_can_rewrite_provider_tool_before_openai_translation() {
let request = ChatRequest::builder()
.message(ChatMessage::user("fetch docs").build())
.tools(vec![Tool::ProviderDefined(
ProviderDefinedTool::new("anthropic.web_fetch_20250910", "web_fetch").with_args(
json!({
"allowedDomains": ["example.com"]
}),
),
)])
.model("gpt-4.1-mini")
.build();
let customization = ProviderToolRewriteCustomization::new().with_rule(
ProviderToolRewriteRule::new("anthropic.web_fetch_20250910", "openai.web_search")
.with_args_mapper(Arc::new(|_ctx, tool, _report| {
let allowed_domains = tool
.args
.get("allowedDomains")
.cloned()
.unwrap_or_else(|| json!([]));
json!({
"filters": {
"allowedDomains": allowed_domains,
}
})
})),
);
let bridged = bridge_chat_request_to_openai_responses_json_with_options(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeOptions::new(BridgeMode::BestEffort).with_customization(Arc::new(customization)),
)
.expect("bridge");
assert!(!bridged.is_rejected());
assert!(
bridged.report.warnings.iter().any(|warning| warning
.message
.contains("rewrote provider-defined tool `anthropic.web_fetch_20250910`")),
"expected provider tool rewrite warning: {:?}",
bridged.report.warnings
);
let value = bridged.value.expect("json body");
let tools = value
.get("tools")
.and_then(|value| value.as_array())
.expect("responses tools array");
assert_eq!(tools[0]["type"], serde_json::json!("web_search"));
assert_eq!(
tools[0]["filters"]["allowed_domains"],
serde_json::json!(["example.com"])
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn anthropic_direct_pair_bridge_replays_assistant_reasoning_without_openai_item_id() {
let assistant = ChatMessage::assistant_with_content(vec![
ContentPart::Reasoning {
text: "hidden chain of thought".to_string(),
provider_options: ProviderOptionsMap(std::collections::BTreeMap::from([(
"anthropic".to_string(),
serde_json::json!({ "redactedData": "enc_payload" }),
)])),
provider_metadata: None,
},
ContentPart::text("final answer"),
])
.build();
let request = ChatRequest::builder()
.message(assistant)
.model("gpt-4.1-mini")
.build();
let bridged = bridge_chat_request_to_openai_responses_json(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
let input = value
.get("input")
.and_then(|value| value.as_array())
.expect("responses input array");
let reasoning_item = input
.iter()
.find(|item| item.get("type").and_then(|value| value.as_str()) == Some("reasoning"))
.expect("expected reasoning item");
assert_eq!(
reasoning_item["id"],
serde_json::json!("anthropic_reasoning_0_0")
);
assert_eq!(
reasoning_item["encrypted_content"],
serde_json::json!("enc_payload")
);
assert_eq!(
reasoning_item["summary"][0]["text"],
serde_json::json!("hidden chain of thought")
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn anthropic_direct_pair_bridge_ignores_reasoning_provider_metadata_fallback() {
let assistant = ChatMessage::assistant_with_content(vec![
ContentPart::Reasoning {
text: "hidden chain of thought".to_string(),
provider_options: ProviderOptionsMap::default(),
provider_metadata: Some(std::collections::HashMap::from([(
"anthropic".to_string(),
serde_json::json!({ "redactedData": "enc_payload" }),
)])),
},
ContentPart::text("final answer"),
])
.build();
let request = ChatRequest::builder()
.message(assistant)
.model("gpt-4.1-mini")
.build();
let bridged = bridge_chat_request_to_openai_responses_json(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
let input = value
.get("input")
.and_then(|value| value.as_array())
.expect("responses input array");
let reasoning_item = input
.iter()
.find(|item| item.get("type").and_then(|value| value.as_str()) == Some("reasoning"))
.expect("expected reasoning item");
assert!(
reasoning_item.get("encrypted_content").is_none(),
"legacy providerMetadata.anthropic should not populate encrypted_content: {reasoning_item:?}"
);
assert!(
!bridged
.report
.carried_provider_metadata
.iter()
.any(|entry| entry == "anthropic.redactedData"),
"legacy providerMetadata.anthropic should not be carried into request replay: {:?}",
bridged.report.carried_provider_metadata
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn anthropic_direct_pair_bridge_maps_tool_result_to_function_call_output() {
let request = ChatRequest::builder()
.message(ChatMessage::tool_result_text("call_1", "web_search", "done").build())
.tools(vec![
siumai_protocol_anthropic::tool_catalog::anthropic::web_search_20250305(),
])
.model("gpt-4.1-mini")
.build();
let bridged = bridge_chat_request_to_openai_responses_json(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
let input = value
.get("input")
.and_then(|value| value.as_array())
.expect("responses input array");
let tool_output = input
.iter()
.find(|item| {
item.get("type")
.and_then(|value| value.as_str())
.is_some_and(|kind| kind == "function_call_output")
})
.expect("expected function_call_output item");
assert_eq!(tool_output["call_id"], serde_json::json!("call_1"));
assert_eq!(tool_output["output"], serde_json::json!("done"));
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn openai_direct_pair_bridge_lifts_instructions_to_system() {
let request = ChatRequest::builder()
.message(ChatMessage::user("hi").build())
.provider_option(
"openai",
json!({
"responsesApi": {
"instructions": "follow system"
}
}),
)
.model("claude-3-5-sonnet-latest")
.build();
let bridged = bridge_chat_request_to_anthropic_messages_json(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
assert_eq!(value["system"], serde_json::json!("follow system"));
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn openai_direct_pair_bridge_maps_web_search_choice_and_parallel_policy() {
let request = ChatRequest::builder()
.message(ChatMessage::user("search rust").build())
.tools(vec![
siumai_protocol_openai::tool_catalog::openai::web_search().with_args(json!({
"filters": {
"allowedDomains": ["example.com"]
}
})),
])
.tool_choice(siumai_core::types::ToolChoice::tool("web_search"))
.provider_option(
"openai",
json!({
"parallelToolCalls": false
}),
)
.model("claude-3-5-sonnet-latest")
.build();
let bridged = bridge_chat_request_to_anthropic_messages_json(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
let tools = value
.get("tools")
.and_then(|value| value.as_array())
.expect("anthropic tools array");
assert_eq!(tools[0]["type"], serde_json::json!("web_search_20250305"));
assert_eq!(tools[0]["name"], serde_json::json!("web_search"));
assert_eq!(
tools[0]["allowed_domains"],
serde_json::json!(["example.com"])
);
assert_eq!(
value["tool_choice"],
serde_json::json!({
"type": "tool",
"name": "web_search",
"disable_parallel_tool_use": true
})
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn openai_direct_pair_bridge_maps_mcp_tool_to_anthropic_mcp_servers() {
let request = ChatRequest::builder()
.message(ChatMessage::user("use docs mcp").build())
.tools(vec![Tool::ProviderDefined(
ProviderDefinedTool::new("openai.mcp", "MCP").with_args(json!({
"serverLabel": "docs",
"serverUrl": "https://example.com/mcp",
"allowedTools": ["search_docs"],
"headers": {
"Authorization": "Bearer secret_token"
},
"requireApproval": "always",
"serverDescription": "Docs MCP server"
})),
)])
.model("claude-3-5-sonnet-latest")
.build();
let bridged = bridge_chat_request_to_anthropic_messages_json(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
let servers = value
.get("mcp_servers")
.and_then(|value| value.as_array())
.expect("anthropic mcp_servers array");
assert_eq!(servers.len(), 1);
assert_eq!(servers[0]["name"], serde_json::json!("docs"));
assert_eq!(
servers[0]["url"],
serde_json::json!("https://example.com/mcp")
);
assert_eq!(
servers[0]["tool_configuration"]["allowed_tools"],
serde_json::json!(["search_docs"])
);
assert_eq!(
servers[0]["authorization_token"],
serde_json::json!("secret_token")
);
assert!(
bridged
.report
.dropped_fields
.iter()
.any(|field| field == "tools[0].args.requireApproval"),
"expected dropped requireApproval warning"
);
assert!(
bridged
.report
.dropped_fields
.iter()
.any(|field| field == "tools[0].args.serverDescription"),
"expected dropped serverDescription warning"
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn openai_direct_pair_bridge_drops_specific_mcp_tool_choice() {
let request = ChatRequest::builder()
.message(ChatMessage::user("use docs mcp").build())
.tools(vec![Tool::ProviderDefined(
ProviderDefinedTool::new("openai.mcp", "MCP").with_args(json!({
"serverLabel": "docs",
"serverUrl": "https://example.com/mcp"
})),
)])
.tool_choice(siumai_core::types::ToolChoice::tool("MCP"))
.model("claude-3-5-sonnet-latest")
.build();
let bridged = bridge_chat_request_to_anthropic_messages_json(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
assert!(
value.get("tool_choice").is_none(),
"specific MCP tool choice should be dropped for anthropic mcp_servers: {value:?}"
);
assert!(
bridged
.report
.lossy_fields
.iter()
.any(|field| field == "tool_choice"),
"expected lossy tool_choice warning"
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn openai_direct_pair_customization_can_rewrite_provider_tool_before_anthropic_translation() {
let request = ChatRequest::builder()
.message(ChatMessage::user("search images").build())
.tools(vec![Tool::ProviderDefined(ProviderDefinedTool::new(
"openai.image_generation",
"generateImage",
))])
.model("claude-3-5-sonnet-latest")
.build();
let customization = ProviderToolRewriteCustomization::new().with_rule(
ProviderToolRewriteRule::new("openai.image_generation", "anthropic.web_search_20250305")
.with_args_mapper(Arc::new(|_ctx, _tool, _report| {
json!({
"allowedDomains": ["example.com"]
})
})),
);
let bridged = bridge_chat_request_to_anthropic_messages_json_with_options(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeOptions::new(BridgeMode::BestEffort).with_customization(Arc::new(customization)),
)
.expect("bridge");
assert!(!bridged.is_rejected());
assert!(
bridged.report.warnings.iter().any(|warning| warning
.message
.contains("rewrote provider-defined tool `openai.image_generation`")),
"expected provider tool rewrite warning: {:?}",
bridged.report.warnings
);
let value = bridged.value.expect("json body");
let tools = value
.get("tools")
.and_then(|value| value.as_array())
.expect("anthropic tools array");
assert_eq!(tools[0]["type"], serde_json::json!("web_search_20250305"));
assert_eq!(
tools[0]["allowed_domains"],
serde_json::json!(["example.com"])
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn openai_direct_pair_bridge_maps_reasoning_encrypted_content_to_redacted_thinking() {
let request = ChatRequest::builder()
.message(
ChatMessage::assistant_with_content(vec![
ContentPart::Reasoning {
text: "hidden chain of thought".to_string(),
provider_options: ProviderOptionsMap(std::collections::BTreeMap::from([(
"openai".to_string(),
json!({
"itemId": "rs_1",
"reasoningEncryptedContent": "enc_payload"
}),
)])),
provider_metadata: None,
},
ContentPart::text("final answer"),
])
.build(),
)
.model("claude-3-5-sonnet-latest")
.build();
let bridged = bridge_chat_request_to_anthropic_messages_json(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
let messages = value
.get("messages")
.and_then(|value| value.as_array())
.expect("anthropic messages array");
let content = messages[0]
.get("content")
.and_then(|value| value.as_array())
.expect("assistant content");
assert!(
content.iter().any(|part| {
part.get("type")
.and_then(|value| value.as_str())
.is_some_and(|kind| kind == "redacted_thinking")
&& part
.get("data")
.and_then(|value| value.as_str())
.is_some_and(|value| value == "enc_payload")
}),
"expected redacted thinking block"
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn openai_direct_pair_bridge_ignores_reasoning_provider_metadata_fallback() {
let request = ChatRequest::builder()
.message(
ChatMessage::assistant_with_content(vec![
ContentPart::Reasoning {
text: "hidden chain of thought".to_string(),
provider_options: ProviderOptionsMap::default(),
provider_metadata: Some(std::collections::HashMap::from([(
"openai".to_string(),
json!({
"itemId": "rs_1",
"reasoningEncryptedContent": "enc_payload"
}),
)])),
},
ContentPart::text("final answer"),
])
.build(),
)
.model("claude-3-5-sonnet-latest")
.build();
let bridged = bridge_chat_request_to_anthropic_messages_json(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeMode::BestEffort,
)
.expect("bridge");
assert!(bridged.report.is_lossy());
assert!(
bridged
.report
.lossy_fields
.iter()
.any(|field| field == "messages[0].content[0].reasoning"),
"expected loss warning for missing canonical Anthropic replay metadata: {:?}",
bridged.report.lossy_fields
);
assert!(
!bridged
.report
.carried_provider_metadata
.iter()
.any(|entry| entry == "openai.reasoning_encrypted_content"),
"legacy providerMetadata.openai should not be carried into Anthropic replay: {:?}",
bridged.report.carried_provider_metadata
);
let value = bridged.value.expect("json body");
let messages = value
.get("messages")
.and_then(|value| value.as_array())
.expect("anthropic messages array");
let content = messages[0]
.get("content")
.and_then(|value| value.as_array())
.expect("assistant content");
assert!(
!content.iter().any(|part| {
part.get("type")
.and_then(|value| value.as_str())
.is_some_and(|kind| kind == "redacted_thinking")
}),
"legacy providerMetadata.openai should not synthesize redacted_thinking: {content:?}"
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn openai_direct_pair_bridge_lifts_response_format_and_reasoning_effort() {
let request = ChatRequest::builder()
.message(ChatMessage::user("hi").build())
.provider_option(
"openai",
json!({
"reasoningEffort": "xhigh",
"responsesApi": {
"responseFormat": {
"type": "json_schema",
"name": "result",
"strict": true,
"schema": {
"type": "object",
"properties": {
"value": { "type": "string" }
},
"required": ["value"],
"additionalProperties": false
}
}
}
}),
)
.model("claude-sonnet-4-5")
.build();
let bridged = bridge_chat_request_to_anthropic_messages_json(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeMode::BestEffort,
)
.expect("bridge");
let value = bridged.value.expect("json body");
assert_eq!(
value["output_config"]["format"]["type"],
serde_json::json!("json_schema")
);
assert_eq!(value["output_config"]["effort"], serde_json::json!("high"));
assert!(
bridged
.report
.lossy_fields
.iter()
.any(|field| { field == "provider_options_map.openai.reasoning_effort" }),
"expected lossy effort mapping warning"
);
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn anthropic_bridge_reports_cache_breakpoints_beyond_limit() {
let mut request = ChatRequest::new(vec![
anthropic_cache_control_message(
ChatMessage::system("s1").build(),
siumai_core::types::CacheControl::Ephemeral,
),
anthropic_cache_control_message(
ChatMessage::system("s2").build(),
siumai_core::types::CacheControl::Ephemeral,
),
anthropic_cache_control_message(
ChatMessage::user("u1").build(),
siumai_core::types::CacheControl::Ephemeral,
),
anthropic_cache_control_message(
ChatMessage::assistant("a1").build(),
siumai_core::types::CacheControl::Ephemeral,
),
anthropic_cache_control_message(
ChatMessage::user("u2").build(),
siumai_core::types::CacheControl::Ephemeral,
),
]);
request.common_params.model = "claude-3-5-sonnet-latest".to_string();
let bridged = bridge_chat_request_to_anthropic_messages_json(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeMode::BestEffort,
)
.expect("bridge");
assert!(bridged.report.is_lossy());
assert_eq!(bridged.report.dropped_fields.len(), 1);
assert_eq!(
bridged.report.dropped_fields[0],
"messages[4].metadata.cache_control"
);
assert!(bridged.value.is_some());
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn anthropic_bridge_reports_part_cache_breakpoints_from_canonical_provider_options() {
let mut request = ChatRequest::new(vec![
ChatMessage::user("u1")
.with_content_parts(vec![anthropic_cache_control_part(
ContentPart::text("cached"),
siumai_core::types::CacheControl::Ephemeral,
)])
.build(),
ChatMessage::user("u2")
.with_content_parts(vec![anthropic_cache_control_part(
ContentPart::text("cached"),
siumai_core::types::CacheControl::Ephemeral,
)])
.build(),
ChatMessage::user("u3")
.with_content_parts(vec![anthropic_cache_control_part(
ContentPart::text("cached"),
siumai_core::types::CacheControl::Ephemeral,
)])
.build(),
ChatMessage::user("u4")
.with_content_parts(vec![anthropic_cache_control_part(
ContentPart::text("cached"),
siumai_core::types::CacheControl::Ephemeral,
)])
.build(),
ChatMessage::user("u5")
.with_content_parts(vec![anthropic_cache_control_part(
ContentPart::text("cached"),
siumai_core::types::CacheControl::Ephemeral,
)])
.build(),
]);
request.common_params.model = "claude-3-5-sonnet-latest".to_string();
let bridged = bridge_chat_request_to_anthropic_messages_json(
&request,
Some(BridgeTarget::OpenAiResponses),
BridgeMode::BestEffort,
)
.expect("bridge");
assert!(bridged.report.is_lossy());
assert_eq!(bridged.report.dropped_fields.len(), 1);
assert_eq!(
bridged.report.dropped_fields[0],
"messages[4].content[1].cache_control"
);
}
#[cfg(feature = "openai")]
#[test]
fn request_bridge_options_can_remap_tool_names_and_tool_choice() {
let request = ChatRequest::builder()
.message(ChatMessage::user("hi").build())
.tools(vec![siumai_core::types::Tool::function(
"weather",
"Get weather",
json!({
"type": "object",
"properties": {
"city": { "type": "string" }
}
}),
)])
.tool_choice(siumai_core::types::ToolChoice::tool("weather"))
.model("gpt-4.1-mini")
.build();
let bridged = bridge_chat_request_to_openai_chat_completions_json_with_options(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeOptions::new(BridgeMode::BestEffort)
.with_route_label("tests.request.remap")
.with_primitive_remapper(Arc::new(PrefixRemapper)),
)
.expect("bridge");
let value = bridged.value.expect("json body");
assert_eq!(value["tools"][0]["function"]["name"], json!("gw_weather"));
assert_eq!(
value["tool_choice"]["function"]["name"],
json!("gw_weather")
);
}
#[cfg(feature = "openai")]
#[test]
fn request_bridge_hook_can_mutate_request_and_validate_target_json() {
let request = ChatRequest::builder()
.message(ChatMessage::user("hi").build())
.model("gpt-4.1-mini")
.build();
let bridged = bridge_chat_request_to_openai_chat_completions_json_with_options(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeOptions::new(BridgeMode::BestEffort)
.with_route_label("tests.request.hook")
.with_request_hook(Arc::new(RequestAuditHook)),
)
.expect("bridge");
assert!(!bridged.is_rejected());
assert!(bridged.report.is_exact());
assert!(
bridged.report.warnings.iter().any(|warning| warning
.message
.contains("request hook validated target json")),
"expected request hook validation warning"
);
let value = bridged.value.expect("json body");
assert_eq!(value["max_tokens"], json!(77));
assert_eq!(value["metadata"]["hooked"], json!(true));
}
#[cfg(feature = "openai")]
#[test]
fn bridge_customization_bundle_can_drive_request_json_validation_and_remap() {
let request = ChatRequest::builder()
.message(ChatMessage::user("hi").build())
.tools(vec![siumai_core::types::Tool::function(
"weather",
"Get weather",
json!({
"type": "object",
"properties": {
"city": { "type": "string" }
}
}),
)])
.tool_choice(siumai_core::types::ToolChoice::tool("weather"))
.model("gpt-4.1-mini")
.build();
let bridged = bridge_chat_request_to_openai_chat_completions_json_with_options(
&request,
Some(BridgeTarget::AnthropicMessages),
BridgeOptions::new(BridgeMode::BestEffort)
.with_route_label("tests.request.customization")
.with_customization(Arc::new(CompositeRequestCustomization)),
)
.expect("bridge");
assert!(!bridged.is_rejected());
assert!(bridged.report.is_exact());
assert!(bridged.report.warnings.iter().any(|warning| {
warning
.message
.contains("bundled customization validated target json")
}));
let value = bridged.value.expect("json body");
assert_eq!(value["max_tokens"], json!(88));
assert_eq!(value["metadata"]["customized"], json!(true));
assert_eq!(
value["metadata"]["target"],
json!(BridgeTarget::OpenAiChatCompletions.as_str())
);
assert_eq!(
value["tools"][0]["function"]["name"],
json!("bundle_weather")
);
assert_eq!(
value["tool_choice"]["function"]["name"],
json!("bundle_weather")
);
}
#[cfg(feature = "openai")]
#[test]
fn openai_chat_request_normalization_roundtrip_preserves_tools_and_response_format() {
use siumai_core::types::ToolChoice;
use siumai_core::types::chat::ResponseFormat;
let schema = json!({
"type": "object",
"properties": {
"value": { "type": "string" }
},
"required": ["value"],
"additionalProperties": false
});
let request = ChatRequest::builder()
.message(ChatMessage::system("sys").build())
.message(
ChatMessage::user("hello")
.with_content_parts(vec![ContentPart::image_url("https://example.com/a.png")])
.build(),
)
.message(
ChatMessage::assistant_with_content(vec![
ContentPart::text("searching"),
ContentPart::tool_call("call_1", "weather", json!({ "city": "Tokyo" }), None),
])
.build(),
)
.message(
ChatMessage::tool_result_json("call_1", "weather", json!({ "temperature": 18 }))
.build(),
)
.tools(vec![siumai_core::types::Tool::function(
"weather",
"Get weather",
json!({
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}),
)])
.tool_choice(ToolChoice::tool("weather"))
.response_format(
ResponseFormat::json_schema(schema)
.with_name("result")
.with_description("desc")
.with_strict(false),
)
.model("gpt-4.1-mini")
.temperature(0.2)
.top_p(0.8)
.max_tokens(128)
.seed(7)
.stream(true)
.build();
let value = bridge_chat_request_to_openai_chat_completions_json(
&request,
Some(BridgeTarget::OpenAiChatCompletions),
BridgeMode::BestEffort,
)
.expect("bridge")
.value
.expect("json body");
let normalized = bridge_openai_chat_completions_json_to_chat_request(&value).expect("parse");
assert_eq!(normalized.common_params.model, "gpt-4.1-mini");
assert_eq!(normalized.common_params.temperature, Some(0.2));
assert_eq!(normalized.common_params.top_p, Some(0.8));
assert_eq!(normalized.common_params.max_tokens, Some(128));
assert_eq!(normalized.common_params.seed, Some(7));
assert!(normalized.stream);
assert_eq!(normalized.tool_choice, Some(ToolChoice::tool("weather")));
assert_eq!(normalized.response_format, request.response_format);
let tools = normalized.tools.expect("tools");
assert_eq!(tools.len(), 1);
let siumai_core::types::Tool::Function { function } = &tools[0] else {
panic!("expected function tool");
};
assert_eq!(function.name, "weather");
assert_eq!(normalized.messages.len(), 4);
assert_eq!(normalized.messages[0].content_text(), Some("sys"));
assert!(normalized.messages[1].content.as_multimodal().is_some());
assert_eq!(normalized.messages[2].tool_calls().len(), 1);
assert_eq!(normalized.messages[3].tool_results().len(), 1);
}
#[cfg(feature = "openai")]
#[test]
fn openai_responses_request_normalization_restores_instructions_and_options() {
use siumai_core::types::ToolChoice;
let value = json!({
"model": "gpt-5-mini",
"instructions": "follow system",
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "hi" }
]
},
{
"type": "reasoning",
"id": "rs_1",
"summary": [
{ "type": "summary_text", "text": "step 1" }
],
"encrypted_content": "enc_payload"
},
{
"type": "function_call",
"id": "fc_1",
"call_id": "call_1",
"name": "web_search",
"arguments": "{\"q\":\"rust\"}"
},
{
"type": "function_call_output",
"call_id": "call_1",
"output": "{\"ok\":true}"
},
{
"role": "assistant",
"id": "msg_1",
"content": [
{ "type": "output_text", "text": "done" }
]
}
],
"tools": [
{
"type": "web_search",
"filters": {
"allowed_domains": ["example.com"]
}
},
{
"type": "function",
"name": "math",
"description": "Math",
"parameters": {
"type": "object",
"properties": {
"x": { "type": "number" }
}
},
"strict": true
}
],
"tool_choice": { "type": "web_search" },
"parallel_tool_calls": false,
"store": false,
"text": {
"format": {
"type": "json_schema",
"name": "answer",
"strict": true,
"schema": {
"type": "object",
"properties": {
"value": { "type": "string" }
},
"required": ["value"],
"additionalProperties": false
}
}
},
"reasoning": {
"effort": "high"
}
});
let normalized = bridge_openai_responses_json_to_chat_request(&value).expect("parse");
assert_eq!(normalized.common_params.model, "gpt-5-mini");
assert_eq!(normalized.tool_choice, Some(ToolChoice::tool("webSearch")));
assert_eq!(
normalized.messages[0].role,
siumai_core::types::MessageRole::System
);
assert_eq!(normalized.messages[0].content_text(), Some("follow system"));
let openai_options = normalized
.provider_option("openai")
.and_then(|value| value.as_object())
.expect("openai options");
assert_eq!(openai_options["parallelToolCalls"], json!(false));
assert_eq!(openai_options["store"], json!(false));
assert_eq!(openai_options["reasoningEffort"], json!("high"));
let tools = normalized.tools.expect("tools");
assert_eq!(tools.len(), 2);
let siumai_core::types::Tool::ProviderDefined(provider_tool) = &tools[0] else {
panic!("expected provider-defined tool");
};
assert_eq!(provider_tool.id, "openai.web_search");
assert_eq!(provider_tool.name, "webSearch");
let reasoning_message = &normalized.messages[2];
let reasoning_parts = reasoning_message
.content
.as_multimodal()
.expect("reasoning multimodal");
let ContentPart::Reasoning {
text,
provider_options,
..
} = &reasoning_parts[0]
else {
panic!("expected reasoning part");
};
assert_eq!(text, "step 1");
assert_eq!(
provider_options
.get_object("openai")
.and_then(|meta| meta.get("itemId"))
.and_then(|value| value.as_str()),
Some("rs_1")
);
assert_eq!(normalized.messages[2].tool_calls().len(), 1);
assert_eq!(normalized.messages[3].tool_results().len(), 1);
assert_eq!(normalized.messages[4].metadata.id.as_deref(), Some("msg_1"));
assert!(normalized.response_format.is_some());
}
#[cfg(feature = "openai")]
#[test]
fn openai_responses_request_normalization_preserves_json_object_response_format() {
let value = json!({
"model": "gpt-5-mini",
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "return json" }
]
}
],
"text": {
"format": { "type": "json_object" }
}
});
let normalized = bridge_openai_responses_json_to_chat_request(&value).expect("parse");
assert_eq!(
normalized.response_format,
Some(ResponseFormat::json_object())
);
}
#[cfg(feature = "openai")]
#[test]
fn openai_responses_request_normalization_with_options_applies_bridge_customization() {
use siumai_core::types::ToolChoice;
let value = json!({
"model": "gpt-5-mini",
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "hi" }
]
}
],
"tools": [
{
"type": "function",
"name": "math",
"description": "Math",
"parameters": {
"type": "object",
"properties": {
"x": { "type": "number" }
}
}
}
],
"tool_choice": {
"type": "function",
"name": "math"
}
});
let bridged = bridge_openai_responses_json_to_chat_request_with_options(
&value,
BridgeOptions::new(BridgeMode::BestEffort)
.with_route_label("tests.normalize.hook")
.with_request_hook(Arc::new(NormalizeAuditHook))
.with_primitive_remapper(Arc::new(PrefixRemapper)),
)
.expect("normalize");
assert!(!bridged.is_rejected());
assert!(bridged.report.is_lossy());
let normalized = bridged.value.expect("normalized request");
assert_eq!(normalized.common_params.max_tokens, Some(55));
assert_eq!(normalized.tool_choice, Some(ToolChoice::tool("gw_math")));
let tools = normalized.tools.expect("tools");
let siumai_core::types::Tool::Function { function } = &tools[0] else {
panic!("expected function tool");
};
assert_eq!(function.name, "gw_math");
}
#[cfg(feature = "openai")]
#[test]
fn openai_responses_request_normalization_accepts_ai_sdk_function_schema_fields() {
let value = json!({
"model": "gpt-5-mini",
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "weather in tokyo" }
]
}
],
"tools": [
{
"type": "function",
"name": "weather",
"description": "Get weather",
"inputSchema": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
},
"outputSchema": {
"type": "object",
"properties": {
"forecast": { "type": "string" }
},
"required": ["forecast"]
},
"inputExamples": [
{
"input": {
"city": "Tokyo"
}
}
],
"strict": true
}
]
});
let normalized = bridge_openai_responses_json_to_chat_request(&value).expect("parse");
let tools = normalized.tools.expect("tools");
let Tool::Function { function } = &tools[0] else {
panic!("expected function tool");
};
assert_eq!(
function.input_schema(),
&json!({
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
})
);
assert_eq!(
function.output_schema(),
Some(&json!({
"type": "object",
"properties": {
"forecast": { "type": "string" }
},
"required": ["forecast"]
}))
);
assert_eq!(function.strict, Some(true));
assert_eq!(
function.input_examples,
Some(vec![json!({
"input": {
"city": "Tokyo"
}
})])
);
}
#[cfg(feature = "openai")]
#[test]
fn openai_responses_request_normalization_with_options_respects_loss_policy() {
let value = json!({
"model": "gpt-5-mini",
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "hi" }
]
}
]
});
let bridged = bridge_openai_responses_json_to_chat_request_with_options(
&value,
BridgeOptions::new(BridgeMode::BestEffort)
.with_route_label("tests.normalize.hook")
.with_request_hook(Arc::new(NormalizeAuditHook))
.with_loss_policy(Arc::new(RejectLossyPolicy)),
)
.expect("normalize");
assert!(bridged.is_rejected());
assert!(bridged.report.is_rejected());
assert!(bridged.report.warnings.iter().any(|warning| {
warning
.message
.contains("bridge policy rejected request normalization conversion")
}));
}
#[cfg(feature = "openai")]
#[test]
fn openai_responses_request_normalization_restores_provider_tool_calls_and_outputs() {
let value = json!({
"model": "gpt-5-codex",
"input": [
{
"type": "local_shell_call",
"id": "lsh_1",
"call_id": "call_shell_1",
"action": {
"type": "exec",
"command": ["ls", "-a"]
}
},
{
"type": "local_shell_call_output",
"call_id": "call_shell_1",
"output": [
{
"stdout": "ok",
"stderr": "",
"outcome": { "type": "exit", "exitCode": 0 }
}
]
},
{
"type": "apply_patch_call",
"id": "apc_1",
"call_id": "call_patch_1",
"status": "completed",
"operation": {
"type": "update_file",
"path": "src/lib.rs",
"diff": "*** Begin Patch\n*** End Patch\n"
}
},
{
"type": "apply_patch_call_output",
"call_id": "call_patch_1",
"status": "completed",
"output": "applied"
}
],
"tools": [
{ "type": "local_shell" },
{ "type": "apply_patch" }
]
});
let normalized = bridge_openai_responses_json_to_chat_request(&value).expect("parse");
let tools = normalized.tools.as_ref().expect("tools");
let siumai_core::types::Tool::ProviderDefined(local_shell) = &tools[0] else {
panic!("expected provider-defined local shell tool");
};
assert_eq!(local_shell.id, "openai.local_shell");
assert_eq!(local_shell.name, "shell");
let siumai_core::types::Tool::ProviderDefined(apply_patch) = &tools[1] else {
panic!("expected provider-defined apply patch tool");
};
assert_eq!(apply_patch.id, "openai.apply_patch");
assert_eq!(apply_patch.name, "apply_patch");
assert_eq!(normalized.messages.len(), 4);
let shell_call_parts = normalized.messages[0]
.content
.as_multimodal()
.expect("shell call parts");
let ContentPart::ToolCall {
tool_call_id,
tool_name,
arguments,
provider_options,
..
} = &shell_call_parts[0]
else {
panic!("expected shell tool call");
};
assert_eq!(tool_call_id, "call_shell_1");
assert_eq!(tool_name, "shell");
assert_eq!(
arguments,
&json!({
"action": {
"type": "exec",
"command": ["ls", "-a"]
}
})
);
assert_eq!(
provider_options
.get_object("openai")
.and_then(|meta| meta.get("itemId"))
.and_then(|value| value.as_str()),
Some("lsh_1")
);
let shell_output_parts = normalized.messages[1]
.content
.as_multimodal()
.expect("shell output parts");
let ContentPart::ToolResult {
tool_call_id,
tool_name,
output,
..
} = &shell_output_parts[0]
else {
panic!("expected shell tool result");
};
assert_eq!(tool_call_id, "call_shell_1");
assert_eq!(tool_name, "shell");
assert_eq!(
output,
&siumai_core::types::ToolResultOutput::json(json!({
"output": [
{
"stdout": "ok",
"stderr": "",
"outcome": { "type": "exit", "exitCode": 0 }
}
]
}))
);
let apply_patch_call_parts = normalized.messages[2]
.content
.as_multimodal()
.expect("apply patch call parts");
let ContentPart::ToolCall {
tool_call_id,
tool_name,
arguments,
provider_options,
..
} = &apply_patch_call_parts[0]
else {
panic!("expected apply patch tool call");
};
assert_eq!(tool_call_id, "call_patch_1");
assert_eq!(tool_name, "apply_patch");
assert_eq!(
arguments,
&json!({
"operation": {
"type": "update_file",
"path": "src/lib.rs",
"diff": "*** Begin Patch\n*** End Patch\n"
}
})
);
assert_eq!(
provider_options
.get_object("openai")
.and_then(|meta| meta.get("itemId"))
.and_then(|value| value.as_str()),
Some("apc_1")
);
let apply_patch_output_parts = normalized.messages[3]
.content
.as_multimodal()
.expect("apply patch output parts");
let ContentPart::ToolResult {
tool_call_id,
tool_name,
output,
..
} = &apply_patch_output_parts[0]
else {
panic!("expected apply patch tool result");
};
assert_eq!(tool_call_id, "call_patch_1");
assert_eq!(tool_name, "apply_patch");
assert_eq!(
output,
&siumai_core::types::ToolResultOutput::json(json!({
"status": "completed",
"output": "applied"
}))
);
}
#[cfg(feature = "anthropic")]
#[test]
fn anthropic_messages_request_normalization_restores_system_thinking_and_provider_options() {
use siumai_core::types::MessageRole;
use siumai_core::types::ToolChoice;
let value = json!({
"model": "claude-sonnet-4-5",
"system": [
{
"type": "text",
"text": "sys",
"cache_control": { "type": "ephemeral" }
},
{
"type": "text",
"text": "Developer instructions: dev"
}
],
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "hi",
"cache_control": { "type": "ephemeral" }
},
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": "Zm9v"
},
"title": "doc.pdf",
"context": "ctx",
"citations": { "enabled": true }
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_1",
"content": "{\"done\":true}",
"is_error": false
}
]
},
{
"role": "assistant",
"content": [
{ "type": "thinking", "thinking": "step", "signature": "sig" },
{ "type": "tool_use", "id": "call_1", "name": "weather", "input": { "city": "Tokyo" } },
{ "type": "text", "text": "ok" }
]
}
],
"temperature": 0.2,
"top_p": 0.9,
"top_k": 8,
"max_tokens": 512,
"stream": true,
"tools": [
{
"name": "weather",
"description": "Get weather",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string" }
}
},
"strict": true,
"defer_loading": true,
"eager_input_streaming": true,
"allowed_callers": ["direct", "code_execution_20260120"]
},
{
"type": "web_search_20250305",
"name": "web_search",
"allowed_domains": ["example.com"]
}
],
"tool_choice": {
"type": "tool",
"name": "weather",
"disable_parallel_tool_use": true
},
"output_config": {
"format": {
"type": "json_schema",
"strict": true,
"schema": {
"type": "object",
"properties": {
"value": { "type": "string" }
}
}
},
"effort": "high",
"task_budget": {
"type": "tokens",
"total": 400000,
"remaining": 215000
}
},
});
let normalized = bridge_anthropic_messages_json_to_chat_request(&value).expect("parse");
assert_eq!(normalized.common_params.model, "claude-sonnet-4-5");
assert_eq!(normalized.common_params.temperature, Some(0.2));
assert_eq!(normalized.common_params.top_p, Some(0.9));
assert_eq!(normalized.common_params.top_k, Some(8.0));
assert_eq!(normalized.common_params.max_tokens, Some(512));
assert!(normalized.stream);
assert_eq!(normalized.tool_choice, Some(ToolChoice::tool("weather")));
assert_eq!(normalized.messages[0].role, MessageRole::System);
assert!(normalized.messages[0].metadata.cache_control.is_some());
assert_eq!(normalized.messages[1].role, MessageRole::Developer);
assert_eq!(normalized.messages[1].content_text(), Some("dev"));
assert_eq!(normalized.messages[3].role, MessageRole::Tool);
assert_eq!(normalized.messages[4].role, MessageRole::Assistant);
assert_eq!(normalized.messages[4].tool_calls().len(), 1);
let user_parts = normalized.messages[2]
.content
.as_multimodal()
.expect("user parts");
let user_text_anthropic = user_parts[0]
.provider_options()
.and_then(|provider_options| provider_options.get_object("anthropic"))
.expect("text anthropic provider options");
assert_eq!(
user_text_anthropic["cacheControl"],
json!({ "type": "ephemeral" })
);
let user_doc_anthropic = user_parts[1]
.provider_options()
.and_then(|provider_options| provider_options.get_object("anthropic"))
.expect("document anthropic provider options");
assert_eq!(user_doc_anthropic["citations"], json!({ "enabled": true }));
assert_eq!(user_doc_anthropic["title"], json!("doc.pdf"));
assert_eq!(user_doc_anthropic["context"], json!("ctx"));
assert!(
!normalized.messages[2]
.metadata
.custom
.contains_key("anthropic_content_cache_controls")
);
assert!(
!normalized.messages[2]
.metadata
.custom
.contains_key("anthropic_document_citations")
);
assert!(
!normalized.messages[2]
.metadata
.custom
.contains_key("anthropic_document_metadata")
);
let anthropic_options = normalized
.provider_option("anthropic")
.and_then(|value| value.as_object())
.expect("anthropic options");
assert_eq!(anthropic_options["disableParallelToolUse"], json!(true));
assert_eq!(
anthropic_options["structuredOutputMode"],
json!("outputFormat")
);
assert_eq!(anthropic_options["effort"], json!("high"));
assert_eq!(
anthropic_options["taskBudget"],
json!({
"type": "tokens",
"total": 400000,
"remaining": 215000
})
);
let tools = normalized.tools.expect("tools");
assert_eq!(tools.len(), 2);
let siumai_core::types::Tool::Function { function } = &tools[0] else {
panic!("expected function tool");
};
assert_eq!(function.name, "weather");
assert_eq!(function.strict, Some(true));
assert_eq!(
function
.provider_options_map
.get("anthropic")
.and_then(|value| value.get("deferLoading"))
.and_then(|value| value.as_bool()),
Some(true)
);
assert_eq!(
function
.provider_options_map
.get("anthropic")
.and_then(|value| value.get("eagerInputStreaming"))
.and_then(|value| value.as_bool()),
Some(true)
);
assert_eq!(
function
.provider_options_map
.get("anthropic")
.and_then(|value| value.get("allowedCallers")),
Some(&json!(["direct", "code_execution_20260120"]))
);
assert!(normalized.response_format.is_some());
}
#[cfg(feature = "anthropic")]
#[test]
fn bridge_anthropic_messages_json_to_chat_request_restores_sdk_shaped_request_provider_options() {
let value = json!({
"model": "claude-3-haiku-20240307",
"messages": [
{
"role": "user",
"content": [
{ "type": "text", "text": "Hello" }
]
}
],
"max_tokens": 4096,
"thinking": {
"type": "enabled",
"budget_tokens": 1000
},
"cache_control": {
"type": "ephemeral",
"ttl": "1h"
},
"metadata": {
"user_id": "user-1"
},
"mcp_servers": [
{
"type": "url",
"name": "echo",
"url": "https://echo.mcp.inevitable.fyi/mcp",
"authorization_token": "secret-token",
"tool_configuration": {
"enabled": true,
"allowed_tools": ["echo"]
}
}
],
"container": "container-1",
"context_management": {
"edits": [
{
"type": "compact_20260112",
"pause_after_compaction": true
}
]
},
"speed": "fast"
});
let normalized = bridge_anthropic_messages_json_to_chat_request(&value).expect("parse");
assert_eq!(
normalized.provider_option("anthropic"),
Some(&json!({
"thinking": {
"type": "enabled",
"budgetTokens": 1000
},
"cacheControl": {
"type": "ephemeral",
"ttl": "1h"
},
"metadata": {
"userId": "user-1"
},
"mcpServers": [
{
"type": "url",
"name": "echo",
"url": "https://echo.mcp.inevitable.fyi/mcp",
"authorizationToken": "secret-token",
"toolConfiguration": {
"enabled": true,
"allowedTools": ["echo"]
}
}
],
"container": {
"id": "container-1"
},
"contextManagement": {
"edits": [
{
"type": "compact_20260112",
"pauseAfterCompaction": true
}
]
},
"speed": "fast"
}))
);
}
#[cfg(feature = "anthropic")]
#[test]
fn bridge_anthropic_messages_json_to_chat_request_restores_custom_container_skill_provider_reference()
{
let value = json!({
"model": "claude-3-haiku-20240307",
"messages": [
{
"role": "user",
"content": [
{ "type": "text", "text": "Hello" }
]
}
],
"max_tokens": 4096,
"container": {
"id": "container-1",
"skills": [
{
"type": "custom",
"skill_id": "skill_custom_1",
"version": "latest"
},
{
"type": "anthropic",
"skill_id": "pptx",
"version": "latest"
}
]
}
});
let normalized = bridge_anthropic_messages_json_to_chat_request(&value).expect("parse");
let anthropic_options = normalized
.provider_option("anthropic")
.expect("anthropic options");
assert_eq!(
anthropic_options["container"],
json!({
"id": "container-1",
"skills": [
{
"type": "custom",
"providerReference": {
"anthropic": "skill_custom_1"
},
"version": "latest"
},
{
"type": "anthropic",
"skillId": "pptx",
"version": "latest"
}
]
})
);
}
#[cfg(feature = "anthropic")]
#[test]
fn bridge_anthropic_messages_json_to_chat_request_restores_latest_provider_defined_tool_shapes() {
let value = json!({
"model": "claude-3-haiku-20240307",
"messages": [
{
"role": "user",
"content": [
{ "type": "text", "text": "Hello" }
]
}
],
"max_tokens": 4096,
"tools": [
{
"type": "web_search_20260209",
"name": "web_search",
"max_uses": 1,
"allowed_domains": ["example.com"],
"blocked_domains": ["blocked.example"],
"user_location": {
"type": "approximate",
"country": "US"
}
},
{
"type": "web_fetch_20260209",
"name": "web_fetch",
"max_uses": 2,
"allowed_domains": ["docs.example"]
},
{
"type": "computer_20251124",
"name": "computer",
"display_width_px": 1280,
"display_height_px": 720
},
{
"type": "code_execution_20260120",
"name": "code_execution"
}
]
});
let normalized = bridge_anthropic_messages_json_to_chat_request(&value).expect("parse");
let tools = normalized.tools.expect("tools");
assert_eq!(tools.len(), 4);
let Tool::ProviderDefined(web_search) = &tools[0] else {
panic!("expected web search provider tool");
};
assert_eq!(web_search.id, "anthropic.web_search_20260209");
assert_eq!(web_search.name, "web_search");
assert_eq!(
web_search.args,
json!({
"maxUses": 1,
"allowedDomains": ["example.com"],
"blockedDomains": ["blocked.example"],
"userLocation": {
"type": "approximate",
"country": "US"
}
})
);
let Tool::ProviderDefined(web_fetch) = &tools[1] else {
panic!("expected web fetch provider tool");
};
assert_eq!(web_fetch.id, "anthropic.web_fetch_20260209");
assert_eq!(web_fetch.name, "web_fetch");
assert_eq!(
web_fetch.args,
json!({
"maxUses": 2,
"allowedDomains": ["docs.example"]
})
);
let Tool::ProviderDefined(computer) = &tools[2] else {
panic!("expected computer provider tool");
};
assert_eq!(computer.id, "anthropic.computer_20251124");
assert_eq!(computer.name, "computer");
assert_eq!(
computer.args,
json!({
"displayWidthPx": 1280,
"displayHeightPx": 720
})
);
let Tool::ProviderDefined(code_execution) = &tools[3] else {
panic!("expected code execution provider tool");
};
assert_eq!(code_execution.id, "anthropic.code_execution_20260120");
assert_eq!(code_execution.name, "code_execution");
assert_eq!(code_execution.args, json!({}));
}
#[test]
fn request_normalization_source_never_populates_legacy_provider_metadata() {
for (path, source) in request_normalize_sources() {
for forbidden in [
"providerMetadata",
".get(\"provider_metadata\")",
".get(\"providerMetadata\")",
"[\"provider_metadata\"]",
"[\"providerMetadata\"]",
] {
assert!(
!source.contains(forbidden),
"{path} must not read legacy provider metadata via {forbidden}"
);
}
for (index, line) in source.lines().enumerate() {
if line.contains("provider_metadata") {
assert_eq!(
line.trim(),
"provider_metadata: None,",
"{path} must not populate legacy provider_metadata at line {}",
index + 1
);
}
}
}
}
#[test]
fn request_normalization_centralizes_legacy_request_content_constructors() {
let normalize_sources = request_normalize_sources();
let request_mod_source =
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/request/mod.rs"));
let adapter_source = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/request/legacy_content.rs"
));
assert!(
request_mod_source.contains("mod legacy_content;"),
"request module should own the request-side legacy ContentPart adapter module"
);
assert!(
normalize_sources
.iter()
.any(|(_, source)| source.contains("legacy_content::")),
"request normalization codecs should call the request-side legacy ContentPart adapter module"
);
assert!(
adapter_source.contains("pub(super) fn request_text_part"),
"request-side legacy ContentPart text construction should live in request/legacy_content.rs"
);
for forbidden in [
"fn request_text_part",
"fn request_reasoning_part",
"fn request_image_part",
"fn request_audio_part",
"fn request_file_part",
"fn request_tool_call_part",
"fn request_tool_result_part",
] {
for (path, source) in &normalize_sources {
assert!(
!source.contains(forbidden),
"{path} should not own legacy ContentPart adapter helper `{forbidden}`"
);
}
}
let mut outside_helper_provider_metadata_lines = Vec::new();
for (path, source) in &normalize_sources {
for (index, line) in source.lines().enumerate() {
if line.contains("provider_metadata: None,") {
outside_helper_provider_metadata_lines.push((
*path,
index + 1,
line.trim().to_string(),
));
}
}
}
assert_eq!(
outside_helper_provider_metadata_lines.len(),
1,
"legacy request ContentPart provider_metadata construction should stay in request/legacy_content.rs; the only normalize.rs occurrence is the plain-text collapse match: {outside_helper_provider_metadata_lines:?}"
);
assert_eq!(
outside_helper_provider_metadata_lines[0].2,
"provider_metadata: None,"
);
for line in adapter_source
.lines()
.filter(|line| line.contains("provider_metadata:"))
{
assert_eq!(
line.trim(),
"provider_metadata: None,",
"request-side legacy ContentPart adapters must never populate response provider_metadata"
);
}
}
fn request_normalize_sources() -> Vec<(&'static str, &'static str)> {
vec![
(
"src/request/normalize.rs",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/request/normalize.rs"
)),
),
(
"src/request/normalize/anthropic_messages.rs",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/request/normalize/anthropic_messages.rs"
)),
),
(
"src/request/normalize/openai_chat_completions.rs",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/request/normalize/openai_chat_completions.rs"
)),
),
(
"src/request/normalize/openai_responses.rs",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/request/normalize/openai_responses.rs"
)),
),
(
"src/request/normalize/gemini_generate_content.rs",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/request/normalize/gemini_generate_content.rs"
)),
),
]
}
#[test]
fn request_normalization_uses_wire_codec_modules() {
let normalize_source = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/request/normalize.rs"
));
for (module, call) in [
(
"mod anthropic_messages;",
"anthropic_messages::parse_json_to_chat_request",
),
(
"mod openai_chat_completions;",
"openai_chat_completions::parse_json_to_chat_request",
),
(
"mod openai_responses;",
"openai_responses::parse_json_to_chat_request",
),
(
"mod gemini_generate_content;",
"gemini_generate_content::parse_json_to_chat_request",
),
] {
assert!(normalize_source.contains(module));
assert!(normalize_source.contains(call));
}
for forbidden in [
"fn parse_anthropic_messages_json_to_chat_request",
"fn parse_openai_chat_completions_json_to_chat_request",
"fn parse_openai_responses_json_to_chat_request",
] {
assert!(
!normalize_source.contains(forbidden),
"wire-format parser should live in a codec module: {forbidden}"
);
}
for (path, source) in request_normalize_sources()
.into_iter()
.filter(|(path, _)| path.contains("/normalize/"))
{
assert!(
source.contains("pub(super) fn parse_json_to_chat_request"),
"{path} should expose a narrow parser entry point to normalize.rs"
);
}
}
#[cfg(all(feature = "anthropic", feature = "openai"))]
#[test]
fn request_bridge_pair_sources_do_not_read_legacy_provider_metadata() {
for (path, source) in [
(
"src/request/pairs/openai_responses_to_anthropic_messages.rs",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/request/pairs/openai_responses_to_anthropic_messages.rs"
)),
),
(
"src/request/pairs/anthropic_messages_to_openai_responses.rs",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/request/pairs/anthropic_messages_to_openai_responses.rs"
)),
),
] {
for forbidden in [
".provider_metadata",
"provider_metadata:",
"providerMetadata",
".get(\"provider_metadata\")",
"[\"provider_metadata\"]",
] {
assert!(
!source.contains(forbidden),
"{path} is request bridge pair code and must not read legacy response provider metadata fragment `{forbidden}`"
);
}
}
}
#[cfg(feature = "google")]
#[test]
fn gemini_generate_content_request_normalization_is_protocol_adapter_backed() {
use std::fs;
use std::path::Path;
let bridge_root = Path::new(env!("CARGO_MANIFEST_DIR"));
let workspace_root = bridge_root
.parent()
.expect("siumai-bridge should live under workspace root");
let monolith = fs::read_to_string(bridge_root.join("src/request/normalize.rs"))
.expect("read normalize.rs");
let shim =
fs::read_to_string(bridge_root.join("src/request/normalize/gemini_generate_content.rs"))
.expect("read Gemini GenerateContent request shim");
let protocol_adapter = fs::read_to_string(
workspace_root
.join("siumai-protocol-gemini")
.join("src/standards/gemini/request_bridge.rs"),
)
.expect("read protocol-owned Gemini request bridge adapter");
assert!(
monolith.contains("mod gemini_generate_content;"),
"Gemini GenerateContent request normalization should keep a narrow bridge shim"
);
assert!(
monolith.contains("gemini_generate_content::parse_json_to_chat_request"),
"normalize.rs should keep only the public wrapper and delegate the Gemini parser"
);
assert!(
shim.contains(
"siumai_protocol_gemini::standards::gemini::request_bridge::parse_json_to_chat_request"
),
"Gemini bridge shim should delegate protocol wire parsing to siumai-protocol-gemini"
);
for forbidden in [
"fn parse_gemini_generate_content_json_to_chat_request",
"fn parse_gemini_content(",
"fn parse_gemini_tools(",
"GeminiGenerateContentRequest",
"GeminiRequestTool",
] {
assert!(
!monolith.contains(forbidden),
"Gemini-specific request parsing policy should live outside normalize.rs: found `{forbidden}`"
);
}
for required in [
"pub fn parse_json_to_chat_request",
"GeminiGenerateContentRequest",
"fn parse_gemini_content(",
"fn parse_gemini_tools(",
] {
assert!(
protocol_adapter.contains(required),
"protocol-owned Gemini GenerateContent adapter should own `{required}`"
);
}
}
#[cfg(feature = "google")]
#[test]
fn gemini_request_normalization_source_uses_provider_options_for_thought_signature() {
use std::path::Path;
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("siumai-bridge should live under workspace root");
let source = std::fs::read_to_string(
workspace_root
.join("siumai-protocol-gemini")
.join("src/standards/gemini/request_bridge.rs"),
)
.expect("read protocol-owned Gemini request bridge adapter");
assert!(source.contains("fn gemini_thought_signature_provider_options"));
for forbidden in [
"gemini_thought_signature_provider_metadata",
"provider_metadata: gemini_thought_signature",
] {
assert!(
!source.contains(forbidden),
"Gemini request normalization should not route thoughtSignature through legacy provider_metadata via {forbidden}"
);
}
}
#[cfg(feature = "google")]
#[test]
fn gemini_generate_content_request_normalization_roundtrip_preserves_core_projection() {
use siumai_core::types::MessageRole;
use siumai_core::types::ToolChoice;
let value = json!({
"model": "gemini-2.5-flash",
"systemInstruction": {
"parts": [
{ "text": "sys" }
]
},
"contents": [
{
"role": "user",
"parts": [
{ "text": "hello" },
{
"fileData": {
"file_uri": "https://example.com/input.txt",
"mime_type": "text/plain"
}
}
]
},
{
"role": "model",
"parts": [
{
"text": "internal step",
"thought": true,
"thoughtSignature": "sig_1"
},
{ "text": "Need tool" },
{
"functionCall": {
"name": "weather",
"args": { "city": "Tokyo" }
}
},
{
"executableCode": {
"language": "PYTHON",
"code": "print(1)"
}
}
]
},
{
"role": "user",
"parts": [
{
"functionResponse": {
"name": "weather",
"response": {
"name": "weather",
"content": { "ok": true }
}
}
},
{
"codeExecutionResult": {
"outcome": "OUTCOME_OK",
"output": "1"
}
}
]
}
],
"tools": [
{
"functionDeclarations": [
{
"name": "weather",
"description": "Get weather",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string" }
}
}
}
]
},
{ "googleSearch": {} },
{
"fileSearch": {
"fileSearchStoreNames": ["fileSearchStores/store-1"],
"topK": 3,
"metadataFilter": "scope = public"
}
},
{
"retrieval": {
"vertex_rag_store": {
"rag_resources": {
"rag_corpus": "projects/p/locations/l/ragCorpora/c"
},
"similarity_top_k": 4
}
}
}
],
"toolConfig": {
"functionCallingConfig": {
"mode": "ANY",
"allowedFunctionNames": ["weather"]
},
"retrievalConfig": {
"latLng": {
"latitude": 35.0,
"longitude": 139.0
}
}
},
"generationConfig": {
"temperature": 0.2,
"topP": 0.9,
"topK": 8,
"maxOutputTokens": 128,
"seed": 7,
"presencePenalty": 0.1,
"frequencyPenalty": 0.2,
"stopSequences": ["END"],
"responseMimeType": "application/json",
"responseJsonSchema": {
"type": "object",
"properties": {
"value": { "type": "string" }
}
},
"thinkingConfig": {
"thinkingBudget": 16
},
"responseModalities": ["TEXT"],
"mediaResolution": "MEDIA_RESOLUTION_LOW",
"responseLogprobs": true,
"logprobs": 5
},
"cachedContent": "cachedContents/test-123",
"safetySettings": [
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"threshold": "BLOCK_ONLY_HIGH"
}
],
"labels": {
"route": "bridge.test"
}
});
let normalized = bridge_gemini_generate_content_json_to_chat_request(&value).expect("parse");
assert_eq!(normalized.common_params.model, "gemini-2.5-flash");
assert_eq!(normalized.common_params.temperature, Some(0.2));
assert_eq!(normalized.common_params.top_p, Some(0.9));
assert_eq!(normalized.common_params.top_k, Some(8.0));
assert_eq!(normalized.common_params.max_tokens, Some(128));
assert_eq!(normalized.common_params.seed, Some(7));
assert_eq!(normalized.common_params.presence_penalty, Some(0.1));
assert_eq!(normalized.common_params.frequency_penalty, Some(0.2));
assert_eq!(
normalized.common_params.stop_sequences,
Some(vec!["END".to_string()])
);
assert_eq!(normalized.tool_choice, Some(ToolChoice::tool("weather")));
assert!(normalized.response_format.is_some());
assert_eq!(normalized.messages.len(), 4);
assert_eq!(normalized.messages[0].role, MessageRole::System);
assert_eq!(normalized.messages[0].content_text(), Some("sys"));
assert_eq!(normalized.messages[1].role, MessageRole::User);
assert_eq!(normalized.messages[2].role, MessageRole::Assistant);
assert_eq!(normalized.messages[2].tool_calls().len(), 2);
assert_eq!(normalized.messages[3].role, MessageRole::Tool);
assert_eq!(normalized.messages[3].tool_results().len(), 2);
let assistant_parts = normalized.messages[2]
.content
.as_multimodal()
.expect("assistant multimodal content");
let ContentPart::Reasoning {
provider_options,
provider_metadata,
..
} = &assistant_parts[0]
else {
panic!("expected assistant reasoning part");
};
assert_eq!(
provider_options
.get_object("google")
.and_then(|google| google.get("thoughtSignature")),
Some(&json!("sig_1"))
);
assert!(provider_metadata.is_none());
let google_options = normalized
.provider_option("google")
.and_then(|value| value.as_object())
.expect("google provider options");
assert_eq!(
google_options["cachedContent"],
json!("cachedContents/test-123")
);
assert_eq!(google_options["structuredOutputs"], json!(false));
assert_eq!(
google_options["responseJsonSchema"]["type"],
json!("object")
);
assert_eq!(
google_options["thinkingConfig"]["thinkingBudget"],
json!(16)
);
assert_eq!(
google_options["retrievalConfig"]["latLng"]["latitude"],
json!(35.0)
);
assert_eq!(google_options["labels"]["route"], json!("bridge.test"));
let tools = normalized.tools.as_ref().expect("tools");
assert_eq!(tools.len(), 4);
let bridged = bridge_chat_request_to_gemini_generate_content_json(
&normalized,
Some(BridgeTarget::GeminiGenerateContent),
BridgeMode::BestEffort,
)
.expect("bridge");
assert!(!bridged.is_rejected());
let bridged_value = bridged.value.expect("bridged json body");
assert_eq!(bridged_value["model"], json!("gemini-2.5-flash"));
assert_eq!(
bridged_value["systemInstruction"]["parts"][0]["text"],
json!("sys")
);
assert_eq!(
bridged_value["contents"][1]["parts"][0]["thoughtSignature"],
json!("sig_1")
);
assert_eq!(
bridged_value["contents"][1]["parts"][2]["functionCall"]["name"],
json!("weather")
);
assert_eq!(
bridged_value["contents"][1]["parts"][3]["executableCode"]["code"],
json!("print(1)")
);
assert_eq!(
bridged_value["contents"][2]["parts"][0]["functionResponse"]["name"],
json!("weather")
);
assert_eq!(
bridged_value["contents"][2]["parts"][1]["codeExecutionResult"]["outcome"],
json!("OUTCOME_OK")
);
assert_eq!(
bridged_value["toolConfig"]["functionCallingConfig"]["allowedFunctionNames"],
json!(["weather"])
);
assert_eq!(
bridged_value["toolConfig"]["retrievalConfig"]["latLng"]["longitude"],
json!(139.0)
);
assert_eq!(
bridged_value["generationConfig"]["responseJsonSchema"]["type"],
json!("object")
);
assert_eq!(
bridged_value["generationConfig"]["thinkingConfig"]["thinkingBudget"],
json!(16)
);
assert_eq!(bridged_value["labels"]["route"], json!("bridge.test"));
}