gproxy_transform/transform/compact/claude_to_openai/
output.rs1use crate::protocol::{claude, openai};
2use crate::transform::TransformContext;
3
4use super::tools::{
5 compact_apply_patch_result_item, compact_function_call_output_item,
6 compact_server_tool_use_item, compact_shell_result_item, compact_tool_search_result_item,
7 compact_tool_use_item, 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(
48 crate::transform::generate_content::common::id::response_reasoning_item_id(
49 &block.signature,
50 ),
51 ),
52 summary: Vec::new(),
53 content: Some(vec![crate::protocol::wire!(
54 openai::ResponseReasoningTextPart {
55 text: block.thinking,
56 type_: openai::ResponseReasoningTextType::ReasoningText,
57 extra: Default::default(),
58 }
59 )]),
60 encrypted_content: Some(block.signature),
61 status: Some(openai::ResponseItemLifecycleStatus::Completed),
62 extra: Default::default(),
63 },
64 ));
65 }
66 claude::ContentBlock::RedactedThinking(block) => {
67 output.push(openai::CompactResponseItem::Typed(
68 openai::TypedResponseItem::Reasoning {
69 id: Some(
70 crate::transform::generate_content::common::id::response_reasoning_item_id(
71 &block.data,
72 ),
73 ),
74 summary: Vec::new(),
75 content: None,
76 encrypted_content: Some(block.data),
77 status: Some(openai::ResponseItemLifecycleStatus::Completed),
78 extra: Default::default(),
79 },
80 ));
81 }
82 claude::ContentBlock::ToolUse(block) => {
83 output.push(compact_tool_use_item(block.id, block.input, block.name));
84 }
85 claude::ContentBlock::ServerToolUse(block) => {
86 output.push(compact_server_tool_use_item(
87 block.id,
88 block.input,
89 block.name,
90 ));
91 }
92 claude::ContentBlock::WebSearchToolResult(block) => {
93 let _ = block;
94 }
95 claude::ContentBlock::WebFetchToolResult(block) => {
96 let _ = block;
97 }
98 claude::ContentBlock::AdvisorToolResult(block) => {
99 output.push(compact_function_call_output_item(
100 block.tool_use_id,
101 server_tool_result_output(&block.content),
102 ));
103 }
104 claude::ContentBlock::CodeExecutionToolResult(block) => {
105 output.push(compact_function_call_output_item(
106 block.tool_use_id,
107 server_tool_result_output(&block.content),
108 ));
109 }
110 claude::ContentBlock::BashCodeExecutionToolResult(block) => {
111 output.push(compact_shell_result_item(block.tool_use_id, &block.content));
112 }
113 claude::ContentBlock::TextEditorCodeExecutionToolResult(block) => {
114 output.push(compact_apply_patch_result_item(
115 block.tool_use_id,
116 &block.content,
117 ));
118 }
119 claude::ContentBlock::ToolSearchToolResult(block) => {
120 output.push(compact_tool_search_result_item(
121 block.tool_use_id,
122 &block.content,
123 ));
124 }
125 claude::ContentBlock::McpToolUse(block) => {
126 output.push(openai::CompactResponseItem::Typed(
127 openai::TypedResponseItem::McpCall {
128 id: block.id,
129 arguments: json_object_to_string(&block.input),
130 name: block.name,
131 server_label: block.server_name,
132 approval_request_id: None,
133 error: None,
134 output: None,
135 status: Some(openai::ResponseMcpCallStatus::Completed),
136 extra: Default::default(),
137 },
138 ));
139 }
140 claude::ContentBlock::McpToolResult(block) => {
141 output.push(compact_function_call_output_item(
142 block.tool_use_id,
143 openai::ResponseOutput::Text(response_mcp_tool_result_content_to_text(
144 block.content,
145 )),
146 ));
147 }
148 claude::ContentBlock::Compaction(block) => {
149 if let Some(text) = block.content {
150 message_parts.push(openai::CompactMessageContentPart::SummaryText(
151 crate::protocol::wire!(openai::CompactSummaryTextContent {
152 text,
153 type_: openai::CompactSummaryTextContentType::SummaryText,
154 extra: Default::default(),
155 }),
156 ));
157 }
158 output.push(openai::CompactResponseItem::Typed(
159 openai::TypedResponseItem::Compaction {
160 encrypted_content: block.encrypted_content,
161 id: None,
162 created_by: None,
163 extra: Default::default(),
164 },
165 ));
166 }
167 _ => {}
168 }
169 }
170
171 if !message_parts.is_empty() {
172 output.insert(
173 0,
174 openai::CompactResponseItem::Message(crate::protocol::wire!(
175 openai::CompactMessageItem {
176 id,
177 type_: openai::ResponseMessageItemType::Message,
178 content: message_parts,
179 role: openai::CompactMessageRole::Assistant,
180 status: compact_message_status(stop_reason),
181 phase: None,
182 extra: Default::default(),
183 }
184 )),
185 );
186 }
187
188 output
189}
190
191fn compact_message_status(stop_reason: &claude::StopReason) -> openai::ResponseItemLifecycleStatus {
192 match stop_reason {
193 claude::StopReason::Known(claude::StopReasonKnown::MaxTokens)
194 | claude::StopReason::Known(claude::StopReasonKnown::Refusal)
195 | claude::StopReason::Known(claude::StopReasonKnown::ModelContextWindowExceeded) => {
196 openai::ResponseItemLifecycleStatus::Incomplete
197 }
198 _ => openai::ResponseItemLifecycleStatus::Completed,
199 }
200}