Skip to main content

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

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}