Skip to main content

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

1use crate::gemini::count_tokens::types::GeminiContentRole;
2use crate::gemini::generate_content::request::GeminiGenerateContentRequest;
3use crate::gemini::generate_content::types::GeminiFunctionCallingMode;
4use crate::openai::create_chat_completions::request::{
5    OpenAiChatCompletionsRequest, PathParameters, QueryParameters, RequestBody, RequestHeaders,
6};
7use crate::openai::create_chat_completions::types::{
8    ChatCompletionAssistantContent, ChatCompletionAssistantMessageParam,
9    ChatCompletionAssistantRole, ChatCompletionContentPart, ChatCompletionContentPartFile,
10    ChatCompletionContentPartFileType, ChatCompletionContentPartImage,
11    ChatCompletionContentPartImageType, ChatCompletionContentPartText,
12    ChatCompletionContentPartTextType, ChatCompletionFileInput, ChatCompletionFunctionCall,
13    ChatCompletionFunctionDefinition, ChatCompletionFunctionTool, ChatCompletionFunctionToolType,
14    ChatCompletionImageUrl, ChatCompletionMessageFunctionToolCall,
15    ChatCompletionMessageFunctionToolCallType, ChatCompletionMessageParam,
16    ChatCompletionMessageToolCall, ChatCompletionNamedFunction, ChatCompletionNamedToolChoice,
17    ChatCompletionNamedToolChoiceType, ChatCompletionResponseFormat,
18    ChatCompletionResponseFormatJsonObject, ChatCompletionResponseFormatJsonObjectType,
19    ChatCompletionResponseFormatJsonSchema, ChatCompletionResponseFormatJsonSchemaConfig,
20    ChatCompletionResponseFormatJsonSchemaType, ChatCompletionResponseFormatText,
21    ChatCompletionResponseFormatTextType, ChatCompletionStop, ChatCompletionSystemMessageParam,
22    ChatCompletionSystemRole, ChatCompletionTextContent, ChatCompletionTool,
23    ChatCompletionToolChoiceMode, ChatCompletionToolChoiceOption, ChatCompletionToolMessageParam,
24    ChatCompletionToolRole, ChatCompletionUserContent, ChatCompletionUserMessageParam,
25    ChatCompletionUserRole, HttpMethod,
26};
27use crate::transform::gemini::utils::{
28    openai_chat_reasoning_effort_from_gemini_thinking, strip_models_prefix,
29};
30use crate::transform::utils::TransformError;
31
32impl TryFrom<GeminiGenerateContentRequest> for OpenAiChatCompletionsRequest {
33    type Error = TransformError;
34
35    fn try_from(value: GeminiGenerateContentRequest) -> Result<Self, TransformError> {
36        let body = value.body;
37        let model = strip_models_prefix(&value.path.model);
38
39        let mut messages = Vec::new();
40        if let Some(system_instruction) = body.system_instruction {
41            let system_text = system_instruction
42                .parts
43                .into_iter()
44                .filter_map(|part| part.text)
45                .filter(|text| !text.is_empty())
46                .collect::<Vec<_>>()
47                .join("\n");
48            if !system_text.is_empty() {
49                messages.push(ChatCompletionMessageParam::System(
50                    ChatCompletionSystemMessageParam {
51                        content: ChatCompletionTextContent::Text(system_text),
52                        role: ChatCompletionSystemRole::System,
53                        name: None,
54                    },
55                ));
56            }
57        }
58
59        let mut tool_output_messages = Vec::new();
60        let mut tool_call_index = 0u64;
61        for content in body.contents {
62            let role = content.role.unwrap_or(GeminiContentRole::User);
63            let mut text_parts = Vec::new();
64            let mut user_parts = Vec::new();
65            let mut tool_calls = Vec::new();
66
67            for part in content.parts {
68                if let Some(text) = part.text
69                    && !text.is_empty()
70                {
71                    text_parts.push(text.clone());
72                    if matches!(role, GeminiContentRole::User) {
73                        user_parts.push(ChatCompletionContentPart::Text(
74                            ChatCompletionContentPartText {
75                                text,
76                                type_: ChatCompletionContentPartTextType::Text,
77                            },
78                        ));
79                    }
80                }
81
82                if let Some(inline_data) = part.inline_data {
83                    if inline_data.mime_type.starts_with("image/") {
84                        user_parts.push(ChatCompletionContentPart::Image(
85                            ChatCompletionContentPartImage {
86                                image_url: ChatCompletionImageUrl {
87                                    url: format!(
88                                        "data:{};base64,{}",
89                                        inline_data.mime_type, inline_data.data
90                                    ),
91                                    detail: None,
92                                },
93                                type_: ChatCompletionContentPartImageType::ImageUrl,
94                            },
95                        ));
96                    } else {
97                        user_parts.push(ChatCompletionContentPart::File(
98                            ChatCompletionContentPartFile {
99                                file: ChatCompletionFileInput {
100                                    file_data: Some(inline_data.data),
101                                    file_id: None,
102                                    file_url: None,
103                                    filename: Some(inline_data.mime_type),
104                                },
105                                type_: ChatCompletionContentPartFileType::File,
106                            },
107                        ));
108                    }
109                }
110
111                if let Some(file_data) = part.file_data {
112                    if file_data
113                        .mime_type
114                        .as_deref()
115                        .unwrap_or_default()
116                        .starts_with("image/")
117                    {
118                        user_parts.push(ChatCompletionContentPart::Image(
119                            ChatCompletionContentPartImage {
120                                image_url: ChatCompletionImageUrl {
121                                    url: file_data.file_uri,
122                                    detail: None,
123                                },
124                                type_: ChatCompletionContentPartImageType::ImageUrl,
125                            },
126                        ));
127                    } else {
128                        user_parts.push(ChatCompletionContentPart::File(
129                            ChatCompletionContentPartFile {
130                                file: ChatCompletionFileInput {
131                                    file_data: None,
132                                    file_id: None,
133                                    file_url: Some(file_data.file_uri),
134                                    filename: None,
135                                },
136                                type_: ChatCompletionContentPartFileType::File,
137                            },
138                        ));
139                    }
140                }
141
142                if let Some(function_call) = part.function_call {
143                    let id = function_call.id.unwrap_or_else(|| {
144                        let id = format!("tool_call_{tool_call_index}");
145                        tool_call_index += 1;
146                        id
147                    });
148                    let arguments = function_call
149                        .args
150                        .and_then(|args| serde_json::to_string(&args).ok())
151                        .unwrap_or_else(|| "{}".to_string());
152                    tool_calls.push(ChatCompletionMessageToolCall::Function(
153                        ChatCompletionMessageFunctionToolCall {
154                            id,
155                            function: ChatCompletionFunctionCall {
156                                arguments,
157                                name: function_call.name,
158                            },
159                            type_: ChatCompletionMessageFunctionToolCallType::Function,
160                        },
161                    ));
162                }
163
164                if let Some(function_response) = part.function_response {
165                    let output = serde_json::to_string(&function_response.response)
166                        .unwrap_or_else(|_| "{}".to_string());
167                    let tool_call_id = function_response.id.unwrap_or(function_response.name);
168                    tool_output_messages.push(ChatCompletionMessageParam::Tool(
169                        ChatCompletionToolMessageParam {
170                            content: ChatCompletionTextContent::Text(output),
171                            role: ChatCompletionToolRole::Tool,
172                            tool_call_id,
173                        },
174                    ));
175                }
176            }
177
178            match role {
179                GeminiContentRole::User => {
180                    if user_parts.is_empty() {
181                        messages.push(ChatCompletionMessageParam::User(
182                            ChatCompletionUserMessageParam {
183                                content: ChatCompletionUserContent::Text(text_parts.join("\n")),
184                                role: ChatCompletionUserRole::User,
185                                name: None,
186                            },
187                        ));
188                    } else {
189                        messages.push(ChatCompletionMessageParam::User(
190                            ChatCompletionUserMessageParam {
191                                content: ChatCompletionUserContent::Parts(user_parts),
192                                role: ChatCompletionUserRole::User,
193                                name: None,
194                            },
195                        ));
196                    }
197                }
198                GeminiContentRole::Model => {
199                    messages.push(ChatCompletionMessageParam::Assistant(
200                        ChatCompletionAssistantMessageParam {
201                            role: ChatCompletionAssistantRole::Assistant,
202                            audio: None,
203                            content: if text_parts.is_empty() {
204                                None
205                            } else {
206                                Some(ChatCompletionAssistantContent::Text(text_parts.join("\n")))
207                            },
208                            reasoning_content: None,
209                            function_call: None,
210                            name: None,
211                            refusal: None,
212                            tool_calls: if tool_calls.is_empty() {
213                                None
214                            } else {
215                                Some(tool_calls)
216                            },
217                        },
218                    ));
219                }
220            }
221        }
222        messages.extend(tool_output_messages);
223
224        let tools = body.tools.and_then(|tool_defs| {
225            let mut mapped = Vec::new();
226            for tool in tool_defs {
227                if let Some(function_declarations) = tool.function_declarations {
228                    for declaration in function_declarations {
229                        let parameters = declaration
230                            .parameters_json_schema
231                            .and_then(|value| {
232                                serde_json::from_value::<crate::openai::create_chat_completions::types::FunctionParameters>(value).ok()
233                            });
234                        mapped.push(ChatCompletionTool::Function(ChatCompletionFunctionTool {
235                            function: ChatCompletionFunctionDefinition {
236                                name: declaration.name,
237                                description: if declaration.description.is_empty() {
238                                    None
239                                } else {
240                                    Some(declaration.description)
241                                },
242                                parameters,
243                                strict: None,
244                            },
245                            type_: ChatCompletionFunctionToolType::Function,
246                        }));
247                    }
248                }
249
250                if tool.code_execution.is_some() {
251                    mapped.push(ChatCompletionTool::Custom(
252                        crate::openai::create_chat_completions::types::ChatCompletionCustomTool {
253                            custom: crate::openai::create_chat_completions::types::ChatCompletionCustomToolSpec {
254                                name: "code_execution".to_string(),
255                                description: None,
256                                format: None,
257                            },
258                            type_: crate::openai::create_chat_completions::types::ChatCompletionCustomToolType::Custom,
259                        },
260                    ));
261                }
262            }
263
264            if mapped.is_empty() {
265                None
266            } else {
267                Some(mapped)
268            }
269        });
270
271        let tool_choice = body
272            .tool_config
273            .and_then(|config| config.function_calling_config)
274            .map(|config| {
275                if let Some(name) = config
276                    .allowed_function_names
277                    .as_ref()
278                    .and_then(|names| names.first())
279                    .cloned()
280                {
281                    return ChatCompletionToolChoiceOption::NamedFunction(
282                        ChatCompletionNamedToolChoice {
283                            function: ChatCompletionNamedFunction { name },
284                            type_: ChatCompletionNamedToolChoiceType::Function,
285                        },
286                    );
287                }
288                match config
289                    .mode
290                    .unwrap_or(GeminiFunctionCallingMode::ModeUnspecified)
291                {
292                    GeminiFunctionCallingMode::Auto
293                    | GeminiFunctionCallingMode::ModeUnspecified => {
294                        ChatCompletionToolChoiceOption::Mode(ChatCompletionToolChoiceMode::Auto)
295                    }
296                    GeminiFunctionCallingMode::Any | GeminiFunctionCallingMode::Validated => {
297                        ChatCompletionToolChoiceOption::Mode(ChatCompletionToolChoiceMode::Required)
298                    }
299                    GeminiFunctionCallingMode::None => {
300                        ChatCompletionToolChoiceOption::Mode(ChatCompletionToolChoiceMode::None)
301                    }
302                }
303            });
304
305        let (max_completion_tokens, reasoning_effort, response_format, stop, temperature, top_p) =
306            if let Some(config) = body.generation_config {
307                let max_completion_tokens = config.max_output_tokens.map(u64::from);
308                let reasoning_effort = config
309                    .thinking_config
310                    .as_ref()
311                    .and_then(openai_chat_reasoning_effort_from_gemini_thinking);
312
313                let schema = config
314                    .response_json_schema
315                    .or(config.response_json_schema_legacy)
316                    .or_else(|| {
317                        config
318                            .response_schema
319                            .and_then(|schema| serde_json::to_value(schema).ok())
320                    })
321                    .and_then(|value| {
322                        serde_json::from_value::<
323                            crate::openai::create_chat_completions::types::JsonObject,
324                        >(value)
325                        .ok()
326                    });
327                let response_format = match config.response_mime_type.as_deref() {
328                    Some("application/json") => Some(if let Some(schema) = schema {
329                        ChatCompletionResponseFormat::JsonSchema(
330                            ChatCompletionResponseFormatJsonSchema {
331                                json_schema: ChatCompletionResponseFormatJsonSchemaConfig {
332                                    name: "output".to_string(),
333                                    description: None,
334                                    schema: Some(schema),
335                                    strict: None,
336                                },
337                                type_: ChatCompletionResponseFormatJsonSchemaType::JsonSchema,
338                            },
339                        )
340                    } else {
341                        ChatCompletionResponseFormat::JsonObject(
342                            ChatCompletionResponseFormatJsonObject {
343                                type_: ChatCompletionResponseFormatJsonObjectType::JsonObject,
344                            },
345                        )
346                    }),
347                    Some("text/plain") => Some(ChatCompletionResponseFormat::Text(
348                        ChatCompletionResponseFormatText {
349                            type_: ChatCompletionResponseFormatTextType::Text,
350                        },
351                    )),
352                    _ => schema.map(|schema| {
353                        ChatCompletionResponseFormat::JsonSchema(
354                            ChatCompletionResponseFormatJsonSchema {
355                                json_schema: ChatCompletionResponseFormatJsonSchemaConfig {
356                                    name: "output".to_string(),
357                                    description: None,
358                                    schema: Some(schema),
359                                    strict: None,
360                                },
361                                type_: ChatCompletionResponseFormatJsonSchemaType::JsonSchema,
362                            },
363                        )
364                    }),
365                };
366
367                let stop = config.stop_sequences.map(|items| {
368                    if items.len() == 1 {
369                        ChatCompletionStop::Single(items[0].clone())
370                    } else {
371                        ChatCompletionStop::Multiple(items)
372                    }
373                });
374
375                (
376                    max_completion_tokens,
377                    reasoning_effort,
378                    response_format,
379                    stop,
380                    config.temperature,
381                    config.top_p,
382                )
383            } else {
384                (None, None, None, None, None, None)
385            };
386
387        Ok(OpenAiChatCompletionsRequest {
388            method: HttpMethod::Post,
389            path: PathParameters::default(),
390            query: QueryParameters::default(),
391            headers: RequestHeaders::default(),
392            body: RequestBody {
393                messages,
394                model,
395                max_completion_tokens,
396                reasoning_effort,
397                response_format,
398                stop,
399                stream: None,
400                temperature,
401                tool_choice,
402                tools,
403                top_p,
404                ..RequestBody::default()
405            },
406        })
407    }
408}