1use crate::claude::count_tokens::types::{
2 BetaContentBlockParam, BetaMcpToolResultBlockParamContent, BetaMessageContent, BetaMessageRole,
3 BetaOutputEffort, BetaThinkingConfigParam, BetaToolChoice, BetaToolInputSchema,
4 BetaToolInputSchemaType, BetaToolResultBlockParamContent, BetaToolResultContentBlockParam,
5 BetaToolUnion,
6};
7use crate::claude::create_message::request::ClaudeCreateMessageRequest;
8use crate::claude::create_message::types::{BetaServiceTierParam, BetaSpeed};
9use crate::openai::create_chat_completions::request::{
10 OpenAiChatCompletionsRequest, PathParameters, QueryParameters, RequestBody, RequestHeaders,
11};
12use crate::openai::create_chat_completions::types::{
13 ChatCompletionAssistantContent, ChatCompletionAssistantMessageParam,
14 ChatCompletionAssistantRole, ChatCompletionContentPart, ChatCompletionContentPartFile,
15 ChatCompletionContentPartFileType, ChatCompletionContentPartImage,
16 ChatCompletionContentPartImageType, ChatCompletionContentPartText,
17 ChatCompletionContentPartTextType, ChatCompletionFileInput, ChatCompletionFunctionCall,
18 ChatCompletionFunctionDefinition, ChatCompletionFunctionTool, ChatCompletionFunctionToolType,
19 ChatCompletionImageUrl, ChatCompletionMessageCustomToolCall,
20 ChatCompletionMessageCustomToolCallPayload, ChatCompletionMessageCustomToolCallType,
21 ChatCompletionMessageFunctionToolCall, ChatCompletionMessageFunctionToolCallType,
22 ChatCompletionMessageParam, ChatCompletionMessageToolCall, ChatCompletionNamedFunction,
23 ChatCompletionNamedToolChoice, ChatCompletionNamedToolChoiceType,
24 ChatCompletionReasoningEffort, ChatCompletionResponseFormat,
25 ChatCompletionResponseFormatJsonSchema, ChatCompletionResponseFormatJsonSchemaConfig,
26 ChatCompletionResponseFormatJsonSchemaType, ChatCompletionServiceTier, ChatCompletionStop,
27 ChatCompletionSystemMessageParam, ChatCompletionSystemRole, ChatCompletionTextContent,
28 ChatCompletionTool, ChatCompletionToolChoiceMode, ChatCompletionToolChoiceOption,
29 ChatCompletionToolMessageParam, ChatCompletionToolRole, ChatCompletionUserContent,
30 ChatCompletionUserMessageParam, ChatCompletionUserRole, ChatCompletionVerbosity, HttpMethod,
31 Metadata,
32};
33use crate::transform::claude::generate_content::utils::{
34 beta_message_content_to_text, beta_system_prompt_to_text, claude_model_to_string,
35};
36use crate::transform::utils::TransformError;
37use serde_json::{Map, Value};
38
39fn tool_input_schema_to_function_parameters(
40 input_schema: BetaToolInputSchema,
41) -> std::collections::BTreeMap<String, Value> {
42 let mut parameters = std::collections::BTreeMap::new();
43 let schema_type = match input_schema.type_ {
44 BetaToolInputSchemaType::Object => "object",
45 };
46 parameters.insert("type".to_string(), Value::String(schema_type.to_string()));
47 if let Some(properties) = input_schema.properties {
48 let properties_object = properties.into_iter().collect::<Map<String, Value>>();
49 parameters.insert("properties".to_string(), Value::Object(properties_object));
50 }
51 if let Some(required) = input_schema.required {
52 parameters.insert(
53 "required".to_string(),
54 Value::Array(required.into_iter().map(Value::String).collect()),
55 );
56 }
57 parameters
58}
59
60impl TryFrom<ClaudeCreateMessageRequest> for OpenAiChatCompletionsRequest {
61 type Error = TransformError;
62
63 fn try_from(value: ClaudeCreateMessageRequest) -> Result<Self, TransformError> {
64 let body = value.body;
65 let model = claude_model_to_string(&body.model);
66
67 let mut messages = Vec::new();
68 if let Some(system) = beta_system_prompt_to_text(body.system) {
69 messages.push(ChatCompletionMessageParam::System(
70 ChatCompletionSystemMessageParam {
71 content: ChatCompletionTextContent::Text(system),
72 role: ChatCompletionSystemRole::System,
73 name: None,
74 },
75 ));
76 }
77
78 for message in body.messages {
79 let fallback_text = beta_message_content_to_text(&message.content);
80 match (message.role, message.content) {
81 (BetaMessageRole::User, BetaMessageContent::Text(text)) => {
82 messages.push(ChatCompletionMessageParam::User(
83 ChatCompletionUserMessageParam {
84 content: ChatCompletionUserContent::Text(text),
85 role: ChatCompletionUserRole::User,
86 name: None,
87 },
88 ));
89 }
90 (BetaMessageRole::User, BetaMessageContent::Blocks(blocks)) => {
91 let mut user_parts = Vec::new();
92 let mut tool_messages = Vec::new();
93
94 for block in blocks {
95 match block {
96 BetaContentBlockParam::Text(block) => {
97 user_parts.push(ChatCompletionContentPart::Text(
98 ChatCompletionContentPartText {
99 text: block.text,
100 type_: ChatCompletionContentPartTextType::Text,
101 },
102 ));
103 }
104 BetaContentBlockParam::Image(block) => match block.source {
105 crate::claude::count_tokens::types::BetaImageSource::Base64(
106 source,
107 ) => {
108 let mime_type = match source.media_type {
109 crate::claude::count_tokens::types::BetaImageMediaType::ImageJpeg => "image/jpeg",
110 crate::claude::count_tokens::types::BetaImageMediaType::ImagePng => "image/png",
111 crate::claude::count_tokens::types::BetaImageMediaType::ImageGif => "image/gif",
112 crate::claude::count_tokens::types::BetaImageMediaType::ImageWebp => "image/webp",
113 };
114 user_parts.push(ChatCompletionContentPart::Image(
115 ChatCompletionContentPartImage {
116 image_url: ChatCompletionImageUrl {
117 url: format!(
118 "data:{mime_type};base64,{}",
119 source.data
120 ),
121 detail: None,
122 },
123 type_: ChatCompletionContentPartImageType::ImageUrl,
124 },
125 ));
126 }
127 crate::claude::count_tokens::types::BetaImageSource::Url(source) => {
128 user_parts.push(ChatCompletionContentPart::Image(
129 ChatCompletionContentPartImage {
130 image_url: ChatCompletionImageUrl {
131 url: source.url,
132 detail: None,
133 },
134 type_: ChatCompletionContentPartImageType::ImageUrl,
135 },
136 ));
137 }
138 crate::claude::count_tokens::types::BetaImageSource::File(source) => {
139 user_parts.push(ChatCompletionContentPart::File(
140 ChatCompletionContentPartFile {
141 file: ChatCompletionFileInput {
142 file_data: None,
143 file_id: Some(source.file_id),
144 file_url: None,
145 filename: None,
146 },
147 type_: ChatCompletionContentPartFileType::File,
148 },
149 ));
150 }
151 },
152 BetaContentBlockParam::RequestDocument(block) => match block.source {
153 crate::claude::count_tokens::types::BetaDocumentSource::Base64Pdf(
154 source,
155 ) => {
156 user_parts.push(ChatCompletionContentPart::File(
157 ChatCompletionContentPartFile {
158 file: ChatCompletionFileInput {
159 file_data: Some(source.data),
160 file_id: None,
161 file_url: None,
162 filename: block.title.clone(),
163 },
164 type_: ChatCompletionContentPartFileType::File,
165 },
166 ));
167 }
168 crate::claude::count_tokens::types::BetaDocumentSource::PlainText(
169 source,
170 ) => {
171 user_parts.push(ChatCompletionContentPart::File(
172 ChatCompletionContentPartFile {
173 file: ChatCompletionFileInput {
174 file_data: Some(source.data),
175 file_id: None,
176 file_url: None,
177 filename: block.title.clone(),
178 },
179 type_: ChatCompletionContentPartFileType::File,
180 },
181 ));
182 }
183 crate::claude::count_tokens::types::BetaDocumentSource::File(
184 source,
185 ) => {
186 user_parts.push(ChatCompletionContentPart::File(
187 ChatCompletionContentPartFile {
188 file: ChatCompletionFileInput {
189 file_data: None,
190 file_id: Some(source.file_id),
191 file_url: None,
192 filename: block.title.clone(),
193 },
194 type_: ChatCompletionContentPartFileType::File,
195 },
196 ));
197 }
198 _ => {}
199 },
200 BetaContentBlockParam::ToolResult(block) => {
201 let output = match block.content {
202 Some(BetaToolResultBlockParamContent::Text(text)) => text,
203 Some(BetaToolResultBlockParamContent::Blocks(parts)) => parts
204 .into_iter()
205 .filter_map(|part| match part {
206 BetaToolResultContentBlockParam::Text(part) => {
207 Some(part.text)
208 }
209 _ => None,
210 })
211 .collect::<Vec<_>>()
212 .join("\n"),
213 None => String::new(),
214 };
215 tool_messages.push(ChatCompletionMessageParam::Tool(
216 ChatCompletionToolMessageParam {
217 content: ChatCompletionTextContent::Text(output),
218 role: ChatCompletionToolRole::Tool,
219 tool_call_id: block.tool_use_id,
220 },
221 ));
222 }
223 BetaContentBlockParam::McpToolResult(block) => {
224 let output = match block.content {
225 Some(BetaMcpToolResultBlockParamContent::Text(text)) => text,
226 Some(BetaMcpToolResultBlockParamContent::Blocks(parts)) => parts
227 .into_iter()
228 .map(|part| part.text)
229 .collect::<Vec<_>>()
230 .join("\n"),
231 None => String::new(),
232 };
233 tool_messages.push(ChatCompletionMessageParam::Tool(
234 ChatCompletionToolMessageParam {
235 content: ChatCompletionTextContent::Text(output),
236 role: ChatCompletionToolRole::Tool,
237 tool_call_id: block.tool_use_id,
238 },
239 ));
240 }
241 _ => {}
242 }
243 }
244
245 if user_parts.is_empty() {
246 messages.push(ChatCompletionMessageParam::User(
247 ChatCompletionUserMessageParam {
248 content: ChatCompletionUserContent::Text(fallback_text),
249 role: ChatCompletionUserRole::User,
250 name: None,
251 },
252 ));
253 } else {
254 messages.push(ChatCompletionMessageParam::User(
255 ChatCompletionUserMessageParam {
256 content: ChatCompletionUserContent::Parts(user_parts),
257 role: ChatCompletionUserRole::User,
258 name: None,
259 },
260 ));
261 }
262
263 messages.extend(tool_messages);
264 }
265 (BetaMessageRole::Assistant, BetaMessageContent::Text(text)) => {
266 messages.push(ChatCompletionMessageParam::Assistant(
267 ChatCompletionAssistantMessageParam {
268 role: ChatCompletionAssistantRole::Assistant,
269 audio: None,
270 content: Some(ChatCompletionAssistantContent::Text(text)),
271 reasoning_content: None,
272 function_call: None,
273 name: None,
274 refusal: None,
275 tool_calls: None,
276 },
277 ));
278 }
279 (BetaMessageRole::Assistant, BetaMessageContent::Blocks(blocks)) => {
280 let mut assistant_text_parts = Vec::new();
281 let mut tool_calls = Vec::new();
282
283 for block in blocks {
284 match block {
285 BetaContentBlockParam::Text(block) => {
286 assistant_text_parts.push(block.text);
287 }
288 BetaContentBlockParam::ToolUse(block) => {
289 tool_calls.push(ChatCompletionMessageToolCall::Function(
290 ChatCompletionMessageFunctionToolCall {
291 id: block.id,
292 function: ChatCompletionFunctionCall {
293 arguments: serde_json::to_string(&block.input)
294 .unwrap_or_else(|_| "{}".to_string()),
295 name: block.name,
296 },
297 type_: ChatCompletionMessageFunctionToolCallType::Function,
298 },
299 ));
300 }
301 BetaContentBlockParam::ServerToolUse(block) => {
302 tool_calls.push(ChatCompletionMessageToolCall::Custom(
303 ChatCompletionMessageCustomToolCall {
304 id: block.id,
305 custom: ChatCompletionMessageCustomToolCallPayload {
306 input: serde_json::to_string(&block.input)
307 .unwrap_or_else(|_| "{}".to_string()),
308 name: match block.name {
309 crate::claude::count_tokens::types::BetaServerToolUseName::WebSearch => "web_search".to_string(),
310 crate::claude::count_tokens::types::BetaServerToolUseName::WebFetch => "web_fetch".to_string(),
311 crate::claude::count_tokens::types::BetaServerToolUseName::CodeExecution => "code_execution".to_string(),
312 crate::claude::count_tokens::types::BetaServerToolUseName::BashCodeExecution => "bash_code_execution".to_string(),
313 crate::claude::count_tokens::types::BetaServerToolUseName::TextEditorCodeExecution => "text_editor_code_execution".to_string(),
314 crate::claude::count_tokens::types::BetaServerToolUseName::ToolSearchToolRegex => "tool_search_tool_regex".to_string(),
315 crate::claude::count_tokens::types::BetaServerToolUseName::ToolSearchToolBm25 => "tool_search_tool_bm25".to_string(),
316 },
317 },
318 type_: ChatCompletionMessageCustomToolCallType::Custom,
319 },
320 ));
321 }
322 BetaContentBlockParam::McpToolUse(block) => {
323 tool_calls.push(ChatCompletionMessageToolCall::Custom(
324 ChatCompletionMessageCustomToolCall {
325 id: block.id,
326 custom: ChatCompletionMessageCustomToolCallPayload {
327 input: serde_json::to_string(&block.input)
328 .unwrap_or_else(|_| "{}".to_string()),
329 name: format!(
330 "mcp:{}:{}",
331 block.server_name, block.name
332 ),
333 },
334 type_: ChatCompletionMessageCustomToolCallType::Custom,
335 },
336 ));
337 }
338 _ => {}
339 }
340 }
341
342 let content_text = if assistant_text_parts.is_empty() {
343 if fallback_text.is_empty() {
344 None
345 } else {
346 Some(fallback_text)
347 }
348 } else {
349 Some(assistant_text_parts.join("\n"))
350 };
351
352 messages.push(ChatCompletionMessageParam::Assistant(
353 ChatCompletionAssistantMessageParam {
354 role: ChatCompletionAssistantRole::Assistant,
355 audio: None,
356 content: content_text.map(ChatCompletionAssistantContent::Text),
357 reasoning_content: None,
358 function_call: None,
359 name: None,
360 refusal: None,
361 tool_calls: if tool_calls.is_empty() {
362 None
363 } else {
364 Some(tool_calls)
365 },
366 },
367 ));
368 }
369 }
370 }
371
372 let parallel_tool_calls = match body.tool_choice.as_ref() {
373 Some(BetaToolChoice::Auto(choice)) => choice.disable_parallel_tool_use.map(|v| !v),
374 Some(BetaToolChoice::Any(choice)) => choice.disable_parallel_tool_use.map(|v| !v),
375 Some(BetaToolChoice::Tool(choice)) => choice.disable_parallel_tool_use.map(|v| !v),
376 Some(BetaToolChoice::None(_)) | None => None,
377 };
378 let tool_choice = match body.tool_choice {
379 Some(BetaToolChoice::Auto(_)) => Some(ChatCompletionToolChoiceOption::Mode(
380 ChatCompletionToolChoiceMode::Auto,
381 )),
382 Some(BetaToolChoice::Any(_)) => Some(ChatCompletionToolChoiceOption::Mode(
383 ChatCompletionToolChoiceMode::Required,
384 )),
385 Some(BetaToolChoice::Tool(choice)) => Some(
386 ChatCompletionToolChoiceOption::NamedFunction(ChatCompletionNamedToolChoice {
387 function: ChatCompletionNamedFunction { name: choice.name },
388 type_: ChatCompletionNamedToolChoiceType::Function,
389 }),
390 ),
391 Some(BetaToolChoice::None(_)) => Some(ChatCompletionToolChoiceOption::Mode(
392 ChatCompletionToolChoiceMode::None,
393 )),
394 None => None,
395 };
396 let reasoning_effort_from_thinking = match body.thinking {
397 Some(BetaThinkingConfigParam::Enabled(config)) => Some(if config.budget_tokens == 0 {
398 ChatCompletionReasoningEffort::None
399 } else if config.budget_tokens <= 4096 {
400 ChatCompletionReasoningEffort::Minimal
401 } else if config.budget_tokens <= 8192 {
402 ChatCompletionReasoningEffort::Low
403 } else if config.budget_tokens <= 16384 {
404 ChatCompletionReasoningEffort::Medium
405 } else if config.budget_tokens <= 32768 {
406 ChatCompletionReasoningEffort::High
407 } else {
408 ChatCompletionReasoningEffort::XHigh
409 }),
410 Some(BetaThinkingConfigParam::Disabled(_)) => Some(ChatCompletionReasoningEffort::None),
411 Some(BetaThinkingConfigParam::Adaptive(_)) => {
412 Some(ChatCompletionReasoningEffort::Medium)
413 }
414 None => None,
415 };
416 let reasoning_effort = reasoning_effort_from_thinking;
417 let verbosity = body
418 .output_config
419 .as_ref()
420 .and_then(|config| config.effort.as_ref())
421 .map(|effort| match effort {
422 BetaOutputEffort::Low => ChatCompletionVerbosity::Low,
423 BetaOutputEffort::Medium => ChatCompletionVerbosity::Medium,
424 BetaOutputEffort::High | BetaOutputEffort::XHigh | BetaOutputEffort::Max => {
425 ChatCompletionVerbosity::High
426 }
427 });
428
429 let response_format = body
430 .output_config
431 .as_ref()
432 .and_then(|config| config.format.as_ref())
433 .map(|schema| {
434 ChatCompletionResponseFormat::JsonSchema(ChatCompletionResponseFormatJsonSchema {
435 json_schema: ChatCompletionResponseFormatJsonSchemaConfig {
436 name: "output".to_string(),
437 description: None,
438 schema: Some(schema.schema.clone()),
439 strict: None,
440 },
441 type_: ChatCompletionResponseFormatJsonSchemaType::JsonSchema,
442 })
443 });
444
445 let tools = body.tools.map(|items| {
446 items
447 .into_iter()
448 .filter_map(|tool| match tool {
449 BetaToolUnion::Custom(tool) => {
450 Some(ChatCompletionTool::Function(ChatCompletionFunctionTool {
451 function: ChatCompletionFunctionDefinition {
452 name: tool.name,
453 description: tool.description,
454 parameters: Some(tool_input_schema_to_function_parameters(
455 tool.input_schema,
456 )),
457 strict: tool.common.strict,
458 },
459 type_: ChatCompletionFunctionToolType::Function,
460 }))
461 }
462 _ => None,
463 })
464 .collect::<Vec<_>>()
465 });
466
467 let stop = body
468 .stop_sequences
469 .and_then(|sequences| match sequences.len() {
470 0 => None,
471 1 => Some(ChatCompletionStop::Single(
472 sequences.into_iter().next().unwrap_or_default(),
473 )),
474 _ => Some(ChatCompletionStop::Multiple(sequences)),
475 });
476 let service_tier = match body.service_tier {
477 Some(BetaServiceTierParam::Auto) => Some(ChatCompletionServiceTier::Auto),
478 Some(BetaServiceTierParam::StandardOnly) => Some(ChatCompletionServiceTier::Default),
479 None => match body.speed {
480 Some(BetaSpeed::Fast) => Some(ChatCompletionServiceTier::Priority),
481 Some(BetaSpeed::Standard) | None => None,
482 },
483 };
484 let metadata = if let Some(user_id) = body
485 .metadata
486 .as_ref()
487 .and_then(|value| value.user_id.clone())
488 {
489 let mut map = Metadata::new();
490 map.insert("user_id".to_string(), user_id);
491 Some(map)
492 } else {
493 None
494 };
495
496 Ok(Self {
497 method: HttpMethod::Post,
498 path: PathParameters::default(),
499 query: QueryParameters::default(),
500 headers: RequestHeaders::default(),
501 body: RequestBody {
502 messages,
503 model,
504 max_completion_tokens: Some(body.max_tokens),
505 metadata,
506 parallel_tool_calls,
507 reasoning_effort,
508 response_format,
509 service_tier,
510 stop,
511 stream: body.stream,
512 temperature: body.temperature,
513 tool_choice,
514 tools,
515 top_p: body.top_p,
516 verbosity,
517 ..RequestBody::default()
518 },
519 })
520 }
521}