Skip to main content

gproxy_protocol/transform/claude/generate_content/openai_chat_completions/
request.rs

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}