Skip to main content

gproxy_protocol/transform/gemini/
utils.rs

1use crate::claude::count_tokens::types::{
2    BetaBase64ImageSource, BetaBase64SourceType, BetaCodeExecutionTool20250825,
3    BetaCodeExecutionTool20250825Type, BetaCodeExecutionToolName, BetaComputerToolName,
4    BetaContentBlockParam, BetaImageBlockParam, BetaImageBlockType, BetaImageMediaType,
5    BetaImageSource, BetaJsonOutputFormat, BetaJsonOutputFormatType, BetaMessageParam,
6    BetaMessageRole, BetaOutputConfig, BetaOutputEffort, BetaSystemPrompt, BetaTextBlockParam,
7    BetaTextBlockType, BetaThinkingBlockParam, BetaThinkingBlockType, BetaThinkingConfigAdaptive,
8    BetaThinkingConfigAdaptiveType, BetaThinkingConfigDisabled, BetaThinkingConfigDisabledType,
9    BetaThinkingConfigEnabled, BetaThinkingConfigEnabledType, BetaThinkingConfigParam, BetaTool,
10    BetaToolChoice, BetaToolChoiceAny, BetaToolChoiceAnyType, BetaToolChoiceAuto,
11    BetaToolChoiceAutoType, BetaToolChoiceNone, BetaToolChoiceNoneType, BetaToolChoiceTool,
12    BetaToolChoiceToolType, BetaToolCommonFields, BetaToolComputerUse20251124,
13    BetaToolComputerUse20251124Type, BetaToolInputSchema, BetaToolInputSchemaType,
14    BetaToolResultBlockParam, BetaToolResultBlockParamContent, BetaToolResultBlockType,
15    BetaToolSearchToolBm25_20251119, BetaToolSearchToolBm25Name, BetaToolSearchToolBm25Type,
16    BetaToolUnion, BetaToolUseBlockParam, BetaToolUseBlockType, BetaWebFetchTool20250910,
17    BetaWebFetchTool20250910Type, BetaWebFetchToolName, BetaWebSearchTool20250305,
18    BetaWebSearchTool20250305Type, BetaWebSearchToolName,
19};
20use crate::gemini::count_tokens::types::{
21    GeminiContent, GeminiContentRole, GeminiFunctionCallingMode, GeminiGenerationConfig,
22    GeminiThinkingConfig, GeminiThinkingLevel, GeminiTool, GeminiToolConfig,
23};
24use crate::openai::count_tokens::types::ResponseReasoningEffort;
25use crate::openai::create_chat_completions::types::ChatCompletionReasoningEffort;
26use crate::transform::claude::utils::claude_model_supports_enabled_thinking;
27use crate::transform::utils::push_message_block;
28
29pub fn strip_models_prefix(value: &str) -> String {
30    value.strip_prefix("models/").unwrap_or(value).to_string()
31}
32
33pub fn gemini_content_to_text(content: &GeminiContent) -> String {
34    content
35        .parts
36        .iter()
37        .filter_map(|part| {
38            if let Some(text) = part.text.as_ref() {
39                return Some(text.clone());
40            }
41            part.file_data.as_ref().map(|file| file.file_uri.clone())
42        })
43        .filter(|text| !text.is_empty())
44        .collect::<Vec<_>>()
45        .join("\n")
46}
47
48pub fn gemini_contents_to_claude_messages(contents: Vec<GeminiContent>) -> Vec<BetaMessageParam> {
49    let mut messages = Vec::new();
50    for content in contents {
51        let role = match content.role.unwrap_or(GeminiContentRole::User) {
52            GeminiContentRole::User => BetaMessageRole::User,
53            GeminiContentRole::Model => BetaMessageRole::Assistant,
54        };
55
56        for (index, part) in content.parts.into_iter().enumerate() {
57            if let Some(text) = part.text
58                && !text.is_empty()
59            {
60                if part.thought.unwrap_or(false) {
61                    push_message_block(
62                        &mut messages,
63                        role.clone(),
64                        BetaContentBlockParam::Thinking(BetaThinkingBlockParam {
65                            signature: part
66                                .thought_signature
67                                .unwrap_or_else(|| format!("thought_{index}")),
68                            thinking: text,
69                            type_: BetaThinkingBlockType::Thinking,
70                        }),
71                    );
72                } else {
73                    push_message_block(
74                        &mut messages,
75                        role.clone(),
76                        BetaContentBlockParam::Text(BetaTextBlockParam {
77                            text,
78                            type_: BetaTextBlockType::Text,
79                            cache_control: None,
80                            citations: None,
81                        }),
82                    );
83                }
84            }
85
86            if let Some(function_call) = part.function_call {
87                push_message_block(
88                    &mut messages,
89                    role.clone(),
90                    BetaContentBlockParam::ToolUse(BetaToolUseBlockParam {
91                        id: function_call
92                            .id
93                            .unwrap_or_else(|| format!("tool_use_{index}")),
94                        input: function_call.args.unwrap_or_default(),
95                        name: function_call.name,
96                        type_: BetaToolUseBlockType::ToolUse,
97                        cache_control: None,
98                        caller: None,
99                    }),
100                );
101            }
102
103            if let Some(function_response) = part.function_response {
104                // Gemini's function_response carries the result for an
105                // earlier function_call. Emit it as a Claude tool_result so
106                // the assistant can see what the tool returned.
107                // `push_message_block` handles the pairing rule by
108                // synthesising a placeholder tool_use when the matching
109                // function_call is not present in the same request — this
110                // happens when a client only sends the new tool outputs
111                // and relies on server-side history reconstruction we
112                // don't have access to.
113                let tool_use_id = function_response
114                    .id
115                    .unwrap_or_else(|| format!("tool_use_{index}"));
116                let response_text = if function_response.response.is_empty() {
117                    String::new()
118                } else {
119                    serde_json::to_string(&function_response.response).unwrap_or_default()
120                };
121                push_message_block(
122                    &mut messages,
123                    BetaMessageRole::User,
124                    BetaContentBlockParam::ToolResult(BetaToolResultBlockParam {
125                        tool_use_id,
126                        type_: BetaToolResultBlockType::ToolResult,
127                        cache_control: None,
128                        content: if response_text.is_empty() {
129                            None
130                        } else {
131                            Some(BetaToolResultBlockParamContent::Text(response_text))
132                        },
133                        is_error: None,
134                    }),
135                );
136            }
137
138            if let Some(inline_data) = part.inline_data {
139                let image_media_type = match inline_data.mime_type.as_str() {
140                    "image/jpeg" => Some(BetaImageMediaType::ImageJpeg),
141                    "image/png" => Some(BetaImageMediaType::ImagePng),
142                    "image/gif" => Some(BetaImageMediaType::ImageGif),
143                    "image/webp" => Some(BetaImageMediaType::ImageWebp),
144                    _ => None,
145                };
146
147                if let Some(media_type) = image_media_type {
148                    push_message_block(
149                        &mut messages,
150                        role.clone(),
151                        BetaContentBlockParam::Image(BetaImageBlockParam {
152                            source: BetaImageSource::Base64(BetaBase64ImageSource {
153                                data: inline_data.data,
154                                media_type,
155                                type_: BetaBase64SourceType::Base64,
156                            }),
157                            type_: BetaImageBlockType::Image,
158                            cache_control: None,
159                        }),
160                    );
161                } else {
162                    push_message_block(
163                        &mut messages,
164                        role.clone(),
165                        BetaContentBlockParam::Text(BetaTextBlockParam {
166                            text: format!(
167                                "inline_data({}): {}",
168                                inline_data.mime_type, inline_data.data
169                            ),
170                            type_: BetaTextBlockType::Text,
171                            cache_control: None,
172                            citations: None,
173                        }),
174                    );
175                }
176            }
177
178            if let Some(file_data) = part.file_data {
179                let text = if let Some(mime_type) = file_data.mime_type {
180                    format!("file_data({mime_type}): {}", file_data.file_uri)
181                } else {
182                    file_data.file_uri
183                };
184                if !text.is_empty() {
185                    push_message_block(
186                        &mut messages,
187                        role.clone(),
188                        BetaContentBlockParam::Text(BetaTextBlockParam {
189                            text,
190                            type_: BetaTextBlockType::Text,
191                            cache_control: None,
192                            citations: None,
193                        }),
194                    );
195                }
196            }
197        }
198    }
199    messages
200}
201
202pub fn gemini_system_instruction_to_claude(
203    system_instruction: Option<GeminiContent>,
204) -> Option<BetaSystemPrompt> {
205    system_instruction.and_then(|instruction| {
206        let text = instruction
207            .parts
208            .into_iter()
209            .filter_map(|part| part.text)
210            .filter(|text| !text.is_empty())
211            .collect::<Vec<_>>()
212            .join("\n");
213        if text.is_empty() {
214            None
215        } else {
216            Some(BetaSystemPrompt::Text(text))
217        }
218    })
219}
220
221pub fn gemini_tool_choice_to_claude(
222    tool_config: Option<GeminiToolConfig>,
223) -> Option<BetaToolChoice> {
224    tool_config
225        .and_then(|config| config.function_calling_config)
226        .map(|config| {
227            if let Some(name) = config
228                .allowed_function_names
229                .as_ref()
230                .and_then(|names| names.first())
231                .cloned()
232            {
233                return BetaToolChoice::Tool(BetaToolChoiceTool {
234                    name,
235                    type_: BetaToolChoiceToolType::Tool,
236                    disable_parallel_tool_use: None,
237                });
238            }
239
240            match config
241                .mode
242                .unwrap_or(GeminiFunctionCallingMode::ModeUnspecified)
243            {
244                GeminiFunctionCallingMode::Auto | GeminiFunctionCallingMode::ModeUnspecified => {
245                    BetaToolChoice::Auto(BetaToolChoiceAuto {
246                        type_: BetaToolChoiceAutoType::Auto,
247                        disable_parallel_tool_use: None,
248                    })
249                }
250                GeminiFunctionCallingMode::Any | GeminiFunctionCallingMode::Validated => {
251                    BetaToolChoice::Any(BetaToolChoiceAny {
252                        type_: BetaToolChoiceAnyType::Any,
253                        disable_parallel_tool_use: None,
254                    })
255                }
256                GeminiFunctionCallingMode::None => BetaToolChoice::None(BetaToolChoiceNone {
257                    type_: BetaToolChoiceNoneType::None,
258                }),
259            }
260        })
261}
262
263pub fn gemini_tools_to_claude(tools: Option<Vec<GeminiTool>>) -> Option<Vec<BetaToolUnion>> {
264    tools.and_then(|tool_defs| {
265        let mut mapped = Vec::new();
266        for tool in tool_defs {
267            if let Some(function_declarations) = tool.function_declarations {
268                for declaration in function_declarations {
269                    let input_schema = declaration
270                        .parameters_json_schema
271                        .or_else(|| {
272                            declaration
273                                .parameters
274                                .and_then(|schema| serde_json::to_value(schema).ok())
275                        })
276                        .map(gemini_parameters_schema_to_claude_input_schema)
277                        .unwrap_or_else(default_claude_tool_input_schema);
278
279                    mapped.push(BetaToolUnion::Custom(BetaTool {
280                        input_schema,
281                        name: declaration.name,
282                        common: BetaToolCommonFields::default(),
283                        description: if declaration.description.is_empty() {
284                            None
285                        } else {
286                            Some(declaration.description)
287                        },
288                        eager_input_streaming: None,
289                        type_: None,
290                    }));
291                }
292            }
293
294            if tool.code_execution.is_some() {
295                mapped.push(BetaToolUnion::CodeExecution20250825(
296                    BetaCodeExecutionTool20250825 {
297                        name: BetaCodeExecutionToolName::CodeExecution,
298                        type_: BetaCodeExecutionTool20250825Type::CodeExecution20250825,
299                        common: BetaToolCommonFields::default(),
300                    },
301                ));
302            }
303
304            if tool.computer_use.is_some() {
305                mapped.push(BetaToolUnion::ComputerUse20251124(
306                    BetaToolComputerUse20251124 {
307                        display_height_px: 1024,
308                        display_width_px: 1024,
309                        name: BetaComputerToolName::Computer,
310                        type_: BetaToolComputerUse20251124Type::Computer20251124,
311                        common: BetaToolCommonFields::default(),
312                        display_number: None,
313                        enable_zoom: None,
314                    },
315                ));
316            }
317
318            if tool.google_search.is_some() {
319                mapped.push(BetaToolUnion::WebSearch20250305(
320                    BetaWebSearchTool20250305 {
321                        name: BetaWebSearchToolName::WebSearch,
322                        type_: BetaWebSearchTool20250305Type::WebSearch20250305,
323                        common: BetaToolCommonFields::default(),
324                        allowed_domains: None,
325                        blocked_domains: None,
326                        max_uses: None,
327                        user_location: None,
328                    },
329                ));
330            }
331
332            if tool.url_context.is_some() {
333                mapped.push(BetaToolUnion::WebFetch20250910(BetaWebFetchTool20250910 {
334                    name: BetaWebFetchToolName::WebFetch,
335                    type_: BetaWebFetchTool20250910Type::WebFetch20250910,
336                    common: BetaToolCommonFields::default(),
337                    allowed_domains: None,
338                    blocked_domains: None,
339                    citations: None,
340                    max_content_tokens: None,
341                    max_uses: None,
342                }));
343            }
344
345            if tool.file_search.is_some() {
346                mapped.push(BetaToolUnion::ToolSearchBm25_20251119(
347                    BetaToolSearchToolBm25_20251119 {
348                        name: BetaToolSearchToolBm25Name::ToolSearchToolBm25,
349                        type_: BetaToolSearchToolBm25Type::ToolSearchToolBm2520251119,
350                        common: BetaToolCommonFields::default(),
351                    },
352                ));
353            }
354        }
355
356        if mapped.is_empty() {
357            None
358        } else {
359            Some(mapped)
360        }
361    })
362}
363
364fn default_claude_tool_input_schema() -> BetaToolInputSchema {
365    BetaToolInputSchema {
366        type_: BetaToolInputSchemaType::Object,
367        properties: None,
368        required: None,
369        extra_fields: Default::default(),
370    }
371}
372
373fn gemini_parameters_schema_to_claude_input_schema(
374    value: serde_json::Value,
375) -> BetaToolInputSchema {
376    let serde_json::Value::Object(mut schema) = value else {
377        return default_claude_tool_input_schema();
378    };
379
380    let required = schema.remove("required").and_then(|value| match value {
381        serde_json::Value::Array(values) => {
382            let required = values
383                .into_iter()
384                .filter_map(|item| item.as_str().map(ToOwned::to_owned))
385                .collect::<Vec<_>>();
386            if required.is_empty() {
387                None
388            } else {
389                Some(required)
390            }
391        }
392        _ => None,
393    });
394
395    let properties = schema.remove("properties").and_then(|value| match value {
396        serde_json::Value::Object(map) => Some(map.into_iter().collect()),
397        _ => None,
398    });
399
400    // Claude custom tool input_schema expects an object schema.
401    let _ = schema.remove("type");
402
403    BetaToolInputSchema {
404        type_: BetaToolInputSchemaType::Object,
405        properties,
406        required,
407        extra_fields: schema.into_iter().collect(),
408    }
409}
410
411fn gemini_thinking_effort_bucket(thinking: &GeminiThinkingConfig) -> Option<u8> {
412    if thinking.include_thoughts == Some(false) {
413        return Some(0);
414    }
415
416    if let Some(level) = thinking.thinking_level.as_ref() {
417        return Some(match level {
418            GeminiThinkingLevel::ThinkingLevelUnspecified => 3,
419            GeminiThinkingLevel::Minimal => 1,
420            GeminiThinkingLevel::Low => 2,
421            GeminiThinkingLevel::Medium => 3,
422            GeminiThinkingLevel::High => 4,
423        });
424    }
425
426    thinking.thinking_budget.map(|budget| {
427        if budget <= 0 {
428            0
429        } else if budget <= 4096 {
430            1
431        } else if budget <= 8192 {
432            2
433        } else if budget <= 16384 {
434            3
435        } else if budget <= 32768 {
436            4
437        } else {
438            5
439        }
440    })
441}
442
443pub fn openai_reasoning_effort_from_gemini_thinking(
444    thinking: &GeminiThinkingConfig,
445) -> Option<ResponseReasoningEffort> {
446    gemini_thinking_effort_bucket(thinking).map(|bucket| match bucket {
447        0 => ResponseReasoningEffort::None,
448        1 => ResponseReasoningEffort::Minimal,
449        2 => ResponseReasoningEffort::Low,
450        3 => ResponseReasoningEffort::Medium,
451        4 => ResponseReasoningEffort::High,
452        _ => ResponseReasoningEffort::XHigh,
453    })
454}
455
456pub fn openai_chat_reasoning_effort_from_gemini_thinking(
457    thinking: &GeminiThinkingConfig,
458) -> Option<ChatCompletionReasoningEffort> {
459    gemini_thinking_effort_bucket(thinking).map(|bucket| match bucket {
460        0 => ChatCompletionReasoningEffort::None,
461        1 => ChatCompletionReasoningEffort::Minimal,
462        2 => ChatCompletionReasoningEffort::Low,
463        3 => ChatCompletionReasoningEffort::Medium,
464        4 => ChatCompletionReasoningEffort::High,
465        _ => ChatCompletionReasoningEffort::XHigh,
466    })
467}
468
469pub fn claude_thinking_from_gemini(
470    thinking: &GeminiThinkingConfig,
471    model: Option<&crate::claude::count_tokens::types::Model>,
472) -> Option<BetaThinkingConfigParam> {
473    if matches!(thinking.include_thoughts, Some(false)) {
474        return Some(BetaThinkingConfigParam::Disabled(
475            BetaThinkingConfigDisabled {
476                type_: BetaThinkingConfigDisabledType::Disabled,
477            },
478        ));
479    }
480
481    if let Some(budget) = thinking.thinking_budget {
482        if !claude_model_supports_enabled_thinking(model) {
483            return Some(BetaThinkingConfigParam::Adaptive(
484                BetaThinkingConfigAdaptive {
485                    type_: BetaThinkingConfigAdaptiveType::Adaptive,
486                    display: None,
487                },
488            ));
489        }
490        return Some(BetaThinkingConfigParam::Enabled(
491            BetaThinkingConfigEnabled {
492                budget_tokens: u64::try_from(budget).unwrap_or(0),
493                type_: BetaThinkingConfigEnabledType::Enabled,
494                display: None,
495            },
496        ));
497    }
498
499    if thinking.thinking_level.is_some() {
500        return Some(BetaThinkingConfigParam::Adaptive(
501            BetaThinkingConfigAdaptive {
502                type_: BetaThinkingConfigAdaptiveType::Adaptive,
503                display: None,
504            },
505        ));
506    }
507
508    None
509}
510
511pub fn claude_output_effort_from_gemini_level(
512    level: &GeminiThinkingLevel,
513) -> Option<BetaOutputEffort> {
514    match level {
515        GeminiThinkingLevel::Minimal | GeminiThinkingLevel::Low => Some(BetaOutputEffort::Low),
516        GeminiThinkingLevel::Medium => Some(BetaOutputEffort::Medium),
517        GeminiThinkingLevel::High => Some(BetaOutputEffort::High),
518        GeminiThinkingLevel::ThinkingLevelUnspecified => None,
519    }
520}
521
522pub fn claude_output_format_from_gemini_generation_config(
523    generation_config: &GeminiGenerationConfig,
524) -> Option<BetaJsonOutputFormat> {
525    generation_config
526        .response_json_schema
527        .clone()
528        .or(generation_config.response_json_schema_legacy.clone())
529        .and_then(|value| {
530            serde_json::from_value::<crate::claude::count_tokens::types::JsonObject>(value).ok()
531        })
532        .map(|schema| BetaJsonOutputFormat {
533            schema,
534            type_: BetaJsonOutputFormatType::JsonSchema,
535        })
536}
537
538pub fn claude_thinking_effort_format_from_gemini_generation_config(
539    generation_config: Option<&GeminiGenerationConfig>,
540    model: Option<&crate::claude::count_tokens::types::Model>,
541) -> (
542    Option<BetaThinkingConfigParam>,
543    Option<BetaOutputEffort>,
544    Option<BetaJsonOutputFormat>,
545) {
546    if let Some(generation_config) = generation_config {
547        let thinking = generation_config
548            .thinking_config
549            .as_ref()
550            .and_then(|thinking_config| claude_thinking_from_gemini(thinking_config, model));
551
552        let output_effort = generation_config
553            .thinking_config
554            .as_ref()
555            .and_then(|thinking_config| thinking_config.thinking_level.as_ref())
556            .and_then(claude_output_effort_from_gemini_level);
557
558        let output_format = claude_output_format_from_gemini_generation_config(generation_config);
559
560        (thinking, output_effort, output_format)
561    } else {
562        (None, None, None)
563    }
564}
565
566pub fn claude_output_config_from_effort_and_format(
567    output_effort: Option<BetaOutputEffort>,
568    output_format: Option<BetaJsonOutputFormat>,
569) -> Option<BetaOutputConfig> {
570    if output_effort.is_some() || output_format.is_some() {
571        Some(BetaOutputConfig {
572            effort: output_effort,
573            format: output_format,
574            task_budget: None,
575        })
576    } else {
577        None
578    }
579}