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#[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 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 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 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 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#[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#[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 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 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 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 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;