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    apply_patch_to_text_editor_input, arguments_to_json_object, code_interpreter_input,
7    local_shell_to_bash_input, response_server_tool_use_block, shell_to_bash_input,
8    string_input_json_object, web_action_to_claude,
9};
10use super::util::join_text;
11
12pub fn response(
13    input: openai::CompactedResponseObject,
14    _: &TransformContext,
15) -> claude::CreateMessageResponseBody {
16    crate::protocol::wire!(claude::CreateMessageResponseBody {
17        id: input.id,
18        type_: claude::MessageObjectType::Known(claude::MessageObjectTypeKnown::Message),
19        role: claude::AssistantRole::Known(claude::AssistantRoleKnown::Assistant),
20        content: compact_output_to_claude_content(input.output),
21        model: claude::ClaudeModel::Unknown(DEFAULT_MODEL.to_owned()),
22        stop_reason: claude::StopReason::Known(claude::StopReasonKnown::Compaction),
23        stop_sequence: None,
24        usage: openai_usage_to_claude(input.usage),
25        container: None,
26        context_management: None,
27        diagnostics: None,
28        stop_details: None,
29        extra: Default::default(),
30    })
31}
32
33fn compact_output_to_claude_content(
34    output: Vec<openai::CompactResponseItem>,
35) -> Vec<claude::ContentBlock> {
36    output
37        .into_iter()
38        .flat_map(compact_item_to_claude_content)
39        .collect()
40}
41
42fn compact_item_to_claude_content(item: openai::CompactResponseItem) -> Vec<claude::ContentBlock> {
43    match item {
44        openai::CompactResponseItem::Message(message) => compact_message_to_claude_content(message),
45        openai::CompactResponseItem::Typed(openai::TypedResponseItem::Compaction {
46            encrypted_content,
47            ..
48        }) => vec![claude::ContentBlock::Compaction(crate::protocol::wire!(
49            claude::ResponseCompactionBlock {
50                content: None,
51                encrypted_content,
52                type_: claude::CompactionBlockType::Compaction,
53                extra: Default::default(),
54            }
55        ))],
56        openai::CompactResponseItem::Typed(openai::TypedResponseItem::FunctionCall {
57            arguments,
58            call_id,
59            name,
60            id,
61            ..
62        }) => vec![claude::ContentBlock::ToolUse(crate::protocol::wire!(
63            claude::ResponseToolUseBlock {
64                id: id.unwrap_or(call_id),
65                input: arguments_to_json_object(&arguments),
66                name,
67                type_: claude::ToolUseBlockType::ToolUse,
68                caller: None,
69                extra: Default::default(),
70            }
71        ))],
72        openai::CompactResponseItem::Typed(openai::TypedResponseItem::CustomToolCall {
73            call_id,
74            input,
75            name,
76            id,
77            ..
78        }) => vec![claude::ContentBlock::ToolUse(crate::protocol::wire!(
79            claude::ResponseToolUseBlock {
80                id: id.unwrap_or(call_id),
81                input: string_input_json_object(input),
82                name,
83                type_: claude::ToolUseBlockType::ToolUse,
84                caller: None,
85                extra: Default::default(),
86            }
87        ))],
88        openai::CompactResponseItem::Typed(openai::TypedResponseItem::WebSearchCall {
89            id,
90            action,
91            ..
92        }) => {
93            let (name, input) = web_action_to_claude(action);
94            vec![claude::ContentBlock::ServerToolUse(
95                response_server_tool_use_block(id, input, name),
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![response_tool_use_block(
115            call_id,
116            local_shell_to_bash_input(action),
117            "bash",
118        )],
119        openai::CompactResponseItem::Typed(openai::TypedResponseItem::ShellCall {
120            action,
121            call_id,
122            environment: None,
123            ..
124        }) => vec![response_tool_use_block(
125            call_id,
126            shell_to_bash_input(action, None),
127            "bash",
128        )],
129        openai::CompactResponseItem::Typed(openai::TypedResponseItem::ShellCall {
130            action,
131            call_id,
132            environment: Some(environment),
133            ..
134        }) => vec![response_tool_use_block(
135            call_id,
136            shell_to_bash_input(action, Some(environment)),
137            "bash",
138        )],
139        openai::CompactResponseItem::Typed(openai::TypedResponseItem::ApplyPatchCall {
140            call_id,
141            operation,
142            ..
143        }) => vec![response_tool_use_block(
144            call_id,
145            apply_patch_to_text_editor_input(operation),
146            "str_replace_based_edit_tool",
147        )],
148        openai::CompactResponseItem::Typed(openai::TypedResponseItem::ToolSearchCall {
149            arguments,
150            id,
151            call_id,
152            execution,
153            ..
154        }) => vec![claude::ContentBlock::ServerToolUse(
155            response_server_tool_use_block(
156                id.or(call_id).unwrap_or_else(|| "tool_search".to_owned()),
157                match arguments {
158                    serde_json::Value::Object(map) => map.into_iter().collect(),
159                    value => {
160                        let mut input = claude::JsonObject::new();
161                        input.insert("value".to_owned(), value);
162                        input
163                    }
164                },
165                if matches!(execution, Some(openai::ToolSearchExecution::Client)) {
166                    claude::ServerToolUseNameKnown::ToolSearchToolRegex
167                } else {
168                    claude::ServerToolUseNameKnown::ToolSearchToolBm25
169                },
170            ),
171        )],
172        openai::CompactResponseItem::Typed(openai::TypedResponseItem::ToolSearchOutput {
173            tools,
174            id,
175            call_id,
176            ..
177        }) => vec![claude::ContentBlock::ToolSearchToolResult(
178            crate::protocol::wire!(claude::ResponseToolSearchToolResultBlock {
179                content: claude::ResponseToolSearchToolResultContent::Result(
180                    crate::protocol::wire!(claude::ResponseToolSearchToolSearchResultBlock {
181                        tool_references: response_tool_references(tools),
182                        type_: claude::ResponseToolSearchToolSearchResultBlockType::ToolSearchToolSearchResult,
183                        extra: Default::default(),
184                    }),
185                ),
186                tool_use_id: call_id.or(id).unwrap_or_else(|| "tool_search".to_owned()),
187                type_: claude::ToolSearchToolResultBlockType::ToolSearchToolResult,
188                extra: Default::default(),
189            }),
190        )],
191        openai::CompactResponseItem::Typed(openai::TypedResponseItem::McpCall {
192            id,
193            arguments,
194            name,
195            server_label,
196            output,
197            error,
198            ..
199        }) => {
200            let mut blocks = vec![claude::ContentBlock::McpToolUse(crate::protocol::wire!(
201                claude::ResponseMcpToolUseBlock {
202                    id: id.clone(),
203                    input: arguments_to_json_object(&arguments),
204                    name,
205                    server_name: server_label,
206                    type_: claude::ResponseMcpToolUseBlockType::McpToolUse,
207                    extra: Default::default(),
208                }
209            ))];
210            if let Some(result) = response_mcp_result_block(id, output, error) {
211                blocks.push(claude::ContentBlock::McpToolResult(result));
212            }
213            blocks
214        }
215        openai::CompactResponseItem::Typed(openai::TypedResponseItem::Reasoning {
216            id,
217            summary,
218            content,
219            encrypted_content,
220            ..
221        }) => reasoning_to_claude_content(id, summary, content, encrypted_content),
222        _ => Vec::new(),
223    }
224}
225
226fn response_tool_use_block(
227    id: String,
228    input: claude::JsonObject,
229    name: &str,
230) -> claude::ContentBlock {
231    claude::ContentBlock::ToolUse(crate::protocol::wire!(claude::ResponseToolUseBlock {
232        id,
233        input,
234        name: name.to_owned(),
235        type_: claude::ToolUseBlockType::ToolUse,
236        caller: None,
237        extra: Default::default(),
238    }))
239}
240
241fn response_tool_references(
242    tools: Vec<openai::ResponseTool>,
243) -> Vec<claude::ResponseToolReferenceBlock> {
244    tools
245        .into_iter()
246        .filter_map(|tool| match tool {
247            openai::ResponseTool::Function { name, .. }
248            | openai::ResponseTool::Custom { name, .. } => Some(name),
249            _ => None,
250        })
251        .map(|tool_name| {
252            crate::protocol::wire!(claude::ResponseToolReferenceBlock {
253                tool_name,
254                type_: claude::ResponseToolReferenceBlockType::ToolReference,
255                extra: Default::default(),
256            })
257        })
258        .collect()
259}
260
261fn compact_message_to_claude_content(
262    message: openai::CompactMessageItem,
263) -> Vec<claude::ContentBlock> {
264    message
265        .content
266        .into_iter()
267        .filter_map(compact_content_part_to_claude)
268        .collect()
269}
270
271fn compact_content_part_to_claude(
272    part: openai::CompactMessageContentPart,
273) -> Option<claude::ContentBlock> {
274    let text = match part {
275        openai::CompactMessageContentPart::Input(openai::ResponseInputContentPart::InputText {
276            text,
277            ..
278        })
279        | openai::CompactMessageContentPart::Output(
280            openai::ResponseOutputContentPart::OutputText { text, .. },
281        )
282        | openai::CompactMessageContentPart::Output(
283            openai::ResponseOutputContentPart::ReasoningText { text, .. },
284        )
285        | openai::CompactMessageContentPart::Text(openai::CompactTextContent { text, .. })
286        | openai::CompactMessageContentPart::SummaryText(openai::CompactSummaryTextContent {
287            text,
288            ..
289        }) => text,
290        openai::CompactMessageContentPart::Output(openai::ResponseOutputContentPart::Refusal {
291            refusal,
292            ..
293        }) => refusal,
294        _ => return None,
295    };
296
297    Some(claude::ContentBlock::Text(crate::protocol::wire!(
298        claude::ResponseTextBlock {
299            citations: None,
300            text,
301            type_: claude::TextBlockType::Text,
302            extra: Default::default(),
303        }
304    )))
305}
306
307fn response_mcp_result_block(
308    tool_use_id: String,
309    output: Option<String>,
310    error: Option<String>,
311) -> Option<claude::ResponseMcpToolResultBlock> {
312    let is_error = error.is_some();
313    let content = error.or(output)?;
314    Some(crate::protocol::wire!(claude::ResponseMcpToolResultBlock {
315        content: claude::ResponseMcpToolResultContent::String(content),
316        is_error,
317        tool_use_id,
318        type_: claude::ResponseMcpToolResultBlockType::McpToolResult,
319        extra: Default::default(),
320    }))
321}
322
323fn reasoning_to_claude_content(
324    _id: Option<String>,
325    summary: Vec<openai::ResponseReasoningSummaryPart>,
326    content: Option<Vec<openai::ResponseReasoningTextPart>>,
327    encrypted_content: Option<String>,
328) -> Vec<claude::ContentBlock> {
329    let mut blocks = Vec::new();
330    let thinking = join_text(content.into_iter().flatten().map(|part| part.text));
331    if !thinking.is_empty() {
332        if let Some(signature) = encrypted_content.filter(|value| !value.is_empty()) {
333            blocks.push(claude::ContentBlock::Thinking(crate::protocol::wire!(
334                claude::ThinkingBlock {
335                    signature,
336                    thinking,
337                    type_: claude::ThinkingBlockType::Thinking,
338                }
339            )));
340        } else {
341            blocks.push(claude::ContentBlock::Text(crate::protocol::wire!(
342                claude::ResponseTextBlock {
343                    citations: None,
344                    text: thinking,
345                    type_: claude::TextBlockType::Text,
346                    extra: Default::default(),
347                }
348            )));
349        }
350    } else if let Some(encrypted_content) = encrypted_content {
351        blocks.push(claude::ContentBlock::RedactedThinking(
352            crate::protocol::wire!(claude::RedactedThinkingBlock {
353                data: encrypted_content,
354                type_: claude::RedactedThinkingBlockType::RedactedThinking,
355            }),
356        ));
357    }
358
359    blocks.extend(summary.into_iter().map(|part| {
360        claude::ContentBlock::Text(crate::protocol::wire!(claude::ResponseTextBlock {
361            citations: None,
362            text: part.text,
363            type_: claude::TextBlockType::Text,
364            extra: Default::default(),
365        }))
366    }));
367    blocks
368}
369
370fn openai_usage_to_claude(usage: openai::ResponseUsage) -> claude::Usage {
371    let details = usage.input_tokens_details;
372    let cached = details.as_ref().map_or(0, |details| details.cached_tokens);
373    let cache_write = details
374        .as_ref()
375        .map_or(0, |details| details.cache_write_tokens);
376    crate::protocol::wire!(claude::Usage {
377        input_tokens: Some(u64::from(
378            usage
379                .input_tokens
380                .saturating_sub(cached)
381                .saturating_sub(cache_write),
382        )),
383        output_tokens: Some(u64::from(usage.output_tokens)),
384        cache_creation_input_tokens: details
385            .as_ref()
386            .filter(|details| details.cache_write_tokens > 0)
387            .map(|details| u64::from(details.cache_write_tokens)),
388        cache_read_input_tokens: details.map(|details| u64::from(details.cached_tokens)),
389        cache_creation: None,
390        output_tokens_details: Some(crate::protocol::wire!(claude::OutputTokensDetails {
391            thinking_tokens: u64::from(usage.output_tokens_details.reasoning_tokens),
392            extra: Default::default(),
393        })),
394        server_tool_use: None,
395        iterations: None,
396        inference_geo: None,
397        service_tier: None,
398        speed: None,
399        extra: Default::default(),
400    })
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn compact_usage_subtracts_cache_from_openai_input_total() {
409        let usage = openai_usage_to_claude(crate::protocol::wire!(openai::ResponseUsage {
410            input_tokens: 100,
411            output_tokens: 20,
412            total_tokens: 120,
413            input_tokens_details: Some(crate::protocol::wire!(
414                openai::ResponseInputTokensDetails {
415                    cached_tokens: 60,
416                    cache_write_tokens: 10,
417                    extra: Default::default(),
418                }
419            )),
420            output_tokens_details: crate::protocol::wire!(openai::ResponseOutputTokensDetails {
421                reasoning_tokens: 5,
422                extra: Default::default(),
423            }),
424            extra: Default::default(),
425        }));
426        assert_eq!(usage.input_tokens, Some(30));
427        assert_eq!(usage.cache_read_input_tokens, Some(60));
428        assert_eq!(usage.cache_creation_input_tokens, Some(10));
429    }
430}