1use crate::protocol::{claude, openai};
2use crate::transform::TransformContext;
3
4use super::DEFAULT_MODEL;
5use super::tools::{
6 apply_patch_to_text_editor_input, arguments_to_json_object, code_interpreter_input,
7 local_shell_to_bash_input, response_server_tool_use_block, shell_to_bash_input,
8 string_input_json_object, web_action_to_claude,
9};
10use super::util::join_text;
11
12pub fn response(
13 input: openai::CompactedResponseObject,
14 _: &TransformContext,
15) -> claude::CreateMessageResponseBody {
16 crate::protocol::wire!(claude::CreateMessageResponseBody {
17 id: input.id,
18 type_: claude::MessageObjectType::Known(claude::MessageObjectTypeKnown::Message),
19 role: claude::AssistantRole::Known(claude::AssistantRoleKnown::Assistant),
20 content: compact_output_to_claude_content(input.output),
21 model: claude::ClaudeModel::Unknown(DEFAULT_MODEL.to_owned()),
22 stop_reason: claude::StopReason::Known(claude::StopReasonKnown::Compaction),
23 stop_sequence: None,
24 usage: openai_usage_to_claude(input.usage),
25 container: None,
26 context_management: None,
27 diagnostics: None,
28 stop_details: None,
29 extra: Default::default(),
30 })
31}
32
33fn compact_output_to_claude_content(
34 output: Vec<openai::CompactResponseItem>,
35) -> Vec<claude::ContentBlock> {
36 output
37 .into_iter()
38 .flat_map(compact_item_to_claude_content)
39 .collect()
40}
41
42fn compact_item_to_claude_content(item: openai::CompactResponseItem) -> Vec<claude::ContentBlock> {
43 match item {
44 openai::CompactResponseItem::Message(message) => compact_message_to_claude_content(message),
45 openai::CompactResponseItem::Typed(openai::TypedResponseItem::Compaction {
46 encrypted_content,
47 ..
48 }) => vec![claude::ContentBlock::Compaction(crate::protocol::wire!(
49 claude::ResponseCompactionBlock {
50 content: None,
51 encrypted_content,
52 type_: claude::CompactionBlockType::Compaction,
53 extra: Default::default(),
54 }
55 ))],
56 openai::CompactResponseItem::Typed(openai::TypedResponseItem::FunctionCall {
57 arguments,
58 call_id,
59 name,
60 id,
61 ..
62 }) => vec![claude::ContentBlock::ToolUse(crate::protocol::wire!(
63 claude::ResponseToolUseBlock {
64 id: id.unwrap_or(call_id),
65 input: arguments_to_json_object(&arguments),
66 name,
67 type_: claude::ToolUseBlockType::ToolUse,
68 caller: None,
69 extra: Default::default(),
70 }
71 ))],
72 openai::CompactResponseItem::Typed(openai::TypedResponseItem::CustomToolCall {
73 call_id,
74 input,
75 name,
76 id,
77 ..
78 }) => vec![claude::ContentBlock::ToolUse(crate::protocol::wire!(
79 claude::ResponseToolUseBlock {
80 id: id.unwrap_or(call_id),
81 input: string_input_json_object(input),
82 name,
83 type_: claude::ToolUseBlockType::ToolUse,
84 caller: None,
85 extra: Default::default(),
86 }
87 ))],
88 openai::CompactResponseItem::Typed(openai::TypedResponseItem::WebSearchCall {
89 id,
90 action,
91 ..
92 }) => {
93 let (name, input) = web_action_to_claude(action);
94 vec![claude::ContentBlock::ServerToolUse(
95 response_server_tool_use_block(id, input, name),
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![response_tool_use_block(
115 call_id,
116 local_shell_to_bash_input(action),
117 "bash",
118 )],
119 openai::CompactResponseItem::Typed(openai::TypedResponseItem::ShellCall {
120 action,
121 call_id,
122 environment: None,
123 ..
124 }) => vec![response_tool_use_block(
125 call_id,
126 shell_to_bash_input(action, None),
127 "bash",
128 )],
129 openai::CompactResponseItem::Typed(openai::TypedResponseItem::ShellCall {
130 action,
131 call_id,
132 environment: Some(environment),
133 ..
134 }) => vec![response_tool_use_block(
135 call_id,
136 shell_to_bash_input(action, Some(environment)),
137 "bash",
138 )],
139 openai::CompactResponseItem::Typed(openai::TypedResponseItem::ApplyPatchCall {
140 call_id,
141 operation,
142 ..
143 }) => vec![response_tool_use_block(
144 call_id,
145 apply_patch_to_text_editor_input(operation),
146 "str_replace_based_edit_tool",
147 )],
148 openai::CompactResponseItem::Typed(openai::TypedResponseItem::ToolSearchCall {
149 arguments,
150 id,
151 call_id,
152 execution,
153 ..
154 }) => vec![claude::ContentBlock::ServerToolUse(
155 response_server_tool_use_block(
156 id.or(call_id).unwrap_or_else(|| "tool_search".to_owned()),
157 match arguments {
158 serde_json::Value::Object(map) => map.into_iter().collect(),
159 value => {
160 let mut input = claude::JsonObject::new();
161 input.insert("value".to_owned(), value);
162 input
163 }
164 },
165 if matches!(execution, Some(openai::ToolSearchExecution::Client)) {
166 claude::ServerToolUseNameKnown::ToolSearchToolRegex
167 } else {
168 claude::ServerToolUseNameKnown::ToolSearchToolBm25
169 },
170 ),
171 )],
172 openai::CompactResponseItem::Typed(openai::TypedResponseItem::ToolSearchOutput {
173 tools,
174 id,
175 call_id,
176 ..
177 }) => vec![claude::ContentBlock::ToolSearchToolResult(
178 crate::protocol::wire!(claude::ResponseToolSearchToolResultBlock {
179 content: claude::ResponseToolSearchToolResultContent::Result(
180 crate::protocol::wire!(claude::ResponseToolSearchToolSearchResultBlock {
181 tool_references: response_tool_references(tools),
182 type_: claude::ResponseToolSearchToolSearchResultBlockType::ToolSearchToolSearchResult,
183 extra: Default::default(),
184 }),
185 ),
186 tool_use_id: call_id.or(id).unwrap_or_else(|| "tool_search".to_owned()),
187 type_: claude::ToolSearchToolResultBlockType::ToolSearchToolResult,
188 extra: Default::default(),
189 }),
190 )],
191 openai::CompactResponseItem::Typed(openai::TypedResponseItem::McpCall {
192 id,
193 arguments,
194 name,
195 server_label,
196 output,
197 error,
198 ..
199 }) => {
200 let mut blocks = vec![claude::ContentBlock::McpToolUse(crate::protocol::wire!(
201 claude::ResponseMcpToolUseBlock {
202 id: id.clone(),
203 input: arguments_to_json_object(&arguments),
204 name,
205 server_name: server_label,
206 type_: claude::ResponseMcpToolUseBlockType::McpToolUse,
207 extra: Default::default(),
208 }
209 ))];
210 if let Some(result) = response_mcp_result_block(id, output, error) {
211 blocks.push(claude::ContentBlock::McpToolResult(result));
212 }
213 blocks
214 }
215 openai::CompactResponseItem::Typed(openai::TypedResponseItem::Reasoning {
216 id,
217 summary,
218 content,
219 encrypted_content,
220 ..
221 }) => reasoning_to_claude_content(id, summary, content, encrypted_content),
222 _ => Vec::new(),
223 }
224}
225
226fn response_tool_use_block(
227 id: String,
228 input: claude::JsonObject,
229 name: &str,
230) -> claude::ContentBlock {
231 claude::ContentBlock::ToolUse(crate::protocol::wire!(claude::ResponseToolUseBlock {
232 id,
233 input,
234 name: name.to_owned(),
235 type_: claude::ToolUseBlockType::ToolUse,
236 caller: None,
237 extra: Default::default(),
238 }))
239}
240
241fn response_tool_references(
242 tools: Vec<openai::ResponseTool>,
243) -> Vec<claude::ResponseToolReferenceBlock> {
244 tools
245 .into_iter()
246 .filter_map(|tool| match tool {
247 openai::ResponseTool::Function { name, .. }
248 | openai::ResponseTool::Custom { name, .. } => Some(name),
249 _ => None,
250 })
251 .map(|tool_name| {
252 crate::protocol::wire!(claude::ResponseToolReferenceBlock {
253 tool_name,
254 type_: claude::ResponseToolReferenceBlockType::ToolReference,
255 extra: Default::default(),
256 })
257 })
258 .collect()
259}
260
261fn compact_message_to_claude_content(
262 message: openai::CompactMessageItem,
263) -> Vec<claude::ContentBlock> {
264 message
265 .content
266 .into_iter()
267 .filter_map(compact_content_part_to_claude)
268 .collect()
269}
270
271fn compact_content_part_to_claude(
272 part: openai::CompactMessageContentPart,
273) -> Option<claude::ContentBlock> {
274 let text = match part {
275 openai::CompactMessageContentPart::Input(openai::ResponseInputContentPart::InputText {
276 text,
277 ..
278 })
279 | openai::CompactMessageContentPart::Output(
280 openai::ResponseOutputContentPart::OutputText { text, .. },
281 )
282 | openai::CompactMessageContentPart::Output(
283 openai::ResponseOutputContentPart::ReasoningText { text, .. },
284 )
285 | openai::CompactMessageContentPart::Text(openai::CompactTextContent { text, .. })
286 | openai::CompactMessageContentPart::SummaryText(openai::CompactSummaryTextContent {
287 text,
288 ..
289 }) => text,
290 openai::CompactMessageContentPart::Output(openai::ResponseOutputContentPart::Refusal {
291 refusal,
292 ..
293 }) => refusal,
294 _ => return None,
295 };
296
297 Some(claude::ContentBlock::Text(crate::protocol::wire!(
298 claude::ResponseTextBlock {
299 citations: None,
300 text,
301 type_: claude::TextBlockType::Text,
302 extra: Default::default(),
303 }
304 )))
305}
306
307fn response_mcp_result_block(
308 tool_use_id: String,
309 output: Option<String>,
310 error: Option<String>,
311) -> Option<claude::ResponseMcpToolResultBlock> {
312 let is_error = error.is_some();
313 let content = error.or(output)?;
314 Some(crate::protocol::wire!(claude::ResponseMcpToolResultBlock {
315 content: claude::ResponseMcpToolResultContent::String(content),
316 is_error,
317 tool_use_id,
318 type_: claude::ResponseMcpToolResultBlockType::McpToolResult,
319 extra: Default::default(),
320 }))
321}
322
323fn reasoning_to_claude_content(
324 _id: Option<String>,
325 summary: Vec<openai::ResponseReasoningSummaryPart>,
326 content: Option<Vec<openai::ResponseReasoningTextPart>>,
327 encrypted_content: Option<String>,
328) -> Vec<claude::ContentBlock> {
329 let mut blocks = Vec::new();
330 let thinking = join_text(content.into_iter().flatten().map(|part| part.text));
331 if !thinking.is_empty() {
332 if let Some(signature) = encrypted_content.filter(|value| !value.is_empty()) {
333 blocks.push(claude::ContentBlock::Thinking(crate::protocol::wire!(
334 claude::ThinkingBlock {
335 signature,
336 thinking,
337 type_: claude::ThinkingBlockType::Thinking,
338 }
339 )));
340 } else {
341 blocks.push(claude::ContentBlock::Text(crate::protocol::wire!(
342 claude::ResponseTextBlock {
343 citations: None,
344 text: thinking,
345 type_: claude::TextBlockType::Text,
346 extra: Default::default(),
347 }
348 )));
349 }
350 } else if let Some(encrypted_content) = encrypted_content {
351 blocks.push(claude::ContentBlock::RedactedThinking(
352 crate::protocol::wire!(claude::RedactedThinkingBlock {
353 data: encrypted_content,
354 type_: claude::RedactedThinkingBlockType::RedactedThinking,
355 }),
356 ));
357 }
358
359 blocks.extend(summary.into_iter().map(|part| {
360 claude::ContentBlock::Text(crate::protocol::wire!(claude::ResponseTextBlock {
361 citations: None,
362 text: part.text,
363 type_: claude::TextBlockType::Text,
364 extra: Default::default(),
365 }))
366 }));
367 blocks
368}
369
370fn openai_usage_to_claude(usage: openai::ResponseUsage) -> claude::Usage {
371 let details = usage.input_tokens_details;
372 let cached = details.as_ref().map_or(0, |details| details.cached_tokens);
373 let cache_write = details
374 .as_ref()
375 .map_or(0, |details| details.cache_write_tokens);
376 crate::protocol::wire!(claude::Usage {
377 input_tokens: Some(u64::from(
378 usage
379 .input_tokens
380 .saturating_sub(cached)
381 .saturating_sub(cache_write),
382 )),
383 output_tokens: Some(u64::from(usage.output_tokens)),
384 cache_creation_input_tokens: details
385 .as_ref()
386 .filter(|details| details.cache_write_tokens > 0)
387 .map(|details| u64::from(details.cache_write_tokens)),
388 cache_read_input_tokens: details.map(|details| u64::from(details.cached_tokens)),
389 cache_creation: None,
390 output_tokens_details: Some(crate::protocol::wire!(claude::OutputTokensDetails {
391 thinking_tokens: u64::from(usage.output_tokens_details.reasoning_tokens),
392 extra: Default::default(),
393 })),
394 server_tool_use: None,
395 iterations: None,
396 inference_geo: None,
397 service_tier: None,
398 speed: None,
399 extra: Default::default(),
400 })
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn compact_usage_subtracts_cache_from_openai_input_total() {
409 let usage = openai_usage_to_claude(crate::protocol::wire!(openai::ResponseUsage {
410 input_tokens: 100,
411 output_tokens: 20,
412 total_tokens: 120,
413 input_tokens_details: Some(crate::protocol::wire!(
414 openai::ResponseInputTokensDetails {
415 cached_tokens: 60,
416 cache_write_tokens: 10,
417 extra: Default::default(),
418 }
419 )),
420 output_tokens_details: crate::protocol::wire!(openai::ResponseOutputTokensDetails {
421 reasoning_tokens: 5,
422 extra: Default::default(),
423 }),
424 extra: Default::default(),
425 }));
426 assert_eq!(usage.input_tokens, Some(30));
427 assert_eq!(usage.cache_read_input_tokens, Some(60));
428 assert_eq!(usage.cache_creation_input_tokens, Some(10));
429 }
430}