Skip to main content

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

1use std::collections::BTreeMap;
2
3use crate::claude::count_tokens::types as ct;
4use crate::claude::create_message::response::ClaudeCreateMessageResponse;
5use crate::claude::create_message::types::{BetaServiceTier, BetaStopReason};
6use crate::openai::count_tokens::types as ot;
7use crate::openai::create_response::response::{OpenAiCreateResponseResponse, ResponseBody};
8use crate::openai::create_response::types as rt;
9use crate::openai::types::OpenAiResponseHeaders;
10use crate::transform::claude::utils::claude_model_to_string;
11use crate::transform::openai::generate_content::openai_chat_completions::claude::utils::{
12    server_tool_name, stdout_stderr_text,
13};
14use crate::transform::openai::model_list::claude::utils::openai_error_response_from_claude;
15use crate::transform::utils::TransformError;
16
17#[derive(Debug, Clone, Copy)]
18enum RecordedCallKind {
19    CodeInterpreter,
20    WebSearch,
21    WebFetch,
22    Mcp,
23    FileSearch,
24}
25
26#[derive(Debug, Clone, Copy)]
27struct RecordedCall {
28    output_index: usize,
29    kind: RecordedCallKind,
30}
31
32fn search_queries(input: &ct::JsonObject) -> (Option<String>, Option<Vec<String>>) {
33    let queries = input
34        .get("queries")
35        .and_then(|value| value.as_array())
36        .map(|values| {
37            values
38                .iter()
39                .filter_map(|value| value.as_str().map(ToString::to_string))
40                .collect::<Vec<_>>()
41        })
42        .unwrap_or_default();
43    let query = input
44        .get("query")
45        .and_then(|value| value.as_str())
46        .map(ToString::to_string)
47        .or_else(|| queries.first().cloned());
48    let queries = if queries.len() > 1 {
49        Some(queries)
50    } else {
51        None
52    };
53    (query, queries)
54}
55
56impl TryFrom<ClaudeCreateMessageResponse> for OpenAiCreateResponseResponse {
57    type Error = TransformError;
58
59    fn try_from(value: ClaudeCreateMessageResponse) -> Result<Self, TransformError> {
60        Ok(match value {
61            ClaudeCreateMessageResponse::Success {
62                stats_code,
63                headers,
64                body,
65            } => {
66                let mut output = Vec::new();
67                let mut message_content = Vec::new();
68                let mut output_text_parts = Vec::new();
69                let mut tool_call_count = 0usize;
70                let mut recorded_calls = BTreeMap::<String, RecordedCall>::new();
71
72                for (index, block) in body.content.into_iter().enumerate() {
73                    match block {
74                    crate::claude::create_message::types::BetaContentBlock::Text(block) => {
75                        if !block.text.is_empty() {
76                            output_text_parts.push(block.text.clone());
77                            message_content.push(ot::ResponseOutputContent::Text(
78                                ot::ResponseOutputText {
79                                    annotations: Vec::new(),
80                                    logprobs: None,
81                                    text: block.text,
82                                    type_: ot::ResponseOutputTextType::OutputText,
83                                },
84                            ));
85                        }
86                    }
87                    crate::claude::create_message::types::BetaContentBlock::Thinking(block) => {
88                        output.push(rt::ResponseOutputItem::ReasoningItem(
89                            ot::ResponseReasoningItem {
90                                id: Some(format!("reasoning_{index}")),
91                                summary: vec![ot::ResponseSummaryTextContent {
92                                    text: block.thinking.clone(),
93                                    type_: ot::ResponseSummaryTextContentType::SummaryText,
94                                }],
95                                type_: ot::ResponseReasoningItemType::Reasoning,
96                                content: Some(vec![ot::ResponseReasoningTextContent {
97                                    text: block.thinking,
98                                    type_: ot::ResponseReasoningTextContentType::ReasoningText,
99                                }]),
100                                encrypted_content: None,
101                                status: Some(ot::ResponseItemStatus::Completed),
102                            },
103                        ));
104                    }
105                    crate::claude::create_message::types::BetaContentBlock::RedactedThinking(
106                        block,
107                    ) => {
108                        output.push(rt::ResponseOutputItem::ReasoningItem(
109                            ot::ResponseReasoningItem {
110                                id: Some(format!("redacted_reasoning_{index}")),
111                                summary: Vec::new(),
112                                type_: ot::ResponseReasoningItemType::Reasoning,
113                                content: None,
114                                encrypted_content: Some(block.data),
115                                status: Some(ot::ResponseItemStatus::Completed),
116                            },
117                        ));
118                    }
119                    crate::claude::create_message::types::BetaContentBlock::ToolUse(block) => {
120                        tool_call_count += 1;
121                        output.push(rt::ResponseOutputItem::FunctionToolCall(
122                            ot::ResponseFunctionToolCall {
123                                arguments: serde_json::to_string(&block.input)
124                                    .unwrap_or_else(|_| "{}".to_string()),
125                                call_id: block.id.clone(),
126                                name: block.name,
127                                type_: ot::ResponseFunctionToolCallType::FunctionCall,
128                                id: Some(block.id),
129                                status: Some(ot::ResponseItemStatus::Completed),
130                            },
131                        ));
132                    }
133                    crate::claude::create_message::types::BetaContentBlock::ServerToolUse(
134                        block,
135                    ) => {
136                        tool_call_count += 1;
137                        let call_id = block.id.clone();
138                        let output_index = output.len();
139                        match block.name {
140                            ct::BetaServerToolUseName::CodeExecution => {
141                                let code = block
142                                    .input
143                                    .get("code")
144                                    .and_then(|value| value.as_str())
145                                    .unwrap_or_default()
146                                    .to_string();
147                                let container_id = block
148                                    .input
149                                    .get("container_id")
150                                    .and_then(|value| value.as_str())
151                                    .unwrap_or_default()
152                                    .to_string();
153                                output.push(rt::ResponseOutputItem::CodeInterpreterToolCall(
154                                    ot::ResponseCodeInterpreterToolCall {
155                                        id: call_id.clone(),
156                                        code,
157                                        container_id,
158                                        outputs: None,
159                                        status: ot::ResponseCodeInterpreterToolCallStatus::InProgress,
160                                        type_: ot::ResponseCodeInterpreterToolCallType::CodeInterpreterCall,
161                                    },
162                                ));
163                                recorded_calls.insert(
164                                    call_id,
165                                    RecordedCall {
166                                        output_index,
167                                        kind: RecordedCallKind::CodeInterpreter,
168                                    },
169                                );
170                            }
171                            ct::BetaServerToolUseName::WebSearch => {
172                                let (query, queries) = search_queries(&block.input);
173                                output.push(rt::ResponseOutputItem::FunctionWebSearch(
174                                    ot::ResponseFunctionWebSearch {
175                                        id: Some(call_id.clone()),
176                                        action: ot::ResponseFunctionWebSearchAction::Search {
177                                            query,
178                                            queries,
179                                            sources: None,
180                                        },
181                                        status: ot::ResponseFunctionWebSearchStatus::Searching,
182                                        type_: ot::ResponseFunctionWebSearchType::WebSearchCall,
183                                    },
184                                ));
185                                recorded_calls.insert(
186                                    call_id,
187                                    RecordedCall {
188                                        output_index,
189                                        kind: RecordedCallKind::WebSearch,
190                                    },
191                                );
192                            }
193                            ct::BetaServerToolUseName::WebFetch => {
194                                let url = block
195                                    .input
196                                    .get("url")
197                                    .and_then(|value| value.as_str())
198                                    .map(ToString::to_string);
199                                output.push(rt::ResponseOutputItem::FunctionWebSearch(
200                                    ot::ResponseFunctionWebSearch {
201                                        id: Some(call_id.clone()),
202                                        action: ot::ResponseFunctionWebSearchAction::OpenPage { url },
203                                        status: ot::ResponseFunctionWebSearchStatus::InProgress,
204                                        type_: ot::ResponseFunctionWebSearchType::WebSearchCall,
205                                    },
206                                ));
207                                recorded_calls.insert(
208                                    call_id,
209                                    RecordedCall {
210                                        output_index,
211                                        kind: RecordedCallKind::WebFetch,
212                                    },
213                                );
214                            }
215                            ct::BetaServerToolUseName::BashCodeExecution => {
216                                let mut commands = block
217                                    .input
218                                    .get("commands")
219                                    .and_then(|value| value.as_array())
220                                    .map(|values| {
221                                        values
222                                            .iter()
223                                            .filter_map(|value| value.as_str().map(ToString::to_string))
224                                            .collect::<Vec<_>>()
225                                    })
226                                    .unwrap_or_default();
227                                if commands.is_empty()
228                                    && let Some(command) = block
229                                        .input
230                                        .get("command")
231                                        .and_then(|value| value.as_str())
232                                {
233                                    commands.push(command.to_string());
234                                }
235                                output.push(rt::ResponseOutputItem::ShellCall(
236                                    ot::ResponseShellCall {
237                                        action: ot::ResponseShellCallAction {
238                                            commands,
239                                            max_output_length: None,
240                                            timeout_ms: None,
241                                        },
242                                        call_id,
243                                        type_: ot::ResponseShellCallType::ShellCall,
244                                        id: Some(block.id),
245                                        environment: None,
246                                        status: Some(ot::ResponseItemStatus::Completed),
247                                    },
248                                ));
249                            }
250                            ct::BetaServerToolUseName::TextEditorCodeExecution => {
251                                output.push(rt::ResponseOutputItem::CustomToolCall(
252                                    ot::ResponseCustomToolCall {
253                                        call_id,
254                                        input: serde_json::to_string(&block.input)
255                                            .unwrap_or_else(|_| "{}".to_string()),
256                                        name: server_tool_name(&block.name),
257                                        type_: ot::ResponseCustomToolCallType::CustomToolCall,
258                                        id: Some(block.id),
259                                    },
260                                ));
261                            }
262                            ct::BetaServerToolUseName::ToolSearchToolRegex
263                            | ct::BetaServerToolUseName::ToolSearchToolBm25 => {
264                                let queries = block
265                                    .input
266                                    .get("queries")
267                                    .and_then(|value| value.as_array())
268                                    .map(|values| {
269                                        values
270                                            .iter()
271                                            .filter_map(|value| value.as_str().map(ToString::to_string))
272                                            .collect::<Vec<_>>()
273                                    })
274                                    .unwrap_or_else(|| {
275                                        block
276                                            .input
277                                            .get("query")
278                                            .and_then(|value| value.as_str())
279                                            .map(|value| vec![value.to_string()])
280                                            .unwrap_or_default()
281                                    });
282                                output.push(rt::ResponseOutputItem::FileSearchToolCall(
283                                    ot::ResponseFileSearchToolCall {
284                                        id: call_id.clone(),
285                                        queries,
286                                        status: ot::ResponseFileSearchToolCallStatus::Searching,
287                                        type_: ot::ResponseFileSearchToolCallType::FileSearchCall,
288                                        results: None,
289                                    },
290                                ));
291                                recorded_calls.insert(
292                                    call_id,
293                                    RecordedCall {
294                                        output_index,
295                                        kind: RecordedCallKind::FileSearch,
296                                    },
297                                );
298                            }
299                        }
300                    }
301                    crate::claude::create_message::types::BetaContentBlock::McpToolUse(block) => {
302                        tool_call_count += 1;
303                        let output_index = output.len();
304                        output.push(rt::ResponseOutputItem::McpCall(ot::ResponseMcpCall {
305                            id: block.id.clone(),
306                            arguments: serde_json::to_string(&block.input)
307                                .unwrap_or_else(|_| "{}".to_string()),
308                            name: block.name,
309                            server_label: block.server_name,
310                            type_: ot::ResponseMcpCallType::McpCall,
311                            approval_request_id: None,
312                            error: None,
313                            output: None,
314                            status: Some(ot::ResponseToolCallStatus::Calling),
315                        }));
316                        recorded_calls.insert(
317                            block.id,
318                            RecordedCall {
319                                output_index,
320                                kind: RecordedCallKind::Mcp,
321                            },
322                        );
323                    }
324                    crate::claude::create_message::types::BetaContentBlock::McpToolResult(block) => {
325                        let is_error = block.is_error.unwrap_or(false);
326                        let output_text = match block.content {
327                            Some(ct::BetaMcpToolResultBlockParamContent::Text(text)) => text,
328                            Some(ct::BetaMcpToolResultBlockParamContent::Blocks(parts)) => parts
329                                .into_iter()
330                                .map(|part| part.text)
331                                .collect::<Vec<_>>()
332                                .join("\n"),
333                            None => String::new(),
334                        };
335                        if let Some(record) = recorded_calls.get(&block.tool_use_id)
336                            && matches!(record.kind, RecordedCallKind::Mcp)
337                            && let Some(rt::ResponseOutputItem::McpCall(call)) =
338                                output.get_mut(record.output_index)
339                        {
340                            call.status = Some(if is_error {
341                                ot::ResponseToolCallStatus::Failed
342                            } else {
343                                ot::ResponseToolCallStatus::Completed
344                            });
345                            call.error = if is_error {
346                                Some(if output_text.is_empty() {
347                                    "mcp_tool_result_error".to_string()
348                                } else {
349                                    output_text.clone()
350                                })
351                            } else {
352                                None
353                            };
354                            call.output = (!is_error && !output_text.is_empty()).then_some(output_text);
355                        } else {
356                            output.push(rt::ResponseOutputItem::FunctionCallOutput(
357                                ot::ResponseFunctionCallOutput {
358                                    call_id: block.tool_use_id,
359                                    output: ot::ResponseFunctionCallOutputContent::Text(output_text),
360                                    type_: ot::ResponseFunctionCallOutputType::FunctionCallOutput,
361                                    id: None,
362                                    status: Some(if is_error {
363                                        ot::ResponseItemStatus::Incomplete
364                                    } else {
365                                        ot::ResponseItemStatus::Completed
366                                    }),
367                                },
368                            ));
369                        }
370                    }
371                    crate::claude::create_message::types::BetaContentBlock::Compaction(block) => {
372                        output.push(rt::ResponseOutputItem::CompactionItem(
373                            ot::ResponseCompactionItemParam {
374                                encrypted_content: block.content.unwrap_or_default(),
375                                type_: ot::ResponseCompactionItemType::Compaction,
376                                id: None,
377                                created_by: None,
378                            },
379                        ));
380                    }
381                    crate::claude::create_message::types::BetaContentBlock::ContainerUpload(block) => {
382                        message_content.push(ot::ResponseOutputContent::Text(
383                            ot::ResponseOutputText {
384                                annotations: Vec::new(),
385                                logprobs: None,
386                                text: format!("container_upload:{}", block.file_id),
387                                type_: ot::ResponseOutputTextType::OutputText,
388                            },
389                        ));
390                    }
391                    crate::claude::create_message::types::BetaContentBlock::WebSearchToolResult(block) => {
392                        let status = match block.content {
393                            crate::claude::create_message::types::BetaWebSearchToolResultBlockContent::Results(results) => {
394                                let sources = results
395                                    .into_iter()
396                                    .map(|result| ot::ResponseFunctionWebSearchSource {
397                                        type_: ot::ResponseFunctionWebSearchSourceType::Url,
398                                        url: result.url,
399                                    })
400                                    .collect::<Vec<_>>();
401                                if let Some(record) = recorded_calls.get(&block.tool_use_id)
402                                    && matches!(record.kind, RecordedCallKind::WebSearch)
403                                    && let Some(rt::ResponseOutputItem::FunctionWebSearch(call)) =
404                                        output.get_mut(record.output_index)
405                                {
406                                    let (query, queries) = match &call.action {
407                                        ot::ResponseFunctionWebSearchAction::Search {
408                                            query,
409                                            queries,
410                                            ..
411                                        } => (query.clone(), queries.clone()),
412                                        _ => (None, None),
413                                    };
414                                    call.action = ot::ResponseFunctionWebSearchAction::Search {
415                                        query,
416                                        queries,
417                                        sources: (!sources.is_empty()).then_some(sources),
418                                    };
419                                    call.status = ot::ResponseFunctionWebSearchStatus::Completed;
420                                }
421                                ot::ResponseFunctionWebSearchStatus::Completed
422                            }
423                            crate::claude::create_message::types::BetaWebSearchToolResultBlockContent::Error(_) => {
424                                if let Some(record) = recorded_calls.get(&block.tool_use_id)
425                                    && matches!(record.kind, RecordedCallKind::WebSearch)
426                                    && let Some(rt::ResponseOutputItem::FunctionWebSearch(call)) =
427                                        output.get_mut(record.output_index)
428                                {
429                                    call.status = ot::ResponseFunctionWebSearchStatus::Failed;
430                                }
431                                ot::ResponseFunctionWebSearchStatus::Failed
432                            }
433                        };
434                        if !recorded_calls.contains_key(&block.tool_use_id) {
435                            output.push(rt::ResponseOutputItem::FunctionWebSearch(
436                                ot::ResponseFunctionWebSearch {
437                                    id: Some(block.tool_use_id),
438                                    action: ot::ResponseFunctionWebSearchAction::Search {
439                                        query: None,
440                                        queries: None,
441                                        sources: None,
442                                    },
443                                    status,
444                                    type_: ot::ResponseFunctionWebSearchType::WebSearchCall,
445                                },
446                            ));
447                        }
448                    }
449                    crate::claude::create_message::types::BetaContentBlock::WebFetchToolResult(block) => {
450                        match block.content {
451                            crate::claude::create_message::types::BetaWebFetchToolResultBlockContent::Result(result) => {
452                                if let Some(record) = recorded_calls.get(&block.tool_use_id)
453                                    && matches!(record.kind, RecordedCallKind::WebFetch)
454                                    && let Some(rt::ResponseOutputItem::FunctionWebSearch(call)) =
455                                        output.get_mut(record.output_index)
456                                {
457                                    call.action = ot::ResponseFunctionWebSearchAction::OpenPage {
458                                        url: Some(result.url.clone()),
459                                    };
460                                    call.status = ot::ResponseFunctionWebSearchStatus::Completed;
461                                } else {
462                                    output.push(rt::ResponseOutputItem::FunctionWebSearch(
463                                        ot::ResponseFunctionWebSearch {
464                                            id: Some(block.tool_use_id),
465                                            action: ot::ResponseFunctionWebSearchAction::OpenPage {
466                                                url: Some(result.url),
467                                            },
468                                            status: ot::ResponseFunctionWebSearchStatus::Completed,
469                                            type_: ot::ResponseFunctionWebSearchType::WebSearchCall,
470                                        },
471                                    ));
472                                }
473                            }
474                            crate::claude::create_message::types::BetaWebFetchToolResultBlockContent::Error(_) => {
475                                if let Some(record) = recorded_calls.get(&block.tool_use_id)
476                                    && matches!(record.kind, RecordedCallKind::WebFetch)
477                                    && let Some(rt::ResponseOutputItem::FunctionWebSearch(call)) =
478                                        output.get_mut(record.output_index)
479                                {
480                                    call.status = ot::ResponseFunctionWebSearchStatus::Failed;
481                                } else {
482                                    output.push(rt::ResponseOutputItem::FunctionWebSearch(
483                                        ot::ResponseFunctionWebSearch {
484                                            id: Some(block.tool_use_id),
485                                            action: ot::ResponseFunctionWebSearchAction::OpenPage {
486                                                url: None,
487                                            },
488                                            status: ot::ResponseFunctionWebSearchStatus::Failed,
489                                            type_: ot::ResponseFunctionWebSearchType::WebSearchCall,
490                                        },
491                                    ));
492                                }
493                            }
494                        }
495                    }
496                    crate::claude::create_message::types::BetaContentBlock::CodeExecutionToolResult(block) => {
497                        let (logs, status) = match block.content {
498                            ct::BetaCodeExecutionToolResultBlockParamContent::Result(result) => (
499                                stdout_stderr_text(result.stdout, result.stderr),
500                                ot::ResponseCodeInterpreterToolCallStatus::Completed,
501                            ),
502                            ct::BetaCodeExecutionToolResultBlockParamContent::Error(err) => (
503                                format!("code_execution_error:{:?}", err.error_code),
504                                ot::ResponseCodeInterpreterToolCallStatus::Failed,
505                            ),
506                        };
507                        if let Some(record) = recorded_calls.get(&block.tool_use_id)
508                            && matches!(record.kind, RecordedCallKind::CodeInterpreter)
509                            && let Some(rt::ResponseOutputItem::CodeInterpreterToolCall(call)) =
510                                output.get_mut(record.output_index)
511                        {
512                            call.outputs = (!logs.is_empty()).then_some(vec![
513                                ot::ResponseCodeInterpreterOutputItem::Logs { logs },
514                            ]);
515                            call.status = status;
516                        } else {
517                            output.push(rt::ResponseOutputItem::CodeInterpreterToolCall(
518                                ot::ResponseCodeInterpreterToolCall {
519                                    id: block.tool_use_id,
520                                    code: String::new(),
521                                    container_id: String::new(),
522                                    outputs: (!logs.is_empty()).then_some(vec![
523                                        ot::ResponseCodeInterpreterOutputItem::Logs { logs },
524                                    ]),
525                                    status,
526                                    type_: ot::ResponseCodeInterpreterToolCallType::CodeInterpreterCall,
527                                },
528                            ));
529                        }
530                    }
531                    crate::claude::create_message::types::BetaContentBlock::BashCodeExecutionToolResult(block) => {
532                        let (stdout, stderr, outcome, status) = match block.content {
533                            ct::BetaBashCodeExecutionToolResultBlockParamContent::Result(result) => (
534                                result.stdout,
535                                result.stderr,
536                                ot::ResponseShellCallOutcome::Exit { exit_code: 0 },
537                                ot::ResponseItemStatus::Completed,
538                            ),
539                            ct::BetaBashCodeExecutionToolResultBlockParamContent::Error(err) => (
540                                String::new(),
541                                format!("bash_code_execution_error:{:?}", err.error_code),
542                                if matches!(
543                                    err.error_code,
544                                    ct::BetaBashCodeExecutionToolResultErrorCode::ExecutionTimeExceeded
545                                ) {
546                                    ot::ResponseShellCallOutcome::Timeout
547                                } else {
548                                    ot::ResponseShellCallOutcome::Exit { exit_code: 1 }
549                                },
550                                ot::ResponseItemStatus::Incomplete,
551                            ),
552                        };
553                        output.push(rt::ResponseOutputItem::ShellCallOutput(
554                            ot::ResponseShellCallOutput {
555                                call_id: block.tool_use_id,
556                                output: vec![ot::ResponseFunctionShellCallOutputContent {
557                                    outcome,
558                                    stderr,
559                                    stdout,
560                                }],
561                                type_: ot::ResponseShellCallOutputType::ShellCallOutput,
562                                id: None,
563                                max_output_length: None,
564                                status: Some(status),
565                            },
566                        ));
567                    }
568                    crate::claude::create_message::types::BetaContentBlock::TextEditorCodeExecutionToolResult(block) => {
569                        let text = match block.content {
570                            ct::BetaTextEditorCodeExecutionToolResultBlockParamContent::View(view) => {
571                                view.content
572                            }
573                            ct::BetaTextEditorCodeExecutionToolResultBlockParamContent::Create(create) => {
574                                format!("file_updated:{}", create.is_file_update)
575                            }
576                            ct::BetaTextEditorCodeExecutionToolResultBlockParamContent::StrReplace(replace) => {
577                                replace.lines.unwrap_or_default().join("\n")
578                            }
579                            ct::BetaTextEditorCodeExecutionToolResultBlockParamContent::Error(err) => err
580                                .error_message
581                                .unwrap_or_else(|| {
582                                    format!("text_editor_code_execution_error:{:?}", err.error_code)
583                                }),
584                        };
585                        output.push(rt::ResponseOutputItem::CustomToolCallOutput(
586                            ot::ResponseCustomToolCallOutput {
587                                call_id: block.tool_use_id,
588                                output: ot::ResponseCustomToolCallOutputContent::Text(text),
589                                type_: ot::ResponseCustomToolCallOutputType::CustomToolCallOutput,
590                                id: None,
591                            },
592                        ));
593                    }
594                    crate::claude::create_message::types::BetaContentBlock::ToolSearchToolResult(block) => {
595                        match block.content {
596                            ct::BetaToolSearchToolResultBlockParamContent::Result(result) => {
597                                let results = result
598                                    .tool_references
599                                    .into_iter()
600                                    .map(|reference| ot::ResponseFileSearchResult {
601                                        filename: Some(reference.tool_name.clone()),
602                                        text: Some(reference.tool_name),
603                                        ..Default::default()
604                                    })
605                                    .collect::<Vec<_>>();
606                                if let Some(record) = recorded_calls.get(&block.tool_use_id)
607                                    && matches!(record.kind, RecordedCallKind::FileSearch)
608                                    && let Some(rt::ResponseOutputItem::FileSearchToolCall(call)) =
609                                        output.get_mut(record.output_index)
610                                {
611                                    call.results = Some(results);
612                                    call.status = ot::ResponseFileSearchToolCallStatus::Completed;
613                                } else {
614                                    output.push(rt::ResponseOutputItem::FileSearchToolCall(
615                                        ot::ResponseFileSearchToolCall {
616                                            id: block.tool_use_id,
617                                            queries: Vec::new(),
618                                            status: ot::ResponseFileSearchToolCallStatus::Completed,
619                                            type_: ot::ResponseFileSearchToolCallType::FileSearchCall,
620                                            results: Some(results),
621                                        },
622                                    ));
623                                }
624                            }
625                            ct::BetaToolSearchToolResultBlockParamContent::Error(err) => {
626                                if let Some(record) = recorded_calls.get(&block.tool_use_id)
627                                    && matches!(record.kind, RecordedCallKind::FileSearch)
628                                    && let Some(rt::ResponseOutputItem::FileSearchToolCall(call)) =
629                                        output.get_mut(record.output_index)
630                                {
631                                    call.status = ot::ResponseFileSearchToolCallStatus::Failed;
632                                    call.results = Some(vec![ot::ResponseFileSearchResult {
633                                        text: Some(format!("tool_search_error:{:?}", err.error_code)),
634                                        ..Default::default()
635                                    }]);
636                                } else {
637                                    output.push(rt::ResponseOutputItem::FileSearchToolCall(
638                                        ot::ResponseFileSearchToolCall {
639                                            id: block.tool_use_id,
640                                            queries: Vec::new(),
641                                            status: ot::ResponseFileSearchToolCallStatus::Failed,
642                                            type_: ot::ResponseFileSearchToolCallType::FileSearchCall,
643                                            results: Some(vec![ot::ResponseFileSearchResult {
644                                                text: Some(format!("tool_search_error:{:?}", err.error_code)),
645                                                ..Default::default()
646                                            }]),
647                                        },
648                                    ));
649                                }
650                            }
651                        }
652                    }
653                }
654                }
655
656                if !message_content.is_empty() {
657                    output.insert(
658                        0,
659                        rt::ResponseOutputItem::Message(ot::ResponseOutputMessage {
660                            id: format!("{}_message_0", body.id),
661                            content: message_content,
662                            role: ot::ResponseOutputMessageRole::Assistant,
663                            phase: Some(ot::ResponseMessagePhase::FinalAnswer),
664                            status: Some(ot::ResponseItemStatus::Completed),
665                            type_: Some(ot::ResponseOutputMessageType::Message),
666                        }),
667                    );
668                }
669
670                let (status, incomplete_details) = match body.stop_reason {
671                    Some(BetaStopReason::MaxTokens)
672                    | Some(BetaStopReason::ModelContextWindowExceeded) => (
673                        Some(rt::ResponseStatus::Incomplete),
674                        Some(rt::ResponseIncompleteDetails {
675                            reason: Some(rt::ResponseIncompleteReason::MaxOutputTokens),
676                        }),
677                    ),
678                    Some(BetaStopReason::Refusal) => (
679                        Some(rt::ResponseStatus::Incomplete),
680                        Some(rt::ResponseIncompleteDetails {
681                            reason: Some(rt::ResponseIncompleteReason::ContentFilter),
682                        }),
683                    ),
684                    _ => (Some(rt::ResponseStatus::Completed), None),
685                };
686                let service_tier = Some(match body.usage.service_tier.clone() {
687                    BetaServiceTier::Standard => rt::ResponseServiceTier::Default,
688                    BetaServiceTier::Priority => rt::ResponseServiceTier::Priority,
689                    BetaServiceTier::Batch => rt::ResponseServiceTier::Flex,
690                });
691                let input_tokens = body
692                    .usage
693                    .input_tokens
694                    .saturating_add(body.usage.cache_creation_input_tokens)
695                    .saturating_add(body.usage.cache_read_input_tokens);
696                let usage = Some(rt::ResponseUsage {
697                    input_tokens,
698                    input_tokens_details: rt::ResponseInputTokensDetails {
699                        cached_tokens: body.usage.cache_read_input_tokens,
700                    },
701                    output_tokens: body.usage.output_tokens,
702                    output_tokens_details: rt::ResponseOutputTokensDetails {
703                        reasoning_tokens: 0,
704                    },
705                    total_tokens: input_tokens.saturating_add(body.usage.output_tokens),
706                });
707
708                OpenAiCreateResponseResponse::Success {
709                    stats_code,
710                    headers: OpenAiResponseHeaders {
711                        extra: headers.extra,
712                    },
713                    body: ResponseBody {
714                        id: body.id,
715                        created_at: 0,
716                        error: None,
717                        incomplete_details,
718                        instructions: Some(ot::ResponseInput::Text(String::new())),
719                        metadata: BTreeMap::new(),
720                        model: claude_model_to_string(&body.model),
721                        object: rt::ResponseObject::Response,
722                        output,
723                        parallel_tool_calls: tool_call_count > 1,
724                        temperature: 1.0,
725                        tool_choice: if tool_call_count > 0 {
726                            ot::ResponseToolChoice::Options(ot::ResponseToolChoiceOptions::Required)
727                        } else {
728                            ot::ResponseToolChoice::Options(ot::ResponseToolChoiceOptions::Auto)
729                        },
730                        tools: Vec::new(),
731                        top_p: 1.0,
732                        background: None,
733                        completed_at: None,
734                        conversation: None,
735                        max_output_tokens: None,
736                        max_tool_calls: None,
737                        output_text: if output_text_parts.is_empty() {
738                            None
739                        } else {
740                            Some(output_text_parts.join("\n"))
741                        },
742                        previous_response_id: None,
743                        prompt: None,
744                        prompt_cache_key: None,
745                        prompt_cache_retention: None,
746                        reasoning: None,
747                        safety_identifier: None,
748                        service_tier,
749                        status,
750                        text: None,
751                        top_logprobs: None,
752                        truncation: None,
753                        usage,
754                        user: None,
755                    },
756                }
757            }
758            ClaudeCreateMessageResponse::Error {
759                stats_code,
760                headers,
761                body,
762            } => OpenAiCreateResponseResponse::Error {
763                stats_code,
764                headers: OpenAiResponseHeaders {
765                    extra: headers.extra,
766                },
767                body: openai_error_response_from_claude(stats_code, body),
768            },
769        })
770    }
771}