Skip to main content

gproxy_transform/transform/compact/claude_to_openai/
output.rs

1use 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}