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::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                // System roles are consumed by the instructions branch above.
90                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}