gproxy_protocol/adapt/generate/image_resources/
read.rs1use super::*;
2use crate::{
3 HttpBody,
4 capability::{ResourceAccess, ResourceReference},
5 codec,
6 transform::images::inspect_image,
7 wire::DeclaredFields,
8};
9use base64::{Engine as _, engine::general_purpose::STANDARD};
10use futures_util::StreamExt;
11
12impl GeminiImageReads {
13 pub async fn materialize<R: ResourceAccess>(
16 &mut self,
17 input: &g::GenerateContentResponseBody,
18 resources: &super::super::GenerationResources<'_, R>,
19 ) -> Result<g::GenerateContentResponseBody, TransformError> {
20 if self.failed {
21 return Err(invalid("image read progress failed"));
22 }
23 let result = self.materialize_inner(input, resources).await;
24 if result.as_ref().err().is_some_and(|error| {
25 matches!(
26 error.kind(),
27 TransformErrorKind::InvalidInput
28 | TransformErrorKind::InvalidResult
29 | TransformErrorKind::Unsupported
30 | TransformErrorKind::Limit
31 )
32 }) {
33 self.pending = None;
34 self.failed = true;
35 }
36 result
37 }
38 async fn materialize_inner<R: ResourceAccess>(
39 &mut self,
40 input: &g::GenerateContentResponseBody,
41 resources: &super::super::GenerationResources<'_, R>,
42 ) -> Result<g::GenerateContentResponseBody, TransformError> {
43 let input = input.clone().into_declared();
44 codec::encode_json(&input, resources.limits).map_err(codec_error)?;
45 if let Some(original) = &self.original {
46 if original != &input {
47 return Err(conflict("native image response changed during recovery"));
48 }
49 } else {
50 self.original = Some(input.clone());
51 }
52 let mut output = input;
53 for (candidate_index, candidate) in output.candidates.iter_mut().flatten().enumerate() {
54 for (part_index, part) in candidate
55 .content
56 .iter_mut()
57 .flat_map(|content| content.parts.iter_mut().flatten())
58 .enumerate()
59 {
60 let Some(file) = &part.file_data else {
61 continue;
62 };
63 if part.inline_data.is_some()
64 || part.text.is_some()
65 || part.thought == Some(true)
66 || part.function_call.is_some()
67 || part.function_response.is_some()
68 || part.executable_code.is_some()
69 || part.code_execution_result.is_some()
70 || part.tool_call.is_some()
71 || part.tool_response.is_some()
72 || part.video_metadata.is_some()
73 || part.media_processing.is_some()
74 || part.audio_transcription.is_some()
75 || part.speech_metadata.is_some()
76 || part.part_metadata.is_some()
77 || part.media_resolution.is_some()
78 || file.file_uri.trim().is_empty()
79 {
80 return Err(invalid(
81 "generated file image must be an unambiguous standalone Part",
82 ));
83 }
84 let key = (candidate_index, part_index);
85 if !self.images.contains_key(&key) {
86 self.read_image(key, file, resources).await?;
87 let pending = self.pending.take().expect("completed retained read");
88 let actual = inspect_image(&pending.buffer)?.mime();
89 if pending
90 .metadata
91 .mime
92 .as_deref()
93 .is_some_and(|mime| mime != actual)
94 || file.mime_type.as_deref().is_some_and(|mime| mime != actual)
95 {
96 return Err(invalid("generated image MIME contradicts actual container"));
97 }
98 self.images.insert(
99 key,
100 MaterializedImage {
101 candidate: candidate_index,
102 part: part_index,
103 original: part.clone(),
104 resource_expiry: pending.metadata.expires_at,
105 materialized: g::Blob::builder(
106 actual.to_owned(),
107 STANDARD.encode(pending.buffer),
108 )
109 .build(),
110 },
111 );
112 }
113 let image = self.images.get(&key).expect("materialized image retained");
114 if &image.original != part {
115 return Err(conflict("native image Part changed"));
116 }
117 part.file_data = None;
118 part.inline_data = Some(image.materialized.clone());
121 }
122 }
123 codec::encode_json(&output, resources.limits).map_err(codec_error)?;
124 Ok(output)
125 }
126 async fn read_image<R: ResourceAccess>(
127 &mut self,
128 key: (usize, usize),
129 file: &g::FileData,
130 resources: &super::super::GenerationResources<'_, R>,
131 ) -> Result<(), TransformError> {
132 let reference = ResourceReference::Url(file.file_uri.clone());
133 let total = resources
134 .limits
135 .max_body_bytes
136 .min(resources.access.limits().read_bytes);
137 if let Some(pending) = &self.pending {
138 if pending.key != key || pending.reference != reference {
139 return Err(conflict("pending image read changed"));
140 }
141 } else {
142 if self.attempted >= resources.max_references || self.bytes >= total {
143 return Err(limit());
144 }
145 self.attempted += 1;
146 let read = resources.access.read(resources.scope, &reference).await?;
147 self.pending = Some(PendingImageRead {
148 key,
149 reference,
150 metadata: read.metadata,
151 body: read.body,
152 buffer: Vec::new(),
153 limit: total
154 .saturating_sub(self.bytes)
155 .min(resources.limits.max_buffer_bytes)
156 .min(resources.limits.max_part_bytes),
157 });
158 }
159 let pending = self.pending.as_mut().expect("retained resource body");
160 if pending
161 .metadata
162 .expires_at
163 .is_some_and(|expiry| expiry <= resources.now)
164 {
165 return Err(missing("generated image resource expired"));
166 }
167 let available = total
168 .saturating_sub(self.bytes)
169 .saturating_add(pending.buffer.len() as u64);
170 pending.limit = pending
171 .limit
172 .min(available)
173 .min(resources.limits.max_buffer_bytes)
174 .min(resources.limits.max_part_bytes);
175 if pending.metadata.length.is_some_and(|n| n > pending.limit) || self.bytes > total {
176 return Err(limit());
177 }
178 let mut chunks_since_yield = 0usize;
179 loop {
180 let next = match &mut self.pending.as_mut().expect("retained body").body {
183 HttpBody::Bytes(bytes) => {
184 if bytes.is_empty() {
185 None
186 } else {
187 Some(Ok(std::mem::take(bytes)))
188 }
189 }
190 HttpBody::Stream(stream) => stream.next().await,
191 };
192 let Some(chunk) = next else {
193 break;
194 };
195 let chunk = chunk.map_err(|error| {
196 TransformError::with_source(
197 TransformErrorKind::Host,
198 "generation.image_resources.read",
199 error.to_string(),
200 error,
201 )
202 })?;
203 self.bytes = self
204 .bytes
205 .checked_add(chunk.len() as u64)
206 .ok_or_else(limit)?;
207 let pending = self.pending.as_mut().expect("retained body");
208 if self.bytes > total
209 || (pending.buffer.len() as u64).saturating_add(chunk.len() as u64) > pending.limit
210 {
211 return Err(limit());
212 }
213 pending.buffer.extend_from_slice(&chunk);
214 chunks_since_yield += 1;
215 if chunks_since_yield == 1024 {
216 chunks_since_yield = 0;
217 let mut yielded = false;
218 std::future::poll_fn(|cx| {
219 if yielded {
220 std::task::Poll::Ready(())
221 } else {
222 yielded = true;
223 cx.waker().wake_by_ref();
224 std::task::Poll::Pending
225 }
226 })
227 .await;
228 }
229 }
230 let pending = self.pending.as_ref().expect("complete retained body");
231 if pending
232 .metadata
233 .length
234 .is_some_and(|n| n != pending.buffer.len() as u64)
235 {
236 return Err(invalid(
237 "generated image length contradicts resource metadata",
238 ));
239 }
240 Ok(())
241 }
242}