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::Absent => continue,
63 c::MessageContent::Text(text) => vec![c::ContentBlock::Text(
64 c::TextBlock::builder(c::TextBlockType::Tag, text).build(),
65 )],
66 c::MessageContent::Blocks(blocks) => blocks,
67 };
68 let parts = super::history::to_gemini(
69 blocks,
70 &context.media,
71 &mut calls,
72 &mut ids,
73 policy,
74 &mut report,
75 )?;
76 if message.role == c::Role::System {
77 crate::transform::instructions::gemini(
78 parts,
79 leading,
80 &mut system,
81 &mut contents,
82 format!("messages[{index}].role"),
83 &mut report,
84 )?;
85 } else if !parts.is_empty() {
86 let role = match message.role {
87 c::Role::User => "user",
88 c::Role::Assistant => "model",
89 c::Role::System => unreachable!(),
91 };
92 contents.push(
93 g::Content::builder()
94 .parts(parts)
95 .role(role.to_owned())
96 .build(),
97 );
98 }
99 }
100 let mut out = g::GenerateContentRequestBody::builder(contents).build();
101 out.tools = tools;
102 out.tool_config = tool_config;
103 out.generation_config = Some(config);
104 if !system.is_empty() {
105 out.system_instruction = Some(g::Content::builder().parts(system).build());
106 }
107 *flow = ids;
108 super::super::gemini_controls::for_model(&mut out, &target_model.into(), &mut report);
109 Ok(Converted { value: out, report })
110}
111
112pub fn gemini_to_claude_request(
113 input: g::GenerateContentRequestBody,
114 target_model: impl Into<String>,
115 max_tokens: Option<i64>,
116 flow: &mut IdentityFlow,
117 policy: &TargetIdPolicy,
118) -> Result<Converted<cg::GenerateContentRequestBody>, TransformError> {
119 let model = target_model.into();
120
121 let input = input.into_declared();
122 let source_max = input
123 .generation_config
124 .as_ref()
125 .and_then(|v| v.max_output_tokens);
126
127 let max = source_max
128 .or(max_tokens)
129 .ok_or_else(|| TransformError::missing_metadata("max_tokens"))?;
130
131 let mut out = cg::GenerateContentRequestBody::builder(max, Vec::new(), model).build();
132 let mut report = Report::default();
133 super::config::to_claude(&input, &mut out, &mut report)?;
134 let (tools, choice) = super::tools::to_claude(input.tools, input.tool_config, &mut report)?;
135 out.tools = tools;
136 out.tool_choice = choice;
137 let mut system = Vec::new();
138 if let Some(content) = input.system_instruction {
139 for part in content.parts.unwrap_or_default() {
140 system.push(
141 part.text
142 .ok_or_else(|| TransformError::shape("system_instruction", "text required"))?,
143 );
144 }
145 }
146 let mut ids = flow.clone();
147 let mut calls = Calls::default();
148 let mut position = crate::transform::instructions::Position::default();
149 for content in input.contents {
150 let role = match content.role.as_deref() {
151 Some("model") => c::Role::Assistant,
152 Some("user") | None => c::Role::User,
153 Some("system") => c::Role::System,
154 Some(_) => return Err(TransformError::shape("contents.role", "unknown role")),
155 };
156 let leading = position.leading(role == c::Role::System);
157 let blocks = super::history::to_claude(
158 content.parts.unwrap_or_default(),
159 &mut calls,
160 &mut ids,
161 policy,
162 &mut report,
163 )?;
164 if role == c::Role::System && leading {
165 system.extend(blocks.into_iter().filter_map(|block| match block {
166 c::ContentBlock::Text(text) => Some(text.text),
167 _ => None,
168 }));
169 continue;
170 }
171 if !blocks.is_empty() {
172 out.messages
173 .push(c::Message::builder(role, c::MessageContent::Blocks(blocks)).build());
174 }
175 }
176 if !system.is_empty() {
177 out.system = Some(ct::SystemPrompt::Text(system.join("\n")));
178 }
179 crate::transform::instructions::claude(&mut out.messages, &out.model, &mut report);
180 *flow = ids;
181 crate::transform::generate::claude_controls::target(
182 &mut out.messages,
183 &out.model,
184 &mut out.thinking,
185 &mut out.tool_choice,
186 &mut report,
187 );
188 Ok(Converted { value: out, report })
189}