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