1mod 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#[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 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 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
446fn 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}