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}