gproxy_transform/transform/compact/claude_to_openai/
output.rs1use crate::protocol::{claude, openai};
2use crate::transform::TransformContext;
3
4use super::DEFAULT_REASONING_ID;
5use super::tools::{
6 compact_function_call_output_item, compact_server_tool_use_item,
7 response_mcp_tool_result_content_to_text, server_tool_result_output,
8};
9use super::util::{claude_usage_to_openai, json_object_to_string};
10
11pub fn response(
12 input: claude::CreateMessageResponseBody,
13 _: &TransformContext,
14) -> openai::CompactedResponseObject {
15 crate::protocol::wire!(openai::CompactedResponseObject {
16 id: input.id.clone(),
17 created_at: 0,
18 object: openai::ResponseCompactionObjectType::ResponseCompaction,
19 output: claude_content_to_compact_output(input.id, input.content, &input.stop_reason),
20 usage: claude_usage_to_openai(input.usage),
21 extra: Default::default(),
22 })
23}
24
25fn claude_content_to_compact_output(
26 id: String,
27 content: Vec<claude::ContentBlock>,
28 stop_reason: &claude::StopReason,
29) -> Vec<openai::CompactResponseItem> {
30 let mut message_parts = Vec::new();
31 let mut output = Vec::new();
32
33 for block in content {
34 match block {
35 claude::ContentBlock::Text(block) => {
36 message_parts.push(openai::CompactMessageContentPart::Text(
37 crate::protocol::wire!(openai::CompactTextContent {
38 text: block.text,
39 type_: openai::CompactTextContentType::Text,
40 extra: Default::default(),
41 }),
42 ));
43 }
44 claude::ContentBlock::Thinking(block) => {
45 output.push(openai::CompactResponseItem::Typed(
46 openai::TypedResponseItem::Reasoning {
47 id: Some(block.signature),
48 summary: Vec::new(),
49 content: Some(vec![crate::protocol::wire!(
50 openai::ResponseReasoningTextPart {
51 text: block.thinking,
52 type_: openai::ResponseReasoningTextType::ReasoningText,
53 extra: Default::default(),
54 }
55 )]),
56 encrypted_content: None,
57 status: Some(openai::ResponseItemLifecycleStatus::Completed),
58 extra: Default::default(),
59 },
60 ));
61 }
62 claude::ContentBlock::RedactedThinking(block) => {
63 output.push(openai::CompactResponseItem::Typed(
64 openai::TypedResponseItem::Reasoning {
65 id: Some(DEFAULT_REASONING_ID.to_owned()),
66 summary: Vec::new(),
67 content: None,
68 encrypted_content: Some(block.data),
69 status: Some(openai::ResponseItemLifecycleStatus::Completed),
70 extra: Default::default(),
71 },
72 ));
73 }
74 claude::ContentBlock::ToolUse(block) => {
75 output.push(openai::CompactResponseItem::Typed(crate::protocol::wire!(
76 openai::TypedResponseItem::FunctionCall {
77 arguments: json_object_to_string(&block.input),
78 call_id: block.id.clone(),
79 name: block.name,
80 id: Some(block.id),
81 caller: None,
82 namespace: None,
83 status: Some(openai::ResponseItemLifecycleStatus::Completed),
84 extra: Default::default(),
85 }
86 )));
87 }
88 claude::ContentBlock::ServerToolUse(block) => {
89 output.push(compact_server_tool_use_item(
90 block.id,
91 block.input,
92 block.name,
93 ));
94 }
95 claude::ContentBlock::WebSearchToolResult(block) => {
96 output.push(compact_function_call_output_item(
97 block.tool_use_id,
98 server_tool_result_output(&block.content),
99 ));
100 }
101 claude::ContentBlock::WebFetchToolResult(block) => {
102 output.push(compact_function_call_output_item(
103 block.tool_use_id,
104 server_tool_result_output(&block.content),
105 ));
106 }
107 claude::ContentBlock::AdvisorToolResult(block) => {
108 output.push(compact_function_call_output_item(
109 block.tool_use_id,
110 server_tool_result_output(&block.content),
111 ));
112 }
113 claude::ContentBlock::CodeExecutionToolResult(block) => {
114 output.push(compact_function_call_output_item(
115 block.tool_use_id,
116 server_tool_result_output(&block.content),
117 ));
118 }
119 claude::ContentBlock::BashCodeExecutionToolResult(block) => {
120 output.push(compact_function_call_output_item(
121 block.tool_use_id,
122 server_tool_result_output(&block.content),
123 ));
124 }
125 claude::ContentBlock::TextEditorCodeExecutionToolResult(block) => {
126 output.push(compact_function_call_output_item(
127 block.tool_use_id,
128 server_tool_result_output(&block.content),
129 ));
130 }
131 claude::ContentBlock::ToolSearchToolResult(block) => {
132 output.push(compact_function_call_output_item(
133 block.tool_use_id,
134 server_tool_result_output(&block.content),
135 ));
136 }
137 claude::ContentBlock::McpToolUse(block) => {
138 output.push(openai::CompactResponseItem::Typed(
139 openai::TypedResponseItem::McpCall {
140 id: block.id,
141 arguments: json_object_to_string(&block.input),
142 name: block.name,
143 server_label: block.server_name,
144 approval_request_id: None,
145 error: None,
146 output: None,
147 status: Some(openai::ResponseMcpCallStatus::Completed),
148 extra: Default::default(),
149 },
150 ));
151 }
152 claude::ContentBlock::McpToolResult(block) => {
153 output.push(compact_function_call_output_item(
154 block.tool_use_id,
155 openai::ResponseOutput::Text(response_mcp_tool_result_content_to_text(
156 block.content,
157 )),
158 ));
159 }
160 claude::ContentBlock::Compaction(block) => {
161 if let Some(text) = block.content {
162 message_parts.push(openai::CompactMessageContentPart::SummaryText(
163 crate::protocol::wire!(openai::CompactSummaryTextContent {
164 text,
165 type_: openai::CompactSummaryTextContentType::SummaryText,
166 extra: Default::default(),
167 }),
168 ));
169 }
170 output.push(openai::CompactResponseItem::Typed(
171 openai::TypedResponseItem::Compaction {
172 encrypted_content: block.encrypted_content,
173 id: None,
174 created_by: None,
175 extra: Default::default(),
176 },
177 ));
178 }
179 _ => {}
180 }
181 }
182
183 if !message_parts.is_empty() {
184 output.insert(
185 0,
186 openai::CompactResponseItem::Message(crate::protocol::wire!(
187 openai::CompactMessageItem {
188 id,
189 type_: openai::ResponseMessageItemType::Message,
190 content: message_parts,
191 role: openai::CompactMessageRole::Assistant,
192 status: compact_message_status(stop_reason),
193 phase: None,
194 extra: Default::default(),
195 }
196 )),
197 );
198 }
199
200 output
201}
202
203fn compact_message_status(stop_reason: &claude::StopReason) -> openai::ResponseItemLifecycleStatus {
204 match stop_reason {
205 claude::StopReason::Known(claude::StopReasonKnown::MaxTokens)
206 | claude::StopReason::Known(claude::StopReasonKnown::Refusal)
207 | claude::StopReason::Known(claude::StopReasonKnown::ModelContextWindowExceeded) => {
208 openai::ResponseItemLifecycleStatus::Incomplete
209 }
210 _ => openai::ResponseItemLifecycleStatus::Completed,
211 }
212}