use crate::{
transform::{Report, TransformError},
wire::{
claude::{count_tokens as cc, tools as c},
openai::responses::{input as i, tools as r},
},
};
pub(crate) fn to_responses(
input: Vec<c::ToolUnion>,
report: &mut Report,
) -> Result<Vec<r::Tool>, TransformError> {
input
.into_iter()
.map(|tool| {
if matches!(tool, c::ToolUnion::WebSearch20250305(_) | c::ToolUnion::WebSearch20260209(_) | c::ToolUnion::WebSearch20260318(_)) {
report.changed("tools.web_search", "executed by Responses web search; provider-specific search controls are omitted");
return Ok(r::Tool::WebSearchLegacy(r::WebSearchTool::builder().build()));
}
let c::ToolUnion::Custom(tool) = tool else {
return Err(TransformError::unsupported(
"tools",
"Claude hosted tool requires Responses host binding",
));
};
if tool
.allowed_callers
.as_ref()
.is_some_and(|v| v.iter().any(|v| !matches!(v, c::AllowedCaller::Direct)))
{
report.omitted(
"tools.allowed_callers",
"caller restriction has no target representation",
);
}
let schema = super::schema::to_chat(&tool.input_schema)?;
let mut target = r::FunctionTool::builder(tool.name, Some(schema), tool.strict).build();
target.description = Some(tool.description);
target.defer_loading = tool.defer_loading;
if tool.cache_control.is_some()
|| tool.eager_input_streaming.is_some()
|| tool.input_examples.is_some()
{
report.omitted(
"tools.advisory",
"target lacks per-tool cache/input examples/eager streaming controls",
);
}
Ok(r::Tool::Function(target))
})
.filter_map(|value| crate::transform::optional(value).transpose())
.collect()
}
pub(crate) fn to_claude(input: Vec<r::Tool>) -> Result<Vec<c::ToolUnion>, TransformError> {
let mut tools: Vec<c::ToolUnion> = input
.into_iter()
.map(|tool| {
if matches!(
tool,
r::Tool::WebSearch(_)
| r::Tool::WebSearchPreview20250311(_)
| r::Tool::WebSearchLegacy(_)
| r::Tool::WebSearch2025(_)
) {
return Ok(c::ToolUnion::WebSearch20250305(
c::WebSearchTool20250305::builder(
c::WebSearchTool20250305Name::Name,
c::WebSearchTool20250305Type::Tag,
)
.build(),
));
}
let r::Tool::Function(tool) = tool else {
return Err(TransformError::unsupported(
"tools",
"Responses hosted/custom tools need explicit Claude binding",
));
};
let schema = match tool.parameters {
Some(schema) => super::schema::from_chat(&schema)?,
None => c::JsonSchema::builder(c::JsonSchemaType::Object).build(),
};
let mut target = c::Tool::builder(schema, tool.name).build();
target.description = tool.description.flatten();
target.strict = tool.strict;
target.defer_loading = tool.defer_loading;
Ok(c::ToolUnion::Custom(target))
})
.filter_map(|value| crate::transform::optional(value).transpose())
.collect::<Result<_, _>>()?;
if tools
.iter()
.any(|tool| matches!(tool, c::ToolUnion::WebSearch20250305(_)))
{
tools.push(c::ToolUnion::WebFetch20250910(
c::WebFetchTool20250910::builder(
c::WebFetchTool20250910Name::Name,
c::WebFetchTool20250910Type::Tag,
)
.build(),
));
}
Ok(tools)
}
pub(crate) fn choice_to_responses(choice: cc::ToolChoice) -> (i::ToolChoice, Option<bool>) {
match choice {
cc::ToolChoice::Auto(v) => (
i::ToolChoice::Mode(i::ToolChoiceMode::Auto),
v.disable_parallel_tool_use.map(|v| !v),
),
cc::ToolChoice::Any(v) => (
i::ToolChoice::Mode(i::ToolChoiceMode::Required),
v.disable_parallel_tool_use.map(|v| !v),
),
cc::ToolChoice::None(_) => (i::ToolChoice::Mode(i::ToolChoiceMode::None), None),
cc::ToolChoice::Tool(v) => (
i::ToolChoice::Function(
i::ToolChoiceFunction::builder(
i::ToolChoiceFunctionType::ToolChoiceFunction,
v.name,
)
.build(),
),
v.disable_parallel_tool_use.map(|v| !v),
),
}
}
pub(crate) fn choice_to_claude(
choice: i::ToolChoice,
parallel: Option<bool>,
) -> Result<cc::ToolChoice, TransformError> {
let disable = parallel.map(|v| !v);
Ok(match choice {
i::ToolChoice::Mode(i::ToolChoiceMode::Auto) => {
let mut v = cc::ToolChoiceAuto::builder().build();
v.disable_parallel_tool_use = disable;
cc::ToolChoice::Auto(v)
}
i::ToolChoice::Mode(i::ToolChoiceMode::Required) => {
let mut v = cc::ToolChoiceAny::builder().build();
v.disable_parallel_tool_use = disable;
cc::ToolChoice::Any(v)
}
i::ToolChoice::Mode(i::ToolChoiceMode::None) => {
cc::ToolChoice::None(cc::ToolChoiceNone::builder().build())
}
i::ToolChoice::Function(v) => {
let mut out = cc::ToolChoiceTool::builder(v.name).build();
out.disable_parallel_tool_use = disable;
cc::ToolChoice::Tool(out)
}
i::ToolChoice::Allowed(_)
| i::ToolChoice::Hosted(_)
| i::ToolChoice::Mcp(_)
| i::ToolChoice::Custom(_)
| i::ToolChoice::Programmatic(_)
| i::ToolChoice::ApplyPatch(_)
| i::ToolChoice::Shell(_) => {
return Err(TransformError::unsupported(
"tool_choice",
"Responses subset/hosted/custom choice requires tool adaptation",
));
}
})
}