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}