use crate::transform::generate::prompt_cache as cache;
mod controls;
pub(crate) mod history;
pub(crate) mod mcp;
mod media;
pub(crate) mod messages;
mod schema;
pub(crate) mod tools;
use crate::{
transform::{
Converted, Report, TransformError,
generate::signature::Carried,
identity::{IdentityFlow, TargetIdPolicy},
},
wire::{
DeclaredFields,
claude::{content as cc, generate_content as c},
openai::responses as r,
},
};
#[derive(Default)]
pub struct ClaudeRequestContext {
pub target: Option<crate::transform::identity::IdentityTarget>,
}
pub fn claude_to_responses_request(
input: c::GenerateContentRequestBody,
target_model: impl Into<String>,
flow: &mut IdentityFlow,
policy: &TargetIdPolicy,
) -> Result<Converted<r::GenerateContentRequestBody>, TransformError> {
let model = target_model.into();
let mut input = input.into_declared();
let mut ids = flow.clone();
let mut report = Report::default();
super::super::claude_controls::project(&mut input, &mut report);
let explicit_cache = cache::prepare_claude(&mut input, &mut report);
let mut out = r::GenerateContentRequestBody::builder().build();
controls::to_responses(&input, &mut out, &mut report)?;
out.model = Some(model.clone());
out.prompt_cache_options = explicit_cache.then(|| {
r::PromptCacheOptions::builder()
.mode(r::PromptCachingMode::Explicit)
.build()
});
out.tools = input
.tools
.map(|v| tools::to_responses(v, &mut report))
.map(crate::transform::optional)
.transpose()?
.flatten();
if let Some(choice) = input.tool_choice {
let (choice, parallel) = tools::choice_to_responses(choice);
out.tool_choice = Some(choice);
out.parallel_tool_calls = parallel.map(Some);
}
if let Some(servers) = input.mcp_servers {
for server in servers {
if let Some(tool) = mcp::to_responses(server)? {
out.tools.get_or_insert_with(Vec::new).push(tool);
}
}
}
let system = input.system.take();
out.input = Some(r::Input::Items(messages::to_responses(
input.messages,
&mut ids,
policy,
&mut report,
)?));
match system {
Some(crate::claude::count_tokens::SystemPrompt::Blocks(blocks))
if blocks.iter().any(|block| block.cache_control.is_some()) =>
{
if let Some(r::Input::Items(items)) = &mut out.input {
items.insert(
0,
r::InputItem::Easy(
r::EasyInputMessage::builder(
r::MessageContent::Parts(
blocks
.into_iter()
.map(|block| {
r::InputContent::Text(cache::responses_text(block))
})
.collect(),
),
r::MessageRole::System,
)
.build(),
),
);
}
}
Some(crate::claude::count_tokens::SystemPrompt::Blocks(blocks)) => {
out.instructions = Some(Some(
blocks
.into_iter()
.map(|block| block.text)
.collect::<Vec<_>>()
.join("\n"),
));
}
Some(crate::claude::count_tokens::SystemPrompt::Text(text)) => {
out.instructions = Some(Some(text))
}
None => {}
}
crate::transform::instructions::responses(
&mut out.input,
&mut out.instructions,
&model,
&mut report,
);
*flow = ids;
crate::transform::generate::openai_controls::target_responses(&mut out, &mut report);
Ok(Converted { value: out, report })
}
pub fn responses_to_claude_request(
input: r::GenerateContentRequestBody,
target_model: impl Into<String>,
context: ClaudeRequestContext,
) -> Result<Converted<c::GenerateContentRequestBody>, TransformError> {
let model = target_model.into();
if let Some(target) = &context.target
&& (target.model != model
|| target.dialect != crate::Dialect::Claude
|| target.origin.as_ref().is_none_or(|v| v.trim().is_empty()))
{
return Err(TransformError::shape(
"reasoning.target",
"native Claude replay requires matching model and explicit upstream origin",
));
}
let mut input = input.into_declared();
let max = input
.max_output_tokens
.flatten()
.ok_or_else(|| TransformError::missing_metadata("max_output_tokens"))?;
let mut out = c::GenerateContentRequestBody::builder(max, Vec::new(), model).build();
let mut report = Report::default();
cache::responses_breakpoint_defaults(&mut input);
crate::transform::generate::openai_controls::project_request(&mut input, &mut report)?;
super::super::client_tools::Bindings::for_target(&input, crate::Dialect::Claude)?
.lower(&mut input, &mut report)?;
controls::to_claude(&input, &mut out, &mut report)?;
out.cache_control = input
.prompt_cache_options
.as_ref()
.and_then(cache::responses_options);
if let Some(options) = &input.prompt_cache_options {
if options.prewarm == Some(true) {
return Err(TransformError::unsupported(
"prompt_cache_options.prewarm",
"Claude has no cache-only generation mode",
));
}
if options.comparison_response_id.is_some() {
report.omitted(
"prompt_cache_options.comparison_response_id",
"Claude has no matching cache comparison control",
);
}
}
if let Some(input_tools) = input.tools {
let mut native = Vec::new();
let mut servers = Vec::new();
for tool in input_tools {
match tool {
r::tools::Tool::Mcp(tool) => servers.push(mcp::to_claude(tool, &mut report)?),
tool => native.push(tool),
}
}
if !native.is_empty() {
out.tools = Some(tools::to_claude(native)?);
}
if !servers.is_empty() {
out.mcp_servers = Some(servers);
}
}
let choice = input.tool_choice.or_else(|| {
input
.parallel_tool_calls
.flatten()
.map(|_| r::ToolChoice::Mode(r::ToolChoiceMode::Auto))
});
out.tool_choice = choice
.map(|v| {
crate::transform::optional(tools::choice_to_claude(
v,
input.parallel_tool_calls.flatten(),
))
})
.transpose()?
.flatten();
let (mut messages, mut system) = history::to_claude(input.input, &mut report)?;
crate::transform::instructions::claude(&mut messages, &out.model, &mut report);
if let Some(Some(text)) = input.instructions {
system.insert(
0,
cc::TextBlock::builder(cc::TextBlockType::Tag, text).build(),
);
}
out.messages.append(&mut messages);
if !system.is_empty() {
out.system = Some(crate::claude::count_tokens::SystemPrompt::Blocks(system));
}
crate::transform::generate::claude_controls::target(
&mut out.messages,
&out.model,
&mut out.thinking,
&mut out.tool_choice,
&mut report,
);
Ok(Converted { value: out, report })
}
pub(super) fn thinking(
reasoning: r::ReasoningItem,
report: &mut Report,
) -> Option<cc::ThinkingBlock> {
let encrypted = reasoning.encrypted_content.flatten();
let Some(Carried::Claude(signature)) = encrypted.as_deref().and_then(Carried::parse) else {
report.omitted(
"reasoning",
"Claude thinking requires a Claude signature; this reasoning carries none",
);
return None;
};
if signature.is_empty() {
report.omitted(
"reasoning",
"Claude thinking requires a non-empty signature",
);
return None;
}
let text = reasoning.content.map_or_else(
|| {
reasoning
.summary
.into_iter()
.map(|v| v.text)
.collect::<Vec<_>>()
.join("")
},
|content| {
content
.into_iter()
.map(|v| v.text)
.collect::<Vec<_>>()
.join("")
},
);
Some(cc::ThinkingBlock::builder(cc::ThinkingBlockType::Tag, signature.to_owned(), text).build())
}