1use 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#[derive(Debug, Serialize, Deserialize, Clone)]
30#[serde(untagged)]
31pub enum StreamingCompletionChunk {
32 Response(Box<ResponseChunk>),
33 Delta(ItemChunk),
34}
35
36#[derive(Debug, Serialize, Deserialize, Clone)]
38pub struct StreamingCompletionResponse {
39 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#[derive(Debug, Serialize, Deserialize, Clone)]
85pub struct ResponseChunk {
86 #[serde(rename = "type")]
88 pub kind: ResponseChunkKind,
89 pub response: CompletionResponse,
91 pub sequence_number: u64,
93}
94
95#[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 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#[derive(Debug, Serialize, Deserialize, Clone)]
706pub struct ItemChunk {
707 pub item_id: Option<String>,
709 pub output_index: u64,
711 #[serde(flatten)]
713 pub data: ItemChunkKind,
714}
715
716#[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 #[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 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 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 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 #[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}