Skip to main content

gproxy_protocol/adapt/generate/
claude_gemini.rs

1use super::{Endpoint, GenerationIdentity, GenerationOutcome, GenerationProgress, transport};
2use crate::transform::generate::claude_gemini as p;
3use crate::wire::claude::generate_content as c;
4use crate::wire::gemini as g;
5use crate::{
6    capability::Upstream,
7    codec::CodecLimits,
8    transform::{Converted, Report, TransformError},
9    wire::DeclaredFields,
10};
11
12/// A prepared Claude client request executed by the selected Gemini endpoint.
13#[derive(Debug)]
14pub struct ClaudeViaGemini {
15    original_request: c::GenerateContentRequestBody,
16    target_request: g::GenerateContentRequestBody,
17    selected_model: String,
18    endpoint: Endpoint,
19    identities: GenerationIdentity,
20    report: Report,
21}
22
23impl ClaudeViaGemini {
24    pub fn prepare(
25        input: c::GenerateContentRequestBody,
26        selected_model: impl Into<String>,
27        endpoint: Endpoint,
28        mut identities: GenerationIdentity,
29        context: p::ClaudeGeminiRequestContext,
30    ) -> Result<Self, TransformError> {
31        let selected_model = selected_model.into();
32        if selected_model.trim().is_empty() {
33            return Err(TransformError::missing_metadata("selected_model"));
34        }
35        let original_request = input.into_declared();
36        if original_request.stream == Some(true) {
37            return Err(TransformError::unsupported(
38                "stream",
39                "use the incremental invocation entrypoint",
40            ));
41        }
42        let converted = p::claude_to_gemini_request(
43            original_request.clone(),
44            &selected_model,
45            context,
46            &mut identities.request,
47            &identities.request_policy,
48        )?;
49        Ok(Self {
50            original_request,
51            target_request: converted.value.into_declared(),
52            selected_model,
53            endpoint,
54            identities,
55            report: converted.report,
56        })
57    }
58    /// Restore exact tool aliases and names from declared history/scoped state before mapping.
59    pub async fn prepare_with_state<S: crate::capability::StateStore>(
60        input: c::GenerateContentRequestBody,
61        endpoint: Endpoint,
62        identities: GenerationIdentity,
63        state: &super::GenerationStateAccess<'_, S>,
64        context: p::ClaudeGeminiRequestContext,
65    ) -> Result<Self, TransformError> {
66        let selected_model = state.target.model.clone();
67        let original = input.into_declared();
68        let (restored, names) = super::history::claude(original.clone(), state).await?;
69        let mut context = context;
70        super::history::merge_names(&mut context.tool_names, names.names)?;
71        let mut prepared = Self::prepare(restored, selected_model, endpoint, identities, context)?;
72        prepared.original_request = original;
73        Ok(prepared)
74    }
75    /// Materialize foreign resources and restore scoped history before preparing the selected endpoint.
76    pub async fn prepare_with_capabilities<
77        S: crate::capability::StateStore,
78        R: crate::capability::ResourceAccess,
79    >(
80        input: c::GenerateContentRequestBody,
81        endpoint: Endpoint,
82        identities: GenerationIdentity,
83        state: &super::GenerationStateAccess<'_, S>,
84        resources: &super::GenerationResources<'_, R>,
85        context: p::ClaudeGeminiRequestContext,
86    ) -> Result<Self, TransformError> {
87        let original = input.into_declared();
88        let materialized = resources.claude(original.clone()).await?;
89        let mut prepared =
90            Self::prepare_with_state(materialized, endpoint, identities, state, context).await?;
91        prepared.original_request = original;
92        Ok(prepared)
93    }
94
95    pub fn original_request(&self) -> &c::GenerateContentRequestBody {
96        &self.original_request
97    }
98    pub fn target_request(&self) -> &g::GenerateContentRequestBody {
99        &self.target_request
100    }
101    pub fn identities(&self) -> &GenerationIdentity {
102        &self.identities
103    }
104    pub fn report(&self) -> &Report {
105        &self.report
106    }
107    pub fn selected_model(&self) -> &str {
108        &self.selected_model
109    }
110    pub fn convert_response(
111        &mut self,
112        native: g::GenerateContentResponseBody,
113        facts: p::ClaudeGeminiUsageFacts,
114    ) -> Result<Converted<c::GenerateContentResponseBody>, TransformError> {
115        let native = native.into_declared();
116        p::gemini_to_claude_response(
117            native,
118            Some(self.selected_model.clone()),
119            facts,
120            &mut self.identities.response,
121            &self.identities.response_policy,
122        )
123    }
124    /// Performs one POST. The factual supplement is obtained from the actual decoded response.
125    /// Conversion errors keep native bytes/results in caller-owned progress.
126    pub async fn invoke<U: Upstream, S: crate::capability::StateStore>(
127        &mut self,
128        upstream: &U,
129        target: &U::Target,
130        limits: CodecLimits,
131        state: &super::GenerationStateAccess<'_, S>,
132        progress: &mut GenerationProgress<g::GenerateContentResponseBody>,
133        facts: impl FnOnce(
134            &g::GenerateContentResponseBody,
135        ) -> Result<p::ClaudeGeminiUsageFacts, TransformError>,
136    ) -> Result<GenerationOutcome<c::GenerateContentResponseBody>, TransformError> {
137        transport::bind(
138            (&self.endpoint, &self.selected_model),
139            &self.identities,
140            (&self.original_request, &self.target_request),
141            state,
142            limits,
143            progress,
144            false,
145        )?;
146        if !transport::send(
147            upstream,
148            target,
149            self.endpoint.clone(),
150            self.target_request.clone(),
151            limits,
152            progress,
153        )
154        .await?
155        {
156            return Ok(transport::rejected(progress));
157        }
158        self.recover(limits, state, progress, facts).await
159    }
160    /// Complete response conversion and pending state writes using the retained result; sends no request.
161    pub async fn recover<S: crate::capability::StateStore>(
162        &mut self,
163        limits: CodecLimits,
164        state: &super::GenerationStateAccess<'_, S>,
165        progress: &mut GenerationProgress<g::GenerateContentResponseBody>,
166        facts: impl FnOnce(
167            &g::GenerateContentResponseBody,
168        ) -> Result<p::ClaudeGeminiUsageFacts, TransformError>,
169    ) -> Result<GenerationOutcome<c::GenerateContentResponseBody>, TransformError> {
170        transport::bind(
171            (&self.endpoint, &self.selected_model),
172            &self.identities,
173            (&self.original_request, &self.target_request),
174            state,
175            limits,
176            progress,
177            true,
178        )?;
179        if progress
180            .raw_response
181            .as_ref()
182            .is_none_or(|r| !r.status.is_success())
183        {
184            return Err(TransformError::invalid_result(
185                "generation.recovery",
186                "successful native response required",
187            ));
188        }
189        let native = transport::recover_native(progress, limits)?;
190        let facts = facts(&native)?;
191        let converted = self.convert_response(native, facts)?;
192        transport::finish(progress, converted, self.report.clone(), limits)
193    }
194}
195
196/// A prepared Gemini client request executed by the selected Claude endpoint.
197#[derive(Debug)]
198pub struct GeminiViaClaude {
199    original_request: g::GenerateContentRequestBody,
200    target_request: c::GenerateContentRequestBody,
201    selected_model: String,
202    endpoint: Endpoint,
203    identities: GenerationIdentity,
204    report: Report,
205}
206
207impl GeminiViaClaude {
208    pub fn prepare(
209        input: g::GenerateContentRequestBody,
210        selected_model: impl Into<String>,
211        endpoint: Endpoint,
212        mut identities: GenerationIdentity,
213        max_tokens: Option<i64>,
214    ) -> Result<Self, TransformError> {
215        let selected_model = selected_model.into();
216        if selected_model.trim().is_empty() {
217            return Err(TransformError::missing_metadata("selected_model"));
218        }
219        let original_request = input.into_declared();
220
221        let converted = p::gemini_to_claude_request(
222            original_request.clone(),
223            &selected_model,
224            max_tokens,
225            &mut identities.request,
226            &identities.request_policy,
227        )?;
228        Ok(Self {
229            original_request,
230            target_request: converted.value.into_declared(),
231            selected_model,
232            endpoint,
233            identities,
234            report: converted.report,
235        })
236    }
237    /// Restore exact tool aliases and names from declared history/scoped state before mapping.
238    pub async fn prepare_with_state<S: crate::capability::StateStore>(
239        input: g::GenerateContentRequestBody,
240        endpoint: Endpoint,
241        identities: GenerationIdentity,
242        state: &super::GenerationStateAccess<'_, S>,
243        max_tokens: Option<i64>,
244    ) -> Result<Self, TransformError> {
245        let selected_model = state.target.model.clone();
246        let original = input.into_declared();
247        let (restored, names) = super::history::gemini(original.clone(), state).await?;
248        let _ = names;
249        let mut prepared =
250            Self::prepare(restored, selected_model, endpoint, identities, max_tokens)?;
251        prepared.original_request = original;
252        Ok(prepared)
253    }
254    /// Materialize foreign resources and restore scoped history before preparing the selected endpoint.
255    pub async fn prepare_with_capabilities<
256        S: crate::capability::StateStore,
257        R: crate::capability::ResourceAccess,
258    >(
259        input: g::GenerateContentRequestBody,
260        endpoint: Endpoint,
261        identities: GenerationIdentity,
262        state: &super::GenerationStateAccess<'_, S>,
263        resources: &super::GenerationResources<'_, R>,
264        max_tokens: Option<i64>,
265    ) -> Result<Self, TransformError> {
266        let original = input.into_declared();
267        let materialized = resources.gemini(original.clone()).await?;
268        let mut prepared =
269            Self::prepare_with_state(materialized, endpoint, identities, state, max_tokens).await?;
270        prepared.original_request = original;
271        Ok(prepared)
272    }
273
274    pub fn original_request(&self) -> &g::GenerateContentRequestBody {
275        &self.original_request
276    }
277    pub fn target_request(&self) -> &c::GenerateContentRequestBody {
278        &self.target_request
279    }
280    pub fn identities(&self) -> &GenerationIdentity {
281        &self.identities
282    }
283    pub fn report(&self) -> &Report {
284        &self.report
285    }
286    pub fn selected_model(&self) -> &str {
287        &self.selected_model
288    }
289    pub fn convert_response(
290        &mut self,
291        native: c::GenerateContentResponseBody,
292        facts: p::ClaudeGeminiUsageFacts,
293    ) -> Result<Converted<g::GenerateContentResponseBody>, TransformError> {
294        let facts = self.usage_facts(facts);
295        let native = native.into_declared();
296        p::claude_to_gemini_response(
297            native,
298            facts,
299            &mut self.identities.response,
300            &self.identities.response_policy,
301        )
302    }
303    pub(crate) fn usage_facts(
304        &self,
305        mut facts: p::ClaudeGeminiUsageFacts,
306    ) -> p::ClaudeGeminiUsageFacts {
307        // The prepared native request, not Gemini's display preference, tells
308        // us whether Claude thinking was enabled. A disabled request has an
309        // exact zero even when Anthropic omits output_tokens_details.
310        if matches!(
311            self.target_request.thinking,
312            None | Some(crate::wire::claude::count_tokens::ThinkingConfig::Disabled(
313                _
314            ))
315        ) {
316            facts.thinking_tokens.get_or_insert(0);
317        }
318        facts
319    }
320    /// Performs one POST. The factual supplement is obtained from the actual decoded response.
321    /// Conversion errors keep native bytes/results in caller-owned progress.
322    pub async fn invoke<U: Upstream, S: crate::capability::StateStore>(
323        &mut self,
324        upstream: &U,
325        target: &U::Target,
326        limits: CodecLimits,
327        state: &super::GenerationStateAccess<'_, S>,
328        progress: &mut GenerationProgress<c::GenerateContentResponseBody>,
329        facts: impl FnOnce(
330            &c::GenerateContentResponseBody,
331        ) -> Result<p::ClaudeGeminiUsageFacts, TransformError>,
332    ) -> Result<GenerationOutcome<g::GenerateContentResponseBody>, TransformError> {
333        transport::bind(
334            (&self.endpoint, &self.selected_model),
335            &self.identities,
336            (&self.original_request, &self.target_request),
337            state,
338            limits,
339            progress,
340            false,
341        )?;
342        if !transport::send(
343            upstream,
344            target,
345            self.endpoint.clone(),
346            self.target_request.clone(),
347            limits,
348            progress,
349        )
350        .await?
351        {
352            return Ok(transport::rejected(progress));
353        }
354        self.recover(limits, state, progress, facts).await
355    }
356    /// Complete response conversion and pending state writes using the retained result; sends no request.
357    pub async fn recover<S: crate::capability::StateStore>(
358        &mut self,
359        limits: CodecLimits,
360        state: &super::GenerationStateAccess<'_, S>,
361        progress: &mut GenerationProgress<c::GenerateContentResponseBody>,
362        facts: impl FnOnce(
363            &c::GenerateContentResponseBody,
364        ) -> Result<p::ClaudeGeminiUsageFacts, TransformError>,
365    ) -> Result<GenerationOutcome<g::GenerateContentResponseBody>, TransformError> {
366        transport::bind(
367            (&self.endpoint, &self.selected_model),
368            &self.identities,
369            (&self.original_request, &self.target_request),
370            state,
371            limits,
372            progress,
373            true,
374        )?;
375        if progress
376            .raw_response
377            .as_ref()
378            .is_none_or(|r| !r.status.is_success())
379        {
380            return Err(TransformError::invalid_result(
381                "generation.recovery",
382                "successful native response required",
383            ));
384        }
385        let native = transport::recover_native(progress, limits)?;
386        let facts = facts(&native)?;
387        let converted = self.convert_response(native, facts)?;
388        transport::finish(progress, converted, self.report.clone(), limits)
389    }
390}
391
392super::prepared::prepared_generation!(ClaudeViaGemini, c::GenerateContentRequestBody => g::GenerateContentRequestBody);
393super::prepared::stream_synthesis!(ClaudeViaGemini, c::GenerateContentRequestBody, context: p::ClaudeGeminiRequestContext);
394super::prepared::prepared_generation!(GeminiViaClaude, g::GenerateContentRequestBody => c::GenerateContentRequestBody);
395super::prepared::stream_synthesis!(GeminiViaClaude, g::GenerateContentRequestBody, max_tokens: Option<i64>);