Skip to main content

gproxy_transform/transform/stream_adapter/
responses.rs

1//! Typed aggregation state machine for Responses SSE streams: fills in
2//! synthetic lifecycle events (item_added / *_done / response.completed) that
3//! sparse upstreams omit, so the inbound side always sees a complete stream.
4
5mod text;
6mod tool;
7
8use std::collections::BTreeMap;
9
10use crate::protocol::openai::{
11    Extra, KnownResponseStreamEvent as KnownEvent, ResponseItem, ResponseMessageItem,
12    ResponseObject, ResponseObjectType, ResponseOutputItem, ResponseStatus, ResponseStreamEvent,
13    TypedResponseItem,
14};
15
16use super::{SseDecoder, SseFrame, encode_responses_event};
17use crate::transform::TransformError;
18use text::ResponsesTextItemState;
19use tool::{ResponsesToolItemState, ResponsesToolKind};
20
21/// Stateful normalizer for an upstream that already speaks Responses SSE.
22/// Frames outside the currently modeled typed schema are forwarded unchanged.
23#[derive(Default)]
24pub struct ResponsesStreamNormalizer {
25    decoder: SseDecoder,
26    responses: ResponsesStreamState,
27    done_seen: bool,
28    failed: bool,
29}
30
31impl ResponsesStreamNormalizer {
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    pub fn push(&mut self, chunk: &[u8]) -> Result<Vec<u8>, TransformError> {
37        if self.failed {
38            return Err(TransformError::InvalidInput {
39                reason: "Responses stream normalizer is failed".to_owned(),
40            });
41        }
42        let mut out = Vec::new();
43        let frames = self
44            .decoder
45            .push(chunk)
46            .inspect_err(|_| self.failed = true)?;
47        for frame in frames {
48            if let Err(error) = self.normalize_into(frame, &mut out) {
49                self.failed = true;
50                return Err(error);
51            }
52        }
53        Ok(out)
54    }
55
56    pub fn finish(&mut self) -> Result<Vec<u8>, TransformError> {
57        if self.failed {
58            return Err(TransformError::InvalidInput {
59                reason: "cannot finish failed Responses stream normalizer".to_owned(),
60            });
61        }
62        let mut out = Vec::new();
63        if let Some(frame) = self.decoder.finish()? {
64            self.normalize_into(frame, &mut out)?;
65        }
66        for event in self.responses.finish() {
67            encode_responses_event(&event, &mut out)?;
68        }
69        if self.done_seen {
70            out.extend_from_slice(b"data: [DONE]\n\n");
71        }
72        Ok(out)
73    }
74
75    fn normalize_into(&mut self, frame: SseFrame, out: &mut Vec<u8>) -> Result<(), TransformError> {
76        if frame.data.trim() == "[DONE]" {
77            self.done_seen = true;
78            return Ok(());
79        }
80        let event = match serde_json::from_str::<ResponseStreamEvent>(&frame.data) {
81            Ok(event) => event,
82            Err(error) => {
83                // Responses evolves faster than the typed protocol model. An
84                // SSE data frame this crate cannot decode must still reach the
85                // client unchanged; the normalizer simply cannot add lifecycle
86                // events for it.
87                tracing::warn!(
88                    error = %error,
89                    "Responses stream event typed decode failed; forwarding original SSE frame"
90                );
91                out.extend_from_slice(frame.encode().as_bytes());
92                return Ok(());
93            }
94        };
95        for event in self.responses.push(event) {
96            encode_responses_event(&event, out)?;
97        }
98        Ok(())
99    }
100}
101
102#[derive(Default)]
103pub(super) struct ResponsesStreamState {
104    message: ResponsesTextItemState,
105    reasoning: ResponsesTextItemState,
106    tools: BTreeMap<u32, ResponsesToolItemState>,
107    done_items: BTreeMap<u32, ResponseOutputItem>,
108    completed: bool,
109}
110
111impl ResponsesStreamState {
112    pub(super) fn push(&mut self, event: ResponseStreamEvent) -> Vec<ResponseStreamEvent> {
113        let mut event = match event {
114            ResponseStreamEvent::Known(known) => known,
115            unknown => return vec![unknown],
116        };
117        let mut out = match &mut event {
118            KnownEvent::ResponseOutputTextDelta {
119                content_index,
120                delta,
121                item_id,
122                output_index,
123                ..
124            } => {
125                let mut out = self.finish_reasoning();
126                out.extend(self.message.ensure(
127                    item_id,
128                    *output_index,
129                    *content_index,
130                    text::message_item_added,
131                ));
132                self.message.text.push_str(delta);
133                out
134            }
135            KnownEvent::ResponseReasoningTextDelta {
136                content_index,
137                delta,
138                item_id,
139                output_index,
140                ..
141            } => {
142                let out = self.reasoning.ensure(
143                    item_id,
144                    *output_index,
145                    *content_index,
146                    text::reasoning_item_added,
147                );
148                self.reasoning.text.push_str(delta);
149                out
150            }
151            KnownEvent::ResponseFunctionCallArgumentsDelta {
152                delta,
153                item_id,
154                output_index,
155                ..
156            } => {
157                self.note_tool_input_delta(
158                    ResponsesToolKind::Function,
159                    *output_index,
160                    item_id,
161                    delta,
162                );
163                Vec::new()
164            }
165            KnownEvent::ResponseCustomToolCallInputDelta {
166                delta,
167                item_id,
168                output_index,
169                ..
170            } => {
171                self.note_tool_input_delta(
172                    ResponsesToolKind::Custom,
173                    *output_index,
174                    item_id,
175                    delta,
176                );
177                Vec::new()
178            }
179            KnownEvent::ResponseFunctionCallArgumentsDone {
180                arguments,
181                item_id,
182                name,
183                output_index,
184                ..
185            } => {
186                self.note_tool_input_done(
187                    ResponsesToolKind::Function,
188                    *output_index,
189                    item_id,
190                    arguments,
191                    (!name.is_empty()).then_some(name.as_str()),
192                );
193                Vec::new()
194            }
195            KnownEvent::ResponseCustomToolCallInputDone {
196                input,
197                item_id,
198                output_index,
199                ..
200            } => {
201                self.note_tool_input_done(
202                    ResponsesToolKind::Custom,
203                    *output_index,
204                    item_id,
205                    input,
206                    None,
207                );
208                Vec::new()
209            }
210            KnownEvent::ResponseCompleted { response, .. } => {
211                let mut out = self.finish_reasoning();
212                out.extend(self.finish_message());
213                out.extend(self.finish_tools());
214                self.patch_completed_output(response);
215                self.completed = true;
216                out
217            }
218            KnownEvent::ResponseOutputItemAdded {
219                item, output_index, ..
220            } => {
221                self.note_item_added(item, *output_index);
222                Vec::new()
223            }
224            KnownEvent::ResponseOutputItemDone {
225                item, output_index, ..
226            } => {
227                self.note_item_done(item, *output_index);
228                self.done_items.insert(*output_index, (**item).clone());
229                Vec::new()
230            }
231            KnownEvent::ResponseOutputTextDone { text, .. } => {
232                self.message.note_done_text(text);
233                Vec::new()
234            }
235            KnownEvent::ResponseReasoningTextDone { text, .. } => {
236                self.reasoning.note_done_text(text);
237                Vec::new()
238            }
239            _ => Vec::new(),
240        };
241        out.push(ResponseStreamEvent::Known(event));
242        out
243    }
244
245    pub(super) fn finish(&mut self) -> Vec<ResponseStreamEvent> {
246        if self.completed {
247            return Vec::new();
248        }
249        let mut out = self.finish_reasoning();
250        out.extend(self.finish_message());
251        out.extend(self.finish_tools());
252        if !out.is_empty() {
253            out.push(known(KnownEvent::ResponseCompleted {
254                response: Box::new(fallback_completed_response()),
255                sequence_number: None,
256                extra: Extra::new(),
257            }));
258            self.completed = true;
259        }
260        out
261    }
262
263    fn finish_message(&mut self) -> Vec<ResponseStreamEvent> {
264        self.message.finish(text::message_done_events)
265    }
266
267    fn finish_reasoning(&mut self) -> Vec<ResponseStreamEvent> {
268        self.reasoning.finish(text::reasoning_done_events)
269    }
270
271    fn note_item_added(&mut self, item: &ResponseOutputItem, output_index: u32) {
272        match &item.0 {
273            ResponseItem::Message(message) if message_has_type(message) => {
274                self.message.note_added(message_id(message), output_index);
275            }
276            ResponseItem::Typed(TypedResponseItem::Reasoning { id, .. }) => {
277                self.reasoning.note_added(id.as_deref(), output_index);
278            }
279            ResponseItem::Typed(typed @ TypedResponseItem::FunctionCall { .. }) => {
280                self.note_tool_added(typed, ResponsesToolKind::Function, output_index);
281            }
282            ResponseItem::Typed(typed @ TypedResponseItem::CustomToolCall { .. }) => {
283                self.note_tool_added(typed, ResponsesToolKind::Custom, output_index);
284            }
285            _ => {}
286        }
287    }
288
289    fn note_item_done(&mut self, item: &ResponseOutputItem, output_index: u32) {
290        match &item.0 {
291            ResponseItem::Message(message) if message_has_type(message) => {
292                self.message
293                    .note_item_done(message_id(message), output_index);
294            }
295            ResponseItem::Typed(TypedResponseItem::Reasoning { id, .. }) => {
296                self.reasoning.note_item_done(id.as_deref(), output_index);
297            }
298            ResponseItem::Typed(typed @ TypedResponseItem::FunctionCall { .. }) => {
299                self.note_tool_item_done(typed, ResponsesToolKind::Function, output_index);
300            }
301            ResponseItem::Typed(typed @ TypedResponseItem::CustomToolCall { .. }) => {
302                self.note_tool_item_done(typed, ResponsesToolKind::Custom, output_index);
303            }
304            _ => {}
305        }
306    }
307
308    fn note_tool_added(&mut self, item: &TypedResponseItem, kind: ResponsesToolKind, index: u32) {
309        let state = self.tools.entry(index).or_default();
310        state.note_kind(kind, index);
311        state.note_item(item);
312    }
313
314    fn note_tool_item_done(
315        &mut self,
316        item: &TypedResponseItem,
317        kind: ResponsesToolKind,
318        index: u32,
319    ) {
320        let state = self.tools.entry(index).or_default();
321        state.note_kind(kind, index);
322        state.item_done = true;
323        state.note_item(item);
324    }
325
326    fn note_tool_input_delta(
327        &mut self,
328        kind: ResponsesToolKind,
329        index: u32,
330        item_id: &mut String,
331        delta: &str,
332    ) {
333        let state = self.tools.entry(index).or_default();
334        state.note_kind(kind, index);
335        state.note_event_item_id(item_id);
336        state.rewrite_event_item_id(item_id);
337        state.input.push_str(delta);
338    }
339
340    fn note_tool_input_done(
341        &mut self,
342        kind: ResponsesToolKind,
343        index: u32,
344        item_id: &mut String,
345        input: &str,
346        name: Option<&str>,
347    ) {
348        let state = self.tools.entry(index).or_default();
349        state.note_kind(kind, index);
350        state.note_event_item_id(item_id);
351        state.rewrite_event_item_id(item_id);
352        input.clone_into(&mut state.input);
353        if let Some(name) = name {
354            state.name.get_or_insert_with(|| name.to_owned());
355        }
356        state.input_done = true;
357    }
358
359    fn finish_tools(&mut self) -> Vec<ResponseStreamEvent> {
360        let mut out = Vec::new();
361        for state in self.tools.values_mut() {
362            if !state.can_finish() {
363                continue;
364            }
365            if !state.input_done {
366                out.push(state.input_done_event());
367                state.input_done = true;
368            }
369            if !state.item_done {
370                out.push(state.item_done_event());
371                state.item_done = true;
372            }
373        }
374        out
375    }
376
377    fn patch_completed_output(&self, response: &mut ResponseObject) {
378        if !response.output.is_empty() {
379            return;
380        }
381        let output = self.completed_output_items();
382        if !output.is_empty() {
383            response.output = output;
384        }
385    }
386
387    /// Rebuild the `output` array a sparse upstream left empty on
388    /// `response.completed`. Items the upstream itself emitted via
389    /// `response.output_item.done` win over the locally reassembled ones: they
390    /// carry fields the reassembly cannot reconstruct (`encrypted_content`,
391    /// reasoning summaries) and omit ones it would invent. Clients replay this
392    /// array verbatim into the next turn, so an invented field is a hard 400.
393    fn completed_output_items(&self) -> Vec<ResponseOutputItem> {
394        use crate::protocol::openai::ResponseItemLifecycleStatus::Completed;
395        let mut items: BTreeMap<u32, ResponseOutputItem> = BTreeMap::new();
396        if self.reasoning.started {
397            items.insert(
398                self.reasoning.output_index(),
399                text::reasoning_item(&self.reasoning, Completed),
400            );
401        }
402        if self.message.started {
403            items.insert(
404                self.message.output_index(),
405                text::message_item(&self.message, Completed),
406            );
407        }
408        for (index, state) in &self.tools {
409            if state.can_finish() {
410                items.insert(*index, state.completed_item());
411            }
412        }
413        for (index, item) in &self.done_items {
414            items.insert(*index, item.clone());
415        }
416        items.into_values().collect()
417    }
418}
419
420fn known(event: KnownEvent) -> ResponseStreamEvent {
421    ResponseStreamEvent::Known(event)
422}
423
424fn message_has_type(message: &ResponseMessageItem) -> bool {
425    match message {
426        ResponseMessageItem::Output(_) => true,
427        ResponseMessageItem::Input(input) => input.type_.is_some(),
428        ResponseMessageItem::EasyInput(easy) => easy.type_.is_some(),
429        _ => {
430            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
431        }
432    }
433}
434
435fn message_id(message: &ResponseMessageItem) -> Option<&str> {
436    match message {
437        ResponseMessageItem::Output(output) => Some(&output.id),
438        ResponseMessageItem::Input(input) => input.id.as_deref(),
439        ResponseMessageItem::EasyInput(_) => None,
440        _ => {
441            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
442        }
443    }
444}
445
446/// Fallback `response.completed` payload for streams that never sent one.
447fn fallback_completed_response() -> ResponseObject {
448    crate::protocol::wire!(ResponseObject {
449        id: "resp_0".to_owned(),
450        created_at: 0,
451        background: None,
452        completed_at: Some(0),
453        conversation: None,
454        error: None,
455        incomplete_details: None,
456        instructions: None,
457        max_output_tokens: None,
458        max_tool_calls: None,
459        metadata: None,
460        model: None,
461        moderation: None,
462        multi_agent: None,
463        object: ResponseObjectType::Response,
464        output: Vec::new(),
465        output_text: None,
466        parallel_tool_calls: None,
467        prompt: None,
468        prompt_cache_key: None,
469        prompt_cache_options: None,
470        prompt_cache_retention: None,
471        previous_response_id: None,
472        reasoning: None,
473        safety_identifier: None,
474        service_tier: None,
475        status: Some(ResponseStatus::Completed),
476        store: None,
477        temperature: None,
478        text: None,
479        tool_choice: None,
480        tools: None,
481        top_logprobs: None,
482        top_p: None,
483        truncation: None,
484        usage: None,
485        user: None,
486        extra: Extra::new(),
487    })
488}