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}