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