1use crate::claude::count_tokens::types as ct;
2use crate::claude::create_message::request::{
3 ClaudeCreateMessageRequest, PathParameters, QueryParameters, RequestBody, RequestHeaders,
4};
5use crate::claude::create_message::types::{
6 BetaMetadata, BetaServiceTierParam, BetaSpeed, HttpMethod as ClaudeHttpMethod, Model,
7};
8use crate::openai::count_tokens::types as ot;
9use crate::openai::create_response::request::OpenAiCreateResponseRequest;
10use crate::openai::create_response::types::{ResponseContextManagementType, ResponseServiceTier};
11use crate::transform::openai::count_tokens::claude::utils::{
12 ClaudeToolUseIdMapper, mcp_allowed_tools_to_configs, openai_mcp_tool_to_server,
13 openai_message_content_to_claude, openai_reasoning_to_claude, openai_role_to_claude,
14 openai_tool_choice_to_claude, parallel_disable, push_message_block,
15 response_input_contents_to_tool_result_content, tool_from_function,
16};
17use crate::transform::openai::count_tokens::utils::{
18 openai_input_to_items, openai_reasoning_summary_to_text,
19};
20use crate::transform::utils::{TransformError, enforce_anthropic_strict_schema};
21
22fn parse_tool_use_input(input: String) -> ct::JsonObject {
23 serde_json::from_str::<ct::JsonObject>(&input).unwrap_or_else(|_| {
24 let mut object = ct::JsonObject::new();
25 object.insert("input".to_string(), serde_json::Value::String(input));
26 object
27 })
28}
29
30fn push_block_message(
31 messages: &mut Vec<ct::BetaMessageParam>,
32 role: ct::BetaMessageRole,
33 block: ct::BetaContentBlockParam,
34) {
35 push_message_block(messages, role, block);
36}
37
38fn web_search_tool_use_id(
39 id: Option<String>,
40 action: &ot::ResponseFunctionWebSearchAction,
41 sequence: usize,
42) -> String {
43 id.unwrap_or_else(|| match action {
44 ot::ResponseFunctionWebSearchAction::Search { .. } => format!("web_search_{sequence}"),
45 ot::ResponseFunctionWebSearchAction::OpenPage { .. } => {
46 format!("web_search_open_page_{sequence}")
47 }
48 ot::ResponseFunctionWebSearchAction::FindInPage { .. } => {
49 format!("web_search_find_in_page_{sequence}")
50 }
51 })
52}
53
54impl TryFrom<OpenAiCreateResponseRequest> for ClaudeCreateMessageRequest {
55 type Error = TransformError;
56
57 fn try_from(value: OpenAiCreateResponseRequest) -> Result<Self, TransformError> {
58 let body = value.body;
59 let mut messages = Vec::new();
60 let mut tool_use_ids = ClaudeToolUseIdMapper::default();
61
62 for item in openai_input_to_items(body.input.clone()) {
63 match item {
64 ot::ResponseInputItem::Message(message) => {
65 messages.push(ct::BetaMessageParam {
66 content: openai_message_content_to_claude(message.content),
67 role: openai_role_to_claude(message.role),
68 });
69 }
70 ot::ResponseInputItem::OutputMessage(message) => {
71 let text = message
72 .content
73 .into_iter()
74 .map(|part| match part {
75 ot::ResponseOutputContent::Text(text) => text.text,
76 ot::ResponseOutputContent::Refusal(refusal) => refusal.refusal,
77 })
78 .filter(|text| !text.is_empty())
79 .collect::<Vec<_>>()
80 .join("\n");
81 if !text.is_empty() {
82 messages.push(ct::BetaMessageParam {
83 content: ct::BetaMessageContent::Text(text),
84 role: ct::BetaMessageRole::Assistant,
85 });
86 }
87 }
88 ot::ResponseInputItem::FunctionToolCall(tool_call) => {
89 let tool_use_id = tool_use_ids.tool_use_id(tool_call.call_id);
90 push_block_message(
91 &mut messages,
92 ct::BetaMessageRole::Assistant,
93 ct::BetaContentBlockParam::ToolUse(ct::BetaToolUseBlockParam {
94 id: tool_use_id,
95 input: parse_tool_use_input(tool_call.arguments),
96 name: tool_call.name,
97 type_: ct::BetaToolUseBlockType::ToolUse,
98 cache_control: None,
99 caller: None,
100 }),
101 );
102 }
103 ot::ResponseInputItem::CustomToolCall(tool_call) => {
104 let tool_use_id = tool_use_ids.tool_use_id(tool_call.call_id);
105 push_block_message(
106 &mut messages,
107 ct::BetaMessageRole::Assistant,
108 ct::BetaContentBlockParam::ToolUse(ct::BetaToolUseBlockParam {
109 id: tool_use_id,
110 input: parse_tool_use_input(tool_call.input),
111 name: tool_call.name,
112 type_: ct::BetaToolUseBlockType::ToolUse,
113 cache_control: None,
114 caller: None,
115 }),
116 );
117 }
118 ot::ResponseInputItem::FunctionCallOutput(tool_result) => {
119 let tool_use_id = tool_use_ids.tool_use_id(tool_result.call_id);
120 let content = match tool_result.output {
121 ot::ResponseFunctionCallOutputContent::Text(text) => (!text.is_empty())
122 .then_some(ct::BetaToolResultBlockParamContent::Text(text)),
123 ot::ResponseFunctionCallOutputContent::Content(parts) => {
124 response_input_contents_to_tool_result_content(parts)
125 }
126 };
127 push_block_message(
128 &mut messages,
129 ct::BetaMessageRole::User,
130 ct::BetaContentBlockParam::ToolResult(ct::BetaToolResultBlockParam {
131 tool_use_id,
132 type_: ct::BetaToolResultBlockType::ToolResult,
133 cache_control: None,
134 content,
135 is_error: None,
136 }),
137 );
138 }
139 ot::ResponseInputItem::CustomToolCallOutput(tool_result) => {
140 let tool_use_id = tool_use_ids.tool_use_id(tool_result.call_id);
141 let content = match tool_result.output {
142 ot::ResponseCustomToolCallOutputContent::Text(text) => (!text.is_empty())
143 .then_some(ct::BetaToolResultBlockParamContent::Text(text)),
144 ot::ResponseCustomToolCallOutputContent::Content(parts) => {
145 response_input_contents_to_tool_result_content(parts)
146 }
147 };
148 push_block_message(
149 &mut messages,
150 ct::BetaMessageRole::User,
151 ct::BetaContentBlockParam::ToolResult(ct::BetaToolResultBlockParam {
152 tool_use_id,
153 type_: ct::BetaToolResultBlockType::ToolResult,
154 cache_control: None,
155 content,
156 is_error: None,
157 }),
158 );
159 }
160 ot::ResponseInputItem::McpCall(call) => {
161 let tool_use_id = call.id.clone();
162 push_block_message(
163 &mut messages,
164 ct::BetaMessageRole::Assistant,
165 ct::BetaContentBlockParam::McpToolUse(ct::BetaMcpToolUseBlockParam {
166 id: tool_use_id.clone(),
167 input: parse_tool_use_input(call.arguments),
168 name: call.name,
169 server_name: call.server_label,
170 type_: ct::BetaMcpToolUseBlockType::McpToolUse,
171 cache_control: None,
172 }),
173 );
174 if call.output.is_some() || call.error.is_some() {
175 let text = call.output.or(call.error).unwrap_or_default();
176 push_block_message(
177 &mut messages,
178 ct::BetaMessageRole::User,
179 ct::BetaContentBlockParam::McpToolResult(
180 ct::BetaRequestMcpToolResultBlockParam {
181 tool_use_id,
182 type_: ct::BetaRequestMcpToolResultBlockType::McpToolResult,
183 cache_control: None,
184 content: (!text.is_empty()).then_some(
185 ct::BetaMcpToolResultBlockParamContent::Text(text),
186 ),
187 is_error: None,
188 },
189 ),
190 );
191 }
192 }
193 ot::ResponseInputItem::CodeInterpreterToolCall(call) => {
194 let mut input = ct::JsonObject::new();
195 input.insert("code".to_string(), serde_json::Value::String(call.code));
196 if !call.container_id.is_empty() {
197 input.insert(
198 "container_id".to_string(),
199 serde_json::Value::String(call.container_id),
200 );
201 }
202 let tool_use_id = tool_use_ids.server_tool_use_id(call.id);
203 push_block_message(
204 &mut messages,
205 ct::BetaMessageRole::Assistant,
206 ct::BetaContentBlockParam::ServerToolUse(ct::BetaServerToolUseBlockParam {
207 id: tool_use_id.clone(),
208 input,
209 name: ct::BetaServerToolUseName::CodeExecution,
210 type_: ct::BetaServerToolUseBlockType::ServerToolUse,
211 cache_control: None,
212 caller: None,
213 }),
214 );
215 if let Some(outputs) = call.outputs {
216 let mut stdout = Vec::new();
217 for output in outputs {
218 match output {
219 ot::ResponseCodeInterpreterOutputItem::Logs { logs } => {
220 if !logs.is_empty() {
221 stdout.push(logs);
222 }
223 }
224 ot::ResponseCodeInterpreterOutputItem::Image { url } => {
225 if !url.is_empty() {
226 stdout.push(url);
227 }
228 }
229 }
230 }
231 push_block_message(
232 &mut messages,
233 ct::BetaMessageRole::User,
234 ct::BetaContentBlockParam::CodeExecutionToolResult(
235 ct::BetaCodeExecutionToolResultBlockParam {
236 content: ct::BetaCodeExecutionToolResultBlockParamContent::Result(
237 ct::BetaCodeExecutionResultBlockParam {
238 content: Vec::new(),
239 return_code: 0,
240 stderr: String::new(),
241 stdout: stdout.join("\n"),
242 type_: ct::BetaCodeExecutionResultBlockType::CodeExecutionResult,
243 },
244 ),
245 tool_use_id,
246 type_: ct::BetaCodeExecutionToolResultBlockType::CodeExecutionToolResult,
247 cache_control: None,
248 },
249 ),
250 );
251 }
252 }
253 ot::ResponseInputItem::FunctionWebSearch(call) => {
254 let raw_tool_use_id =
255 web_search_tool_use_id(call.id.clone(), &call.action, messages.len());
256 match call.action {
257 ot::ResponseFunctionWebSearchAction::Search {
258 query,
259 queries,
260 sources,
261 } => {
262 let tool_use_id = tool_use_ids.server_tool_use_id(raw_tool_use_id);
263 let mut input = ct::JsonObject::new();
264 if let Some(query) = query.clone() {
265 input.insert("query".to_string(), serde_json::Value::String(query));
266 }
267 if let Some(queries) = queries.clone() {
268 input.insert(
269 "queries".to_string(),
270 serde_json::Value::Array(
271 queries
272 .into_iter()
273 .map(serde_json::Value::String)
274 .collect(),
275 ),
276 );
277 }
278 push_block_message(
279 &mut messages,
280 ct::BetaMessageRole::Assistant,
281 ct::BetaContentBlockParam::ServerToolUse(
282 ct::BetaServerToolUseBlockParam {
283 id: tool_use_id.clone(),
284 input,
285 name: ct::BetaServerToolUseName::WebSearch,
286 type_: ct::BetaServerToolUseBlockType::ServerToolUse,
287 cache_control: None,
288 caller: None,
289 },
290 ),
291 );
292 if let Some(sources) = sources {
293 let text = sources
294 .into_iter()
295 .map(|source| source.url)
296 .filter(|url| !url.is_empty())
297 .collect::<Vec<_>>()
298 .join("\n");
299 push_block_message(
300 &mut messages,
301 ct::BetaMessageRole::User,
302 ct::BetaContentBlockParam::ToolResult(
303 ct::BetaToolResultBlockParam {
304 tool_use_id,
305 type_: ct::BetaToolResultBlockType::ToolResult,
306 cache_control: None,
307 content: (!text.is_empty()).then_some(
308 ct::BetaToolResultBlockParamContent::Text(text),
309 ),
310 is_error: None,
311 },
312 ),
313 );
314 }
315 }
316 ot::ResponseFunctionWebSearchAction::OpenPage { url } => {
317 let tool_use_id = tool_use_ids.server_tool_use_id(raw_tool_use_id);
318 let mut input = ct::JsonObject::new();
319 if let Some(url) = url.clone() {
320 input.insert("url".to_string(), serde_json::Value::String(url));
321 }
322 push_block_message(
323 &mut messages,
324 ct::BetaMessageRole::Assistant,
325 ct::BetaContentBlockParam::ServerToolUse(
326 ct::BetaServerToolUseBlockParam {
327 id: tool_use_id.clone(),
328 input,
329 name: ct::BetaServerToolUseName::WebFetch,
330 type_: ct::BetaServerToolUseBlockType::ServerToolUse,
331 cache_control: None,
332 caller: None,
333 },
334 ),
335 );
336 }
337 ot::ResponseFunctionWebSearchAction::FindInPage { pattern, url } => {
338 let tool_use_id = tool_use_ids.tool_use_id(raw_tool_use_id);
339 let mut input = ct::JsonObject::new();
340 input.insert("pattern".to_string(), serde_json::Value::String(pattern));
341 input.insert("url".to_string(), serde_json::Value::String(url));
342 push_block_message(
343 &mut messages,
344 ct::BetaMessageRole::Assistant,
345 ct::BetaContentBlockParam::ToolUse(ct::BetaToolUseBlockParam {
346 id: tool_use_id,
347 input,
348 name: "web_fetch".to_string(),
349 type_: ct::BetaToolUseBlockType::ToolUse,
350 cache_control: None,
351 caller: None,
352 }),
353 );
354 }
355 }
356 }
357 ot::ResponseInputItem::ShellCall(call) => {
358 let mut input = ct::JsonObject::new();
359 input.insert(
360 "commands".to_string(),
361 serde_json::Value::Array(
362 call.action
363 .commands
364 .into_iter()
365 .map(serde_json::Value::String)
366 .collect(),
367 ),
368 );
369 if let Some(timeout_ms) = call.action.timeout_ms {
370 input.insert(
371 "timeout_ms".to_string(),
372 serde_json::Value::Number(timeout_ms.into()),
373 );
374 }
375 let tool_use_id = tool_use_ids.server_tool_use_id(call.call_id);
376 push_block_message(
377 &mut messages,
378 ct::BetaMessageRole::Assistant,
379 ct::BetaContentBlockParam::ServerToolUse(ct::BetaServerToolUseBlockParam {
380 id: tool_use_id,
381 input,
382 name: ct::BetaServerToolUseName::BashCodeExecution,
383 type_: ct::BetaServerToolUseBlockType::ServerToolUse,
384 cache_control: None,
385 caller: None,
386 }),
387 );
388 }
389 ot::ResponseInputItem::ShellCallOutput(call) => {
390 if let Some(first) = call.output.into_iter().next() {
391 let tool_use_id = tool_use_ids.server_tool_use_id(call.call_id);
392 let content = match first.outcome {
393 ot::ResponseShellCallOutcome::Timeout => {
394 ct::BetaBashCodeExecutionToolResultBlockParamContent::Error(
395 ct::BetaBashCodeExecutionToolResultErrorParam {
396 error_code: ct::BetaBashCodeExecutionToolResultErrorCode::ExecutionTimeExceeded,
397 type_: ct::BetaBashCodeExecutionToolResultErrorType::BashCodeExecutionToolResultError,
398 },
399 )
400 }
401 ot::ResponseShellCallOutcome::Exit { exit_code } => {
402 ct::BetaBashCodeExecutionToolResultBlockParamContent::Result(
403 ct::BetaBashCodeExecutionResultBlockParam {
404 content: Vec::new(),
405 return_code: i64::from(exit_code),
406 stderr: first.stderr,
407 stdout: first.stdout,
408 type_: ct::BetaBashCodeExecutionResultBlockType::BashCodeExecutionResult,
409 },
410 )
411 }
412 };
413 push_block_message(
414 &mut messages,
415 ct::BetaMessageRole::User,
416 ct::BetaContentBlockParam::BashCodeExecutionToolResult(
417 ct::BetaBashCodeExecutionToolResultBlockParam {
418 content,
419 tool_use_id,
420 type_: ct::BetaBashCodeExecutionToolResultBlockType::BashCodeExecutionToolResult,
421 cache_control: None,
422 },
423 ),
424 );
425 }
426 }
427 ot::ResponseInputItem::LocalShellCall(call) => {
428 let input = serde_json::to_value(call.action)
429 .ok()
430 .and_then(|value| serde_json::from_value::<ct::JsonObject>(value).ok())
431 .unwrap_or_default();
432 let tool_use_id = tool_use_ids.tool_use_id(call.call_id);
433 push_block_message(
434 &mut messages,
435 ct::BetaMessageRole::Assistant,
436 ct::BetaContentBlockParam::ToolUse(ct::BetaToolUseBlockParam {
437 id: tool_use_id,
438 input,
439 name: "bash".to_string(),
440 type_: ct::BetaToolUseBlockType::ToolUse,
441 cache_control: None,
442 caller: None,
443 }),
444 );
445 }
446 ot::ResponseInputItem::LocalShellCallOutput(call) => {
447 let tool_use_id = tool_use_ids.tool_use_id(call.id);
448 push_block_message(
449 &mut messages,
450 ct::BetaMessageRole::User,
451 ct::BetaContentBlockParam::ToolResult(ct::BetaToolResultBlockParam {
452 tool_use_id,
453 type_: ct::BetaToolResultBlockType::ToolResult,
454 cache_control: None,
455 content: (!call.output.is_empty())
456 .then_some(ct::BetaToolResultBlockParamContent::Text(call.output)),
457 is_error: None,
458 }),
459 );
460 }
461 ot::ResponseInputItem::ApplyPatchCall(call) => {
462 let input = serde_json::to_value(call.operation)
463 .ok()
464 .and_then(|value| serde_json::from_value::<ct::JsonObject>(value).ok())
465 .unwrap_or_default();
466 let tool_use_id = tool_use_ids.tool_use_id(call.call_id);
467 push_block_message(
468 &mut messages,
469 ct::BetaMessageRole::Assistant,
470 ct::BetaContentBlockParam::ToolUse(ct::BetaToolUseBlockParam {
471 id: tool_use_id,
472 input,
473 name: "str_replace_based_edit_tool".to_string(),
474 type_: ct::BetaToolUseBlockType::ToolUse,
475 cache_control: None,
476 caller: None,
477 }),
478 );
479 }
480 ot::ResponseInputItem::ApplyPatchCallOutput(call) => {
481 let text = call
482 .output
483 .unwrap_or_else(|| format!("status:{:?}", call.status));
484 let tool_use_id = tool_use_ids.tool_use_id(call.call_id);
485 push_block_message(
486 &mut messages,
487 ct::BetaMessageRole::User,
488 ct::BetaContentBlockParam::ToolResult(ct::BetaToolResultBlockParam {
489 tool_use_id,
490 type_: ct::BetaToolResultBlockType::ToolResult,
491 cache_control: None,
492 content: (!text.is_empty())
493 .then_some(ct::BetaToolResultBlockParamContent::Text(text)),
494 is_error: None,
495 }),
496 );
497 }
498 ot::ResponseInputItem::ComputerToolCall(call) => {
499 let input = serde_json::to_value(call.action)
500 .ok()
501 .and_then(|value| serde_json::from_value::<ct::JsonObject>(value).ok())
502 .unwrap_or_default();
503 let tool_use_id = tool_use_ids.tool_use_id(call.call_id);
504 push_block_message(
505 &mut messages,
506 ct::BetaMessageRole::Assistant,
507 ct::BetaContentBlockParam::ToolUse(ct::BetaToolUseBlockParam {
508 id: tool_use_id,
509 input,
510 name: "computer".to_string(),
511 type_: ct::BetaToolUseBlockType::ToolUse,
512 cache_control: None,
513 caller: None,
514 }),
515 );
516 }
517 ot::ResponseInputItem::ComputerCallOutput(call) => {
518 let mut parts = Vec::new();
519 if let Some(file_id) = call.output.file_id {
520 parts.push(ot::ResponseInputContent::Image(ot::ResponseInputImage {
521 detail: None,
522 type_: ot::ResponseInputImageType::InputImage,
523 file_id: Some(file_id),
524 image_url: None,
525 }));
526 } else if let Some(image_url) = call.output.image_url {
527 parts.push(ot::ResponseInputContent::Image(ot::ResponseInputImage {
528 detail: None,
529 type_: ot::ResponseInputImageType::InputImage,
530 file_id: None,
531 image_url: Some(image_url),
532 }));
533 }
534 let tool_use_id = tool_use_ids.tool_use_id(call.call_id);
535 push_block_message(
536 &mut messages,
537 ct::BetaMessageRole::User,
538 ct::BetaContentBlockParam::ToolResult(ct::BetaToolResultBlockParam {
539 tool_use_id,
540 type_: ct::BetaToolResultBlockType::ToolResult,
541 cache_control: None,
542 content: response_input_contents_to_tool_result_content(parts),
543 is_error: None,
544 }),
545 );
546 }
547 ot::ResponseInputItem::FileSearchToolCall(call) => {
548 let mut input = ct::JsonObject::new();
549 if let Some(query) = call.queries.first().cloned() {
550 input.insert("query".to_string(), serde_json::Value::String(query));
551 }
552 if call.queries.len() > 1 {
553 input.insert(
554 "queries".to_string(),
555 serde_json::Value::Array(
556 call.queries
557 .iter()
558 .cloned()
559 .map(serde_json::Value::String)
560 .collect(),
561 ),
562 );
563 }
564 let tool_use_id = tool_use_ids.server_tool_use_id(call.id);
565 push_block_message(
566 &mut messages,
567 ct::BetaMessageRole::Assistant,
568 ct::BetaContentBlockParam::ServerToolUse(ct::BetaServerToolUseBlockParam {
569 id: tool_use_id.clone(),
570 input,
571 name: ct::BetaServerToolUseName::ToolSearchToolBm25,
572 type_: ct::BetaServerToolUseBlockType::ServerToolUse,
573 cache_control: None,
574 caller: None,
575 }),
576 );
577 if let Some(results) = call.results {
578 let tool_references = results
579 .into_iter()
580 .filter_map(|result| result.filename.or(result.text))
581 .filter(|name| !name.is_empty())
582 .map(|tool_name| ct::BetaToolReferenceBlockParam {
583 tool_name,
584 type_: ct::BetaToolReferenceBlockType::ToolReference,
585 cache_control: None,
586 })
587 .collect::<Vec<_>>();
588 push_block_message(
589 &mut messages,
590 ct::BetaMessageRole::User,
591 ct::BetaContentBlockParam::ToolSearchToolResult(
592 ct::BetaToolSearchToolResultBlockParam {
593 content: ct::BetaToolSearchToolResultBlockParamContent::Result(
594 ct::BetaToolSearchToolSearchResultBlockParam {
595 tool_references,
596 type_: ct::BetaToolSearchToolSearchResultBlockType::ToolSearchToolSearchResult,
597 },
598 ),
599 tool_use_id,
600 type_: ct::BetaToolSearchToolResultBlockType::ToolSearchToolResult,
601 cache_control: None,
602 },
603 ),
604 );
605 }
606 }
607 ot::ResponseInputItem::ReasoningItem(reasoning) => {
608 let mut thinking = openai_reasoning_summary_to_text(&reasoning.summary);
609 if thinking.is_empty()
610 && let Some(encrypted) = reasoning.encrypted_content
611 {
612 thinking = encrypted;
613 }
614
615 if !thinking.is_empty()
616 && let Some(signature) = reasoning.id.filter(|id| !id.is_empty())
617 {
618 push_block_message(
619 &mut messages,
620 ct::BetaMessageRole::Assistant,
621 ct::BetaContentBlockParam::Thinking(ct::BetaThinkingBlockParam {
622 signature,
623 thinking,
624 type_: ct::BetaThinkingBlockType::Thinking,
625 }),
626 );
627 }
628 }
629 other => {
630 let text = format!("{other:?}");
631 if !text.is_empty() {
632 messages.push(ct::BetaMessageParam {
633 content: ct::BetaMessageContent::Text(text),
634 role: ct::BetaMessageRole::User,
635 });
636 }
637 }
638 }
639 }
640
641 let claude_model = Model::Custom(body.model.clone().unwrap_or_default());
642 let disable_parallel_tool_use = parallel_disable(body.parallel_tool_calls);
643 let tool_choice = openai_tool_choice_to_claude(body.tool_choice, disable_parallel_tool_use);
644 let thinking = openai_reasoning_to_claude(
645 body.reasoning.clone(),
646 body.max_output_tokens,
647 Some(&claude_model),
648 );
649 let claude_max_tokens = body.max_output_tokens.unwrap_or(8_192);
650
651 let output_effort = body
652 .text
653 .as_ref()
654 .and_then(|text| text.verbosity.as_ref())
655 .map(|verbosity| match verbosity {
656 ot::ResponseTextVerbosity::Low => ct::BetaOutputEffort::Low,
657 ot::ResponseTextVerbosity::Medium => ct::BetaOutputEffort::Medium,
658 ot::ResponseTextVerbosity::High => ct::BetaOutputEffort::High,
659 });
660
661 let output_format = body
662 .text
663 .as_ref()
664 .and_then(|text| text.format.as_ref())
665 .and_then(|format| match format {
666 ot::ResponseTextFormatConfig::JsonSchema(schema) => {
667 let mut patched = schema.schema.clone();
668 enforce_anthropic_strict_schema(&mut patched);
669 Some(ct::BetaJsonOutputFormat {
670 schema: patched,
671 type_: ct::BetaJsonOutputFormatType::JsonSchema,
672 })
673 }
674 _ => None,
675 });
676
677 let output_config = if output_effort.is_some() || output_format.is_some() {
678 Some(ct::BetaOutputConfig {
679 effort: output_effort,
680 format: output_format.clone(),
681 task_budget: None,
682 })
683 } else {
684 None
685 };
686
687 let context_management = {
688 let mut edits = Vec::new();
689
690 if let Some(entries) = body.context_management {
691 for entry in entries {
692 if entry.type_ == ResponseContextManagementType::Compaction {
693 edits.push(ct::BetaContextManagementEdit::Compact(
694 ct::BetaCompact20260112Edit {
695 type_: ct::BetaCompactType::Compact20260112,
696 instructions: None,
697 pause_after_compaction: None,
698 trigger: entry.compact_threshold.map(|value| {
699 ct::BetaInputTokensTrigger {
700 type_: ct::BetaInputTokensCounterType::InputTokens,
701 value,
702 }
703 }),
704 },
705 ));
706 }
707 }
708 }
709
710 if matches!(body.truncation, Some(ot::ResponseTruncation::Auto)) && edits.is_empty() {
711 edits.push(ct::BetaContextManagementEdit::Compact(
712 ct::BetaCompact20260112Edit {
713 type_: ct::BetaCompactType::Compact20260112,
714 instructions: None,
715 pause_after_compaction: None,
716 trigger: None,
717 },
718 ));
719 }
720
721 if edits.is_empty() {
722 None
723 } else {
724 Some(ct::BetaContextManagementConfig { edits: Some(edits) })
725 }
726 };
727
728 let mut converted_tools = Vec::new();
729 let mut mcp_servers = Vec::new();
730 if let Some(tools) = body.tools {
731 for tool in tools {
732 match tool {
733 ot::ResponseTool::Function(tool) => {
734 converted_tools.push(tool_from_function(tool))
735 }
736 ot::ResponseTool::Custom(tool) => {
737 converted_tools.push(ct::BetaToolUnion::Custom(ct::BetaTool {
738 input_schema: ct::BetaToolInputSchema {
739 type_: ct::BetaToolInputSchemaType::Object,
740 properties: None,
741 required: None,
742 extra_fields: Default::default(),
743 },
744 name: tool.name,
745 common: ct::BetaToolCommonFields::default(),
746 description: tool.description,
747 eager_input_streaming: None,
748 type_: Some(ct::BetaCustomToolType::Custom),
749 }));
750 }
751 ot::ResponseTool::CodeInterpreter(_) => {
752 converted_tools.push(ct::BetaToolUnion::CodeExecution20250825(
753 ct::BetaCodeExecutionTool20250825 {
754 name: ct::BetaCodeExecutionToolName::CodeExecution,
755 type_: ct::BetaCodeExecutionTool20250825Type::CodeExecution20250825,
756 common: ct::BetaToolCommonFields::default(),
757 },
758 ));
759 }
760 ot::ResponseTool::LocalShell(_) | ot::ResponseTool::Shell(_) => {
761 converted_tools.push(ct::BetaToolUnion::Bash20250124(
762 ct::BetaToolBash20250124 {
763 name: ct::BetaBashToolName::Bash,
764 type_: ct::BetaToolBash20250124Type::Bash20250124,
765 common: ct::BetaToolCommonFields::default(),
766 },
767 ));
768 }
769 ot::ResponseTool::ApplyPatch(_) => {
770 converted_tools.push(ct::BetaToolUnion::TextEditor20250728(
771 ct::BetaToolTextEditor20250728 {
772 name: ct::BetaTextEditorToolNameV2::StrReplaceBasedEditTool,
773 type_: ct::BetaToolTextEditor20250728Type::TextEditor20250728,
774 common: ct::BetaToolCommonFields::default(),
775 max_characters: None,
776 },
777 ));
778 }
779 ot::ResponseTool::Computer(tool) => {
780 converted_tools.push(ct::BetaToolUnion::ComputerUse20251124(
781 ct::BetaToolComputerUse20251124 {
782 display_height_px: tool.display_height_or_default(),
783 display_width_px: tool.display_width_or_default(),
784 name: ct::BetaComputerToolName::Computer,
785 type_: ct::BetaToolComputerUse20251124Type::Computer20251124,
786 common: ct::BetaToolCommonFields::default(),
787 display_number: None,
788 enable_zoom: None,
789 },
790 ));
791 }
792 ot::ResponseTool::WebSearch(tool) => {
793 converted_tools.push(ct::BetaToolUnion::WebSearch20250305(
794 ct::BetaWebSearchTool20250305 {
795 name: ct::BetaWebSearchToolName::WebSearch,
796 type_: ct::BetaWebSearchTool20250305Type::WebSearch20250305,
797 common: ct::BetaToolCommonFields::default(),
798 allowed_domains: tool.filters.and_then(|f| f.allowed_domains),
799 blocked_domains: None,
800 max_uses: None,
801 user_location: tool.user_location.map(|location| {
802 ct::BetaWebSearchUserLocation {
803 type_: ct::BetaWebSearchUserLocationType::Approximate,
804 city: location.city,
805 country: location.country,
806 region: location.region,
807 timezone: location.timezone,
808 }
809 }),
810 },
811 ));
812 }
813 ot::ResponseTool::WebSearchPreview(tool) => {
814 converted_tools.push(ct::BetaToolUnion::WebSearch20250305(
815 ct::BetaWebSearchTool20250305 {
816 name: ct::BetaWebSearchToolName::WebSearch,
817 type_: ct::BetaWebSearchTool20250305Type::WebSearch20250305,
818 common: ct::BetaToolCommonFields::default(),
819 allowed_domains: None,
820 blocked_domains: None,
821 max_uses: None,
822 user_location: tool.user_location.map(|location| {
823 ct::BetaWebSearchUserLocation {
824 type_: ct::BetaWebSearchUserLocationType::Approximate,
825 city: location.city,
826 country: location.country,
827 region: location.region,
828 timezone: location.timezone,
829 }
830 }),
831 },
832 ));
833 }
834 ot::ResponseTool::FileSearch(_) => {
835 converted_tools.push(ct::BetaToolUnion::ToolSearchBm25_20251119(
836 ct::BetaToolSearchToolBm25_20251119 {
837 name: ct::BetaToolSearchToolBm25Name::ToolSearchToolBm25,
838 type_: ct::BetaToolSearchToolBm25Type::ToolSearchToolBm2520251119,
839 common: ct::BetaToolCommonFields::default(),
840 },
841 ));
842 }
843 ot::ResponseTool::Mcp(tool) => {
844 if let Some(server) = openai_mcp_tool_to_server(&tool) {
845 mcp_servers.push(server);
846 }
847 converted_tools.push(ct::BetaToolUnion::McpToolset(ct::BetaMcpToolset {
848 mcp_server_name: tool.server_label,
849 type_: ct::BetaMcpToolsetType::McpToolset,
850 cache_control: None,
851 configs: mcp_allowed_tools_to_configs(tool.allowed_tools.as_ref()),
852 default_config: None,
853 }));
854 }
855 ot::ResponseTool::Namespace(_) | ot::ResponseTool::ToolSearch(_) => {}
856 ot::ResponseTool::ImageGeneration(_) => {}
857 }
858 }
859 }
860
861 let service_tier = match body.service_tier.as_ref() {
862 Some(ResponseServiceTier::Auto) => Some(BetaServiceTierParam::Auto),
863 Some(
864 ResponseServiceTier::Default
865 | ResponseServiceTier::Flex
866 | ResponseServiceTier::Scale
867 | ResponseServiceTier::Priority,
868 ) => Some(BetaServiceTierParam::StandardOnly),
869 None => None,
870 };
871 let speed = match body.service_tier.as_ref() {
872 Some(ResponseServiceTier::Priority) => Some(BetaSpeed::Fast),
873 _ => None,
874 };
875
876 let metadata_user_id = body.user.or_else(|| {
877 body.metadata
878 .as_ref()
879 .and_then(|map| map.get("user_id").cloned())
880 });
881 let metadata = metadata_user_id.map(|user_id| BetaMetadata {
882 user_id: Some(user_id),
883 });
884
885 let system = body.instructions.and_then(|text| {
886 if text.is_empty() {
887 None
888 } else {
889 Some(ct::BetaSystemPrompt::Text(text))
890 }
891 });
892
893 Ok(ClaudeCreateMessageRequest {
894 method: ClaudeHttpMethod::Post,
895 path: PathParameters::default(),
896 query: QueryParameters::default(),
897 headers: RequestHeaders::default(),
898 body: RequestBody {
899 max_tokens: claude_max_tokens,
900 messages,
901 model: claude_model,
902 container: None,
903 context_management,
904 inference_geo: None,
905 mcp_servers: if mcp_servers.is_empty() {
906 None
907 } else {
908 Some(mcp_servers)
909 },
910 metadata,
911 cache_control: None,
912 output_config,
913 service_tier,
914 speed,
915 stop_sequences: None,
916 stream: body.stream,
917 system,
918 temperature: body.temperature,
919 thinking,
920 tool_choice,
921 tools: if converted_tools.is_empty() {
922 None
923 } else {
924 Some(converted_tools)
925 },
926 top_k: None,
927 top_p: body.top_p,
928 },
929 })
930 }
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936
937 #[test]
938 fn converts_assistant_output_text_item_without_message_type() {
939 let body = serde_json::from_value(serde_json::json!({
940 "model": "claude-jupiter-v1-p",
941 "input": [
942 {
943 "role": "user",
944 "content": [{ "type": "input_text", "text": "hello" }]
945 },
946 {
947 "role": "assistant",
948 "content": [{ "type": "output_text", "text": "hello there" }]
949 },
950 {
951 "role": "user",
952 "content": [{ "type": "input_text", "text": "who are you" }]
953 }
954 ],
955 "store": false,
956 "stream": true,
957 "temperature": 1.0
958 }))
959 .expect("Cherry Studio Responses history should deserialize");
960 let request = OpenAiCreateResponseRequest {
961 body,
962 ..Default::default()
963 };
964
965 let converted = ClaudeCreateMessageRequest::try_from(request).expect("request converts");
966
967 assert_eq!(converted.body.messages.len(), 3);
968 assert_eq!(
969 converted.body.messages[1].role,
970 ct::BetaMessageRole::Assistant
971 );
972 assert_eq!(
973 converted.body.messages[1].content,
974 ct::BetaMessageContent::Text("hello there".to_string())
975 );
976 }
977}