Skip to main content

rig_core/providers/openai/responses_api/
streaming.rs

1//! The streaming module for the OpenAI Responses API.
2//! Please see the `openai_streaming` or `openai_streaming_with_tools` example for more practical usage.
3use crate::completion::{self, CompletionError, GetTokenUsage};
4use crate::http_client::HttpClientExt;
5use crate::http_client::sse::{Event, GenericEventSource};
6use crate::message::ReasoningContent;
7use crate::providers::openai::responses_api::{ReasoningSummary, ResponsesUsage};
8use crate::streaming;
9use crate::streaming::RawStreamingChoice;
10use crate::telemetry::{CompletionOperation, CompletionSpanBuilder};
11use crate::wasm_compat::WasmCompatSend;
12use async_stream::stream;
13use futures::StreamExt;
14use serde::{Deserialize, Serialize};
15use tracing::{Level, debug, enabled};
16use tracing_futures::Instrument as _;
17
18use super::{CompletionResponse, GenericResponsesCompletionModel, Output};
19
20type StreamingRawChoice = RawStreamingChoice<StreamingCompletionResponse>;
21
22// ================================================================
23// OpenAI Responses Streaming API
24// ================================================================
25
26/// A streaming completion chunk.
27/// Streaming chunks can come in one of two forms:
28/// - A response chunk (where the completed response will have the total token usage)
29/// - An item chunk commonly referred to as a delta. In the completions API this would be referred to as the message delta.
30#[derive(Debug, Serialize, Deserialize, Clone)]
31#[serde(untagged)]
32pub enum StreamingCompletionChunk {
33    Response(Box<ResponseChunk>),
34    Delta(ItemChunk),
35}
36
37/// The final streaming response from the OpenAI Responses API.
38#[derive(Debug, Serialize, Deserialize, Clone)]
39pub struct StreamingCompletionResponse {
40    /// Token usage
41    pub usage: ResponsesUsage,
42    /// The complete object-shaped reasoning metadata from the terminal response event.
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub reasoning_metadata: Option<serde_json::Map<String, serde_json::Value>>,
45    /// The effective reasoning context from the terminal response event.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub reasoning_context: Option<String>,
48}
49
50pub(crate) fn reasoning_choices_from_done_item(
51    id: &str,
52    summary: &[ReasoningSummary],
53    content: &[String],
54    encrypted_content: Option<&str>,
55) -> Vec<RawStreamingChoice<StreamingCompletionResponse>> {
56    let mut choices = summary
57        .iter()
58        .map(|reasoning_summary| match reasoning_summary {
59            ReasoningSummary::SummaryText { text } => RawStreamingChoice::Reasoning {
60                id: Some(id.to_owned()),
61                content: ReasoningContent::Summary(text.to_owned()),
62            },
63        })
64        .collect::<Vec<_>>();
65
66    choices.extend(content.iter().map(|text| RawStreamingChoice::Reasoning {
67        id: Some(id.to_owned()),
68        content: ReasoningContent::Text {
69            text: text.to_owned(),
70            signature: None,
71        },
72    }));
73
74    if let Some(encrypted_content) = encrypted_content.filter(|s| !s.is_empty()) {
75        choices.push(RawStreamingChoice::Reasoning {
76            id: Some(id.to_owned()),
77            content: ReasoningContent::Encrypted(encrypted_content.to_owned()),
78        });
79    }
80
81    choices
82}
83
84impl GetTokenUsage for StreamingCompletionResponse {
85    fn token_usage(&self) -> crate::completion::Usage {
86        self.usage.token_usage()
87    }
88}
89
90/// A response chunk from OpenAI's response API.
91#[derive(Debug, Serialize, Deserialize, Clone)]
92pub struct ResponseChunk {
93    /// The response chunk type
94    #[serde(rename = "type")]
95    pub kind: ResponseChunkKind,
96    /// The response itself
97    pub response: CompletionResponse,
98    /// The item sequence
99    pub sequence_number: u64,
100}
101
102/// Response chunk type.
103/// Renames are used to ensure that this type gets (de)serialized properly.
104#[derive(Debug, Serialize, Deserialize, Clone)]
105pub enum ResponseChunkKind {
106    #[serde(rename = "response.created")]
107    ResponseCreated,
108    #[serde(rename = "response.in_progress")]
109    ResponseInProgress,
110    #[serde(rename = "response.completed")]
111    ResponseCompleted,
112    #[serde(rename = "response.failed")]
113    ResponseFailed,
114    #[serde(rename = "response.incomplete")]
115    ResponseIncomplete,
116}
117
118fn provider_response_from_responses_error_value(
119    value: &serde_json::Value,
120    data: &str,
121) -> CompletionError {
122    if let Some(message) = value
123        .get("error")
124        .and_then(|error| error.get("message"))
125        .and_then(serde_json::Value::as_str)
126    {
127        tracing::warn!(message, "provider returned a streaming error event");
128    }
129
130    crate::provider_response::completion_error_from_body(data)
131}
132
133fn provider_response_from_responses_sse_data(data: &str) -> Option<CompletionError> {
134    let value = serde_json::from_str::<serde_json::Value>(data).ok()?;
135    (value.get("type").and_then(serde_json::Value::as_str) == Some("error"))
136        .then(|| provider_response_from_responses_error_value(&value, data))
137}
138
139#[derive(Clone, Copy)]
140pub(crate) enum ResponsesStreamOptions {
141    Strict,
142    StrictWithImmediateToolCalls,
143}
144
145impl ResponsesStreamOptions {
146    pub(crate) const fn strict() -> Self {
147        Self::Strict
148    }
149
150    pub(crate) const fn strict_with_immediate_tool_calls() -> Self {
151        Self::StrictWithImmediateToolCalls
152    }
153
154    const fn emits_completed_tool_calls_immediately(self) -> bool {
155        matches!(self, Self::StrictWithImmediateToolCalls)
156    }
157}
158
159pub(crate) fn parse_sse_completion_body(
160    body: &str,
161    provider_name: &str,
162) -> Result<CompletionResponse, CompletionError> {
163    let mut completed = None;
164
165    for line in body.lines() {
166        let data = line
167            .strip_prefix("data:")
168            .map(str::trim)
169            .unwrap_or_default();
170        if data.is_empty() || data == "[DONE]" {
171            continue;
172        }
173
174        if let Ok(chunk) = serde_json::from_str::<StreamingCompletionChunk>(data) {
175            if let StreamingCompletionChunk::Response(chunk) = chunk {
176                let ResponseChunk { kind, response, .. } = *chunk;
177                match kind {
178                    ResponseChunkKind::ResponseCompleted => {
179                        completed = Some(response);
180                        break;
181                    }
182                    ResponseChunkKind::ResponseFailed | ResponseChunkKind::ResponseIncomplete => {
183                        return Err(crate::provider_response::completion_error_from_body(data));
184                    }
185                    _ => {}
186                }
187            }
188            continue;
189        }
190
191        let value = match serde_json::from_str::<serde_json::Value>(data) {
192            Ok(value) => value,
193            Err(_) => continue,
194        };
195
196        match value.get("type").and_then(serde_json::Value::as_str) {
197            Some("response.completed") => {
198                if let Some(response) = value.get("response") {
199                    completed = Some(serde_json::from_value(response.clone())?);
200                    break;
201                }
202            }
203            Some("response.failed") | Some("response.incomplete") => {
204                return Err(crate::provider_response::completion_error_from_body(data));
205            }
206            Some("error") => {
207                return Err(provider_response_from_responses_error_value(&value, data));
208            }
209            _ => {}
210        }
211    }
212
213    completed.ok_or_else(|| {
214        CompletionError::ProviderError(format!(
215            "{provider_name} stream did not yield response.completed"
216        ))
217    })
218}
219
220struct RawChoiceAccumulator {
221    final_usage: ResponsesUsage,
222    reasoning_metadata: Option<serde_json::Map<String, serde_json::Value>>,
223    reasoning_context: Option<String>,
224    tool_calls: Vec<StreamingRawChoice>,
225    tool_call_internal_ids: std::collections::HashMap<String, String>,
226}
227
228impl RawChoiceAccumulator {
229    fn new(initial_usage: ResponsesUsage) -> Self {
230        Self {
231            final_usage: initial_usage,
232            reasoning_metadata: None,
233            reasoning_context: None,
234            tool_calls: Vec::new(),
235            tool_call_internal_ids: std::collections::HashMap::new(),
236        }
237    }
238
239    fn decode_item_chunk(
240        &mut self,
241        chunk: ItemChunk,
242        options: ResponsesStreamOptions,
243    ) -> Vec<StreamingRawChoice> {
244        let mut immediate = Vec::new();
245
246        let ItemChunk {
247            item_id: outer_item_id,
248            data: item,
249            ..
250        } = chunk;
251
252        match item {
253            ItemChunkKind::OutputItemAdded(StreamingItemDoneOutput {
254                item: Output::FunctionCall(func),
255                ..
256            }) => {
257                let internal_call_id = self
258                    .tool_call_internal_ids
259                    .entry(func.id.clone())
260                    .or_insert_with(crate::id::generate)
261                    .clone();
262                immediate.push(streaming::RawStreamingChoice::ToolCallDelta {
263                    id: func.id,
264                    internal_call_id,
265                    content: streaming::ToolCallDeltaContent::Name(func.name),
266                });
267            }
268            ItemChunkKind::OutputItemDone(message) => {
269                self.push_output_item_done(
270                    message.item,
271                    &mut immediate,
272                    options.emits_completed_tool_calls_immediately(),
273                );
274            }
275            ItemChunkKind::OutputTextDelta(delta) => {
276                immediate.push(streaming::RawStreamingChoice::Message(delta.delta));
277            }
278            ItemChunkKind::ReasoningSummaryTextDelta(delta) => {
279                immediate.push(streaming::RawStreamingChoice::ReasoningDelta {
280                    id: None,
281                    reasoning: delta.delta,
282                });
283            }
284            ItemChunkKind::ReasoningTextDelta(delta) => {
285                immediate.push(streaming::RawStreamingChoice::ReasoningDelta {
286                    id: outer_item_id,
287                    reasoning: delta.delta,
288                });
289            }
290            ItemChunkKind::RefusalDelta(delta) => {
291                immediate.push(streaming::RawStreamingChoice::Message(delta.delta));
292            }
293            ItemChunkKind::FunctionCallArgsDelta(delta) => {
294                if let Some(item_id) = outer_item_id {
295                    let internal_call_id = self
296                        .tool_call_internal_ids
297                        .entry(item_id.clone())
298                        .or_insert_with(crate::id::generate)
299                        .clone();
300                    immediate.push(streaming::RawStreamingChoice::ToolCallDelta {
301                        id: item_id,
302                        internal_call_id,
303                        content: streaming::ToolCallDeltaContent::Delta(delta.delta),
304                    });
305                }
306            }
307            _ => {}
308        }
309
310        immediate
311    }
312
313    fn record_response_chunk(
314        &mut self,
315        kind: ResponseChunkKind,
316        response: CompletionResponse,
317        raw_event_data: &str,
318    ) -> Result<(), CompletionError> {
319        match kind {
320            ResponseChunkKind::ResponseCompleted => {
321                if let Some(usage) = response.usage {
322                    self.final_usage = usage;
323                }
324                if response.reasoning_metadata.is_some() {
325                    self.reasoning_metadata = response.reasoning_metadata;
326                }
327                if response.reasoning_context.is_some() {
328                    self.reasoning_context = response.reasoning_context;
329                }
330                Ok(())
331            }
332            ResponseChunkKind::ResponseFailed | ResponseChunkKind::ResponseIncomplete => Err(
333                crate::provider_response::completion_error_from_body(raw_event_data),
334            ),
335            _ => Ok(()),
336        }
337    }
338
339    fn push_output_item_done(
340        &mut self,
341        item: Output,
342        immediate: &mut Vec<StreamingRawChoice>,
343        emit_completed_tool_calls_immediately: bool,
344    ) {
345        match item {
346            Output::FunctionCall(func) => {
347                let internal_call_id = self
348                    .tool_call_internal_ids
349                    .entry(func.id.clone())
350                    .or_insert_with(crate::id::generate)
351                    .clone();
352                let tool_call =
353                    streaming::RawStreamingToolCall::new(func.id, func.name, func.arguments)
354                        .with_internal_call_id(internal_call_id)
355                        .with_call_id(func.call_id);
356
357                if emit_completed_tool_calls_immediately {
358                    immediate.push(streaming::RawStreamingChoice::ToolCall(tool_call));
359                } else {
360                    self.tool_calls
361                        .push(streaming::RawStreamingChoice::ToolCall(tool_call));
362                }
363            }
364            Output::Reasoning {
365                id,
366                summary,
367                content,
368                encrypted_content,
369                ..
370            } => {
371                immediate.extend(reasoning_choices_from_done_item(
372                    &id,
373                    &summary,
374                    &content,
375                    encrypted_content.as_deref(),
376                ));
377            }
378            Output::Message(message) => {
379                immediate.push(streaming::RawStreamingChoice::MessageId(message.id));
380            }
381            // An unmodeled output item (e.g. a hosted-tool result such as
382            // `web_search_call`) arriving on `response.output_item.done`. Surface
383            // the raw item to stream consumers, mirroring how the non-streaming
384            // decode preserves it on `CompletionResponse.output`.
385            Output::Unknown(value) => {
386                immediate.push(streaming::RawStreamingChoice::Unknown(value));
387            }
388        }
389    }
390
391    fn finish(mut self) -> Vec<StreamingRawChoice> {
392        let mut choices = Vec::new();
393        choices.append(&mut self.tool_calls);
394        choices.push(RawStreamingChoice::FinalResponse(
395            StreamingCompletionResponse {
396                usage: self.final_usage,
397                reasoning_metadata: self.reasoning_metadata,
398                reasoning_context: self.reasoning_context,
399            },
400        ));
401        choices
402    }
403}
404
405pub(crate) fn raw_choices_from_sse_body(
406    body: &str,
407    initial_usage: ResponsesUsage,
408) -> Result<Vec<StreamingRawChoice>, CompletionError> {
409    let mut raw_choices = Vec::new();
410    let mut accumulator = RawChoiceAccumulator::new(initial_usage);
411    let options = ResponsesStreamOptions::strict();
412
413    for line in body.lines() {
414        let data = line
415            .strip_prefix("data:")
416            .map(str::trim)
417            .unwrap_or_default();
418        if data.is_empty() || data == "[DONE]" {
419            continue;
420        }
421
422        if let Ok(chunk) = serde_json::from_str::<StreamingCompletionChunk>(data) {
423            match chunk {
424                StreamingCompletionChunk::Delta(chunk) => {
425                    raw_choices.extend(accumulator.decode_item_chunk(chunk, options));
426                }
427                StreamingCompletionChunk::Response(chunk) => {
428                    let ResponseChunk { kind, response, .. } = *chunk;
429                    accumulator.record_response_chunk(kind, response, data)?;
430                }
431            }
432            continue;
433        }
434
435        let value = match serde_json::from_str::<serde_json::Value>(data) {
436            Ok(value) => value,
437            Err(_) => continue,
438        };
439
440        match value.get("type").and_then(serde_json::Value::as_str) {
441            Some("response.output_text.delta") | Some("response.refusal.delta") => {
442                if let Some(delta) = value.get("delta").and_then(serde_json::Value::as_str) {
443                    raw_choices.push(streaming::RawStreamingChoice::Message(delta.to_owned()));
444                }
445            }
446            Some("response.reasoning_summary_text.delta") => {
447                if let Some(delta) = value.get("delta").and_then(serde_json::Value::as_str) {
448                    raw_choices.push(streaming::RawStreamingChoice::ReasoningDelta {
449                        id: None,
450                        reasoning: delta.to_owned(),
451                    });
452                }
453            }
454            Some("response.reasoning_text.delta") => {
455                if let Some(delta) = value.get("delta").and_then(serde_json::Value::as_str) {
456                    raw_choices.push(streaming::RawStreamingChoice::ReasoningDelta {
457                        id: value
458                            .get("item_id")
459                            .and_then(serde_json::Value::as_str)
460                            .map(ToOwned::to_owned),
461                        reasoning: delta.to_owned(),
462                    });
463                }
464            }
465            Some("response.output_item.added") => {
466                if let Some(item) = value
467                    .get("item")
468                    .cloned()
469                    .and_then(|item| serde_json::from_value::<Output>(item).ok())
470                    && let Output::FunctionCall(func) = item
471                {
472                    let internal_call_id = accumulator
473                        .tool_call_internal_ids
474                        .entry(func.id.clone())
475                        .or_insert_with(crate::id::generate)
476                        .clone();
477                    raw_choices.push(streaming::RawStreamingChoice::ToolCallDelta {
478                        id: func.id,
479                        internal_call_id,
480                        content: streaming::ToolCallDeltaContent::Name(func.name),
481                    });
482                }
483            }
484            Some("response.output_item.done") => {
485                if let Some(item) = value
486                    .get("item")
487                    .cloned()
488                    .and_then(|item| serde_json::from_value::<Output>(item).ok())
489                {
490                    accumulator.push_output_item_done(item, &mut raw_choices, false);
491                }
492            }
493            Some("response.function_call_arguments.delta") => {
494                if let (Some(item_id), Some(delta)) = (
495                    value.get("item_id").and_then(serde_json::Value::as_str),
496                    value.get("delta").and_then(serde_json::Value::as_str),
497                ) {
498                    let internal_call_id = accumulator
499                        .tool_call_internal_ids
500                        .entry(item_id.to_owned())
501                        .or_insert_with(crate::id::generate)
502                        .clone();
503                    raw_choices.push(streaming::RawStreamingChoice::ToolCallDelta {
504                        id: item_id.to_owned(),
505                        internal_call_id,
506                        content: streaming::ToolCallDeltaContent::Delta(delta.to_owned()),
507                    });
508                }
509            }
510            Some("response.completed") | Some("response.failed") | Some("response.incomplete") => {
511                if let Some(response) = value.get("response").cloned() {
512                    let response = serde_json::from_value::<CompletionResponse>(response)?;
513                    let Some(kind) = (match value.get("type").and_then(serde_json::Value::as_str) {
514                        Some("response.completed") => Some(ResponseChunkKind::ResponseCompleted),
515                        Some("response.failed") => Some(ResponseChunkKind::ResponseFailed),
516                        Some("response.incomplete") => Some(ResponseChunkKind::ResponseIncomplete),
517                        _ => None,
518                    }) else {
519                        continue;
520                    };
521                    accumulator.record_response_chunk(kind, response, data)?;
522                }
523            }
524            Some("error") => {
525                return Err(provider_response_from_responses_error_value(&value, data));
526            }
527            _ => {}
528        }
529    }
530
531    raw_choices.extend(accumulator.finish());
532    Ok(raw_choices)
533}
534
535pub(crate) async fn completion_response_from_sse_body(
536    body: &str,
537    raw_response: CompletionResponse,
538) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
539    let raw_choices = raw_choices_from_sse_body(
540        body,
541        raw_response
542            .usage
543            .clone()
544            .unwrap_or_else(ResponsesUsage::new),
545    )?;
546    let stream = futures::stream::iter(
547        raw_choices
548            .into_iter()
549            .map(Ok::<_, CompletionError>)
550            .collect::<Vec<_>>(),
551    );
552    let mut stream = crate::streaming::StreamingCompletionResponse::stream(Box::pin(stream));
553
554    while let Some(item) = stream.next().await {
555        item?;
556    }
557
558    if choice_is_empty(&stream.choice) {
559        return Err(CompletionError::ResponseError(
560            "Response contained no parts".to_owned(),
561        ));
562    }
563
564    Ok(completion::CompletionResponse {
565        usage: stream
566            .response
567            .as_ref()
568            .map(GetTokenUsage::token_usage)
569            .unwrap_or_else(|| usage_from_raw_response(&raw_response)),
570        message_id: stream
571            .message_id
572            .clone()
573            .or_else(|| message_id_from_response(&raw_response)),
574        choice: stream.choice,
575        raw_response,
576    })
577}
578
579fn choice_is_empty(choice: &crate::OneOrMany<completion::AssistantContent>) -> bool {
580    choice.iter().all(|content| match content {
581        completion::AssistantContent::Text(text) => text.text.trim().is_empty(),
582        completion::AssistantContent::Reasoning(reasoning) => reasoning.content.is_empty(),
583        completion::AssistantContent::Image(_) => false,
584        completion::AssistantContent::ToolCall(_) => false,
585    })
586}
587
588fn message_id_from_response(response: &CompletionResponse) -> Option<String> {
589    response.output.iter().find_map(|item| match item {
590        Output::Message(message) => Some(message.id.clone()),
591        _ => None,
592    })
593}
594
595fn usage_from_raw_response(response: &CompletionResponse) -> completion::Usage {
596    response
597        .usage
598        .as_ref()
599        .map(GetTokenUsage::token_usage)
600        .unwrap_or_default()
601}
602
603pub(crate) fn stream_from_event_source<HttpClient, RequestBody>(
604    event_source: GenericEventSource<HttpClient, RequestBody>,
605    span: tracing::Span,
606) -> streaming::StreamingCompletionResponse<StreamingCompletionResponse>
607where
608    HttpClient: HttpClientExt + Clone + 'static,
609    RequestBody: Into<bytes::Bytes> + Clone + WasmCompatSend + 'static,
610{
611    stream_from_event_source_with_options(event_source, span, ResponsesStreamOptions::strict())
612}
613
614pub(crate) fn stream_from_event_source_with_options<HttpClient, RequestBody>(
615    mut event_source: GenericEventSource<HttpClient, RequestBody>,
616    span: tracing::Span,
617    options: ResponsesStreamOptions,
618) -> streaming::StreamingCompletionResponse<StreamingCompletionResponse>
619where
620    HttpClient: HttpClientExt + Clone + 'static,
621    RequestBody: Into<bytes::Bytes> + Clone + WasmCompatSend + 'static,
622{
623    let stream = stream! {
624        let mut accumulator = RawChoiceAccumulator::new(ResponsesUsage::new());
625        let span = tracing::Span::current();
626
627        let mut terminated_with_error = false;
628
629        while let Some(event_result) = event_source.next().await {
630            match event_result {
631                Ok(Event::Open) => {
632                    tracing::trace!("SSE connection opened");
633                    continue;
634                }
635                Ok(Event::Message(evt)) => {
636                    if evt.data.trim().is_empty() || evt.data == "[DONE]" {
637                        continue;
638                    }
639
640                    if let Some(error) = provider_response_from_responses_sse_data(&evt.data) {
641                        terminated_with_error = true;
642                        yield Err(error);
643                        break;
644                    }
645
646                    let data = serde_json::from_str::<StreamingCompletionChunk>(&evt.data);
647
648                    let Ok(data) = data else {
649                        let Err(err) = data else {
650                            continue;
651                        };
652                        debug!(
653                            "Couldn't deserialize SSE data as StreamingCompletionChunk: {:?}",
654                            err
655                        );
656                        continue;
657                    };
658
659                    match data {
660                        StreamingCompletionChunk::Delta(chunk) => {
661                            for choice in accumulator.decode_item_chunk(chunk, options) {
662                                yield Ok(choice);
663                            }
664                        }
665                        StreamingCompletionChunk::Response(chunk) => {
666                            let ResponseChunk { kind, response, .. } = *chunk;
667                            if matches!(kind, ResponseChunkKind::ResponseCompleted) {
668                                span.record("gen_ai.response.id", response.id.as_str());
669                                span.record("gen_ai.response.model", response.model.as_str());
670                            }
671                            if let Err(error) = accumulator.record_response_chunk(
672                                kind,
673                                response,
674                                &evt.data,
675                            ) {
676                                terminated_with_error = true;
677                                yield Err(error);
678                                break;
679                            }
680                        }
681                    }
682                }
683                Err(crate::http_client::Error::StreamEnded) => {
684                    event_source.close();
685                }
686                Err(error) => {
687                    tracing::error!(?error, "SSE error");
688                    terminated_with_error = true;
689                    yield Err(CompletionError::from_stream_transport(error));
690                    break;
691                }
692            }
693        }
694
695        event_source.close();
696
697        if terminated_with_error {
698            return;
699        }
700
701        let final_usage = accumulator.final_usage.clone();
702
703        for tool_call in accumulator.finish() {
704            yield Ok(tool_call)
705        }
706
707        span.record("gen_ai.usage.input_tokens", final_usage.input_tokens);
708        span.record("gen_ai.usage.output_tokens", final_usage.output_tokens);
709        let cached_tokens = final_usage
710            .input_tokens_details
711            .as_ref()
712            .map(|d| d.cached_tokens)
713            .unwrap_or(0);
714        span.record("gen_ai.usage.cache_read.input_tokens", cached_tokens);
715
716    }
717    .instrument(span);
718
719    streaming::StreamingCompletionResponse::stream(Box::pin(stream))
720}
721
722/// An item message chunk from OpenAI's Responses API.
723/// See
724#[derive(Debug, Serialize, Deserialize, Clone)]
725pub struct ItemChunk {
726    /// Item ID. Optional.
727    pub item_id: Option<String>,
728    /// The output index of the item from a given streamed response.
729    pub output_index: u64,
730    /// The item type chunk, as well as the inner data.
731    #[serde(flatten)]
732    pub data: ItemChunkKind,
733}
734
735/// The item chunk type from OpenAI's Responses API.
736#[derive(Debug, Serialize, Deserialize, Clone)]
737#[serde(tag = "type")]
738pub enum ItemChunkKind {
739    #[serde(rename = "response.output_item.added")]
740    OutputItemAdded(StreamingItemDoneOutput),
741    #[serde(rename = "response.output_item.done")]
742    OutputItemDone(StreamingItemDoneOutput),
743    #[serde(rename = "response.content_part.added")]
744    ContentPartAdded(ContentPartChunk),
745    #[serde(rename = "response.content_part.done")]
746    ContentPartDone(ContentPartChunk),
747    #[serde(rename = "response.output_text.delta")]
748    OutputTextDelta(DeltaTextChunk),
749    #[serde(rename = "response.output_text.done")]
750    OutputTextDone(OutputTextChunk),
751    #[serde(rename = "response.refusal.delta")]
752    RefusalDelta(DeltaTextChunk),
753    #[serde(rename = "response.refusal.done")]
754    RefusalDone(RefusalTextChunk),
755    #[serde(rename = "response.function_call_arguments.delta")]
756    FunctionCallArgsDelta(DeltaTextChunkWithItemId),
757    #[serde(rename = "response.function_call_arguments.done")]
758    FunctionCallArgsDone(ArgsTextChunk),
759    #[serde(rename = "response.reasoning_summary_part.added")]
760    ReasoningSummaryPartAdded(SummaryPartChunk),
761    #[serde(rename = "response.reasoning_summary_part.done")]
762    ReasoningSummaryPartDone(SummaryPartChunk),
763    #[serde(rename = "response.reasoning_summary_text.delta")]
764    ReasoningSummaryTextDelta(SummaryTextChunk),
765    #[serde(rename = "response.reasoning_summary_text.done")]
766    ReasoningSummaryTextDone(SummaryTextChunk),
767    #[serde(rename = "response.reasoning_text.delta")]
768    ReasoningTextDelta(DeltaTextChunkWithItemId),
769    /// Catch-all for unknown item chunk types (e.g., `web_search_call` events).
770    /// This prevents unknown streaming events from breaking deserialization.
771    #[serde(other)]
772    Unknown,
773}
774
775#[derive(Debug, Serialize, Deserialize, Clone)]
776pub struct StreamingItemDoneOutput {
777    pub sequence_number: u64,
778    pub item: Output,
779}
780
781#[derive(Debug, Serialize, Deserialize, Clone)]
782pub struct ContentPartChunk {
783    pub content_index: u64,
784    pub sequence_number: u64,
785    pub part: ContentPartChunkPart,
786}
787
788#[derive(Debug, Serialize, Deserialize, Clone)]
789#[serde(tag = "type", rename_all = "snake_case")]
790pub enum ContentPartChunkPart {
791    OutputText { text: String },
792    SummaryText { text: String },
793}
794
795#[derive(Debug, Serialize, Deserialize, Clone)]
796pub struct DeltaTextChunk {
797    pub content_index: u64,
798    pub sequence_number: u64,
799    pub delta: String,
800}
801
802#[derive(Debug, Serialize, Deserialize, Clone)]
803pub struct DeltaTextChunkWithItemId {
804    #[serde(default, skip_serializing_if = "Option::is_none")]
805    pub content_index: Option<u64>,
806    pub sequence_number: u64,
807    pub delta: String,
808}
809
810#[derive(Debug, Serialize, Deserialize, Clone)]
811pub struct OutputTextChunk {
812    pub content_index: u64,
813    pub sequence_number: u64,
814    pub text: String,
815}
816
817#[derive(Debug, Serialize, Deserialize, Clone)]
818pub struct RefusalTextChunk {
819    pub content_index: u64,
820    pub sequence_number: u64,
821    pub refusal: String,
822}
823
824#[derive(Debug, Serialize, Deserialize, Clone)]
825pub struct ArgsTextChunk {
826    #[serde(default, skip_serializing_if = "Option::is_none")]
827    pub content_index: Option<u64>,
828    pub sequence_number: u64,
829    pub arguments: serde_json::Value,
830}
831
832#[derive(Debug, Serialize, Deserialize, Clone)]
833pub struct SummaryPartChunk {
834    pub summary_index: u64,
835    pub sequence_number: u64,
836    pub part: SummaryPartChunkPart,
837}
838
839#[derive(Debug, Serialize, Deserialize, Clone)]
840pub struct SummaryTextChunk {
841    pub summary_index: u64,
842    pub sequence_number: u64,
843    pub delta: String,
844}
845
846#[derive(Debug, Serialize, Deserialize, Clone)]
847#[serde(tag = "type", rename_all = "snake_case")]
848pub enum SummaryPartChunkPart {
849    SummaryText { text: String },
850}
851
852impl<Ext, H> GenericResponsesCompletionModel<Ext, H>
853where
854    crate::client::Client<Ext, H>:
855        HttpClientExt + Clone + std::fmt::Debug + WasmCompatSend + 'static,
856    Ext: crate::client::Provider + super::ResponsesProviderExt + Clone + 'static,
857    H: Clone + Default + std::fmt::Debug + WasmCompatSend + 'static,
858{
859    pub(crate) async fn stream(
860        &self,
861        completion_request: crate::completion::CompletionRequest,
862    ) -> Result<streaming::StreamingCompletionResponse<StreamingCompletionResponse>, CompletionError>
863    {
864        let system_instructions = completion_request.preamble.clone();
865        let record_telemetry_content = completion_request.record_telemetry_content;
866        let mut request = self.create_completion_request(completion_request)?;
867        request.stream = Some(true);
868
869        if enabled!(Level::TRACE) {
870            tracing::trace!(
871                target: "rig::completions",
872                "OpenAI Responses streaming completion request: {}",
873                serde_json::to_string_pretty(&request)?
874            );
875        }
876
877        let body = serde_json::to_vec(&request)?;
878
879        let req = self
880            .client
881            .post("/responses")?
882            .body(body)
883            .map_err(|e| CompletionError::HttpError(e.into()))?;
884
885        let span = CompletionSpanBuilder::new(
886            "openai",
887            &request.model,
888            CompletionOperation::ChatStreaming,
889        )
890        .system_instructions(system_instructions.as_deref(), record_telemetry_content)
891        .build();
892        let client = self.client.clone();
893        let event_source = GenericEventSource::new(client, req);
894
895        Ok(stream_from_event_source(event_source, span))
896    }
897}
898
899#[cfg(test)]
900mod tests {
901    use super::{
902        ItemChunkKind, StreamingCompletionChunk, raw_choices_from_sse_body,
903        reasoning_choices_from_done_item,
904    };
905    use crate::completion::CompletionModel;
906    use crate::message::ReasoningContent;
907    use crate::providers::internal::openai_chat_completions_compatible::test_support::sse_bytes_from_json_events;
908    use crate::providers::openai::responses_api::{
909        AdditionalParameters, CompletionResponse, IncompleteDetailsReason, OutputTokensDetails,
910        ReasoningSummary, ResponseError, ResponseObject, ResponseStatus, ResponsesUsage,
911    };
912    use crate::streaming::{RawStreamingChoice, StreamedAssistantContent};
913    use crate::test_utils::MockStreamingClient;
914    use crate::{client::CompletionClient, providers::openai};
915    use futures::StreamExt;
916    use serde_json::{self, json};
917
918    fn sample_response(status: ResponseStatus) -> CompletionResponse {
919        CompletionResponse {
920            id: "resp_123".to_string(),
921            object: ResponseObject::Response,
922            created_at: 0,
923            status,
924            error: None,
925            incomplete_details: None,
926            instructions: None,
927            max_output_tokens: None,
928            model: "gpt-5.4".to_string(),
929            provider_reasoning: None,
930            reasoning_metadata: None,
931            reasoning_context: None,
932            usage: None,
933            output: Vec::new(),
934            tools: Vec::new(),
935            additional_parameters: AdditionalParameters::default(),
936        }
937    }
938
939    async fn first_error_from_event(
940        event: serde_json::Value,
941    ) -> crate::completion::CompletionError {
942        let client = openai::Client::builder()
943            .http_client(MockStreamingClient {
944                sse_bytes: sse_bytes_from_json_events(&[event]),
945            })
946            .api_key("test-key")
947            .build()
948            .expect("client should build");
949        let model = client.completion_model("gpt-5.4");
950        let request = model.completion_request("hello").build();
951        let mut stream = model.stream(request).await.expect("stream should start");
952
953        stream
954            .next()
955            .await
956            .expect("stream should yield an item")
957            .expect_err("stream should surface a provider error")
958    }
959
960    async fn final_response_from_event(
961        event: serde_json::Value,
962    ) -> super::StreamingCompletionResponse {
963        let client = openai::Client::builder()
964            .http_client(MockStreamingClient {
965                sse_bytes: sse_bytes_from_json_events(&[event]),
966            })
967            .api_key("test-key")
968            .build()
969            .expect("client should build");
970        let model = client.completion_model("gpt-5.4");
971        let request = model.completion_request("hello").build();
972        let mut stream = model.stream(request).await.expect("stream should start");
973
974        while let Some(item) = stream.next().await {
975            match item.expect("completed stream should not error") {
976                StreamedAssistantContent::Final(response) => return response,
977                _ => continue,
978            }
979        }
980
981        panic!("stream should yield a final response");
982    }
983
984    #[test]
985    fn parse_sse_completion_body_preserves_error_payloads() {
986        let mut response = sample_response(ResponseStatus::Failed);
987        response.error = Some(ResponseError {
988            code: "server_error".to_string(),
989            message: "response failed".to_string(),
990        });
991        let events = [
992            json!({
993                "type": "response.failed",
994                "sequence_number": 1,
995                "response": response,
996            }),
997            json!({
998                "type": "error",
999                "error": {
1000                    "message": "boom",
1001                    "code": "server_error",
1002                    "type": "server_error"
1003                }
1004            }),
1005        ];
1006
1007        for event in events {
1008            let payload = serde_json::to_string(&event).expect("event should serialize");
1009            let body = format!("data: {payload}\n");
1010            let err = super::parse_sse_completion_body(&body, "ChatGPT")
1011                .expect_err("error payload should surface as provider response");
1012
1013            assert!(matches!(
1014                err,
1015                crate::completion::CompletionError::ProviderResponse(_)
1016            ));
1017            assert_eq!(err.provider_response_status(), None);
1018            assert_eq!(err.provider_response_body(), Some(payload.as_str()));
1019        }
1020    }
1021
1022    #[test]
1023    fn reasoning_done_item_emits_summary_then_encrypted() {
1024        let summary = vec![
1025            ReasoningSummary::SummaryText {
1026                text: "step 1".to_string(),
1027            },
1028            ReasoningSummary::SummaryText {
1029                text: "step 2".to_string(),
1030            },
1031        ];
1032        let content = vec!["private reasoning".to_string()];
1033        let choices =
1034            reasoning_choices_from_done_item("rs_1", &summary, &content, Some("enc_blob"));
1035
1036        assert_eq!(choices.len(), 4);
1037        assert!(matches!(
1038            choices.first(),
1039            Some(RawStreamingChoice::Reasoning {
1040                id: Some(id),
1041                content: ReasoningContent::Summary(text),
1042            }) if id == "rs_1" && text == "step 1"
1043        ));
1044        assert!(matches!(
1045            choices.get(1),
1046            Some(RawStreamingChoice::Reasoning {
1047                id: Some(id),
1048                content: ReasoningContent::Summary(text),
1049            }) if id == "rs_1" && text == "step 2"
1050        ));
1051        assert!(matches!(
1052            choices.get(2),
1053            Some(RawStreamingChoice::Reasoning {
1054                id: Some(id),
1055                content: ReasoningContent::Text { text, signature: None },
1056            }) if id == "rs_1" && text == "private reasoning"
1057        ));
1058        assert!(matches!(
1059            choices.get(3),
1060            Some(RawStreamingChoice::Reasoning {
1061                id: Some(id),
1062                content: ReasoningContent::Encrypted(data),
1063            }) if id == "rs_1" && data == "enc_blob"
1064        ));
1065    }
1066
1067    #[test]
1068    fn reasoning_output_item_done_emits_reasoning_text_content() {
1069        let body = format!(
1070            "data: {}\n",
1071            json!({
1072                "type": "response.output_item.done",
1073                "output_index": 0,
1074                "sequence_number": 1,
1075                "item": {
1076                    "type": "reasoning",
1077                    "id": "rs_text_1",
1078                    "summary": [],
1079                    "content": [{ "type": "reasoning_text", "text": "visible reasoning" }],
1080                    "status": "completed"
1081                },
1082            })
1083        );
1084
1085        let choices = raw_choices_from_sse_body(&body, ResponsesUsage::new())
1086            .expect("sse body should decode");
1087
1088        assert!(matches!(
1089            choices.first(),
1090            Some(RawStreamingChoice::Reasoning {
1091                id: Some(id),
1092                content: ReasoningContent::Text { text, signature: None },
1093            }) if id == "rs_text_1" && text == "visible reasoning"
1094        ));
1095    }
1096
1097    #[test]
1098    fn reasoning_text_delta_emits_reasoning_delta() {
1099        let body = format!(
1100            "data: {}\n",
1101            json!({
1102                "type": "response.reasoning_text.delta",
1103                "item_id": "rs_delta_1",
1104                "output_index": 0,
1105                "content_index": 0,
1106                "sequence_number": 1,
1107                "delta": "thinking",
1108            })
1109        );
1110
1111        let choices = raw_choices_from_sse_body(&body, ResponsesUsage::new())
1112            .expect("sse body should decode");
1113
1114        assert!(matches!(
1115            choices.first(),
1116            Some(RawStreamingChoice::ReasoningDelta { id: Some(id), reasoning })
1117                if id == "rs_delta_1" && reasoning == "thinking"
1118        ));
1119    }
1120
1121    #[test]
1122    fn unknown_output_item_surfaces_as_raw_unknown_choice() {
1123        // A hosted-tool item (web_search_call) arriving on
1124        // `response.output_item.done` must surface to stream consumers as
1125        // `RawStreamingChoice::Unknown` carrying the verbatim item, mirroring how
1126        // the non-streaming decode preserves it on `CompletionResponse.output`.
1127        let item = json!({
1128            "type": "web_search_call",
1129            "id": "ws_001",
1130            "status": "completed",
1131            "action": { "type": "search", "queries": ["rig framework"] },
1132        });
1133        let body = format!(
1134            "data: {}\n",
1135            json!({
1136                "type": "response.output_item.done",
1137                "output_index": 0,
1138                "sequence_number": 1,
1139                "item": item,
1140            })
1141        );
1142
1143        let choices = raw_choices_from_sse_body(&body, ResponsesUsage::new())
1144            .expect("sse body should decode");
1145
1146        let unknown = choices.iter().find_map(|choice| match choice {
1147            RawStreamingChoice::Unknown(value) => Some(value),
1148            _ => None,
1149        });
1150        assert_eq!(
1151            unknown,
1152            Some(&item),
1153            "the raw web_search_call item should reach the consumer verbatim",
1154        );
1155    }
1156
1157    #[test]
1158    fn reasoning_done_item_without_encrypted_emits_summary_only() {
1159        let summary = vec![ReasoningSummary::SummaryText {
1160            text: "only summary".to_string(),
1161        }];
1162        let choices = reasoning_choices_from_done_item("rs_2", &summary, &[], None);
1163
1164        assert_eq!(choices.len(), 1);
1165        assert!(matches!(
1166            choices.first(),
1167            Some(RawStreamingChoice::Reasoning {
1168                id: Some(id),
1169                content: ReasoningContent::Summary(text),
1170            }) if id == "rs_2" && text == "only summary"
1171        ));
1172    }
1173
1174    #[test]
1175    fn empty_encrypted_reasoning_is_not_emitted() {
1176        let content = vec!["visible reasoning".to_string()];
1177
1178        let choices = reasoning_choices_from_done_item("rs_1", &[], &content, Some(""));
1179
1180        assert_eq!(choices.len(), 1);
1181        assert!(matches!(
1182            choices.first(),
1183            Some(RawStreamingChoice::Reasoning {
1184                content: ReasoningContent::Text { text, .. },
1185                ..
1186            }) if text == "visible reasoning"
1187        ));
1188    }
1189
1190    #[test]
1191    fn content_part_added_deserializes_snake_case_part_type() {
1192        let chunk: StreamingCompletionChunk = serde_json::from_value(json!({
1193            "type": "response.content_part.added",
1194            "item_id": "msg_1",
1195            "output_index": 0,
1196            "content_index": 0,
1197            "sequence_number": 3,
1198            "part": {
1199                "type": "output_text",
1200                "text": "hello"
1201            }
1202        }))
1203        .expect("content part event should deserialize");
1204
1205        assert!(matches!(
1206            chunk,
1207            StreamingCompletionChunk::Delta(chunk)
1208                if matches!(
1209                    chunk.data,
1210                    ItemChunkKind::ContentPartAdded(_)
1211                )
1212        ));
1213    }
1214
1215    #[test]
1216    fn content_part_done_deserializes_snake_case_part_type() {
1217        let chunk: StreamingCompletionChunk = serde_json::from_value(json!({
1218            "type": "response.content_part.done",
1219            "item_id": "msg_1",
1220            "output_index": 0,
1221            "content_index": 0,
1222            "sequence_number": 4,
1223            "part": {
1224                "type": "summary_text",
1225                "text": "done"
1226            }
1227        }))
1228        .expect("content part done event should deserialize");
1229
1230        assert!(matches!(
1231            chunk,
1232            StreamingCompletionChunk::Delta(chunk)
1233                if matches!(
1234                    chunk.data,
1235                    ItemChunkKind::ContentPartDone(_)
1236                )
1237        ));
1238    }
1239
1240    #[test]
1241    fn reasoning_summary_part_added_deserializes_snake_case_part_type() {
1242        let chunk: StreamingCompletionChunk = serde_json::from_value(json!({
1243            "type": "response.reasoning_summary_part.added",
1244            "item_id": "rs_1",
1245            "output_index": 0,
1246            "summary_index": 0,
1247            "sequence_number": 5,
1248            "part": {
1249                "type": "summary_text",
1250                "text": "step 1"
1251            }
1252        }))
1253        .expect("reasoning summary part event should deserialize");
1254
1255        assert!(matches!(
1256            chunk,
1257            StreamingCompletionChunk::Delta(chunk)
1258                if matches!(
1259                    chunk.data,
1260                    ItemChunkKind::ReasoningSummaryPartAdded(_)
1261                )
1262        ));
1263    }
1264
1265    #[test]
1266    fn reasoning_summary_part_done_deserializes_snake_case_part_type() {
1267        let chunk: StreamingCompletionChunk = serde_json::from_value(json!({
1268            "type": "response.reasoning_summary_part.done",
1269            "item_id": "rs_1",
1270            "output_index": 0,
1271            "summary_index": 0,
1272            "sequence_number": 6,
1273            "part": {
1274                "type": "summary_text",
1275                "text": "step 2"
1276            }
1277        }))
1278        .expect("reasoning summary part done event should deserialize");
1279
1280        assert!(matches!(
1281            chunk,
1282            StreamingCompletionChunk::Delta(chunk)
1283                if matches!(
1284                    chunk.data,
1285                    ItemChunkKind::ReasoningSummaryPartDone(_)
1286                )
1287        ));
1288    }
1289
1290    #[tokio::test]
1291    async fn response_failed_chunk_surfaces_provider_error_without_empty_code_prefix() {
1292        let mut response = sample_response(ResponseStatus::Failed);
1293        response.error = Some(ResponseError {
1294            code: String::new(),
1295            message: "maximum context length exceeded".to_string(),
1296        });
1297
1298        let event = json!({
1299            "type": "response.failed",
1300            "sequence_number": 1,
1301            "response": response,
1302        });
1303
1304        let err = first_error_from_event(event).await;
1305
1306        assert!(matches!(
1307            err,
1308            crate::completion::CompletionError::ProviderResponse(_)
1309        ));
1310        assert_eq!(err.provider_response_status(), None);
1311        assert!(err.provider_response_body().is_some_and(|body| {
1312            body.contains("response.failed") && body.contains("maximum context length exceeded")
1313        }));
1314    }
1315
1316    #[tokio::test]
1317    async fn response_failed_chunk_surfaces_provider_error_with_code_prefix() {
1318        let mut response = sample_response(ResponseStatus::Failed);
1319        response.error = Some(ResponseError {
1320            code: "context_length_exceeded".to_string(),
1321            message: "maximum context length exceeded".to_string(),
1322        });
1323
1324        let event = json!({
1325            "type": "response.failed",
1326            "sequence_number": 1,
1327            "response": response,
1328        });
1329
1330        let err = first_error_from_event(event).await;
1331
1332        assert!(matches!(
1333            err,
1334            crate::completion::CompletionError::ProviderResponse(_)
1335        ));
1336        assert_eq!(err.provider_response_status(), None);
1337        assert!(err.provider_response_body().is_some_and(|body| {
1338            body.contains("response.failed")
1339                && body.contains("context_length_exceeded")
1340                && body.contains("maximum context length exceeded")
1341        }));
1342    }
1343
1344    #[tokio::test]
1345    async fn response_incomplete_chunk_uses_incomplete_details_reason() {
1346        let mut response = sample_response(ResponseStatus::Incomplete);
1347        response.incomplete_details = Some(IncompleteDetailsReason {
1348            reason: "max_output_tokens".to_string(),
1349        });
1350
1351        let event = json!({
1352            "type": "response.incomplete",
1353            "sequence_number": 1,
1354            "response": response,
1355        });
1356
1357        let err = first_error_from_event(event).await;
1358
1359        assert!(matches!(
1360            err,
1361            crate::completion::CompletionError::ProviderResponse(_)
1362        ));
1363        assert_eq!(err.provider_response_status(), None);
1364        assert!(err.provider_response_body().is_some_and(|body| {
1365            body.contains("response.incomplete") && body.contains("max_output_tokens")
1366        }));
1367    }
1368
1369    #[tokio::test]
1370    async fn response_failed_chunk_terminates_stream_without_followup_items() {
1371        let tool_call_done = json!({
1372            "type": "response.output_item.done",
1373            "sequence_number": 1,
1374            "item": {
1375                "type": "function_call",
1376                "id": "fc_123",
1377                "arguments": "{}",
1378                "call_id": "call_123",
1379                "name": "example_tool",
1380                "status": "completed"
1381            }
1382        });
1383
1384        let mut response = sample_response(ResponseStatus::Failed);
1385        response.error = Some(ResponseError {
1386            code: "server_error".to_string(),
1387            message: "response stream failed".to_string(),
1388        });
1389
1390        let failed = json!({
1391            "type": "response.failed",
1392            "sequence_number": 2,
1393            "response": response,
1394        });
1395
1396        let client = openai::Client::builder()
1397            .http_client(MockStreamingClient {
1398                sse_bytes: sse_bytes_from_json_events(&[tool_call_done, failed]),
1399            })
1400            .api_key("test-key")
1401            .build()
1402            .expect("client should build");
1403        let model = client.completion_model("gpt-5.4");
1404        let request = model.completion_request("hello").build();
1405        let mut stream = model.stream(request).await.expect("stream should start");
1406
1407        let err = stream
1408            .next()
1409            .await
1410            .expect("stream should yield an item")
1411            .expect_err("stream should surface a provider error");
1412        assert!(matches!(
1413            err,
1414            crate::completion::CompletionError::ProviderResponse(_)
1415        ));
1416        assert_eq!(err.provider_response_status(), None);
1417        assert!(err.provider_response_body().is_some_and(|body| {
1418            body.contains("response.failed") && body.contains("response stream failed")
1419        }));
1420        assert!(
1421            stream.next().await.is_none(),
1422            "stream should terminate immediately after the first terminal error"
1423        );
1424    }
1425
1426    #[tokio::test]
1427    async fn streaming_error_event_preserves_full_payload_in_live_loop() {
1428        use crate::providers::internal::openai_chat_completions_compatible::test_support::sse_bytes_from_json_events;
1429        use crate::test_utils::MockStreamingClient;
1430
1431        let payload = json!({
1432            "type": "error",
1433            "error": {
1434                "message": "boom",
1435                "code": "server_error",
1436                "type": "server_error"
1437            }
1438        });
1439
1440        let client = openai::Client::builder()
1441            .http_client(MockStreamingClient {
1442                sse_bytes: sse_bytes_from_json_events(&[payload]),
1443            })
1444            .api_key("test-key")
1445            .build()
1446            .expect("client should build");
1447        let model = client.completion_model("gpt-5.4");
1448        let request = model.completion_request("hello").build();
1449        let mut stream = model.stream(request).await.expect("stream should start");
1450
1451        let err = stream
1452            .next()
1453            .await
1454            .expect("stream should yield an item")
1455            .expect_err("stream should surface a provider response error");
1456        assert_eq!(err.provider_response_status(), None);
1457        assert!(
1458            err.provider_response_body().is_some_and(|body| {
1459                body.contains("\"type\":\"error\"") && body.contains("boom")
1460            })
1461        );
1462        assert!(
1463            stream.next().await.is_none(),
1464            "stream should terminate after error event"
1465        );
1466    }
1467
1468    #[tokio::test]
1469    async fn streaming_http_non_success_preserves_status_and_body() {
1470        use crate::http_client::sse::GenericEventSource;
1471        use crate::test_utils::HttpErrorStreamingClient;
1472
1473        let body = r#"{"error":{"message":"quota exceeded"}}"#;
1474        let client = HttpErrorStreamingClient::new(http::StatusCode::TOO_MANY_REQUESTS, body);
1475        let req = http::Request::builder()
1476            .method("POST")
1477            .uri("http://localhost/v1/responses")
1478            .body(Vec::new())
1479            .expect("request should build");
1480        let event_source = GenericEventSource::new(client, req);
1481        let span = tracing::Span::none();
1482        let mut stream = super::stream_from_event_source(event_source, span);
1483
1484        let err = stream
1485            .next()
1486            .await
1487            .expect("stream should yield transport error")
1488            .expect_err("HTTP non-success should surface as a stream error");
1489        assert_eq!(
1490            err.provider_response_status(),
1491            Some(http::StatusCode::TOO_MANY_REQUESTS)
1492        );
1493        assert_eq!(err.provider_response_body(), Some(body));
1494        assert_eq!(
1495            err.provider_response_json().expect("valid JSON body"),
1496            Some(serde_json::json!({"error": {"message": "quota exceeded"}}))
1497        );
1498        assert!(
1499            stream.next().await.is_none(),
1500            "stream should terminate after HTTP non-success"
1501        );
1502    }
1503
1504    #[test]
1505    fn streaming_error_event_preserves_full_payload() {
1506        let payload = r#"{"type":"error","error":{"message":"boom","code":"server_error","type":"server_error"}}"#;
1507        let body = format!("data: {payload}\n");
1508
1509        let err = super::raw_choices_from_sse_body(&body, super::ResponsesUsage::new())
1510            .expect_err("error event should surface as a provider response error");
1511
1512        assert_eq!(err.provider_response_status(), None);
1513        assert_eq!(err.provider_response_body(), Some(payload));
1514        let json = err
1515            .provider_response_json()
1516            .expect("raw body should be valid JSON")
1517            .expect("parsed JSON should be present");
1518        assert_eq!(json["error"]["code"], "server_error");
1519    }
1520
1521    #[tokio::test]
1522    async fn streaming_non_http_transport_error_stays_provider_error() {
1523        use crate::http_client::sse::GenericEventSource;
1524        use crate::test_utils::SequencedStreamingHttpClient;
1525
1526        let chunks = vec![Err(crate::http_client::Error::InvalidContentType(
1527            http::HeaderValue::from_static("application/json"),
1528        ))];
1529        let client = SequencedStreamingHttpClient::new(chunks);
1530        let req = http::Request::builder()
1531            .method("POST")
1532            .uri("http://localhost/v1/responses")
1533            .body(Vec::new())
1534            .expect("request should build");
1535        let event_source = GenericEventSource::new(client, req);
1536        let span = tracing::Span::none();
1537        let mut stream = super::stream_from_event_source(event_source, span);
1538
1539        let err = stream
1540            .next()
1541            .await
1542            .expect("stream should yield transport error")
1543            .expect_err("non-HTTP transport failure should surface as provider error");
1544        assert_eq!(
1545            err.to_string(),
1546            "ProviderError: Invalid content type was returned: \"application/json\""
1547        );
1548        assert!(matches!(
1549            err,
1550            crate::completion::CompletionError::ProviderError(_)
1551        ));
1552        // Rig-generated transport diagnostics are not provider response bodies.
1553        assert_eq!(err.provider_response_body(), None);
1554        assert_eq!(err.provider_response_status(), None);
1555    }
1556
1557    #[tokio::test]
1558    async fn response_completed_chunk_populates_final_usage() {
1559        let mut response = sample_response(ResponseStatus::Completed);
1560        response.usage = Some(ResponsesUsage {
1561            input_tokens: 10,
1562            input_tokens_details: None,
1563            output_tokens: 5,
1564            output_tokens_details: Some(OutputTokensDetails {
1565                reasoning_tokens: 0,
1566            }),
1567            total_tokens: 15,
1568        });
1569
1570        let event = json!({
1571            "type": "response.completed",
1572            "sequence_number": 1,
1573            "response": response,
1574        });
1575
1576        let usage = final_response_from_event(event).await.usage;
1577        assert_eq!(usage.input_tokens, 10);
1578        assert_eq!(usage.output_tokens, 5);
1579        assert_eq!(usage.total_tokens, 15);
1580    }
1581
1582    #[tokio::test]
1583    async fn response_completed_chunk_populates_reasoning_metadata_and_context() {
1584        let response = sample_response(ResponseStatus::Completed);
1585        let mut event = json!({
1586            "type": "response.completed",
1587            "sequence_number": 1,
1588            "response": response,
1589        });
1590        let metadata = json!({
1591            "context": "all_turns",
1592            "effort": "ultra",
1593            "summary": null,
1594            "future_control": true
1595        });
1596        event["response"]["reasoning"] = metadata.clone();
1597
1598        let response = final_response_from_event(event).await;
1599        assert_eq!(response.reasoning_context.as_deref(), Some("all_turns"));
1600        assert_eq!(response.reasoning_metadata.as_ref(), metadata.as_object());
1601    }
1602
1603    #[tokio::test]
1604    async fn done_sentinel_is_ignored_without_debug_parse_noise() {
1605        use std::io::{self, Write};
1606        use std::sync::{Arc, Mutex};
1607
1608        #[derive(Clone)]
1609        struct SharedWriter(Arc<Mutex<Vec<u8>>>);
1610
1611        impl Write for SharedWriter {
1612            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1613                self.0
1614                    .lock()
1615                    .expect("log buffer mutex should not be poisoned")
1616                    .extend_from_slice(buf);
1617                Ok(buf.len())
1618            }
1619
1620            fn flush(&mut self) -> io::Result<()> {
1621                Ok(())
1622            }
1623        }
1624
1625        let mut response = sample_response(ResponseStatus::Completed);
1626        response.usage = Some(ResponsesUsage {
1627            input_tokens: 4,
1628            input_tokens_details: None,
1629            output_tokens: 2,
1630            output_tokens_details: Some(OutputTokensDetails {
1631                reasoning_tokens: 0,
1632            }),
1633            total_tokens: 6,
1634        });
1635
1636        // Scoped-subscriber tests must not run concurrently; see
1637        // `test_utils::scoped_tracing_subscriber_guard`.
1638        let _isolation = crate::test_utils::scoped_tracing_subscriber_guard().await;
1639        let captured = Arc::new(Mutex::new(Vec::new()));
1640        let subscriber = tracing_subscriber::fmt()
1641            .with_max_level(tracing::Level::DEBUG)
1642            .with_ansi(false)
1643            .without_time()
1644            .with_writer({
1645                let captured = captured.clone();
1646                move || SharedWriter(captured.clone())
1647            })
1648            .finish();
1649        let _guard = tracing::subscriber::set_default(subscriber);
1650
1651        let client = openai::Client::builder()
1652            .http_client(MockStreamingClient {
1653                sse_bytes: bytes::Bytes::from(format!(
1654                    "data: {}\n\ndata: [DONE]\n\n",
1655                    serde_json::to_string(&json!({
1656                        "type": "response.completed",
1657                        "sequence_number": 1,
1658                        "response": response,
1659                    }))
1660                    .expect("response event should serialize")
1661                )),
1662            })
1663            .api_key("test-key")
1664            .build()
1665            .expect("client should build");
1666        let model = client.completion_model("gpt-5.4");
1667        let request = model.completion_request("hello").build();
1668        let mut stream = model.stream(request).await.expect("stream should start");
1669
1670        let mut final_usage = None;
1671        while let Some(item) = stream.next().await {
1672            if let StreamedAssistantContent::Final(response) =
1673                item.expect("stream should complete successfully")
1674            {
1675                final_usage = Some(response.usage);
1676            }
1677        }
1678
1679        let usage = final_usage.expect("expected final response");
1680        assert_eq!(usage.input_tokens, 4);
1681        assert_eq!(usage.output_tokens, 2);
1682        assert_eq!(usage.total_tokens, 6);
1683
1684        let logs = String::from_utf8(
1685            captured
1686                .lock()
1687                .expect("log buffer mutex should not be poisoned")
1688                .clone(),
1689        )
1690        .expect("captured logs should be valid UTF-8");
1691        assert!(
1692            !logs.contains("Couldn't deserialize SSE data as StreamingCompletionChunk"),
1693            "expected [DONE] to bypass the parse-failure debug path, logs were: {logs}"
1694        );
1695    }
1696}