Skip to main content

gproxy_protocol/adapt/generate/
chat_gemini.rs

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