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    let thinking = join_text(content.into_iter().flatten().map(|part| part.text));
250    if !thinking.is_empty() {
251        if let Some(signature) = encrypted_content.filter(|value| !value.is_empty()) {
252            blocks.push(claude::ContentBlock::Thinking(crate::protocol::wire!(
253                claude::ThinkingBlock {
254                    signature,
255                    thinking,
256                    type_: claude::ThinkingBlockType::Thinking,
257                }
258            )));
259        } else {
260            blocks.push(claude::ContentBlock::Text(crate::protocol::wire!(
261                claude::ResponseTextBlock {
262                    citations: None,
263                    text: thinking,
264                    type_: claude::TextBlockType::Text,
265                    extra: Default::default(),
266                }
267            )));
268        }
269    } else if let Some(encrypted_content) = encrypted_content {
270        blocks.push(claude::ContentBlock::RedactedThinking(
271            crate::protocol::wire!(claude::RedactedThinkingBlock {
272                data: encrypted_content,
273                type_: claude::RedactedThinkingBlockType::RedactedThinking,
274            }),
275        ));
276    }
277
278    blocks.extend(summary.into_iter().map(|part| {
279        claude::ContentBlock::Text(crate::protocol::wire!(claude::ResponseTextBlock {
280            citations: None,
281            text: part.text,
282            type_: claude::TextBlockType::Text,
283            extra: Default::default(),
284        }))
285    }));
286    blocks
287}
288
289fn openai_usage_to_claude(usage: openai::ResponseUsage) -> claude::Usage {
290    let details = usage.input_tokens_details;
291    let cached = details.as_ref().map_or(0, |details| details.cached_tokens);
292    let cache_write = details
293        .as_ref()
294        .map_or(0, |details| details.cache_write_tokens);
295    crate::protocol::wire!(claude::Usage {
296        input_tokens: Some(u64::from(
297            usage
298                .input_tokens
299                .saturating_sub(cached)
300                .saturating_sub(cache_write),
301        )),
302        output_tokens: Some(u64::from(usage.output_tokens)),
303        cache_creation_input_tokens: details
304            .as_ref()
305            .filter(|details| details.cache_write_tokens > 0)
306            .map(|details| u64::from(details.cache_write_tokens)),
307        cache_read_input_tokens: details.map(|details| u64::from(details.cached_tokens)),
308        cache_creation: None,
309        output_tokens_details: Some(crate::protocol::wire!(claude::OutputTokensDetails {
310            thinking_tokens: u64::from(usage.output_tokens_details.reasoning_tokens),
311            extra: Default::default(),
312        })),
313        server_tool_use: None,
314        iterations: None,
315        inference_geo: None,
316        service_tier: None,
317        speed: None,
318        extra: Default::default(),
319    })
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn compact_usage_subtracts_cache_from_openai_input_total() {
328        let usage = openai_usage_to_claude(crate::protocol::wire!(openai::ResponseUsage {
329            input_tokens: 100,
330            output_tokens: 20,
331            total_tokens: 120,
332            input_tokens_details: Some(crate::protocol::wire!(
333                openai::ResponseInputTokensDetails {
334                    cached_tokens: 60,
335                    cache_write_tokens: 10,
336                    extra: Default::default(),
337                }
338            )),
339            output_tokens_details: crate::protocol::wire!(openai::ResponseOutputTokensDetails {
340                reasoning_tokens: 5,
341                extra: Default::default(),
342            }),
343            extra: Default::default(),
344        }));
345        assert_eq!(usage.input_tokens, Some(30));
346        assert_eq!(usage.cache_read_input_tokens, Some(60));
347        assert_eq!(usage.cache_creation_input_tokens, Some(10));
348    }
349}