Skip to main content

gproxy_protocol/adapt/generate/
gemini_responses.rs

1use super::{Endpoint, GenerationIdentity, GenerationOutcome, GenerationProgress, transport};
2use crate::transform::generate::gemini_responses as p;
3use crate::wire::gemini as g;
4use crate::wire::openai::responses as r;
5use crate::{
6    Dialect,
7    capability::Upstream,
8    codec::CodecLimits,
9    transform::{Converted, Report, TransformError},
10    wire::DeclaredFields,
11};
12
13/// A prepared Gemini client request executed by the selected Responses endpoint.
14#[derive(Debug)]
15pub struct GeminiViaResponses {
16    original_request: g::GenerateContentRequestBody,
17    target_request: r::GenerateContentRequestBody,
18    selected_model: String,
19    endpoint: Endpoint,
20    identities: GenerationIdentity,
21    report: Report,
22}
23
24impl GeminiViaResponses {
25    pub fn prepare(
26        input: g::GenerateContentRequestBody,
27        selected_model: impl Into<String>,
28        endpoint: Endpoint,
29        mut identities: GenerationIdentity,
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
37        let converted = p::gemini_to_responses_request(
38            original_request.clone(),
39            &selected_model,
40            &mut identities.request,
41            &identities.request_policy,
42        )?;
43        Ok(Self {
44            original_request,
45            target_request: converted.value.into_declared(),
46            selected_model,
47            endpoint,
48            identities,
49            report: converted.report,
50        })
51    }
52    /// Restore exact tool aliases and names from declared history/scoped state before mapping.
53    pub async fn prepare_with_state<S: crate::capability::StateStore>(
54        input: g::GenerateContentRequestBody,
55        endpoint: Endpoint,
56        identities: GenerationIdentity,
57        state: &super::GenerationStateAccess<'_, S>,
58    ) -> Result<Self, TransformError> {
59        let selected_model = state.target.model.clone();
60        let original = input.into_declared();
61        let (restored, names) = super::history::gemini(original.clone(), state).await?;
62        let _ = names;
63        let mut prepared = Self::prepare(restored, selected_model, endpoint, identities)?;
64        prepared.original_request = original;
65        Ok(prepared)
66    }
67    /// Materialize foreign resources and restore scoped history before preparing the selected endpoint.
68    pub async fn prepare_with_capabilities<
69        S: crate::capability::StateStore,
70        R: crate::capability::ResourceAccess,
71    >(
72        input: g::GenerateContentRequestBody,
73        endpoint: Endpoint,
74        identities: GenerationIdentity,
75        state: &super::GenerationStateAccess<'_, S>,
76        resources: &super::GenerationResources<'_, R>,
77    ) -> Result<Self, TransformError> {
78        let original = input.into_declared();
79
80        let mut materialized = resources.gemini(original.clone()).await?;
81        if let Some(image) = materialized
82            .generation_config
83            .as_mut()
84            .and_then(|v| v.response_format.as_mut())
85            .and_then(|v| v.image.as_mut())
86            && image.delivery == Some(g::Delivery::Uri)
87        {
88            image.delivery = Some(g::Delivery::Inline);
89        }
90        let mut prepared =
91            Self::prepare_with_state(materialized, endpoint, identities, state).await?;
92        prepared.original_request = original;
93        Ok(prepared)
94    }
95
96    pub fn original_request(&self) -> &g::GenerateContentRequestBody {
97        &self.original_request
98    }
99    pub fn target_request(&self) -> &r::GenerateContentRequestBody {
100        &self.target_request
101    }
102    pub fn identities(&self) -> &GenerationIdentity {
103        &self.identities
104    }
105    pub fn report(&self) -> &Report {
106        &self.report
107    }
108    pub fn selected_model(&self) -> &str {
109        &self.selected_model
110    }
111    pub fn convert_response(
112        &mut self,
113        native: r::GenerateContentResponseBody,
114        facts: p::GeminiReplayContext,
115    ) -> Result<Converted<g::GenerateContentResponseBody>, TransformError> {
116        if self
117            .original_request
118            .generation_config
119            .as_ref()
120            .and_then(|v| v.response_format.as_ref())
121            .and_then(|v| v.image.as_ref())
122            .and_then(|v| v.delivery.as_ref())
123            == Some(&g::Delivery::Uri)
124        {
125            return Err(TransformError::new(
126                crate::transform::TransformErrorKind::MissingState,
127                "image.delivery",
128                "URI output requires the image resource invocation adapter",
129            ));
130        }
131        self.convert_response_inline(native, facts)
132    }
133    pub(super) fn convert_response_inline(
134        &mut self,
135        native: r::GenerateContentResponseBody,
136        facts: p::GeminiReplayContext,
137    ) -> Result<Converted<g::GenerateContentResponseBody>, TransformError> {
138        let native = native.into_declared();
139        let modalities = self
140            .original_request
141            .generation_config
142            .as_ref()
143            .and_then(|v| v.response_modalities.as_deref());
144        let mut converted =
145            p::responses_to_gemini_response_with_modalities(native.clone(), facts, modalities)?;
146        if self
147            .original_request
148            .generation_config
149            .as_ref()
150            .and_then(|v| v.response_format.as_ref())
151            .and_then(|v| v.image.as_ref())
152            .and_then(|v| v.mime_type.as_ref())
153            == Some(&g::ImageMimeType::ImageJpeg)
154        {
155            for blob in converted
156                .value
157                .candidates
158                .iter()
159                .flatten()
160                .filter_map(|c| c.content.as_ref())
161                .flat_map(|c| c.parts.iter().flatten())
162                .filter_map(|p| p.inline_data.as_ref())
163            {
164                if blob.mime_type != "image/jpeg" {
165                    return Err(TransformError::invalid_result(
166                        "image.mime",
167                        "actual image does not match requested JPEG output",
168                    ));
169                }
170            }
171        }
172        super::request_ids::gemini_response(&mut converted.value, &mut self.identities, &native)?;
173        Ok(converted)
174    }
175    /// Performs one POST. The factual supplement is obtained from the actual decoded response.
176    /// Conversion errors keep native bytes/results in caller-owned progress.
177    pub async fn invoke<U: Upstream, S: crate::capability::StateStore>(
178        &mut self,
179        upstream: &U,
180        target: &U::Target,
181        limits: CodecLimits,
182        state: &super::GenerationStateAccess<'_, S>,
183        progress: &mut GenerationProgress<r::GenerateContentResponseBody>,
184        facts: impl FnOnce(
185            &r::GenerateContentResponseBody,
186        ) -> Result<p::GeminiReplayContext, TransformError>,
187    ) -> Result<GenerationOutcome<g::GenerateContentResponseBody>, TransformError> {
188        transport::bind(
189            (&self.endpoint, &self.selected_model),
190            &self.identities,
191            (&self.original_request, &self.target_request),
192            state,
193            limits,
194            progress,
195            false,
196        )?;
197        if !transport::send(
198            upstream,
199            target,
200            self.endpoint.clone(),
201            self.target_request.clone(),
202            limits,
203            progress,
204        )
205        .await?
206        {
207            return Ok(transport::rejected(progress));
208        }
209        self.recover(limits, state, progress, facts).await
210    }
211    /// Complete response conversion and pending state writes using the retained result; sends no request.
212    pub async fn recover<S: crate::capability::StateStore>(
213        &mut self,
214        limits: CodecLimits,
215        state: &super::GenerationStateAccess<'_, S>,
216        progress: &mut GenerationProgress<r::GenerateContentResponseBody>,
217        facts: impl FnOnce(
218            &r::GenerateContentResponseBody,
219        ) -> Result<p::GeminiReplayContext, TransformError>,
220    ) -> Result<GenerationOutcome<g::GenerateContentResponseBody>, TransformError> {
221        transport::bind(
222            (&self.endpoint, &self.selected_model),
223            &self.identities,
224            (&self.original_request, &self.target_request),
225            state,
226            limits,
227            progress,
228            true,
229        )?;
230        if progress
231            .raw_response
232            .as_ref()
233            .is_none_or(|r| !r.status.is_success())
234        {
235            return Err(TransformError::invalid_result(
236                "generation.recovery",
237                "successful native response required",
238            ));
239        }
240        let native = transport::recover_native(progress, limits)?;
241        let facts = facts(&native)?;
242        let converted = self.convert_response(native, facts)?;
243        transport::finish(progress, converted, self.report.clone(), limits)
244    }
245}
246
247/// Effective settings and measured facts absent from the upstream wire response.
248#[derive(Debug)]
249pub struct GeminiReturnFacts {
250    pub parallel_tool_calls: bool,
251    pub tool_choice: r::input::ToolChoice,
252    pub prompt_cache_options: Option<r::response::ResponsePromptCacheOptions>,
253    pub usage: p::GeminiUsageFacts,
254    pub created_at: i64,
255}
256
257/// A prepared Responses client request executed by the selected Gemini endpoint.
258#[derive(Debug)]
259pub struct ResponsesViaGemini {
260    original_request: r::GenerateContentRequestBody,
261    target_request: g::GenerateContentRequestBody,
262    selected_model: String,
263    endpoint: Endpoint,
264    identities: GenerationIdentity,
265    report: Report,
266}
267
268impl ResponsesViaGemini {
269    pub fn prepare(
270        input: r::GenerateContentRequestBody,
271        selected_model: impl Into<String>,
272        endpoint: Endpoint,
273        mut identities: GenerationIdentity,
274        context: p::GeminiReplayContext,
275    ) -> Result<Self, TransformError> {
276        let selected_model = selected_model.into();
277        if selected_model.trim().is_empty() {
278            return Err(TransformError::missing_metadata("selected_model"));
279        }
280        let original_request = input.into_declared();
281        if original_request.stream.flatten() == Some(true) {
282            return Err(TransformError::unsupported(
283                "stream",
284                "use the incremental invocation entrypoint",
285            ));
286        }
287        let mut converted =
288            p::responses_to_gemini_request(original_request.clone(), &selected_model, context)?;
289        super::request_ids::gemini_request(&mut converted.value, &mut identities, Dialect::OpenAi)?;
290        Ok(Self {
291            original_request,
292            target_request: converted.value.into_declared(),
293            selected_model,
294            endpoint,
295            identities,
296            report: converted.report,
297        })
298    }
299    /// Restore exact tool aliases and names from declared history/scoped state before mapping.
300    pub async fn prepare_with_state<S: crate::capability::StateStore>(
301        input: r::GenerateContentRequestBody,
302        endpoint: Endpoint,
303        identities: GenerationIdentity,
304        state: &super::GenerationStateAccess<'_, S>,
305        context: p::GeminiReplayContext,
306    ) -> Result<Self, TransformError> {
307        let selected_model = state.target.model.clone();
308        let original = input.into_declared();
309        let mut lowered = original.clone();
310        let mut tool_report = Report::default();
311        crate::transform::generate::client_tools::Bindings::for_target(&original, Dialect::Gemini)?
312            .lower(&mut lowered, &mut tool_report)?;
313        let (restored, _) = super::history::responses(lowered, state).await?;
314        if context
315            .target
316            .as_ref()
317            .is_some_and(|target| target != &state.target)
318        {
319            return Err(TransformError::shape(
320                "signature.context",
321                "caller replay binding conflicts with selected state",
322            ));
323        }
324        let mut prepared = Self::prepare(restored, selected_model, endpoint, identities, context)?;
325        prepared.original_request = original;
326        prepared.report.diagnostics.extend(tool_report.diagnostics);
327        Ok(prepared)
328    }
329    /// Materialize foreign resources and restore scoped history before preparing the selected endpoint.
330    pub async fn prepare_with_capabilities<
331        S: crate::capability::StateStore,
332        R: crate::capability::ResourceAccess,
333    >(
334        input: r::GenerateContentRequestBody,
335        endpoint: Endpoint,
336        identities: GenerationIdentity,
337        state: &super::GenerationStateAccess<'_, S>,
338        resources: &super::GenerationResources<'_, R>,
339        context: p::GeminiReplayContext,
340    ) -> Result<Self, TransformError> {
341        let original = input.into_declared();
342        let materialized = resources.responses(original.clone()).await?;
343        let mut prepared =
344            Self::prepare_with_state(materialized, endpoint, identities, state, context).await?;
345        prepared.original_request = original;
346        Ok(prepared)
347    }
348
349    pub fn original_request(&self) -> &r::GenerateContentRequestBody {
350        &self.original_request
351    }
352    pub fn target_request(&self) -> &g::GenerateContentRequestBody {
353        &self.target_request
354    }
355    pub fn identities(&self) -> &GenerationIdentity {
356        &self.identities
357    }
358    pub fn report(&self) -> &Report {
359        &self.report
360    }
361    pub fn selected_model(&self) -> &str {
362        &self.selected_model
363    }
364    pub fn convert_response(
365        &mut self,
366        native: g::GenerateContentResponseBody,
367        facts: GeminiReturnFacts,
368    ) -> Result<Converted<r::GenerateContentResponseBody>, TransformError> {
369        let native = native.into_declared();
370        p::gemini_to_responses_response(
371            native,
372            p::GeminiResponseContext {
373                request: self.original_request.clone(),
374                effective_parallel_tool_calls: facts.parallel_tool_calls,
375                effective_tool_choice: facts.tool_choice,
376                usage: facts.usage,
377                created_at: facts.created_at,
378                effective_prompt_cache_options: facts.prompt_cache_options,
379            },
380            &mut self.identities.response,
381            &self.identities.response_policy,
382        )
383    }
384    /// Performs one POST. The factual supplement is obtained from the actual decoded response.
385    /// Conversion errors keep native bytes/results in caller-owned progress.
386    pub async fn invoke<U: Upstream, S: crate::capability::StateStore>(
387        &mut self,
388        upstream: &U,
389        target: &U::Target,
390        limits: CodecLimits,
391        state: &super::GenerationStateAccess<'_, S>,
392        progress: &mut GenerationProgress<g::GenerateContentResponseBody>,
393        facts: impl FnOnce(&g::GenerateContentResponseBody) -> Result<GeminiReturnFacts, TransformError>,
394    ) -> Result<GenerationOutcome<r::GenerateContentResponseBody>, TransformError> {
395        transport::bind(
396            (&self.endpoint, &self.selected_model),
397            &self.identities,
398            (&self.original_request, &self.target_request),
399            state,
400            limits,
401            progress,
402            false,
403        )?;
404        if !transport::send(
405            upstream,
406            target,
407            self.endpoint.clone(),
408            self.target_request.clone(),
409            limits,
410            progress,
411        )
412        .await?
413        {
414            return Ok(transport::rejected(progress));
415        }
416        self.recover(limits, state, progress, facts).await
417    }
418    /// Complete response conversion and pending state writes using the retained result; sends no request.
419    pub async fn recover<S: crate::capability::StateStore>(
420        &mut self,
421        limits: CodecLimits,
422        state: &super::GenerationStateAccess<'_, S>,
423        progress: &mut GenerationProgress<g::GenerateContentResponseBody>,
424        facts: impl FnOnce(&g::GenerateContentResponseBody) -> Result<GeminiReturnFacts, TransformError>,
425    ) -> Result<GenerationOutcome<r::GenerateContentResponseBody>, TransformError> {
426        transport::bind(
427            (&self.endpoint, &self.selected_model),
428            &self.identities,
429            (&self.original_request, &self.target_request),
430            state,
431            limits,
432            progress,
433            true,
434        )?;
435        if progress
436            .raw_response
437            .as_ref()
438            .is_none_or(|r| !r.status.is_success())
439        {
440            return Err(TransformError::invalid_result(
441                "generation.recovery",
442                "successful native response required",
443            ));
444        }
445        let native = transport::recover_native(progress, limits)?;
446        let facts = facts(&native)?;
447        let converted = self.convert_response(native, facts)?;
448        transport::finish(progress, converted, self.report.clone(), limits)
449    }
450}
451
452mod synthesis;
453
454mod image_resources;