Skip to main content

gproxy_protocol/transform/generate/claude_gemini/
request.rs

1use 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/// Concrete media facts obtained by the caller's resource capabilities.
15#[derive(Default)]
16pub struct ClaudeGeminiRequestContext {
17    pub media: MediaFacts,
18    /// Tool IDs and names recovered from declared history/state when history is truncated.
19    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                // System roles are consumed by the instructions branch above.
89                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}