Skip to main content

gproxy_protocol/adapt/generate/image_resources/
read.rs

1use 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    /// Returns a separate inline conversion view; original fileData and its
14    /// signature stay in retained progress for exact scoped replay.
15    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                // The signature stays: the client carries it back with these
119                // bytes, which replay inline in place of the fileData URI.
120                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            // Body, accumulated bytes and accounting are owned by progress,
181            // rather than by the cancellable future calling this method.
182            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}