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 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(
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(
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(
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(
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(claude::ResponseTextBlock {
217 citations: None,
218 text,
219 type_: claude::TextBlockType::Text,
220 extra: Default::default(),
221 }))
222}
223
224fn response_mcp_result_block(
225 tool_use_id: String,
226 output: Option<String>,
227 error: Option<String>,
228) -> Option<claude::ResponseMcpToolResultBlock> {
229 let is_error = error.is_some();
230 let content = error.or(output)?;
231 Some(claude::ResponseMcpToolResultBlock {
232 content: claude::ResponseMcpToolResultContent::String(content),
233 is_error,
234 tool_use_id,
235 type_: claude::ResponseMcpToolResultBlockType::McpToolResult,
236 extra: Default::default(),
237 })
238}
239
240fn reasoning_to_claude_content(
241 id: Option<String>,
242 summary: Vec<openai::ResponseReasoningSummaryPart>,
243 content: Option<Vec<openai::ResponseReasoningTextPart>>,
244 encrypted_content: Option<String>,
245) -> Vec<claude::ContentBlock> {
246 let mut blocks = Vec::new();
247 if let Some(encrypted_content) = encrypted_content {
248 blocks.push(claude::ContentBlock::RedactedThinking(
249 claude::RedactedThinkingBlock {
250 data: encrypted_content,
251 type_: claude::RedactedThinkingBlockType::RedactedThinking,
252 },
253 ));
254 }
255
256 let thinking = join_text(content.into_iter().flatten().map(|part| part.text));
257 if !thinking.is_empty() {
258 if let Some(signature) = id.filter(|signature| !signature.is_empty()) {
259 blocks.push(claude::ContentBlock::Thinking(claude::ThinkingBlock {
260 signature,
261 thinking,
262 type_: claude::ThinkingBlockType::Thinking,
263 }));
264 } else {
265 blocks.push(claude::ContentBlock::Text(claude::ResponseTextBlock {
266 citations: None,
267 text: thinking,
268 type_: claude::TextBlockType::Text,
269 extra: Default::default(),
270 }));
271 }
272 }
273
274 blocks.extend(summary.into_iter().map(|part| {
275 claude::ContentBlock::Text(claude::ResponseTextBlock {
276 citations: None,
277 text: part.text,
278 type_: claude::TextBlockType::Text,
279 extra: Default::default(),
280 })
281 }));
282 blocks
283}
284
285fn openai_usage_to_claude(usage: openai::ResponseUsage) -> claude::Usage {
286 let details = usage.input_tokens_details;
287 claude::Usage {
288 input_tokens: Some(u64::from(usage.input_tokens)),
289 output_tokens: Some(u64::from(usage.output_tokens)),
290 cache_creation_input_tokens: details
291 .as_ref()
292 .filter(|details| details.cache_write_tokens > 0)
293 .map(|details| u64::from(details.cache_write_tokens)),
294 cache_read_input_tokens: details.map(|details| u64::from(details.cached_tokens)),
295 cache_creation: None,
296 output_tokens_details: Some(claude::OutputTokensDetails {
297 thinking_tokens: u64::from(usage.output_tokens_details.reasoning_tokens),
298 extra: Default::default(),
299 }),
300 server_tool_use: None,
301 iterations: None,
302 inference_geo: None,
303 service_tier: None,
304 speed: None,
305 extra: Default::default(),
306 }
307}