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 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 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}