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::DEFAULT_REASONING_ID;
5use super::tools::{
6    compact_function_call_output_item, compact_server_tool_use_item,
7    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(block.signature),
48                        summary: Vec::new(),
49                        content: Some(vec![crate::protocol::wire!(
50                            openai::ResponseReasoningTextPart {
51                                text: block.thinking,
52                                type_: openai::ResponseReasoningTextType::ReasoningText,
53                                extra: Default::default(),
54                            }
55                        )]),
56                        encrypted_content: None,
57                        status: Some(openai::ResponseItemLifecycleStatus::Completed),
58                        extra: Default::default(),
59                    },
60                ));
61            }
62            claude::ContentBlock::RedactedThinking(block) => {
63                output.push(openai::CompactResponseItem::Typed(
64                    openai::TypedResponseItem::Reasoning {
65                        id: Some(DEFAULT_REASONING_ID.to_owned()),
66                        summary: Vec::new(),
67                        content: None,
68                        encrypted_content: Some(block.data),
69                        status: Some(openai::ResponseItemLifecycleStatus::Completed),
70                        extra: Default::default(),
71                    },
72                ));
73            }
74            claude::ContentBlock::ToolUse(block) => {
75                output.push(openai::CompactResponseItem::Typed(crate::protocol::wire!(
76                    openai::TypedResponseItem::FunctionCall {
77                        arguments: json_object_to_string(&block.input),
78                        call_id: block.id.clone(),
79                        name: block.name,
80                        id: Some(block.id),
81                        caller: None,
82                        namespace: None,
83                        status: Some(openai::ResponseItemLifecycleStatus::Completed),
84                        extra: Default::default(),
85                    }
86                )));
87            }
88            claude::ContentBlock::ServerToolUse(block) => {
89                output.push(compact_server_tool_use_item(
90                    block.id,
91                    block.input,
92                    block.name,
93                ));
94            }
95            claude::ContentBlock::WebSearchToolResult(block) => {
96                output.push(compact_function_call_output_item(
97                    block.tool_use_id,
98                    server_tool_result_output(&block.content),
99                ));
100            }
101            claude::ContentBlock::WebFetchToolResult(block) => {
102                output.push(compact_function_call_output_item(
103                    block.tool_use_id,
104                    server_tool_result_output(&block.content),
105                ));
106            }
107            claude::ContentBlock::AdvisorToolResult(block) => {
108                output.push(compact_function_call_output_item(
109                    block.tool_use_id,
110                    server_tool_result_output(&block.content),
111                ));
112            }
113            claude::ContentBlock::CodeExecutionToolResult(block) => {
114                output.push(compact_function_call_output_item(
115                    block.tool_use_id,
116                    server_tool_result_output(&block.content),
117                ));
118            }
119            claude::ContentBlock::BashCodeExecutionToolResult(block) => {
120                output.push(compact_function_call_output_item(
121                    block.tool_use_id,
122                    server_tool_result_output(&block.content),
123                ));
124            }
125            claude::ContentBlock::TextEditorCodeExecutionToolResult(block) => {
126                output.push(compact_function_call_output_item(
127                    block.tool_use_id,
128                    server_tool_result_output(&block.content),
129                ));
130            }
131            claude::ContentBlock::ToolSearchToolResult(block) => {
132                output.push(compact_function_call_output_item(
133                    block.tool_use_id,
134                    server_tool_result_output(&block.content),
135                ));
136            }
137            claude::ContentBlock::McpToolUse(block) => {
138                output.push(openai::CompactResponseItem::Typed(
139                    openai::TypedResponseItem::McpCall {
140                        id: block.id,
141                        arguments: json_object_to_string(&block.input),
142                        name: block.name,
143                        server_label: block.server_name,
144                        approval_request_id: None,
145                        error: None,
146                        output: None,
147                        status: Some(openai::ResponseMcpCallStatus::Completed),
148                        extra: Default::default(),
149                    },
150                ));
151            }
152            claude::ContentBlock::McpToolResult(block) => {
153                output.push(compact_function_call_output_item(
154                    block.tool_use_id,
155                    openai::ResponseOutput::Text(response_mcp_tool_result_content_to_text(
156                        block.content,
157                    )),
158                ));
159            }
160            claude::ContentBlock::Compaction(block) => {
161                if let Some(text) = block.content {
162                    message_parts.push(openai::CompactMessageContentPart::SummaryText(
163                        crate::protocol::wire!(openai::CompactSummaryTextContent {
164                            text,
165                            type_: openai::CompactSummaryTextContentType::SummaryText,
166                            extra: Default::default(),
167                        }),
168                    ));
169                }
170                output.push(openai::CompactResponseItem::Typed(
171                    openai::TypedResponseItem::Compaction {
172                        encrypted_content: block.encrypted_content,
173                        id: None,
174                        created_by: None,
175                        extra: Default::default(),
176                    },
177                ));
178            }
179            _ => {}
180        }
181    }
182
183    if !message_parts.is_empty() {
184        output.insert(
185            0,
186            openai::CompactResponseItem::Message(crate::protocol::wire!(
187                openai::CompactMessageItem {
188                    id,
189                    type_: openai::ResponseMessageItemType::Message,
190                    content: message_parts,
191                    role: openai::CompactMessageRole::Assistant,
192                    status: compact_message_status(stop_reason),
193                    phase: None,
194                    extra: Default::default(),
195                }
196            )),
197        );
198    }
199
200    output
201}
202
203fn compact_message_status(stop_reason: &claude::StopReason) -> openai::ResponseItemLifecycleStatus {
204    match stop_reason {
205        claude::StopReason::Known(claude::StopReasonKnown::MaxTokens)
206        | claude::StopReason::Known(claude::StopReasonKnown::Refusal)
207        | claude::StopReason::Known(claude::StopReasonKnown::ModelContextWindowExceeded) => {
208            openai::ResponseItemLifecycleStatus::Incomplete
209        }
210        _ => openai::ResponseItemLifecycleStatus::Completed,
211    }
212}