use crate::protocol::{claude, openai};
use crate::transform::TransformContext;
use super::DEFAULT_MODEL;
use super::tools::{
arguments_to_json_object, code_interpreter_input, response_server_tool_use_block,
serializable_to_json_object, shell_input, string_input_json_object,
};
use super::util::join_text;
pub fn response(
input: openai::CompactedResponseObject,
_: &TransformContext,
) -> claude::CreateMessageResponseBody {
crate::protocol::wire!(claude::CreateMessageResponseBody {
id: input.id,
type_: claude::MessageObjectType::Known(claude::MessageObjectTypeKnown::Message),
role: claude::AssistantRole::Known(claude::AssistantRoleKnown::Assistant),
content: compact_output_to_claude_content(input.output),
model: claude::ClaudeModel::Unknown(DEFAULT_MODEL.to_owned()),
stop_reason: claude::StopReason::Known(claude::StopReasonKnown::Compaction),
stop_sequence: None,
usage: openai_usage_to_claude(input.usage),
container: None,
context_management: None,
diagnostics: None,
stop_details: None,
extra: Default::default(),
})
}
fn compact_output_to_claude_content(
output: Vec<openai::CompactResponseItem>,
) -> Vec<claude::ContentBlock> {
output
.into_iter()
.flat_map(compact_item_to_claude_content)
.collect()
}
fn compact_item_to_claude_content(item: openai::CompactResponseItem) -> Vec<claude::ContentBlock> {
match item {
openai::CompactResponseItem::Message(message) => compact_message_to_claude_content(message),
openai::CompactResponseItem::Typed(openai::TypedResponseItem::Compaction {
encrypted_content,
..
}) => vec![claude::ContentBlock::Compaction(crate::protocol::wire!(
claude::ResponseCompactionBlock {
content: None,
encrypted_content,
type_: claude::CompactionBlockType::Compaction,
extra: Default::default(),
}
))],
openai::CompactResponseItem::Typed(openai::TypedResponseItem::FunctionCall {
arguments,
call_id,
name,
id,
..
}) => vec![claude::ContentBlock::ToolUse(crate::protocol::wire!(
claude::ResponseToolUseBlock {
id: id.unwrap_or(call_id),
input: arguments_to_json_object(&arguments),
name,
type_: claude::ToolUseBlockType::ToolUse,
caller: None,
extra: Default::default(),
}
))],
openai::CompactResponseItem::Typed(openai::TypedResponseItem::CustomToolCall {
call_id,
input,
name,
id,
..
}) => vec![claude::ContentBlock::ToolUse(crate::protocol::wire!(
claude::ResponseToolUseBlock {
id: id.unwrap_or(call_id),
input: string_input_json_object(input),
name,
type_: claude::ToolUseBlockType::ToolUse,
caller: None,
extra: Default::default(),
}
))],
openai::CompactResponseItem::Typed(openai::TypedResponseItem::WebSearchCall {
id,
action,
..
}) => vec![claude::ContentBlock::ServerToolUse(
response_server_tool_use_block(
id,
serializable_to_json_object(&action),
claude::ServerToolUseNameKnown::WebSearch,
),
)],
openai::CompactResponseItem::Typed(openai::TypedResponseItem::CodeInterpreterCall {
id,
code,
container_id,
..
}) => vec![claude::ContentBlock::ServerToolUse(
response_server_tool_use_block(
id,
code_interpreter_input(code, container_id),
claude::ServerToolUseNameKnown::CodeExecution,
),
)],
openai::CompactResponseItem::Typed(openai::TypedResponseItem::LocalShellCall {
action,
call_id,
..
}) => vec![claude::ContentBlock::ServerToolUse(
response_server_tool_use_block(
call_id,
serializable_to_json_object(&action),
claude::ServerToolUseNameKnown::BashCodeExecution,
),
)],
openai::CompactResponseItem::Typed(openai::TypedResponseItem::ShellCall {
action,
call_id,
environment: None,
..
}) => vec![claude::ContentBlock::ServerToolUse(
response_server_tool_use_block(
call_id,
serializable_to_json_object(&action),
claude::ServerToolUseNameKnown::BashCodeExecution,
),
)],
openai::CompactResponseItem::Typed(openai::TypedResponseItem::ShellCall {
action,
call_id,
environment: Some(environment),
..
}) => vec![claude::ContentBlock::ServerToolUse(
response_server_tool_use_block(
call_id,
shell_input(action, environment),
claude::ServerToolUseNameKnown::BashCodeExecution,
),
)],
openai::CompactResponseItem::Typed(openai::TypedResponseItem::McpCall {
id,
arguments,
name,
server_label,
output,
error,
..
}) => {
let mut blocks = vec![claude::ContentBlock::McpToolUse(crate::protocol::wire!(
claude::ResponseMcpToolUseBlock {
id: id.clone(),
input: arguments_to_json_object(&arguments),
name,
server_name: server_label,
type_: claude::ResponseMcpToolUseBlockType::McpToolUse,
extra: Default::default(),
}
))];
if let Some(result) = response_mcp_result_block(id, output, error) {
blocks.push(claude::ContentBlock::McpToolResult(result));
}
blocks
}
openai::CompactResponseItem::Typed(openai::TypedResponseItem::Reasoning {
id,
summary,
content,
encrypted_content,
..
}) => reasoning_to_claude_content(id, summary, content, encrypted_content),
_ => Vec::new(),
}
}
fn compact_message_to_claude_content(
message: openai::CompactMessageItem,
) -> Vec<claude::ContentBlock> {
message
.content
.into_iter()
.filter_map(compact_content_part_to_claude)
.collect()
}
fn compact_content_part_to_claude(
part: openai::CompactMessageContentPart,
) -> Option<claude::ContentBlock> {
let text = match part {
openai::CompactMessageContentPart::Input(openai::ResponseInputContentPart::InputText {
text,
..
})
| openai::CompactMessageContentPart::Output(
openai::ResponseOutputContentPart::OutputText { text, .. },
)
| openai::CompactMessageContentPart::Output(
openai::ResponseOutputContentPart::ReasoningText { text, .. },
)
| openai::CompactMessageContentPart::Text(openai::CompactTextContent { text, .. })
| openai::CompactMessageContentPart::SummaryText(openai::CompactSummaryTextContent {
text,
..
}) => text,
openai::CompactMessageContentPart::Output(openai::ResponseOutputContentPart::Refusal {
refusal,
..
}) => refusal,
_ => return None,
};
Some(claude::ContentBlock::Text(crate::protocol::wire!(
claude::ResponseTextBlock {
citations: None,
text,
type_: claude::TextBlockType::Text,
extra: Default::default(),
}
)))
}
fn response_mcp_result_block(
tool_use_id: String,
output: Option<String>,
error: Option<String>,
) -> Option<claude::ResponseMcpToolResultBlock> {
let is_error = error.is_some();
let content = error.or(output)?;
Some(crate::protocol::wire!(claude::ResponseMcpToolResultBlock {
content: claude::ResponseMcpToolResultContent::String(content),
is_error,
tool_use_id,
type_: claude::ResponseMcpToolResultBlockType::McpToolResult,
extra: Default::default(),
}))
}
fn reasoning_to_claude_content(
id: Option<String>,
summary: Vec<openai::ResponseReasoningSummaryPart>,
content: Option<Vec<openai::ResponseReasoningTextPart>>,
encrypted_content: Option<String>,
) -> Vec<claude::ContentBlock> {
let mut blocks = Vec::new();
if let Some(encrypted_content) = encrypted_content {
blocks.push(claude::ContentBlock::RedactedThinking(
crate::protocol::wire!(claude::RedactedThinkingBlock {
data: encrypted_content,
type_: claude::RedactedThinkingBlockType::RedactedThinking,
}),
));
}
let thinking = join_text(content.into_iter().flatten().map(|part| part.text));
if !thinking.is_empty() {
if let Some(signature) = id.filter(|signature| !signature.is_empty()) {
blocks.push(claude::ContentBlock::Thinking(crate::protocol::wire!(
claude::ThinkingBlock {
signature,
thinking,
type_: claude::ThinkingBlockType::Thinking,
}
)));
} else {
blocks.push(claude::ContentBlock::Text(crate::protocol::wire!(
claude::ResponseTextBlock {
citations: None,
text: thinking,
type_: claude::TextBlockType::Text,
extra: Default::default(),
}
)));
}
}
blocks.extend(summary.into_iter().map(|part| {
claude::ContentBlock::Text(crate::protocol::wire!(claude::ResponseTextBlock {
citations: None,
text: part.text,
type_: claude::TextBlockType::Text,
extra: Default::default(),
}))
}));
blocks
}
fn openai_usage_to_claude(usage: openai::ResponseUsage) -> claude::Usage {
let details = usage.input_tokens_details;
let cached = details.as_ref().map_or(0, |details| details.cached_tokens);
let cache_write = details
.as_ref()
.map_or(0, |details| details.cache_write_tokens);
crate::protocol::wire!(claude::Usage {
input_tokens: Some(u64::from(
usage
.input_tokens
.saturating_sub(cached)
.saturating_sub(cache_write),
)),
output_tokens: Some(u64::from(usage.output_tokens)),
cache_creation_input_tokens: details
.as_ref()
.filter(|details| details.cache_write_tokens > 0)
.map(|details| u64::from(details.cache_write_tokens)),
cache_read_input_tokens: details.map(|details| u64::from(details.cached_tokens)),
cache_creation: None,
output_tokens_details: Some(crate::protocol::wire!(claude::OutputTokensDetails {
thinking_tokens: u64::from(usage.output_tokens_details.reasoning_tokens),
extra: Default::default(),
})),
server_tool_use: None,
iterations: None,
inference_geo: None,
service_tier: None,
speed: None,
extra: Default::default(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compact_usage_subtracts_cache_from_openai_input_total() {
let usage = openai_usage_to_claude(crate::protocol::wire!(openai::ResponseUsage {
input_tokens: 100,
output_tokens: 20,
total_tokens: 120,
input_tokens_details: Some(crate::protocol::wire!(
openai::ResponseInputTokensDetails {
cached_tokens: 60,
cache_write_tokens: 10,
extra: Default::default(),
}
)),
output_tokens_details: crate::protocol::wire!(openai::ResponseOutputTokensDetails {
reasoning_tokens: 5,
extra: Default::default(),
}),
extra: Default::default(),
}));
assert_eq!(usage.input_tokens, Some(30));
assert_eq!(usage.cache_read_input_tokens, Some(60));
assert_eq!(usage.cache_creation_input_tokens, Some(10));
}
}