Skip to main content

gproxy_protocol/transform/generate/stream/responses/
lifecycle.rs

1use super::{
2    items::{Item, item_id},
3    parts::Part,
4    *,
5};
6use crate::transform::Converted;
7
8impl ResponsesStreamCollector {
9    pub(super) fn check_response(
10        &self,
11        v: &r::GenerateContentResponseBody,
12        terminal: bool,
13    ) -> Result<(), TransformError> {
14        if v.id.is_empty() || v.model.is_empty() || v.created_at < 0 {
15            return Err(invalid("missing/invalid native response identity facts"));
16        }
17        if let Some(old) = &self.response
18            && (old.id != v.id || old.model != v.model || old.created_at != v.created_at)
19        {
20            return Err(invalid("response identity/model/creation changed"));
21        }
22        if !terminal
23            && (!matches!(
24                v.status,
25                Some(r::ResponseStatus::InProgress | r::ResponseStatus::Queued)
26            ) || v.error.is_some()
27                || v.incomplete_details.is_some()
28                || v.completed_at.flatten().is_some())
29        {
30            return Err(invalid(
31                "nonterminal response contains terminal status/facts",
32            ));
33        }
34        Ok(())
35    }
36    pub(super) fn terminal(
37        &mut self,
38        v: r::GenerateContentResponseBody,
39        status: r::ResponseStatus,
40    ) -> Result<(), TransformError> {
41        self.check_response(&v, true)?;
42        for item in &v.output {
43            super::status::item(item, status == r::ResponseStatus::Completed)?;
44        }
45        let (text, json) = v
46            .output
47            .iter()
48            .map(item_sizes)
49            .fold((0usize, 0usize), |(a, b), (c, d)| {
50                (a.saturating_add(c), b.saturating_add(d))
51            });
52        if text > self.limits.max_text_bytes || json > self.limits.max_json_bytes {
53            return Err(limit());
54        }
55
56        if v.status != Some(status)
57            || v.error.is_some()
58            || (status == r::ResponseStatus::Completed && v.incomplete_details.is_some())
59        {
60            return Err(invalid("terminal response status mismatch"));
61        }
62        if status == r::ResponseStatus::Incomplete
63            && v.incomplete_details
64                .as_ref()
65                .and_then(|v| v.reason)
66                .is_none()
67        {
68            return Err(invalid("incomplete response missing reason"));
69        }
70        if status == r::ResponseStatus::Completed
71            && self
72                .items
73                .iter()
74                .any(|item| item.parts.values().any(|part| part.incomplete))
75        {
76            return Err(invalid(
77                "completed response contains incomplete summary part",
78            ));
79        }
80        if self.items.len() != v.output.len()
81            || self
82                .items
83                .iter()
84                .zip(&v.output)
85                .any(|(a, b)| !a.done || &a.value != b)
86        {
87            return Err(invalid(
88                "terminal response contradicts/misses completed output items",
89            ));
90        }
91        if let Some(Some(text)) = &v.output_text {
92            let actual: String = v
93                .output
94                .iter()
95                .filter_map(|v| {
96                    if let r::ResponseOutputItem::Message(v) = v {
97                        Some(v)
98                    } else {
99                        None
100                    }
101                })
102                .flat_map(|v| &v.content)
103                .filter_map(|v| {
104                    if let i::OutputContent::Text(v) = v {
105                        Some(v.text.as_str())
106                    } else {
107                        None
108                    }
109                })
110                .collect();
111            if text != &actual {
112                return Err(invalid("output_text contradicts output"));
113            }
114        }
115        if let Some(u) = v.usage.as_ref().and_then(Option::as_ref)
116            && ([
117                u.input_tokens,
118                u.output_tokens,
119                u.total_tokens,
120                u.input_tokens_details.cached_tokens,
121                u.input_tokens_details.cache_write_tokens,
122                u.output_tokens_details.reasoning_tokens,
123            ]
124            .iter()
125            .any(|v| *v < 0)
126                || u.input_tokens.checked_add(u.output_tokens) != Some(u.total_tokens)
127                || u.input_tokens_details
128                    .cached_tokens
129                    .checked_add(u.input_tokens_details.cache_write_tokens)
130                    .is_none_or(|total| total > u.input_tokens)
131                || u.output_tokens_details.reasoning_tokens > u.output_tokens)
132        {
133            return Err(invalid("invalid terminal usage"));
134        }
135        self.response = Some(v);
136        self.done = true;
137        Ok(())
138    }
139    pub(super) fn item(&mut self, index: i64, id: &str) -> Result<&mut Item, TransformError> {
140        let item = self
141            .items
142            .get_mut(usize::try_from(index).map_err(|_| invalid("negative output index"))?)
143            .ok_or_else(|| invalid("unknown output index"))?;
144        if !item.done && item_id(&item.value).is_none() {
145            if id.is_empty() || !self.ids.insert(id.to_owned()) {
146                return Err(invalid("empty/duplicate late item ID"));
147            }
148            match &mut item.value {
149                r::ResponseOutputItem::FunctionCall(v) => v.id = Some(id.into()),
150                r::ResponseOutputItem::CustomToolCall(v) => v.id = Some(id.into()),
151                _ => return Err(invalid("item cannot receive late ID")),
152            }
153        }
154        if item.done || item_id(&item.value) != Some(id) {
155            return Err(invalid("item ID/index/phase mismatch"));
156        }
157        Ok(item)
158    }
159    pub(super) fn part(
160        &mut self,
161        index: i64,
162        id: &str,
163        content: i64,
164        summary: bool,
165    ) -> Result<&mut Part, TransformError> {
166        self.item(index, id)?
167            .parts
168            .get_mut(&(summary, content))
169            .ok_or_else(|| invalid("content part not added"))
170    }
171    pub(super) fn add_part(
172        &mut self,
173        index: i64,
174        id: &str,
175        content: i64,
176        summary: bool,
177        value: s::OutputContentPart,
178    ) -> Result<(), TransformError> {
179        let text = match &value {
180            s::OutputContentPart::Text(v) => &v.text,
181            s::OutputContentPart::Refusal(v) => &v.refusal,
182            s::OutputContentPart::Reasoning(v) => &v.text,
183        };
184        self.charge(text.len(), false)?;
185        let item = self.item(index, id)?;
186        let expected =
187            item.base_parts(summary)? + item.parts.keys().filter(|(s, _)| *s == summary).count();
188        if content != expected as i64 {
189            return Err(invalid("noncontiguous/duplicate content index"));
190        }
191        if !matches!(
192            (&item.value, &value, summary),
193            (
194                r::ResponseOutputItem::Message(_),
195                s::OutputContentPart::Text(_) | s::OutputContentPart::Refusal(_),
196                false
197            ) | (
198                r::ResponseOutputItem::Reasoning(_),
199                s::OutputContentPart::Reasoning(_),
200                _
201            )
202        ) {
203            return Err(invalid("content type incompatible with item"));
204        }
205        item.parts
206            .insert((summary, content), Part::new(value, summary));
207        Ok(())
208    }
209    pub(super) fn delta(
210        &mut self,
211        index: i64,
212        id: &str,
213        content: i64,
214        kind: u8,
215        text: &str,
216    ) -> Result<&mut Part, TransformError> {
217        self.charge(text.len(), false)?;
218        let p = self.part(index, id, content, kind == 3)?;
219        p.check_kind(kind)?;
220        p.seeded = false;
221        p.text_mut().push_str(text);
222        Ok(p)
223    }
224    pub(super) fn text_done(
225        &mut self,
226        index: i64,
227        id: &str,
228        content: i64,
229        kind: u8,
230        text: &str,
231        logs: &[s::StreamLogprob],
232    ) -> Result<(), TransformError> {
233        let p = self.part(index, id, content, kind == 3)?;
234        p.check_kind(kind)?;
235        p.finish_text(text, logs)
236    }
237    pub(super) fn argument(
238        &mut self,
239        index: i64,
240        id: &str,
241        kind: u8,
242        text: &str,
243        done: bool,
244    ) -> Result<(), TransformError> {
245        if !done {
246            self.charge(text.len(), true)?;
247        }
248        let item = self.item(index, id)?;
249        let actual = match &item.value {
250            r::ResponseOutputItem::FunctionCall(_) => 0,
251            r::ResponseOutputItem::CustomToolCall(_) => 1,
252            r::ResponseOutputItem::CodeInterpreterCall(_) => 2,
253            r::ResponseOutputItem::McpCall(_) => 3,
254            _ => 255,
255        };
256        if actual != kind || item.argument_done {
257            return Err(invalid("argument event type/phase mismatch"));
258        }
259        let mut completed_bytes = 0;
260        if done {
261            if item.argument_streamed {
262                if item.arguments()? != text {
263                    return Err(invalid("argument done contradicts deltas"));
264                }
265            } else {
266                let suffix = text
267                    .strip_prefix(item.arguments()?)
268                    .ok_or_else(|| invalid("argument done contradicts initial arguments"))?;
269                completed_bytes = suffix.len();
270                item.arguments_mut()?.push_str(suffix);
271            }
272            item.argument_done = true;
273        } else {
274            item.arguments_mut()?.push_str(text);
275        }
276        item.argument_streamed = true;
277        self.charge(completed_bytes, true)
278    }
279    pub(super) fn charge(&mut self, n: usize, json: bool) -> Result<(), TransformError> {
280        let (used, cap) = if json {
281            (&mut self.json_bytes, self.limits.max_json_bytes)
282        } else {
283            (&mut self.text_bytes, self.limits.max_text_bytes)
284        };
285        *used = used.checked_add(n).ok_or_else(limit)?;
286        if *used > cap { Err(limit()) } else { Ok(()) }
287    }
288    pub(super) fn charge_item(
289        &mut self,
290        item: &r::ResponseOutputItem,
291    ) -> Result<(), TransformError> {
292        let (text, json) = item_sizes(item);
293        if text > self.limits.max_text_bytes || json > self.limits.max_json_bytes {
294            Err(limit())
295        } else {
296            Ok(())
297        }
298    }
299
300    pub(super) fn check_retained_sizes(&self) -> Result<(), TransformError> {
301        let (mut text, mut json) = (0usize, 0usize);
302        for item in &self.items {
303            let (t, j) = item_sizes(&item.value);
304            text = text.saturating_add(t);
305            json = json.saturating_add(j);
306            if !item.done {
307                for part in item.parts.values() {
308                    text = text.saturating_add(part.text().len());
309                }
310            }
311        }
312        if text > self.limits.max_text_bytes || json > self.limits.max_json_bytes {
313            Err(limit())
314        } else {
315            Ok(())
316        }
317    }
318    pub fn finish(self) -> Result<Converted<r::GenerateContentResponseBody>, TransformError> {
319        if self.failed || !self.done {
320            return Err(invalid("stream failed or ended before terminal response"));
321        }
322        Ok(Converted {
323            value: self.response.ok_or_else(|| invalid("missing response"))?,
324            report: self.report,
325        })
326    }
327}
328
329pub(super) fn item_sizes(item: &r::ResponseOutputItem) -> (usize, usize) {
330    let (mut text, mut json) = (0usize, 0usize);
331    match item {
332        r::ResponseOutputItem::Message(v) => {
333            for p in &v.content {
334                text = text.saturating_add(match p {
335                    i::OutputContent::Text(v) => v.text.len(),
336                    i::OutputContent::Refusal(v) => v.refusal.len(),
337                });
338            }
339        }
340        r::ResponseOutputItem::Reasoning(v) => {
341            for p in &v.summary {
342                text = text.saturating_add(p.text.len());
343            }
344            for p in v.content.iter().flatten() {
345                text = text.saturating_add(p.text.len());
346            }
347        }
348        r::ResponseOutputItem::FunctionCall(v) => json = v.arguments.len(),
349        r::ResponseOutputItem::CustomToolCall(v) => json = v.input.len(),
350        r::ResponseOutputItem::McpCall(v) => json = v.arguments.len(),
351        r::ResponseOutputItem::CodeInterpreterCall(v) => {
352            json = v.code.as_ref().map_or(0, String::len)
353        }
354        _ => {}
355    }
356    (text, json)
357}