gproxy_protocol/transform/generate/claude_gemini/
request.rs1use super::{history::Calls, media::MediaFacts};
2use crate::{
3 transform::{
4 Converted, Report, TransformError,
5 identity::{IdentityFlow, TargetIdPolicy},
6 },
7 wire::{
8 DeclaredFields,
9 claude::{content as c, count_tokens as ct, generate_content as cg},
10 gemini as g,
11 },
12};
13
14#[derive(Default)]
16pub struct ClaudeGeminiRequestContext {
17 pub media: MediaFacts,
18 pub tool_names: std::collections::BTreeMap<String, String>,
20}
21
22pub fn claude_to_gemini_request(
23 input: cg::GenerateContentRequestBody,
24 target_model: impl Into<String>,
25 context: ClaudeGeminiRequestContext,
26 flow: &mut IdentityFlow,
27 policy: &TargetIdPolicy,
28) -> Result<Converted<g::GenerateContentRequestBody>, TransformError> {
29 let mut input = input.into_declared();
30 let mut ids = flow.clone();
31 let mut report = Report::default();
32 super::super::claude_controls::project(&mut input, &mut report);
33 let config = super::config::to_gemini(&input, &mut report)?;
34 let (tools, tool_config) =
35 super::tools::to_gemini(input.tools, input.tool_choice, &mut report)?;
36 let mut calls = Calls::default();
37 for (id, name) in context.tool_names {
38 calls.seed(id, name)?;
39 }
40 let mut system = Vec::new();
41 if let Some(prompt) = input.system {
42 match prompt {
43 ct::SystemPrompt::Text(text) => system.push(g::Part::builder().text(text).build()),
44 ct::SystemPrompt::Blocks(blocks) => {
45 for block in blocks {
46 if block.cache_control.is_some() {
47 report.omitted(
48 "system.cache_control",
49 "Gemini has no per-block cache field",
50 );
51 }
52 system.push(g::Part::builder().text(block.text).build());
53 }
54 }
55 }
56 }
57 let mut contents = Vec::new();
58 let mut position = crate::transform::instructions::Position::default();
59 for (index, message) in input.messages.into_iter().enumerate() {
60 let leading = position.leading(message.role == c::Role::System);
61 let blocks = match message.content {
62 c::MessageContent::Text(text) => vec![c::ContentBlock::Text(
63 c::TextBlock::builder(c::TextBlockType::Tag, text).build(),
64 )],
65 c::MessageContent::Blocks(blocks) => blocks,
66 };
67 let parts = super::history::to_gemini(
68 blocks,
69 &context.media,
70 &mut calls,
71 &mut ids,
72 policy,
73 &mut report,
74 )?;
75 if message.role == c::Role::System {
76 crate::transform::instructions::gemini(
77 parts,
78 leading,
79 &mut system,
80 &mut contents,
81 format!("messages[{index}].role"),
82 &mut report,
83 )?;
84 } else if !parts.is_empty() {
85 let role = match message.role {
86 c::Role::User => "user",
87 c::Role::Assistant => "model",
88 c::Role::System => unreachable!(),
90 };
91 contents.push(
92 g::Content::builder()
93 .parts(parts)
94 .role(role.to_owned())
95 .build(),
96 );
97 }
98 }
99 let mut out = g::GenerateContentRequestBody::builder(contents).build();
100 out.tools = tools;
101 out.tool_config = tool_config;
102 out.generation_config = Some(config);
103 if !system.is_empty() {
104 out.system_instruction = Some(g::Content::builder().parts(system).build());
105 }
106 *flow = ids;
107 super::super::gemini_controls::for_model(&mut out, &target_model.into(), &mut report);
108 Ok(Converted { value: out, report })
109}
110
111pub fn gemini_to_claude_request(
112 input: g::GenerateContentRequestBody,
113 target_model: impl Into<String>,
114 max_tokens: Option<i64>,
115 flow: &mut IdentityFlow,
116 policy: &TargetIdPolicy,
117) -> Result<Converted<cg::GenerateContentRequestBody>, TransformError> {
118 let model = target_model.into();
119
120 let input = input.into_declared();
121 let source_max = input
122 .generation_config
123 .as_ref()
124 .and_then(|v| v.max_output_tokens);
125
126 let max = source_max
127 .or(max_tokens)
128 .ok_or_else(|| TransformError::missing_metadata("max_tokens"))?;
129
130 let mut out = cg::GenerateContentRequestBody::builder(max, Vec::new(), model).build();
131 let mut report = Report::default();
132 super::config::to_claude(&input, &mut out, &mut report)?;
133 let (tools, choice) = super::tools::to_claude(input.tools, input.tool_config, &mut report)?;
134 out.tools = tools;
135 out.tool_choice = choice;
136 let mut system = Vec::new();
137 if let Some(content) = input.system_instruction {
138 for part in content.parts.unwrap_or_default() {
139 system.push(
140 part.text
141 .ok_or_else(|| TransformError::shape("system_instruction", "text required"))?,
142 );
143 }
144 }
145 let mut ids = flow.clone();
146 let mut calls = Calls::default();
147 let mut position = crate::transform::instructions::Position::default();
148 for content in input.contents {
149 let role = match content.role.as_deref() {
150 Some("model") => c::Role::Assistant,
151 Some("user") | None => c::Role::User,
152 Some("system") => c::Role::System,
153 Some(_) => return Err(TransformError::shape("contents.role", "unknown role")),
154 };
155 let leading = position.leading(role == c::Role::System);
156 let blocks = super::history::to_claude(
157 content.parts.unwrap_or_default(),
158 &mut calls,
159 &mut ids,
160 policy,
161 &mut report,
162 )?;
163 if role == c::Role::System && leading {
164 system.extend(blocks.into_iter().filter_map(|block| match block {
165 c::ContentBlock::Text(text) => Some(text.text),
166 _ => None,
167 }));
168 continue;
169 }
170 if !blocks.is_empty() {
171 out.messages
172 .push(c::Message::builder(role, c::MessageContent::Blocks(blocks)).build());
173 }
174 }
175 if !system.is_empty() {
176 out.system = Some(ct::SystemPrompt::Text(system.join("\n")));
177 }
178 crate::transform::instructions::claude(&mut out.messages, &out.model, &mut report);
179 *flow = ids;
180 crate::transform::generate::claude_controls::target(
181 &out.model,
182 &mut out.thinking,
183 &mut out.tool_choice,
184 &mut report,
185 );
186 Ok(Converted { value: out, report })
187}