1use crate::protocol::{claude, openai};
2use crate::transform::TransformContext;
3
4use super::DEFAULT_MODEL;
5use super::tools::{
6 arguments_to_json_object, code_interpreter_input, response_server_tool_use_block,
7 serializable_to_json_object, shell_input, string_input_json_object,
8};
9use super::util::join_text;
10
11pub fn response(
12 input: openai::CompactedResponseObject,
13 _: &TransformContext,
14) -> claude::CreateMessageResponseBody {
15 crate::protocol::wire!(claude::CreateMessageResponseBody {
16 id: input.id,
17 type_: claude::MessageObjectType::Known(claude::MessageObjectTypeKnown::Message),
18 role: claude::AssistantRole::Known(claude::AssistantRoleKnown::Assistant),
19 content: compact_output_to_claude_content(input.output),
20 model: claude::ClaudeModel::Unknown(DEFAULT_MODEL.to_owned()),
21 stop_reason: claude::StopReason::Known(claude::StopReasonKnown::Compaction),
22 stop_sequence: None,
23 usage: openai_usage_to_claude(input.usage),
24 container: None,
25 context_management: None,
26 diagnostics: None,
27 stop_details: None,
28 extra: Default::default(),
29 })
30}
31
32fn compact_output_to_claude_content(
33 output: Vec<openai::CompactResponseItem>,
34) -> Vec<claude::ContentBlock> {
35 output
36 .into_iter()
37 .flat_map(compact_item_to_claude_content)
38 .collect()
39}
40
41fn compact_item_to_claude_content(item: openai::CompactResponseItem) -> Vec<claude::ContentBlock> {
42 match item {
43 openai::CompactResponseItem::Message(message) => compact_message_to_claude_content(message),
44 openai::CompactResponseItem::Typed(openai::TypedResponseItem::Compaction {
45 encrypted_content,
46 ..
47 }) => vec![claude::ContentBlock::Compaction(crate::protocol::wire!(
48 claude::ResponseCompactionBlock {
49 content: None,
50 encrypted_content,
51 type_: claude::CompactionBlockType::Compaction,
52 extra: Default::default(),
53 }
54 ))],
55 openai::CompactResponseItem::Typed(openai::TypedResponseItem::FunctionCall {
56 arguments,
57 call_id,
58 name,
59 id,
60 ..
61 }) => vec![claude::ContentBlock::ToolUse(crate::protocol::wire!(
62 claude::ResponseToolUseBlock {
63 id: id.unwrap_or(call_id),
64 input: arguments_to_json_object(&arguments),
65 name,
66 type_: claude::ToolUseBlockType::ToolUse,
67 caller: None,
68 extra: Default::default(),
69 }
70 ))],
71 openai::CompactResponseItem::Typed(openai::TypedResponseItem::CustomToolCall {
72 call_id,
73 input,
74 name,
75 id,
76 ..
77 }) => vec![claude::ContentBlock::ToolUse(crate::protocol::wire!(
78 claude::ResponseToolUseBlock {
79 id: id.unwrap_or(call_id),
80 input: string_input_json_object(input),
81 name,
82 type_: claude::ToolUseBlockType::ToolUse,
83 caller: None,
84 extra: Default::default(),
85 }
86 ))],
87 openai::CompactResponseItem::Typed(openai::TypedResponseItem::WebSearchCall {
88 id,
89 action,
90 ..
91 }) => vec![claude::ContentBlock::ServerToolUse(
92 response_server_tool_use_block(
93 id,
94 serializable_to_json_object(&action),
95 claude::ServerToolUseNameKnown::WebSearch,
96 ),
97 )],
98 openai::CompactResponseItem::Typed(openai::TypedResponseItem::CodeInterpreterCall {
99 id,
100 code,
101 container_id,
102 ..
103 }) => vec![claude::ContentBlock::ServerToolUse(
104 response_server_tool_use_block(
105 id,
106 code_interpreter_input(code, container_id),
107 claude::ServerToolUseNameKnown::CodeExecution,
108 ),
109 )],
110 openai::CompactResponseItem::Typed(openai::TypedResponseItem::LocalShellCall {
111 action,
112 call_id,
113 ..
114 }) => vec![claude::ContentBlock::ServerToolUse(
115 response_server_tool_use_block(
116 call_id,
117 serializable_to_json_object(&action),
118 claude::ServerToolUseNameKnown::BashCodeExecution,
119 ),
120 )],
121 openai::CompactResponseItem::Typed(openai::TypedResponseItem::ShellCall {
122 action,
123 call_id,
124 environment: None,
125 ..
126 }) => vec![claude::ContentBlock::ServerToolUse(
127 response_server_tool_use_block(
128 call_id,
129 serializable_to_json_object(&action),
130 claude::ServerToolUseNameKnown::BashCodeExecution,
131 ),
132 )],
133 openai::CompactResponseItem::Typed(openai::TypedResponseItem::ShellCall {
134 action,
135 call_id,
136 environment: Some(environment),
137 ..
138 }) => vec![claude::ContentBlock::ServerToolUse(
139 response_server_tool_use_block(
140 call_id,
141 shell_input(action, environment),
142 claude::ServerToolUseNameKnown::BashCodeExecution,
143 ),
144 )],
145 openai::CompactResponseItem::Typed(openai::TypedResponseItem::McpCall {
146 id,
147 arguments,
148 name,
149 server_label,
150 output,
151 error,
152 ..
153 }) => {
154 let mut blocks = vec![claude::ContentBlock::McpToolUse(crate::protocol::wire!(
155 claude::ResponseMcpToolUseBlock {
156 id: id.clone(),
157 input: arguments_to_json_object(&arguments),
158 name,
159 server_name: server_label,
160 type_: claude::ResponseMcpToolUseBlockType::McpToolUse,
161 extra: Default::default(),
162 }
163 ))];
164 if let Some(result) = response_mcp_result_block(id, output, error) {
165 blocks.push(claude::ContentBlock::McpToolResult(result));
166 }
167 blocks
168 }
169 openai::CompactResponseItem::Typed(openai::TypedResponseItem::Reasoning {
170 id,
171 summary,
172 content,
173 encrypted_content,
174 ..
175 }) => reasoning_to_claude_content(id, summary, content, encrypted_content),
176 _ => Vec::new(),
177 }
178}
179
180fn compact_message_to_claude_content(
181 message: openai::CompactMessageItem,
182) -> Vec<claude::ContentBlock> {
183 message
184 .content
185 .into_iter()
186 .filter_map(compact_content_part_to_claude)
187 .collect()
188}
189
190fn compact_content_part_to_claude(
191 part: openai::CompactMessageContentPart,
192) -> Option<claude::ContentBlock> {
193 let text = match part {
194 openai::CompactMessageContentPart::Input(openai::ResponseInputContentPart::InputText {
195 text,
196 ..
197 })
198 | openai::CompactMessageContentPart::Output(
199 openai::ResponseOutputContentPart::OutputText { text, .. },
200 )
201 | openai::CompactMessageContentPart::Output(
202 openai::ResponseOutputContentPart::ReasoningText { text, .. },
203 )
204 | openai::CompactMessageContentPart::Text(openai::CompactTextContent { text, .. })
205 | openai::CompactMessageContentPart::SummaryText(openai::CompactSummaryTextContent {
206 text,
207 ..
208 }) => text,
209 openai::CompactMessageContentPart::Output(openai::ResponseOutputContentPart::Refusal {
210 refusal,
211 ..
212 }) => refusal,
213 _ => return None,
214 };
215
216 Some(claude::ContentBlock::Text(crate::protocol::wire!(
217 claude::ResponseTextBlock {
218 citations: None,
219 text,
220 type_: claude::TextBlockType::Text,
221 extra: Default::default(),
222 }
223 )))
224}
225
226fn response_mcp_result_block(
227 tool_use_id: String,
228 output: Option<String>,
229 error: Option<String>,
230) -> Option<claude::ResponseMcpToolResultBlock> {
231 let is_error = error.is_some();
232 let content = error.or(output)?;
233 Some(crate::protocol::wire!(claude::ResponseMcpToolResultBlock {
234 content: claude::ResponseMcpToolResultContent::String(content),
235 is_error,
236 tool_use_id,
237 type_: claude::ResponseMcpToolResultBlockType::McpToolResult,
238 extra: Default::default(),
239 }))
240}
241
242fn reasoning_to_claude_content(
243 id: Option<String>,
244 summary: Vec<openai::ResponseReasoningSummaryPart>,
245 content: Option<Vec<openai::ResponseReasoningTextPart>>,
246 encrypted_content: Option<String>,
247) -> Vec<claude::ContentBlock> {
248 let mut blocks = Vec::new();
249 if let Some(encrypted_content) = encrypted_content {
250 blocks.push(claude::ContentBlock::RedactedThinking(
251 crate::protocol::wire!(claude::RedactedThinkingBlock {
252 data: encrypted_content,
253 type_: claude::RedactedThinkingBlockType::RedactedThinking,
254 }),
255 ));
256 }
257
258 let thinking = join_text(content.into_iter().flatten().map(|part| part.text));
259 if !thinking.is_empty() {
260 if let Some(signature) = id.filter(|signature| !signature.is_empty()) {
261 blocks.push(claude::ContentBlock::Thinking(crate::protocol::wire!(
262 claude::ThinkingBlock {
263 signature,
264 thinking,
265 type_: claude::ThinkingBlockType::Thinking,
266 }
267 )));
268 } else {
269 blocks.push(claude::ContentBlock::Text(crate::protocol::wire!(
270 claude::ResponseTextBlock {
271 citations: None,
272 text: thinking,
273 type_: claude::TextBlockType::Text,
274 extra: Default::default(),
275 }
276 )));
277 }
278 }
279
280 blocks.extend(summary.into_iter().map(|part| {
281 claude::ContentBlock::Text(crate::protocol::wire!(claude::ResponseTextBlock {
282 citations: None,
283 text: part.text,
284 type_: claude::TextBlockType::Text,
285 extra: Default::default(),
286 }))
287 }));
288 blocks
289}
290
291fn openai_usage_to_claude(usage: openai::ResponseUsage) -> claude::Usage {
292 let details = usage.input_tokens_details;
293 let cached = details.as_ref().map_or(0, |details| details.cached_tokens);
294 let cache_write = details
295 .as_ref()
296 .map_or(0, |details| details.cache_write_tokens);
297 crate::protocol::wire!(claude::Usage {
298 input_tokens: Some(u64::from(
299 usage
300 .input_tokens
301 .saturating_sub(cached)
302 .saturating_sub(cache_write),
303 )),
304 output_tokens: Some(u64::from(usage.output_tokens)),
305 cache_creation_input_tokens: details
306 .as_ref()
307 .filter(|details| details.cache_write_tokens > 0)
308 .map(|details| u64::from(details.cache_write_tokens)),
309 cache_read_input_tokens: details.map(|details| u64::from(details.cached_tokens)),
310 cache_creation: None,
311 output_tokens_details: Some(crate::protocol::wire!(claude::OutputTokensDetails {
312 thinking_tokens: u64::from(usage.output_tokens_details.reasoning_tokens),
313 extra: Default::default(),
314 })),
315 server_tool_use: None,
316 iterations: None,
317 inference_geo: None,
318 service_tier: None,
319 speed: None,
320 extra: Default::default(),
321 })
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn compact_usage_subtracts_cache_from_openai_input_total() {
330 let usage = openai_usage_to_claude(crate::protocol::wire!(openai::ResponseUsage {
331 input_tokens: 100,
332 output_tokens: 20,
333 total_tokens: 120,
334 input_tokens_details: Some(crate::protocol::wire!(
335 openai::ResponseInputTokensDetails {
336 cached_tokens: 60,
337 cache_write_tokens: 10,
338 extra: Default::default(),
339 }
340 )),
341 output_tokens_details: crate::protocol::wire!(openai::ResponseOutputTokensDetails {
342 reasoning_tokens: 5,
343 extra: Default::default(),
344 }),
345 extra: Default::default(),
346 }));
347 assert_eq!(usage.input_tokens, Some(30));
348 assert_eq!(usage.cache_read_input_tokens, Some(60));
349 assert_eq!(usage.cache_creation_input_tokens, Some(10));
350 }
351}