Skip to main content

gproxy_transform/transform/compact/openai_to_claude/
output.rs

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}