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 text::ResponsesTextItemState;
18use tool::{ResponsesToolItemState, ResponsesToolKind};
19
20/// Stateful normalizer for an upstream that already speaks Responses SSE.
21#[derive(Default)]
22pub struct ResponsesStreamNormalizer {
23    decoder: SseDecoder,
24    responses: ResponsesStreamState,
25}
26
27impl ResponsesStreamNormalizer {
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    pub fn push(&mut self, chunk: &[u8]) -> Vec<u8> {
33        let mut out = Vec::new();
34        for frame in self.decoder.push(chunk) {
35            self.normalize_into(frame, &mut out);
36        }
37        out
38    }
39
40    pub fn finish(&mut self) -> Vec<u8> {
41        let mut out = Vec::new();
42        if let Some(frame) = self.decoder.finish() {
43            self.normalize_into(frame, &mut out);
44        }
45        out
46    }
47
48    fn normalize_into(&mut self, frame: SseFrame, out: &mut Vec<u8>) {
49        if frame.data.trim() == "[DONE]" {
50            out.extend_from_slice(frame.encode().as_bytes());
51            return;
52        }
53        let Ok(event) = serde_json::from_str::<ResponseStreamEvent>(&frame.data) else {
54            out.extend_from_slice(frame.encode().as_bytes());
55            return;
56        };
57        for event in self.responses.push(event) {
58            encode_responses_event(&event, out);
59        }
60    }
61}
62
63#[derive(Default)]
64pub(super) struct ResponsesStreamState {
65    message: ResponsesTextItemState,
66    reasoning: ResponsesTextItemState,
67    tools: BTreeMap<u32, ResponsesToolItemState>,
68    completed: bool,
69}
70
71impl ResponsesStreamState {
72    pub(super) fn push(&mut self, event: ResponseStreamEvent) -> Vec<ResponseStreamEvent> {
73        let mut event = match event {
74            ResponseStreamEvent::Known(known) => known,
75            unknown => return vec![unknown],
76        };
77        let mut out = match &mut event {
78            KnownEvent::ResponseOutputTextDelta {
79                content_index,
80                delta,
81                item_id,
82                output_index,
83                ..
84            } => {
85                let mut out = self.finish_reasoning();
86                out.extend(self.message.ensure(
87                    item_id,
88                    *output_index,
89                    *content_index,
90                    text::message_item_added,
91                ));
92                self.message.text.push_str(delta);
93                out
94            }
95            KnownEvent::ResponseReasoningTextDelta {
96                content_index,
97                delta,
98                item_id,
99                output_index,
100                ..
101            } => {
102                let out = self.reasoning.ensure(
103                    item_id,
104                    *output_index,
105                    *content_index,
106                    text::reasoning_item_added,
107                );
108                self.reasoning.text.push_str(delta);
109                out
110            }
111            KnownEvent::ResponseFunctionCallArgumentsDelta {
112                delta,
113                item_id,
114                output_index,
115                ..
116            } => {
117                self.note_tool_input_delta(
118                    ResponsesToolKind::Function,
119                    *output_index,
120                    item_id,
121                    delta,
122                );
123                Vec::new()
124            }
125            KnownEvent::ResponseCustomToolCallInputDelta {
126                delta,
127                item_id,
128                output_index,
129                ..
130            } => {
131                self.note_tool_input_delta(
132                    ResponsesToolKind::Custom,
133                    *output_index,
134                    item_id,
135                    delta,
136                );
137                Vec::new()
138            }
139            KnownEvent::ResponseFunctionCallArgumentsDone {
140                arguments,
141                item_id,
142                name,
143                output_index,
144                ..
145            } => {
146                self.note_tool_input_done(
147                    ResponsesToolKind::Function,
148                    *output_index,
149                    item_id,
150                    arguments,
151                    Some(name),
152                );
153                Vec::new()
154            }
155            KnownEvent::ResponseCustomToolCallInputDone {
156                input,
157                item_id,
158                output_index,
159                ..
160            } => {
161                self.note_tool_input_done(
162                    ResponsesToolKind::Custom,
163                    *output_index,
164                    item_id,
165                    input,
166                    None,
167                );
168                Vec::new()
169            }
170            KnownEvent::ResponseCompleted { response, .. } => {
171                let mut out = self.finish_reasoning();
172                out.extend(self.finish_message());
173                out.extend(self.finish_tools());
174                self.patch_completed_output(response);
175                self.completed = true;
176                out
177            }
178            KnownEvent::ResponseOutputItemAdded {
179                item, output_index, ..
180            } => {
181                self.note_item_added(item, *output_index);
182                Vec::new()
183            }
184            KnownEvent::ResponseOutputItemDone {
185                item, output_index, ..
186            } => {
187                self.note_item_done(item, *output_index);
188                Vec::new()
189            }
190            KnownEvent::ResponseOutputTextDone { text, .. } => {
191                self.message.note_done_text(text);
192                Vec::new()
193            }
194            KnownEvent::ResponseReasoningTextDone { text, .. } => {
195                self.reasoning.note_done_text(text);
196                Vec::new()
197            }
198            _ => Vec::new(),
199        };
200        out.push(ResponseStreamEvent::Known(event));
201        out
202    }
203
204    pub(super) fn finish(&mut self) -> Vec<ResponseStreamEvent> {
205        if self.completed {
206            return Vec::new();
207        }
208        let mut out = self.finish_reasoning();
209        out.extend(self.finish_message());
210        if !out.is_empty() {
211            out.extend(self.finish_tools());
212            out.push(known(KnownEvent::ResponseCompleted {
213                response: Box::new(fallback_completed_response()),
214                sequence_number: None,
215                extra: Extra::new(),
216            }));
217            self.completed = true;
218        }
219        out
220    }
221
222    fn finish_message(&mut self) -> Vec<ResponseStreamEvent> {
223        self.message.finish(text::message_done_events)
224    }
225
226    fn finish_reasoning(&mut self) -> Vec<ResponseStreamEvent> {
227        self.reasoning.finish(text::reasoning_done_events)
228    }
229
230    fn note_item_added(&mut self, item: &ResponseOutputItem, output_index: u32) {
231        match &item.0 {
232            ResponseItem::Message(message) if message_has_type(message) => {
233                self.message.note_added(message_id(message), output_index);
234            }
235            ResponseItem::Typed(TypedResponseItem::Reasoning { id, .. }) => {
236                self.reasoning.note_added(id.as_deref(), output_index);
237            }
238            ResponseItem::Typed(typed @ TypedResponseItem::FunctionCall { .. }) => {
239                self.note_tool_added(typed, ResponsesToolKind::Function, output_index);
240            }
241            ResponseItem::Typed(typed @ TypedResponseItem::CustomToolCall { .. }) => {
242                self.note_tool_added(typed, ResponsesToolKind::Custom, output_index);
243            }
244            _ => {}
245        }
246    }
247
248    fn note_item_done(&mut self, item: &ResponseOutputItem, output_index: u32) {
249        match &item.0 {
250            ResponseItem::Message(message) if message_has_type(message) => {
251                self.message
252                    .note_item_done(message_id(message), output_index);
253            }
254            ResponseItem::Typed(TypedResponseItem::Reasoning { id, .. }) => {
255                self.reasoning.note_item_done(id.as_deref(), output_index);
256            }
257            ResponseItem::Typed(typed @ TypedResponseItem::FunctionCall { .. }) => {
258                self.note_tool_item_done(typed, ResponsesToolKind::Function, output_index);
259            }
260            ResponseItem::Typed(typed @ TypedResponseItem::CustomToolCall { .. }) => {
261                self.note_tool_item_done(typed, ResponsesToolKind::Custom, output_index);
262            }
263            _ => {}
264        }
265    }
266
267    fn note_tool_added(&mut self, item: &TypedResponseItem, kind: ResponsesToolKind, index: u32) {
268        let state = self.tools.entry(index).or_default();
269        state.note_kind(kind, index);
270        state.note_item(item);
271    }
272
273    fn note_tool_item_done(
274        &mut self,
275        item: &TypedResponseItem,
276        kind: ResponsesToolKind,
277        index: u32,
278    ) {
279        let state = self.tools.entry(index).or_default();
280        state.note_kind(kind, index);
281        state.item_done = true;
282        state.note_item(item);
283    }
284
285    fn note_tool_input_delta(
286        &mut self,
287        kind: ResponsesToolKind,
288        index: u32,
289        item_id: &mut String,
290        delta: &str,
291    ) {
292        let state = self.tools.entry(index).or_default();
293        state.note_kind(kind, index);
294        state.note_event_item_id(item_id);
295        state.rewrite_event_item_id(item_id);
296        state.input.push_str(delta);
297    }
298
299    fn note_tool_input_done(
300        &mut self,
301        kind: ResponsesToolKind,
302        index: u32,
303        item_id: &mut String,
304        input: &str,
305        name: Option<&str>,
306    ) {
307        let state = self.tools.entry(index).or_default();
308        state.note_kind(kind, index);
309        state.note_event_item_id(item_id);
310        state.rewrite_event_item_id(item_id);
311        input.clone_into(&mut state.input);
312        if let Some(name) = name {
313            state.name.get_or_insert_with(|| name.to_owned());
314        }
315        state.input_done = true;
316    }
317
318    fn finish_tools(&mut self) -> Vec<ResponseStreamEvent> {
319        let mut out = Vec::new();
320        for state in self.tools.values_mut() {
321            if !state.can_finish() {
322                continue;
323            }
324            if !state.input_done {
325                out.push(state.input_done_event());
326                state.input_done = true;
327            }
328            if !state.item_done {
329                out.push(state.item_done_event());
330                state.item_done = true;
331            }
332        }
333        out
334    }
335
336    fn patch_completed_output(&self, response: &mut ResponseObject) {
337        if !response.output.is_empty() {
338            return;
339        }
340        let output = self.completed_output_items();
341        if !output.is_empty() {
342            response.output = output;
343        }
344    }
345
346    fn completed_output_items(&self) -> Vec<ResponseOutputItem> {
347        use crate::protocol::openai::ResponseItemLifecycleStatus::Completed;
348        let mut output = Vec::new();
349        if self.reasoning.started {
350            output.push(text::reasoning_item(&self.reasoning, Completed));
351        }
352        if self.message.started {
353            output.push(text::message_item(&self.message, Completed));
354        }
355        output.extend(
356            self.tools
357                .values()
358                .filter(|state| state.can_finish())
359                .map(ResponsesToolItemState::completed_item),
360        );
361        output
362    }
363}
364
365fn known(event: KnownEvent) -> ResponseStreamEvent {
366    ResponseStreamEvent::Known(event)
367}
368
369fn message_has_type(message: &ResponseMessageItem) -> bool {
370    match message {
371        ResponseMessageItem::Output(_) => true,
372        ResponseMessageItem::Input(input) => input.type_.is_some(),
373        ResponseMessageItem::EasyInput(easy) => easy.type_.is_some(),
374    }
375}
376
377fn message_id(message: &ResponseMessageItem) -> Option<&str> {
378    match message {
379        ResponseMessageItem::Output(output) => Some(&output.id),
380        ResponseMessageItem::Input(input) => input.id.as_deref(),
381        ResponseMessageItem::EasyInput(_) => None,
382    }
383}
384
385/// Fallback `response.completed` payload for streams that never sent one.
386fn fallback_completed_response() -> ResponseObject {
387    ResponseObject {
388        id: "resp_0".to_owned(),
389        created_at: 0,
390        background: None,
391        completed_at: Some(0),
392        conversation: None,
393        error: None,
394        incomplete_details: None,
395        instructions: None,
396        max_output_tokens: None,
397        max_tool_calls: None,
398        metadata: None,
399        model: None,
400        moderation: None,
401        multi_agent: None,
402        object: ResponseObjectType::Response,
403        output: Vec::new(),
404        output_text: None,
405        parallel_tool_calls: None,
406        prompt: None,
407        prompt_cache_key: None,
408        prompt_cache_options: None,
409        prompt_cache_retention: None,
410        previous_response_id: None,
411        reasoning: None,
412        safety_identifier: None,
413        service_tier: None,
414        status: Some(ResponseStatus::Completed),
415        store: None,
416        temperature: None,
417        text: None,
418        tool_choice: None,
419        tools: None,
420        top_logprobs: None,
421        top_p: None,
422        truncation: None,
423        usage: None,
424        user: None,
425        extra: Extra::new(),
426    }
427}