use std::collections::BTreeMap;
use crate::protocol::{claude, openai};
use super::tools::{
ApproximateToolKind, apply_patch_result_item, approximate_tool_result_item,
function_call_output_item, mcp_tool_result_content_to_text, server_tool_result_output,
server_tool_use_item, shell_result_item, tool_result_content_to_openai,
tool_search_result_item, tool_use_item,
};
use super::util::{
document_source_to_input_part, image_source_to_input_part, join_text, json_object_to_string,
};
pub(super) fn system_to_openai_item(text: String) -> openai::ResponseItem {
openai::ResponseItem::Message(openai::ResponseMessageItem::EasyInput(
crate::protocol::wire!(openai::ResponseEasyInputMessageItem {
type_: Some(openai::ResponseMessageItemType::Message),
role: openai::ResponseEasyInputMessageRole::System,
content: openai::ResponseEasyInputContent::Text(text),
phase: None,
extra: Default::default(),
}),
))
}
pub(crate) fn claude_messages_to_openai_items(
messages: Vec<claude::MessageParam>,
) -> Vec<openai::ResponseItem> {
let mut approximate_tools = BTreeMap::new();
messages
.into_iter()
.flat_map(|message| claude_message_to_openai_items(message, &mut approximate_tools))
.collect()
}
fn claude_message_to_openai_items(
message: claude::MessageParam,
approximate_tools: &mut BTreeMap<String, ApproximateToolKind>,
) -> Vec<openai::ResponseItem> {
let role = claude_role_to_openai(message.role);
let assistant = role == openai::ResponseEasyInputMessageRole::Assistant;
let mut items = Vec::new();
let mut input_parts = Vec::new();
let mut output_parts = Vec::new();
match message.content {
claude::MessageContent::String(text) => {
if !text.is_empty() {
if assistant {
output_parts.push(response_output_text(text, None));
} else {
input_parts.push(response_input_text(text, None));
}
}
}
claude::MessageContent::Array(blocks) => {
for block in blocks {
match claude_request_block_to_openai(block, assistant, approximate_tools) {
ClaudeRequestBlockItem::InputMessagePart(part) => input_parts.push(part),
ClaudeRequestBlockItem::OutputMessagePart(part) => output_parts.push(part),
ClaudeRequestBlockItem::Item(item) => items.push(item),
ClaudeRequestBlockItem::None => {}
}
}
}
_ => {
unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
}
}
let content = if assistant {
(!output_parts.is_empty())
.then_some(openai::ResponseEasyInputContent::OutputParts(output_parts))
} else {
(!input_parts.is_empty()).then_some(openai::ResponseEasyInputContent::Parts(input_parts))
};
if let Some(content) = content {
items.push(openai::ResponseItem::Message(
openai::ResponseMessageItem::EasyInput(crate::protocol::wire!(
openai::ResponseEasyInputMessageItem {
type_: Some(openai::ResponseMessageItemType::Message),
role,
content,
phase: None,
extra: Default::default(),
}
)),
));
}
items
}
pub(super) enum ClaudeRequestBlockItem {
InputMessagePart(openai::ResponseInputContentPart),
OutputMessagePart(openai::ResponseMessageOutputContentPart),
Item(openai::ResponseItem),
None,
}
fn claude_role_to_openai(role: claude::MessageRole) -> openai::ResponseEasyInputMessageRole {
match role {
claude::MessageRole::Known(claude::MessageRoleKnown::Assistant) => {
openai::ResponseEasyInputMessageRole::Assistant
}
claude::MessageRole::Known(claude::MessageRoleKnown::System) => {
openai::ResponseEasyInputMessageRole::System
}
claude::MessageRole::Known(claude::MessageRoleKnown::User)
| claude::MessageRole::Unknown(_) => openai::ResponseEasyInputMessageRole::User,
_ => {
unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
}
}
}
fn claude_request_block_to_openai(
block: claude::ContentBlockParam,
assistant: bool,
approximate_tools: &mut BTreeMap<String, ApproximateToolKind>,
) -> ClaudeRequestBlockItem {
match block {
claude::ContentBlockParam::Text(block) => match assistant {
true => ClaudeRequestBlockItem::OutputMessagePart(response_output_text(
block.text,
crate::transform::generate_content::common::cache::openai_breakpoint(
block.cache_control,
),
)),
false => ClaudeRequestBlockItem::InputMessagePart(response_input_text(
block.text,
crate::transform::generate_content::common::cache::openai_breakpoint(
block.cache_control,
),
)),
},
claude::ContentBlockParam::Image(block) => image_source_to_input_part(block.source)
.filter(|_| !assistant)
.map(ClaudeRequestBlockItem::InputMessagePart)
.unwrap_or(ClaudeRequestBlockItem::None),
claude::ContentBlockParam::Document(block) => {
document_source_to_input_part(block.source, block.title)
.filter(|_| !assistant)
.map(ClaudeRequestBlockItem::InputMessagePart)
.unwrap_or(ClaudeRequestBlockItem::None)
}
claude::ContentBlockParam::ToolUse(block) => {
let id = block.id;
let (item, kind) = tool_use_item(id.clone(), block.input, block.name);
if let Some(kind) = kind {
approximate_tools.insert(id, kind);
}
item
}
claude::ContentBlockParam::ToolResult(block) => {
if let Some(kind) = approximate_tools.get(&block.tool_use_id).copied() {
approximate_tool_result_item(kind, block.tool_use_id, block.content, block.is_error)
} else {
function_call_output_item(
block.tool_use_id,
tool_result_content_to_openai(block.content),
)
}
}
claude::ContentBlockParam::Thinking(block) => ClaudeRequestBlockItem::Item(
openai::ResponseItem::Typed(openai::TypedResponseItem::Reasoning {
id: Some(
crate::transform::generate_content::common::id::response_reasoning_item_id(
&block.signature,
),
),
summary: Vec::new(),
content: Some(vec![crate::protocol::wire!(
openai::ResponseReasoningTextPart {
text: block.thinking,
type_: openai::ResponseReasoningTextType::ReasoningText,
extra: Default::default(),
}
)]),
encrypted_content: Some(block.signature),
status: Some(openai::ResponseItemLifecycleStatus::Completed),
extra: Default::default(),
}),
),
claude::ContentBlockParam::RedactedThinking(block) => ClaudeRequestBlockItem::Item(
openai::ResponseItem::Typed(openai::TypedResponseItem::Reasoning {
id: Some(
crate::transform::generate_content::common::id::response_reasoning_item_id(
&block.data,
),
),
summary: Vec::new(),
content: None,
encrypted_content: Some(block.data),
status: Some(openai::ResponseItemLifecycleStatus::Completed),
extra: Default::default(),
}),
),
claude::ContentBlockParam::Compaction(block) => {
let Some(encrypted_content) = block.encrypted_content else {
return block
.content
.map(|text| {
if assistant {
ClaudeRequestBlockItem::OutputMessagePart(response_output_text(
text, None,
))
} else {
ClaudeRequestBlockItem::InputMessagePart(response_input_text(
text, None,
))
}
})
.unwrap_or(ClaudeRequestBlockItem::None);
};
ClaudeRequestBlockItem::Item(openai::ResponseItem::Typed(
openai::TypedResponseItem::Compaction {
encrypted_content,
id: None,
created_by: None,
extra: Default::default(),
},
))
}
claude::ContentBlockParam::ServerToolUse(block) => {
server_tool_use_item(block.id, block.input, block.name)
}
claude::ContentBlockParam::WebSearchToolResult(block) => {
let _ = block;
ClaudeRequestBlockItem::None
}
claude::ContentBlockParam::WebFetchToolResult(block) => {
let _ = block;
ClaudeRequestBlockItem::None
}
claude::ContentBlockParam::AdvisorToolResult(block) => {
function_call_output_item(block.tool_use_id, server_tool_result_output(&block.content))
}
claude::ContentBlockParam::CodeExecutionToolResult(block) => {
function_call_output_item(block.tool_use_id, server_tool_result_output(&block.content))
}
claude::ContentBlockParam::BashCodeExecutionToolResult(block) => {
shell_result_item(block.tool_use_id, &block.content)
}
claude::ContentBlockParam::TextEditorCodeExecutionToolResult(block) => {
apply_patch_result_item(block.tool_use_id, &block.content)
}
claude::ContentBlockParam::ToolSearchToolResult(block) => {
tool_search_result_item(block.tool_use_id, &block.content)
}
claude::ContentBlockParam::McpToolUse(block) => ClaudeRequestBlockItem::Item(
openai::ResponseItem::Typed(openai::TypedResponseItem::McpCall {
id: block.id,
arguments: json_object_to_string(&block.input),
name: block.name,
server_label: block.server_name,
approval_request_id: None,
error: None,
output: None,
status: Some(openai::ResponseMcpCallStatus::Completed),
extra: Default::default(),
}),
),
claude::ContentBlockParam::McpToolResult(block) => function_call_output_item(
block.tool_use_id,
openai::ResponseOutput::Text(mcp_tool_result_content_to_text(block.content)),
),
claude::ContentBlockParam::MidConversationSystem(block) => {
let text = join_text(block.content.into_iter().filter_map(|block| match block {
claude::MidConversationSystemContentBlock::Text(block) => Some(block.text),
_ => None,
}));
if text.is_empty() {
ClaudeRequestBlockItem::None
} else {
ClaudeRequestBlockItem::Item(system_to_openai_item(text))
}
}
_ => ClaudeRequestBlockItem::None,
}
}
fn response_input_text(
text: String,
prompt_cache_breakpoint: Option<openai::PromptCacheBreakpoint>,
) -> openai::ResponseInputContentPart {
openai::ResponseInputContentPart::InputText {
text,
prompt_cache_breakpoint,
extra: Default::default(),
}
}
fn response_output_text(
text: String,
prompt_cache_breakpoint: Option<openai::PromptCacheBreakpoint>,
) -> openai::ResponseMessageOutputContentPart {
let mut extra = openai::Extra::new();
if let Some(breakpoint) = prompt_cache_breakpoint {
extra.insert(
"prompt_cache_breakpoint".to_owned(),
serde_json::to_value(breakpoint).expect("prompt cache breakpoint serializes"),
);
}
openai::ResponseMessageOutputContentPart::OutputText {
annotations: Vec::new(),
logprobs: None,
text,
extra,
}
}