use super::{
content::{claude_message_to_openai, openai_message_to_claude},
tools::{
claude_choice_to_openai, claude_tools_to_openai, openai_choice_to_claude,
openai_tools_to_claude,
},
};
use crate::transform::generate::prompt_cache as cache;
use crate::wire::claude::count_tokens as cc;
use crate::{
Rest,
transform::{Converted, Report, TransformError},
wire::{claude::generate_content as cg, openai::chat},
};
pub fn claude_to_openai(
input: &cg::GenerateContentRequestBody,
target_model: impl Into<String>,
) -> Result<Converted<chat::GenerateContentRequestBody>, TransformError> {
let target_model = target_model.into();
let mut report = Report::default();
let mut projected = input.clone();
super::super::claude_controls::project(&mut projected, &mut report);
let explicit_cache = cache::prepare_claude(&mut projected, &mut report);
let input = &projected;
super::requirements::claude(input, &mut report)?;
let mut messages = Vec::new();
if let Some(system) = &input.system {
let content = match system {
cc::SystemPrompt::Text(text) => chat::TextContent::Text(text.clone()),
cc::SystemPrompt::Blocks(blocks)
if blocks.iter().any(|block| block.cache_control.is_some()) =>
{
chat::TextContent::Parts(blocks.iter().map(cache::chat_text).collect())
}
cc::SystemPrompt::Blocks(blocks) => chat::TextContent::Text(
blocks
.iter()
.map(|block| block.text.clone())
.collect::<Vec<_>>()
.join("\n"),
),
};
messages.push(chat::ChatMessage::System(chat::SystemMessage {
role: chat::SystemRole::System,
content,
name: None,
rest: Rest::new(),
}));
}
for message in &input.messages {
messages.extend(claude_message_to_openai(message, &mut report)?);
}
crate::transform::instructions::chat(&mut messages, &target_model, &mut report);
let tool_choice = claude_choice_to_openai(input.tool_choice.as_ref())?;
let parallel_tool_calls = input.tool_choice.as_ref().and_then(|choice| match choice {
cc::ToolChoice::Auto(choice) => choice.disable_parallel_tool_use.map(|value| !value),
cc::ToolChoice::Any(choice) => choice.disable_parallel_tool_use.map(|value| !value),
cc::ToolChoice::Tool(choice) => choice.disable_parallel_tool_use.map(|value| !value),
cc::ToolChoice::None(_) => None,
});
let format = input
.output_config
.as_ref()
.and_then(|config| config.format.as_ref())
.or(input.output_format.as_ref());
let response_format = format.and_then(|format| {
let schema = format.schema.as_object().cloned();
if schema.is_none() {
report.omitted("output_format", "target requires an object schema");
}
schema.map(|schema| {
chat::ResponseFormat::JsonSchema(chat::JsonSchemaResponseFormat {
type_: chat::JsonSchemaResponseType::JsonSchema,
json_schema: chat::JsonSchemaFormat {
name: "claude_output".into(),
description: None,
schema: Some(schema),
strict: Some(Some(true)),
rest: Rest::new(),
},
rest: Rest::new(),
})
})
});
let mut reasoning_effort = input
.output_config
.as_ref()
.and_then(|config| config.effort.as_ref())
.map(|effort| match effort {
cc::Effort::Low => chat::ReasoningEffort::Low,
cc::Effort::Medium => chat::ReasoningEffort::Medium,
cc::Effort::High => chat::ReasoningEffort::High,
cc::Effort::Xhigh => chat::ReasoningEffort::XHigh,
cc::Effort::Max => chat::ReasoningEffort::Max,
})
.map(Some);
match input.thinking.as_ref() {
Some(cc::ThinkingConfig::BetweenTools) => {
reasoning_effort = Some(Some(chat::ReasoningEffort::None));
report.changed(
"thinking.type",
"between_tools mapped to no reasoning; target has no between-tool progress mode",
);
}
Some(cc::ThinkingConfig::Disabled(_)) => {
reasoning_effort = Some(Some(chat::ReasoningEffort::None));
}
Some(cc::ThinkingConfig::Enabled(_)) => {
report.omitted("thinking.budget_tokens", "Chat has no token budget control");
}
Some(cc::ThinkingConfig::Adaptive(config)) if config.display.is_some() => {
report.omitted("thinking.display", "field has no target representation");
}
Some(cc::ThinkingConfig::Adaptive(_)) | None => {}
}
let mut converted = Converted {
value: chat::GenerateContentRequestBody {
messages,
model: target_model,
audio: None,
frequency_penalty: None,
function_call: None,
functions: None,
logit_bias: None,
logprobs: None,
max_completion_tokens: Some(Some(input.max_tokens)),
max_tokens: None,
metadata: None,
modalities: None,
moderation: None,
n: None,
parallel_tool_calls,
prediction: None,
presence_penalty: None,
prompt_cache_key: None,
prompt_cache_options: explicit_cache.then(|| {
chat::PromptCacheOptions::builder()
.mode(chat::PromptCacheOptionMode::Explicit)
.build()
}),
prompt_cache_retention: None,
reasoning_effort,
response_format,
safety_identifier: None,
seed: None,
service_tier: input.service_tier.map(|tier| {
Some(match tier {
cg::ServiceTier::Auto => chat::ServiceTier::Auto,
cg::ServiceTier::StandardOnly => chat::ServiceTier::Default,
})
}),
stop: input
.stop_sequences
.clone()
.map(chat::Stop::Sequences)
.map(Some),
store: None,
stream: input.stream.map(Some),
stream_options: None,
temperature: None,
tool_choice,
tools: claude_tools_to_openai(input.tools.as_ref(), &mut report)?,
top_logprobs: None,
top_p: None,
user: input
.metadata
.as_ref()
.and_then(|metadata| metadata.user_id.clone())
.map(Some),
verbosity: None,
web_search_options: None,
rest: Rest::new(),
},
report,
};
super::super::openai_controls::target_chat(&mut converted.value, &mut converted.report);
Ok(converted)
}
pub fn openai_to_claude(
input: &chat::GenerateContentRequestBody,
target_model: impl Into<String>,
) -> Result<Converted<cg::GenerateContentRequestBody>, TransformError> {
let target_model = target_model.into();
let mut report = Report::default();
if input.n.flatten().is_some_and(|value| value != 1) {
report.omitted("n", "field has no target representation");
}
super::requirements::chat(input, &mut report)?;
let mut messages = Vec::new();
let mut system = Vec::new();
let mut position = crate::transform::instructions::Position::default();
for message in &input.messages {
let leading = position.leading(matches!(
message,
chat::ChatMessage::System(_) | chat::ChatMessage::Developer(_)
));
match message {
chat::ChatMessage::System(message) if leading => {
system.extend(super::content::chat_text_blocks(&message.content))
}
chat::ChatMessage::Developer(message) if leading => {
system.extend(super::content::chat_text_blocks(&message.content))
}
chat::ChatMessage::System(_)
| chat::ChatMessage::Developer(_)
| chat::ChatMessage::User(_)
| chat::ChatMessage::Assistant(_)
| chat::ChatMessage::Tool(_)
| chat::ChatMessage::Function(_) => {
messages.extend(openai_message_to_claude(message, &mut report)?)
}
}
}
crate::transform::instructions::claude(&mut messages, &target_model, &mut report);
let max_tokens = input
.max_completion_tokens
.flatten()
.or(input.max_tokens.flatten())
.ok_or_else(|| {
TransformError::shape("max_tokens", "Claude Messages requires a token budget")
})?;
let stop_sequences = match input.stop.clone().flatten() {
None => None,
Some(chat::Stop::String(value)) => Some(vec![value]),
Some(chat::Stop::Sequences(values)) => Some(values),
};
let mut tool_choice = if let Some(choice) = input.function_call.as_ref() {
Some(super::tools::legacy_choice(choice))
} else {
openai_choice_to_claude(input.tool_choice.as_ref())?
};
if let Some(parallel) = input.parallel_tool_calls {
tool_choice = Some(apply_parallel(tool_choice, parallel));
}
let output_format = match input.response_format.as_ref() {
None => None,
Some(chat::ResponseFormat::JsonSchema(format)) => {
if format.json_schema.strict.flatten() != Some(true) {
report.changed("response_format.json_schema.strict","Claude enforces the supplied schema; Chat strict preference is not independently configurable");
}
if format.json_schema.description.is_some() {
report.omitted(
"response_format.json_schema.description",
"Claude format has no schema-wrapper description field",
);
}
report.omitted(
"response_format.json_schema.name",
"Claude format has no schema-wrapper name field",
);
format
.json_schema
.schema
.clone()
.map(|schema| cc::JsonOutputFormat {
type_: cc::JsonOutputFormatType::JsonSchema,
schema: serde_json::Value::Object(schema),
rest: Rest::new(),
})
}
Some(chat::ResponseFormat::Text(_)) => None,
Some(chat::ResponseFormat::JsonObject(_)) => {
report.omitted("response_format", "Claude has no matching format");
None
}
};
let output_effort = match input.reasoning_effort.flatten() {
Some(chat::ReasoningEffort::Low) => Some(cc::Effort::Low),
Some(chat::ReasoningEffort::Medium) => Some(cc::Effort::Medium),
Some(chat::ReasoningEffort::High) => Some(cc::Effort::High),
Some(chat::ReasoningEffort::XHigh) => Some(cc::Effort::Xhigh),
Some(chat::ReasoningEffort::Max) => Some(cc::Effort::Max),
_ => None,
};
let thinking = if input.reasoning_effort.flatten() == Some(chat::ReasoningEffort::None) {
Some(cc::ThinkingConfig::Disabled(
cc::ThinkingDisabled::builder().build(),
))
} else {
output_effort.map(|_| cc::ThinkingConfig::Adaptive(cc::ThinkingAdaptive::builder().build()))
};
let mut converted = Converted {
value: cg::GenerateContentRequestBody {
compaction: None,
max_tokens,
messages,
model: target_model,
cache_control: input
.prompt_cache_options
.as_ref()
.and_then(cache::chat_options),
container: None,
context_management: None,
diagnostics: None,
fallback_credit_token: None,
fallbacks: None,
inference_geo: None,
mcp_servers: None,
metadata: input.user.as_ref().and_then(Option::as_ref).map(|user| {
let mut metadata = cg::Metadata::builder().build();
metadata.user_id = Some(user.clone());
metadata
}),
output_config: output_effort.map(|effort| cc::OutputConfig {
effort: Some(effort),
format: None,
task_budget: None,
rest: Rest::new(),
}),
output_format,
service_tier: match input.service_tier.flatten() {
None => None,
Some(chat::ServiceTier::Auto) => Some(cg::ServiceTier::Auto),
Some(chat::ServiceTier::Default) => Some(cg::ServiceTier::StandardOnly),
Some(
chat::ServiceTier::Flex
| chat::ServiceTier::Scale
| chat::ServiceTier::Priority
| chat::ServiceTier::Fast
| chat::ServiceTier::Ultrafast,
) => {
report.omitted("service_tier", "Claude has no matching tier");
None
}
},
speed: None,
stop_sequences,
stream: input.stream.flatten(),
system: (!system.is_empty()).then(|| {
if system.iter().any(|block| block.cache_control.is_some()) {
cc::SystemPrompt::Blocks(system)
} else {
cc::SystemPrompt::Text(
system
.into_iter()
.map(|block| block.text)
.collect::<Vec<_>>()
.join("\n"),
)
}
}),
thinking,
tool_choice,
tools: if let Some(functions) = input.functions.as_ref() {
Some(super::tools::legacy_tools(functions)?)
} else {
openai_tools_to_claude(input.tools.as_ref())?
},
rest: Rest::new(),
},
report,
};
super::super::claude_controls::target(
&mut converted.value.messages,
&converted.value.model,
&mut converted.value.thinking,
&mut converted.value.tool_choice,
&mut converted.report,
);
Ok(converted)
}
fn apply_parallel(choice: Option<cc::ToolChoice>, parallel: bool) -> cc::ToolChoice {
let disable = Some(!parallel);
match choice {
Some(cc::ToolChoice::Auto(mut choice)) => {
choice.disable_parallel_tool_use = disable;
cc::ToolChoice::Auto(choice)
}
Some(cc::ToolChoice::Any(mut choice)) => {
choice.disable_parallel_tool_use = disable;
cc::ToolChoice::Any(choice)
}
Some(cc::ToolChoice::Tool(mut choice)) => {
choice.disable_parallel_tool_use = disable;
cc::ToolChoice::Tool(choice)
}
Some(choice) => choice,
None => cc::ToolChoice::Auto(cc::ToolChoiceAuto {
disable_parallel_tool_use: disable,
rest: Rest::new(),
}),
}
}