Skip to main content

gproxy_protocol/transform/claude/generate_content/openai_response/
response.rs

1use crate::claude::count_tokens::types::{
2    BetaCompactionBlockType, BetaMcpToolResultBlockParamContent, BetaMcpToolUseBlockType,
3    BetaRequestMcpToolResultBlockType, BetaServerToolUseBlockType, BetaServerToolUseName,
4    BetaThinkingBlockType, BetaToolUseBlockType,
5};
6use crate::claude::create_message::response::ClaudeCreateMessageResponse;
7use crate::claude::create_message::types::{
8    BetaContentBlock, BetaMessage, BetaMessageRole, BetaMessageType, BetaServiceTier,
9    BetaStopReason, BetaTextBlock, BetaTextBlockType, BetaUsage, Model,
10};
11use crate::claude::types::ClaudeResponseHeaders;
12use crate::openai::count_tokens::types::{ResponseInputContent, ResponseOutputContent};
13use crate::openai::create_response::response::OpenAiCreateResponseResponse;
14use crate::openai::create_response::types::{
15    ResponseIncompleteReason, ResponseOutputItem, ResponseServiceTier,
16};
17use crate::transform::claude::generate_content::utils::{
18    beta_usage_from_counts, parse_json_object_or_empty,
19};
20use crate::transform::claude::utils::beta_error_response_from_status_message;
21use crate::transform::utils::TransformError;
22
23fn web_search_tool_use_id(
24    id: Option<String>,
25    action: &crate::openai::count_tokens::types::ResponseFunctionWebSearchAction,
26) -> String {
27    id.unwrap_or_else(|| match action {
28        crate::openai::count_tokens::types::ResponseFunctionWebSearchAction::Search {
29            query,
30            queries,
31            ..
32        } => query
33            .clone()
34            .or_else(|| queries.as_ref().and_then(|items| items.first().cloned()))
35            .unwrap_or_else(|| "web_search".to_string()),
36        crate::openai::count_tokens::types::ResponseFunctionWebSearchAction::OpenPage { url } => {
37            url.clone()
38                .unwrap_or_else(|| "web_search_open_page".to_string())
39        }
40        crate::openai::count_tokens::types::ResponseFunctionWebSearchAction::FindInPage {
41            pattern,
42            url,
43        } => format!("web_search_find_in_page:{pattern}:{url}"),
44    })
45}
46
47impl TryFrom<OpenAiCreateResponseResponse> for ClaudeCreateMessageResponse {
48    type Error = TransformError;
49
50    fn try_from(value: OpenAiCreateResponseResponse) -> Result<Self, TransformError> {
51        Ok(match value {
52            OpenAiCreateResponseResponse::Success {
53                stats_code,
54                headers,
55                body,
56            } => {
57                let mut content = Vec::new();
58                let mut has_tool_use = false;
59                let mut has_refusal = false;
60                let mut has_compaction = false;
61
62                let response_input_content_to_text = |items: Vec<ResponseInputContent>| {
63                    items
64                        .into_iter()
65                        .filter_map(|item| match item {
66                            ResponseInputContent::Text(text) => Some(text.text),
67                            ResponseInputContent::Image(image) => {
68                                if let Some(url) = image.image_url {
69                                    Some(url)
70                                } else {
71                                    image.file_id.map(|file_id| format!("file:{file_id}"))
72                                }
73                            }
74                            ResponseInputContent::File(file) => {
75                                if let Some(data) = file.file_data {
76                                    Some(data)
77                                } else if let Some(url) = file.file_url {
78                                    Some(url)
79                                } else if let Some(file_id) = file.file_id {
80                                    Some(format!("file:{file_id}"))
81                                } else {
82                                    file.filename
83                                }
84                            }
85                        })
86                        .collect::<Vec<_>>()
87                        .join("\n")
88                };
89
90                for item in body.output {
91                    match item {
92                        ResponseOutputItem::Message(message) => {
93                            for part in message.content {
94                                match part {
95                                    ResponseOutputContent::Text(text) => {
96                                        content.push(BetaContentBlock::Text(BetaTextBlock {
97                                            citations: None,
98                                            text: text.text,
99                                            type_: BetaTextBlockType::Text,
100                                        }));
101                                    }
102                                    ResponseOutputContent::Refusal(refusal) => {
103                                        has_refusal = true;
104                                        content.push(BetaContentBlock::Text(BetaTextBlock {
105                                            citations: None,
106                                            text: refusal.refusal,
107                                            type_: BetaTextBlockType::Text,
108                                        }));
109                                    }
110                                }
111                            }
112                        }
113                        ResponseOutputItem::FunctionToolCall(call) => {
114                            has_tool_use = true;
115                            content.push(BetaContentBlock::ToolUse(
116                                crate::claude::create_message::types::BetaToolUseBlock {
117                                    id: call.id.unwrap_or_else(|| call.call_id.clone()),
118                                    input: parse_json_object_or_empty(&call.arguments),
119                                    name: call.name,
120                                    type_: BetaToolUseBlockType::ToolUse,
121                                    cache_control: None,
122                                    caller: None,
123                                },
124                            ));
125                        }
126                        ResponseOutputItem::CustomToolCall(call) => {
127                            has_tool_use = true;
128                            content.push(BetaContentBlock::ToolUse(
129                                crate::claude::create_message::types::BetaToolUseBlock {
130                                    id: call.id.unwrap_or_else(|| call.call_id.clone()),
131                                    input: parse_json_object_or_empty(&call.input),
132                                    name: call.name,
133                                    type_: BetaToolUseBlockType::ToolUse,
134                                    cache_control: None,
135                                    caller: None,
136                                },
137                            ));
138                        }
139                        ResponseOutputItem::FunctionCallOutput(call) => {
140                            let output = match call.output {
141                                crate::openai::count_tokens::types::ResponseFunctionCallOutputContent::Text(text) => text,
142                                crate::openai::count_tokens::types::ResponseFunctionCallOutputContent::Content(items) => response_input_content_to_text(items),
143                            };
144                            if !output.is_empty() {
145                                content.push(BetaContentBlock::Text(BetaTextBlock {
146                                    citations: None,
147                                    text: format!("tool_result({}): {}", call.call_id, output),
148                                    type_: BetaTextBlockType::Text,
149                                }));
150                            }
151                        }
152                        ResponseOutputItem::CustomToolCallOutput(call) => {
153                            let output = match call.output {
154                                crate::openai::count_tokens::types::ResponseCustomToolCallOutputContent::Text(text) => text,
155                                crate::openai::count_tokens::types::ResponseCustomToolCallOutputContent::Content(items) => response_input_content_to_text(items),
156                            };
157                            if !output.is_empty() {
158                                content.push(BetaContentBlock::Text(BetaTextBlock {
159                                    citations: None,
160                                    text: format!(
161                                        "custom_tool_result({}): {}",
162                                        call.call_id, output
163                                    ),
164                                    type_: BetaTextBlockType::Text,
165                                }));
166                            }
167                        }
168                        ResponseOutputItem::McpCall(call) => {
169                            has_tool_use = true;
170                            let tool_use_id = call.id.clone();
171                            let is_error = call.error.is_some();
172                            let result_text = call.output.or(call.error);
173                            content.push(BetaContentBlock::McpToolUse(
174                                crate::claude::create_message::types::BetaMcpToolUseBlock {
175                                    id: tool_use_id.clone(),
176                                    input: parse_json_object_or_empty(&call.arguments),
177                                    name: call.name,
178                                    server_name: call.server_label,
179                                    type_: BetaMcpToolUseBlockType::McpToolUse,
180                                    cache_control: None,
181                                },
182                            ));
183                            if let Some(result_text) = result_text {
184                                content.push(BetaContentBlock::McpToolResult(
185                                    crate::claude::create_message::types::BetaMcpToolResultBlock {
186                                        tool_use_id,
187                                        type_: BetaRequestMcpToolResultBlockType::McpToolResult,
188                                        cache_control: None,
189                                        content: Some(BetaMcpToolResultBlockParamContent::Text(
190                                            result_text,
191                                        )),
192                                        is_error: Some(is_error),
193                                    },
194                                ));
195                            }
196                        }
197                        ResponseOutputItem::CodeInterpreterToolCall(call) => {
198                            has_tool_use = true;
199                            content.push(BetaContentBlock::ServerToolUse(
200                                crate::claude::create_message::types::BetaServerToolUseBlock {
201                                    id: call.id,
202                                    input: Default::default(),
203                                    name: BetaServerToolUseName::CodeExecution,
204                                    type_: BetaServerToolUseBlockType::ServerToolUse,
205                                    cache_control: None,
206                                    caller: None,
207                                },
208                            ));
209                        }
210                        ResponseOutputItem::FunctionWebSearch(call) => {
211                            has_tool_use = true;
212                            let crate::openai::count_tokens::types::ResponseFunctionWebSearch {
213                                id,
214                                action,
215                                ..
216                            } = call;
217                            content.push(BetaContentBlock::ServerToolUse(
218                                crate::claude::create_message::types::BetaServerToolUseBlock {
219                                    id: web_search_tool_use_id(id, &action),
220                                    input: Default::default(),
221                                    name: BetaServerToolUseName::WebSearch,
222                                    type_: BetaServerToolUseBlockType::ServerToolUse,
223                                    cache_control: None,
224                                    caller: None,
225                                },
226                            ));
227                        }
228                        ResponseOutputItem::ShellCall(call) => {
229                            has_tool_use = true;
230                            content.push(BetaContentBlock::ServerToolUse(
231                                crate::claude::create_message::types::BetaServerToolUseBlock {
232                                    id: call.id.unwrap_or_else(|| call.call_id.clone()),
233                                    input: Default::default(),
234                                    name: BetaServerToolUseName::BashCodeExecution,
235                                    type_: BetaServerToolUseBlockType::ServerToolUse,
236                                    cache_control: None,
237                                    caller: None,
238                                },
239                            ));
240                        }
241                        ResponseOutputItem::ShellCallOutput(call) => {
242                            let output = call
243                                .output
244                                .into_iter()
245                                .map(|entry| {
246                                    format!("stdout: {}\nstderr: {}", entry.stdout, entry.stderr)
247                                })
248                                .collect::<Vec<_>>()
249                                .join("\n");
250                            if !output.is_empty() {
251                                content.push(BetaContentBlock::Text(BetaTextBlock {
252                                    citations: None,
253                                    text: format!("shell_output({}): {}", call.call_id, output),
254                                    type_: BetaTextBlockType::Text,
255                                }));
256                            }
257                        }
258                        ResponseOutputItem::LocalShellCall(call) => {
259                            has_tool_use = true;
260                            content.push(BetaContentBlock::ServerToolUse(
261                                crate::claude::create_message::types::BetaServerToolUseBlock {
262                                    id: call.id,
263                                    input: Default::default(),
264                                    name: BetaServerToolUseName::BashCodeExecution,
265                                    type_: BetaServerToolUseBlockType::ServerToolUse,
266                                    cache_control: None,
267                                    caller: None,
268                                },
269                            ));
270                        }
271                        ResponseOutputItem::LocalShellCallOutput(call)
272                            if !call.output.is_empty() =>
273                        {
274                            content.push(BetaContentBlock::Text(BetaTextBlock {
275                                citations: None,
276                                text: format!("local_shell_output({}): {}", call.id, call.output),
277                                type_: BetaTextBlockType::Text,
278                            }));
279                        }
280                        ResponseOutputItem::ApplyPatchCall(call) => {
281                            has_tool_use = true;
282                            content.push(BetaContentBlock::ServerToolUse(
283                                crate::claude::create_message::types::BetaServerToolUseBlock {
284                                    id: call.id.unwrap_or_else(|| call.call_id.clone()),
285                                    input: Default::default(),
286                                    name: BetaServerToolUseName::TextEditorCodeExecution,
287                                    type_: BetaServerToolUseBlockType::ServerToolUse,
288                                    cache_control: None,
289                                    caller: None,
290                                },
291                            ));
292                        }
293                        ResponseOutputItem::ApplyPatchCallOutput(call) => {
294                            let status = match call.status {
295                                crate::openai::count_tokens::types::ResponseApplyPatchCallOutputStatus::Completed => "completed",
296                                crate::openai::count_tokens::types::ResponseApplyPatchCallOutputStatus::Failed => "failed",
297                            };
298                            let text = if let Some(output) = call.output {
299                                format!(
300                                    "apply_patch_output({}): {}\n{}",
301                                    call.call_id, status, output
302                                )
303                            } else {
304                                format!("apply_patch_output({}): {}", call.call_id, status)
305                            };
306                            content.push(BetaContentBlock::Text(BetaTextBlock {
307                                citations: None,
308                                text,
309                                type_: BetaTextBlockType::Text,
310                            }));
311                        }
312                        ResponseOutputItem::ReasoningItem(reasoning) => {
313                            let signature = reasoning.id.filter(|id| !id.is_empty());
314                            let mut thinking = reasoning
315                                .summary
316                                .into_iter()
317                                .map(|item| item.text)
318                                .collect::<Vec<_>>();
319                            if thinking.is_empty()
320                                && let Some(reasoning_content) = reasoning.content
321                            {
322                                thinking
323                                    .extend(reasoning_content.into_iter().map(|item| item.text));
324                            }
325                            let thinking = thinking.join("\n");
326                            if !thinking.is_empty()
327                                && let Some(signature) = signature
328                            {
329                                content.push(BetaContentBlock::Thinking(
330                                    crate::claude::create_message::types::BetaThinkingBlock {
331                                        signature,
332                                        thinking,
333                                        type_: BetaThinkingBlockType::Thinking,
334                                    },
335                                ));
336                            }
337                        }
338                        ResponseOutputItem::CompactionItem(compaction) => {
339                            has_compaction = true;
340                            content.push(BetaContentBlock::Compaction(
341                                crate::claude::create_message::types::BetaCompactionBlock {
342                                    content: Some(compaction.encrypted_content),
343                                    type_: BetaCompactionBlockType::Compaction,
344                                    cache_control: None,
345                                },
346                            ));
347                        }
348                        ResponseOutputItem::ImageGenerationCall(call)
349                            if call.result.as_deref().is_some_and(|s| !s.is_empty()) =>
350                        {
351                            content.push(BetaContentBlock::Text(BetaTextBlock {
352                                citations: None,
353                                text: call.result.unwrap_or_default(),
354                                type_: BetaTextBlockType::Text,
355                            }));
356                        }
357                        _ => {}
358                    }
359                }
360
361                if content.is_empty() {
362                    content.push(BetaContentBlock::Text(BetaTextBlock {
363                        citations: None,
364                        text: String::new(),
365                        type_: BetaTextBlockType::Text,
366                    }));
367                }
368
369                let stop_reason = if has_compaction {
370                    Some(BetaStopReason::Compaction)
371                } else if has_tool_use {
372                    Some(BetaStopReason::ToolUse)
373                } else if matches!(
374                    body.incomplete_details
375                        .as_ref()
376                        .and_then(|details| details.reason.as_ref()),
377                    Some(ResponseIncompleteReason::MaxOutputTokens)
378                ) {
379                    Some(BetaStopReason::MaxTokens)
380                } else if has_refusal
381                    || matches!(
382                        body.incomplete_details
383                            .as_ref()
384                            .and_then(|details| details.reason.as_ref()),
385                        Some(ResponseIncompleteReason::ContentFilter)
386                    )
387                {
388                    Some(BetaStopReason::Refusal)
389                } else {
390                    Some(BetaStopReason::EndTurn)
391                };
392
393                let (input_tokens, cached_tokens, output_tokens) = body
394                    .usage
395                    .as_ref()
396                    .map(|usage| {
397                        let cached_tokens = usage.input_tokens_details.cached_tokens;
398                        let total_input_tokens = if usage.total_tokens >= usage.output_tokens {
399                            usage.total_tokens.saturating_sub(usage.output_tokens)
400                        } else {
401                            usage.input_tokens
402                        };
403                        (
404                            total_input_tokens.saturating_sub(cached_tokens),
405                            cached_tokens,
406                            usage.output_tokens,
407                        )
408                    })
409                    .unwrap_or((0, 0, 0));
410                let service_tier = match body.service_tier {
411                    Some(ResponseServiceTier::Priority) => BetaServiceTier::Priority,
412                    _ => BetaServiceTier::Standard,
413                };
414                let usage: BetaUsage = beta_usage_from_counts(
415                    input_tokens,
416                    cached_tokens,
417                    output_tokens,
418                    service_tier,
419                );
420
421                ClaudeCreateMessageResponse::Success {
422                    stats_code,
423                    headers: ClaudeResponseHeaders {
424                        extra: headers.extra,
425                    },
426                    body: BetaMessage {
427                        id: body.id,
428                        container: None,
429                        content,
430                        context_management: None,
431                        model: Model::Custom(body.model),
432                        role: BetaMessageRole::Assistant,
433                        stop_reason,
434                        stop_sequence: None,
435                        type_: BetaMessageType::Message,
436                        usage,
437                    },
438                }
439            }
440            OpenAiCreateResponseResponse::Error {
441                stats_code,
442                headers,
443                body,
444            } => ClaudeCreateMessageResponse::Error {
445                stats_code,
446                headers: ClaudeResponseHeaders {
447                    extra: headers.extra,
448                },
449                body: beta_error_response_from_status_message(stats_code, body.error.message),
450            },
451        })
452    }
453}