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