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    crate::protocol::wire!(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(crate::protocol::wire!(
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(crate::protocol::wire!(
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(crate::protocol::wire!(
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(crate::protocol::wire!(
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(crate::protocol::wire!(
217        claude::ResponseTextBlock {
218            citations: None,
219            text,
220            type_: claude::TextBlockType::Text,
221            extra: Default::default(),
222        }
223    )))
224}
225
226fn response_mcp_result_block(
227    tool_use_id: String,
228    output: Option<String>,
229    error: Option<String>,
230) -> Option<claude::ResponseMcpToolResultBlock> {
231    let is_error = error.is_some();
232    let content = error.or(output)?;
233    Some(crate::protocol::wire!(claude::ResponseMcpToolResultBlock {
234        content: claude::ResponseMcpToolResultContent::String(content),
235        is_error,
236        tool_use_id,
237        type_: claude::ResponseMcpToolResultBlockType::McpToolResult,
238        extra: Default::default(),
239    }))
240}
241
242fn reasoning_to_claude_content(
243    id: Option<String>,
244    summary: Vec<openai::ResponseReasoningSummaryPart>,
245    content: Option<Vec<openai::ResponseReasoningTextPart>>,
246    encrypted_content: Option<String>,
247) -> Vec<claude::ContentBlock> {
248    let mut blocks = Vec::new();
249    if let Some(encrypted_content) = encrypted_content {
250        blocks.push(claude::ContentBlock::RedactedThinking(
251            crate::protocol::wire!(claude::RedactedThinkingBlock {
252                data: encrypted_content,
253                type_: claude::RedactedThinkingBlockType::RedactedThinking,
254            }),
255        ));
256    }
257
258    let thinking = join_text(content.into_iter().flatten().map(|part| part.text));
259    if !thinking.is_empty() {
260        if let Some(signature) = id.filter(|signature| !signature.is_empty()) {
261            blocks.push(claude::ContentBlock::Thinking(crate::protocol::wire!(
262                claude::ThinkingBlock {
263                    signature,
264                    thinking,
265                    type_: claude::ThinkingBlockType::Thinking,
266                }
267            )));
268        } else {
269            blocks.push(claude::ContentBlock::Text(crate::protocol::wire!(
270                claude::ResponseTextBlock {
271                    citations: None,
272                    text: thinking,
273                    type_: claude::TextBlockType::Text,
274                    extra: Default::default(),
275                }
276            )));
277        }
278    }
279
280    blocks.extend(summary.into_iter().map(|part| {
281        claude::ContentBlock::Text(crate::protocol::wire!(claude::ResponseTextBlock {
282            citations: None,
283            text: part.text,
284            type_: claude::TextBlockType::Text,
285            extra: Default::default(),
286        }))
287    }));
288    blocks
289}
290
291fn openai_usage_to_claude(usage: openai::ResponseUsage) -> claude::Usage {
292    let details = usage.input_tokens_details;
293    let cached = details.as_ref().map_or(0, |details| details.cached_tokens);
294    let cache_write = details
295        .as_ref()
296        .map_or(0, |details| details.cache_write_tokens);
297    crate::protocol::wire!(claude::Usage {
298        input_tokens: Some(u64::from(
299            usage
300                .input_tokens
301                .saturating_sub(cached)
302                .saturating_sub(cache_write),
303        )),
304        output_tokens: Some(u64::from(usage.output_tokens)),
305        cache_creation_input_tokens: details
306            .as_ref()
307            .filter(|details| details.cache_write_tokens > 0)
308            .map(|details| u64::from(details.cache_write_tokens)),
309        cache_read_input_tokens: details.map(|details| u64::from(details.cached_tokens)),
310        cache_creation: None,
311        output_tokens_details: Some(crate::protocol::wire!(claude::OutputTokensDetails {
312            thinking_tokens: u64::from(usage.output_tokens_details.reasoning_tokens),
313            extra: Default::default(),
314        })),
315        server_tool_use: None,
316        iterations: None,
317        inference_geo: None,
318        service_tier: None,
319        speed: None,
320        extra: Default::default(),
321    })
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn compact_usage_subtracts_cache_from_openai_input_total() {
330        let usage = openai_usage_to_claude(crate::protocol::wire!(openai::ResponseUsage {
331            input_tokens: 100,
332            output_tokens: 20,
333            total_tokens: 120,
334            input_tokens_details: Some(crate::protocol::wire!(
335                openai::ResponseInputTokensDetails {
336                    cached_tokens: 60,
337                    cache_write_tokens: 10,
338                    extra: Default::default(),
339                }
340            )),
341            output_tokens_details: crate::protocol::wire!(openai::ResponseOutputTokensDetails {
342                reasoning_tokens: 5,
343                extra: Default::default(),
344            }),
345            extra: Default::default(),
346        }));
347        assert_eq!(usage.input_tokens, Some(30));
348        assert_eq!(usage.cache_read_input_tokens, Some(60));
349        assert_eq!(usage.cache_creation_input_tokens, Some(10));
350    }
351}