1use crate::OneOrMany;
9use crate::completion::{CompletionError, CompletionResponse, GetTokenUsage, Usage};
10use crate::message::{
11 AssistantContent, Reasoning, ReasoningContent, Text, ToolCall, ToolFunction, ToolResult,
12};
13use futures::stream::{AbortHandle, Abortable};
14use futures::{Stream, StreamExt};
15use serde::{Deserialize, Serialize};
16use std::pin::Pin;
17use std::sync::atomic::AtomicBool;
18use std::task::{Context, Poll};
19use tokio::sync::watch;
20
21pub struct PauseControl {
23 pub(crate) paused_tx: watch::Sender<bool>,
24 pub(crate) paused_rx: watch::Receiver<bool>,
25}
26
27impl PauseControl {
28 pub fn new() -> Self {
30 let (paused_tx, paused_rx) = watch::channel(false);
31 Self {
32 paused_tx,
33 paused_rx,
34 }
35 }
36
37 pub fn pause(&self) {
39 let _ = self.paused_tx.send(true);
40 }
41
42 pub fn resume(&self) {
44 let _ = self.paused_tx.send(false);
45 }
46
47 pub fn is_paused(&self) -> bool {
49 *self.paused_rx.borrow()
50 }
51}
52
53impl Default for PauseControl {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
61pub enum ToolCallDeltaContent {
62 Name(String),
64 Delta(String),
66}
67
68#[derive(Debug, Clone)]
70pub enum RawStreamingChoice<R>
71where
72 R: Clone,
73{
74 Message(String),
76
77 TextStart {
83 additional_params: Option<serde_json::Value>,
85 },
86
87 TextAdditionalParams(serde_json::Value),
92
93 ToolCall(RawStreamingToolCall),
95 ToolCallDelta {
97 id: String,
99 internal_call_id: String,
101 content: ToolCallDeltaContent,
102 },
103 Reasoning {
105 id: Option<String>,
107 content: ReasoningContent,
109 },
110 ReasoningDelta {
112 id: Option<String>,
114 reasoning: String,
116 },
117
118 FinalResponse(R),
121
122 MessageId(String),
125
126 Unknown(serde_json::Value),
133}
134
135#[derive(Debug, Clone)]
137pub struct RawStreamingToolCall {
138 pub id: String,
140 pub internal_call_id: String,
142 pub call_id: Option<String>,
144 pub name: String,
146 pub arguments: serde_json::Value,
148 pub signature: Option<String>,
150 pub additional_params: Option<serde_json::Value>,
152}
153
154impl RawStreamingToolCall {
155 pub fn empty() -> Self {
157 Self {
158 id: String::new(),
159 internal_call_id: crate::id::generate(),
160 call_id: None,
161 name: String::new(),
162 arguments: serde_json::Value::Null,
163 signature: None,
164 additional_params: None,
165 }
166 }
167
168 pub fn new(id: String, name: String, arguments: serde_json::Value) -> Self {
170 Self {
171 id,
172 internal_call_id: crate::id::generate(),
173 call_id: None,
174 name,
175 arguments,
176 signature: None,
177 additional_params: None,
178 }
179 }
180
181 pub fn with_internal_call_id(mut self, internal_call_id: String) -> Self {
183 self.internal_call_id = internal_call_id;
184 self
185 }
186
187 pub fn with_call_id(mut self, call_id: String) -> Self {
189 self.call_id = Some(call_id);
190 self
191 }
192
193 pub fn with_signature(mut self, signature: Option<String>) -> Self {
195 self.signature = signature;
196 self
197 }
198
199 pub fn with_additional_params(mut self, additional_params: Option<serde_json::Value>) -> Self {
201 self.additional_params = additional_params;
202 self
203 }
204}
205
206impl From<RawStreamingToolCall> for ToolCall {
207 fn from(tool_call: RawStreamingToolCall) -> Self {
208 ToolCall {
209 id: tool_call.id,
210 call_id: tool_call.call_id,
211 function: ToolFunction {
212 name: tool_call.name,
213 arguments: tool_call.arguments,
214 },
215 signature: tool_call.signature,
216 additional_params: tool_call.additional_params,
217 }
218 }
219}
220
221#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
222pub type StreamingResult<R> =
224 Pin<Box<dyn Stream<Item = Result<RawStreamingChoice<R>, CompletionError>> + Send>>;
225
226#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
227pub type StreamingResult<R> =
229 Pin<Box<dyn Stream<Item = Result<RawStreamingChoice<R>, CompletionError>>>>;
230
231pub struct StreamingCompletionResponse<R>
235where
236 R: Clone + Unpin + GetTokenUsage,
237{
238 pub(crate) inner: Abortable<StreamingResult<R>>,
239 pub(crate) abort_handle: AbortHandle,
240 pub(crate) pause_control: PauseControl,
241 assistant_items: Vec<AssistantContent>,
242 text_item_index: Option<usize>,
243 reasoning_item_index: Option<usize>,
244 pub choice: OneOrMany<AssistantContent>,
247 pub response: Option<R>,
250 pub final_response_yielded: AtomicBool,
251 pub message_id: Option<String>,
253}
254
255impl<R> StreamingCompletionResponse<R>
256where
257 R: Clone + Unpin + GetTokenUsage,
258{
259 pub fn stream(inner: StreamingResult<R>) -> StreamingCompletionResponse<R> {
261 let (abort_handle, abort_registration) = AbortHandle::new_pair();
262 let abortable_stream = Abortable::new(inner, abort_registration);
263 let pause_control = PauseControl::new();
264 Self {
265 inner: abortable_stream,
266 abort_handle,
267 pause_control,
268 assistant_items: vec![],
269 text_item_index: None,
270 reasoning_item_index: None,
271 choice: OneOrMany::one(AssistantContent::text("")),
272 response: None,
273 final_response_yielded: AtomicBool::new(false),
274 message_id: None,
275 }
276 }
277
278 pub fn cancel(&mut self) {
281 self.abort_handle.abort();
282 let (abort_handle, abort_registration) = AbortHandle::new_pair();
283 let empty: StreamingResult<R> = Box::pin(futures::stream::poll_fn(|_| Poll::Ready(None)));
284 self.inner = Abortable::new(empty, abort_registration);
285 self.abort_handle = abort_handle;
286 }
287
288 pub fn pause(&self) {
290 self.pause_control.pause();
291 }
292
293 pub fn resume(&self) {
295 self.pause_control.resume();
296 }
297
298 pub fn is_paused(&self) -> bool {
300 self.pause_control.is_paused()
301 }
302
303 pub fn usage(&self) -> Usage {
310 self.response.token_usage()
311 }
312
313 fn append_text_chunk(&mut self, text: &str) {
314 if let Some(index) = self.text_item_index
315 && let Some(AssistantContent::Text(existing_text)) = self.assistant_items.get_mut(index)
316 {
317 existing_text.text.push_str(text);
318 return;
319 }
320
321 self.assistant_items
322 .push(AssistantContent::text(text.to_owned()));
323 self.text_item_index = Some(self.assistant_items.len() - 1);
324 }
325
326 fn append_text_additional_params(&mut self, additional_params: serde_json::Value) {
327 if additional_params.is_null() {
328 return;
329 }
330
331 let index = if let Some(index) = self.text_item_index
332 && matches!(
333 self.assistant_items.get(index),
334 Some(AssistantContent::Text(_))
335 ) {
336 index
337 } else {
338 self.assistant_items.push(AssistantContent::text(""));
339 let index = self.assistant_items.len() - 1;
340 self.text_item_index = Some(index);
341 index
342 };
343
344 let Some(AssistantContent::Text(text)) = self.assistant_items.get_mut(index) else {
345 return;
346 };
347
348 match text.additional_params.as_mut() {
349 Some(existing) => merge_text_additional_params(existing, additional_params),
350 None => text.additional_params = Some(additional_params),
351 }
352 }
353
354 fn append_reasoning_chunk(&mut self, id: &Option<String>, text: &str) {
358 if let Some(index) = self.reasoning_item_index
359 && let Some(AssistantContent::Reasoning(existing)) = self.assistant_items.get_mut(index)
360 && let Some(ReasoningContent::Text {
361 text: existing_text,
362 ..
363 }) = existing.content.last_mut()
364 {
365 existing_text.push_str(text);
366 return;
367 }
368
369 self.assistant_items
370 .push(AssistantContent::Reasoning(Reasoning {
371 id: id.clone(),
372 content: vec![ReasoningContent::Text {
373 text: text.to_string(),
374 signature: None,
375 }],
376 }));
377 self.reasoning_item_index = Some(self.assistant_items.len() - 1);
378 }
379}
380
381fn merge_text_additional_params(existing: &mut serde_json::Value, incoming: serde_json::Value) {
382 match (existing, incoming) {
383 (serde_json::Value::Object(existing_map), serde_json::Value::Object(incoming_map)) => {
384 for (key, incoming_value) in incoming_map {
385 match existing_map.get_mut(&key) {
386 Some(existing_value) => match (existing_value, incoming_value) {
387 (
388 serde_json::Value::Array(existing_array),
389 serde_json::Value::Array(mut incoming_array),
390 ) => existing_array.append(&mut incoming_array),
391 (existing_value, incoming_value) => {
392 merge_text_additional_params(existing_value, incoming_value);
393 }
394 },
395 None => {
396 existing_map.insert(key, incoming_value);
397 }
398 }
399 }
400 }
401 (existing, incoming) => {
402 *existing = incoming;
403 }
404 }
405}
406
407impl<R> From<StreamingCompletionResponse<R>> for CompletionResponse<Option<R>>
408where
409 R: Clone + Unpin + GetTokenUsage,
410{
411 fn from(value: StreamingCompletionResponse<R>) -> CompletionResponse<Option<R>> {
412 CompletionResponse {
413 choice: value.choice,
414 usage: value.response.token_usage(),
418 raw_response: value.response,
419 message_id: value.message_id,
420 }
421 }
422}
423
424impl<R> Stream for StreamingCompletionResponse<R>
425where
426 R: Clone + Unpin + GetTokenUsage,
427{
428 type Item = Result<StreamedAssistantContent<R>, CompletionError>;
429
430 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
431 let stream = self.get_mut();
432
433 if stream.is_paused() {
434 cx.waker().wake_by_ref();
435 return Poll::Pending;
436 }
437
438 match Pin::new(&mut stream.inner).poll_next(cx) {
439 Poll::Pending => Poll::Pending,
440 Poll::Ready(None) => {
441 if stream.assistant_items.is_empty() {
444 stream.assistant_items.push(AssistantContent::text(""));
445 }
446
447 if let Some(choice) =
448 OneOrMany::from_iter_optional(std::mem::take(&mut stream.assistant_items))
449 {
450 stream.choice = choice;
451 }
452
453 Poll::Ready(None)
454 }
455 Poll::Ready(Some(Err(err))) => {
456 if matches!(err, CompletionError::ProviderError(ref e) if e.to_string().contains("aborted"))
457 {
458 return Poll::Ready(None); }
460 Poll::Ready(Some(Err(err)))
461 }
462 Poll::Ready(Some(Ok(choice))) => match choice {
463 RawStreamingChoice::Message(text) => {
464 stream.reasoning_item_index = None;
465 stream.append_text_chunk(&text);
466 Poll::Ready(Some(Ok(StreamedAssistantContent::text(&text))))
467 }
468 RawStreamingChoice::TextStart { additional_params } => {
469 stream.reasoning_item_index = None;
470 stream.text_item_index = None;
471 if let Some(additional_params) = additional_params {
472 stream.append_text_additional_params(additional_params);
473 }
474 stream.poll_next_unpin(cx)
475 }
476 RawStreamingChoice::TextAdditionalParams(additional_params) => {
477 stream.append_text_additional_params(additional_params);
478 stream.poll_next_unpin(cx)
479 }
480 RawStreamingChoice::ToolCallDelta {
481 id,
482 internal_call_id,
483 content,
484 } => Poll::Ready(Some(Ok(StreamedAssistantContent::ToolCallDelta {
485 id,
486 internal_call_id,
487 content,
488 }))),
489 RawStreamingChoice::Reasoning { id, content } => {
490 let reasoning = Reasoning {
491 id,
492 content: vec![content],
493 };
494 stream.text_item_index = None;
495 stream.reasoning_item_index = None;
497 stream
498 .assistant_items
499 .push(AssistantContent::Reasoning(reasoning.clone()));
500 Poll::Ready(Some(Ok(StreamedAssistantContent::Reasoning(reasoning))))
501 }
502 RawStreamingChoice::ReasoningDelta { id, reasoning } => {
503 stream.text_item_index = None;
504 stream.append_reasoning_chunk(&id, &reasoning);
505 Poll::Ready(Some(Ok(StreamedAssistantContent::ReasoningDelta {
506 id,
507 reasoning,
508 })))
509 }
510 RawStreamingChoice::ToolCall(raw_tool_call) => {
511 let internal_call_id = raw_tool_call.internal_call_id.clone();
512 let tool_call: ToolCall = raw_tool_call.into();
513 stream.text_item_index = None;
514 stream.reasoning_item_index = None;
515 stream
516 .assistant_items
517 .push(AssistantContent::ToolCall(tool_call.clone()));
518 Poll::Ready(Some(Ok(StreamedAssistantContent::ToolCall {
519 tool_call,
520 internal_call_id,
521 })))
522 }
523 RawStreamingChoice::FinalResponse(response) => {
524 if stream
525 .final_response_yielded
526 .load(std::sync::atomic::Ordering::SeqCst)
527 {
528 stream.poll_next_unpin(cx)
529 } else {
530 stream.response = Some(response.clone());
532 stream
533 .final_response_yielded
534 .store(true, std::sync::atomic::Ordering::SeqCst);
535 let final_response = StreamedAssistantContent::final_response(response);
536 Poll::Ready(Some(Ok(final_response)))
537 }
538 }
539 RawStreamingChoice::MessageId(id) => {
540 stream.message_id = Some(id);
541 stream.poll_next_unpin(cx)
542 }
543 RawStreamingChoice::Unknown(value) => {
544 Poll::Ready(Some(Ok(StreamedAssistantContent::Unknown(value))))
548 }
549 },
550 }
551 }
552}
553
554#[cfg(test)]
556mod tests {
557 use std::time::Duration;
558
559 use super::*;
560 use crate::test_utils::MockResponse;
561 use async_stream::stream;
562 use tokio::time::sleep;
563
564 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
565 fn to_stream_result(
566 stream: impl futures::Stream<Item = Result<RawStreamingChoice<MockResponse>, CompletionError>>
567 + Send
568 + 'static,
569 ) -> StreamingResult<MockResponse> {
570 Box::pin(stream)
571 }
572
573 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
574 fn to_stream_result(
575 stream: impl futures::Stream<Item = Result<RawStreamingChoice<MockResponse>, CompletionError>>
576 + 'static,
577 ) -> StreamingResult<MockResponse> {
578 Box::pin(stream)
579 }
580
581 fn create_mock_stream() -> StreamingCompletionResponse<MockResponse> {
582 let stream = stream! {
583 yield Ok(RawStreamingChoice::Message("hello 1".to_string()));
584 sleep(Duration::from_millis(100)).await;
585 yield Ok(RawStreamingChoice::Message("hello 2".to_string()));
586 sleep(Duration::from_millis(100)).await;
587 yield Ok(RawStreamingChoice::Message("hello 3".to_string()));
588 sleep(Duration::from_millis(100)).await;
589 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(15)));
590 };
591
592 StreamingCompletionResponse::stream(to_stream_result(stream))
593 }
594
595 fn create_reasoning_stream() -> StreamingCompletionResponse<MockResponse> {
596 let stream = stream! {
597 yield Ok(RawStreamingChoice::Reasoning {
598 id: Some("rs_1".to_string()),
599 content: ReasoningContent::Text {
600 text: "step one".to_string(),
601 signature: Some("sig_1".to_string()),
602 },
603 });
604 yield Ok(RawStreamingChoice::Message("final answer".to_string()));
605 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(5)));
606 };
607
608 StreamingCompletionResponse::stream(to_stream_result(stream))
609 }
610
611 fn create_reasoning_only_stream() -> StreamingCompletionResponse<MockResponse> {
612 let stream = stream! {
613 yield Ok(RawStreamingChoice::Reasoning {
614 id: Some("rs_only".to_string()),
615 content: ReasoningContent::Summary("hidden summary".to_string()),
616 });
617 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(2)));
618 };
619
620 StreamingCompletionResponse::stream(to_stream_result(stream))
621 }
622
623 fn create_interleaved_stream() -> StreamingCompletionResponse<MockResponse> {
624 let stream = stream! {
625 yield Ok(RawStreamingChoice::Reasoning {
626 id: Some("rs_interleaved".to_string()),
627 content: ReasoningContent::Text {
628 text: "chain-of-thought".to_string(),
629 signature: None,
630 },
631 });
632 yield Ok(RawStreamingChoice::Message("final-text".to_string()));
633 yield Ok(RawStreamingChoice::ToolCall(
634 RawStreamingToolCall::new(
635 "tool_1".to_string(),
636 "mock_tool".to_string(),
637 serde_json::json!({"arg": 1}),
638 ),
639 ));
640 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(3)));
641 };
642
643 StreamingCompletionResponse::stream(to_stream_result(stream))
644 }
645
646 fn create_text_tool_text_stream() -> StreamingCompletionResponse<MockResponse> {
647 let stream = stream! {
648 yield Ok(RawStreamingChoice::Message("first".to_string()));
649 yield Ok(RawStreamingChoice::ToolCall(
650 RawStreamingToolCall::new(
651 "tool_split".to_string(),
652 "mock_tool".to_string(),
653 serde_json::json!({"arg": "x"}),
654 ),
655 ));
656 yield Ok(RawStreamingChoice::Message("second".to_string()));
657 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(3)));
658 };
659
660 StreamingCompletionResponse::stream(to_stream_result(stream))
661 }
662
663 fn create_text_metadata_stream() -> StreamingCompletionResponse<MockResponse> {
664 let stream = stream! {
665 yield Ok(RawStreamingChoice::TextStart {
666 additional_params: None,
667 });
668 yield Ok(RawStreamingChoice::Message("first".to_string()));
669 yield Ok(RawStreamingChoice::TextAdditionalParams(serde_json::json!({
670 "citations": [{
671 "type": "char_location",
672 "cited_text": "First citation.",
673 "document_index": 0,
674 "start_char_index": 0,
675 "end_char_index": 15
676 }]
677 })));
678 yield Ok(RawStreamingChoice::TextAdditionalParams(serde_json::json!({
679 "citations": [{
680 "type": "char_location",
681 "cited_text": "Second citation.",
682 "document_index": 0,
683 "start_char_index": 16,
684 "end_char_index": 32
685 }]
686 })));
687 yield Ok(RawStreamingChoice::TextStart {
688 additional_params: Some(serde_json::json!({
689 "block": 2
690 })),
691 });
692 yield Ok(RawStreamingChoice::Message("second".to_string()));
693 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(3)));
694 };
695
696 StreamingCompletionResponse::stream(to_stream_result(stream))
697 }
698
699 #[tokio::test]
700 async fn into_completion_response_derives_usage_from_final_response() {
701 let mut stream = create_mock_stream();
702
703 while stream.next().await.is_some() {}
705
706 assert_eq!(stream.usage().total_tokens, 15);
708
709 let response: CompletionResponse<Option<MockResponse>> = stream.into();
711 assert_eq!(response.usage.total_tokens, 15);
712 }
713
714 #[tokio::test]
715 async fn usage_is_zero_sentinel_before_final_response() {
716 let stream = StreamingCompletionResponse::stream(to_stream_result(stream! {
718 yield Ok(RawStreamingChoice::Message("no final response".to_string()));
719 }));
720 assert_eq!(stream.usage().total_tokens, 0);
721 }
722
723 #[tokio::test]
724 async fn test_stream_cancellation() {
725 let mut stream = create_mock_stream();
726
727 println!("Response: ");
728 let mut chunk_count = 0;
729 while let Some(chunk) = stream.next().await {
730 match chunk {
731 Ok(StreamedAssistantContent::Text(text)) => {
732 print!("{}", text.text);
733 std::io::Write::flush(&mut std::io::stdout()).unwrap();
734 chunk_count += 1;
735 }
736 Ok(StreamedAssistantContent::ToolCall {
737 tool_call,
738 internal_call_id,
739 }) => {
740 println!("\nTool Call: {tool_call:?}, internal_call_id={internal_call_id:?}");
741 chunk_count += 1;
742 }
743 Ok(StreamedAssistantContent::ToolCallDelta {
744 id,
745 internal_call_id,
746 content,
747 }) => {
748 println!(
749 "\nTool Call delta: id={id:?}, internal_call_id={internal_call_id:?}, content={content:?}"
750 );
751 chunk_count += 1;
752 }
753 Ok(StreamedAssistantContent::Final(res)) => {
754 println!("\nFinal response: {res:?}");
755 }
756 Ok(StreamedAssistantContent::Reasoning(reasoning)) => {
757 let reasoning = reasoning.display_text();
758 print!("{reasoning}");
759 std::io::Write::flush(&mut std::io::stdout()).unwrap();
760 }
761 Ok(StreamedAssistantContent::ReasoningDelta { reasoning, .. }) => {
762 println!("Reasoning delta: {reasoning}");
763 chunk_count += 1;
764 }
765 Ok(StreamedAssistantContent::Unknown(value)) => {
766 println!("\nUnknown item: {value:?}");
767 chunk_count += 1;
768 }
769 Err(e) => {
770 eprintln!("Error: {e:?}");
771 break;
772 }
773 }
774
775 if chunk_count >= 2 {
776 println!("\nCancelling stream...");
777 stream.cancel();
778 println!("Stream cancelled.");
779 break;
780 }
781 }
782
783 let next_chunk = stream.next().await;
784 assert!(
785 next_chunk.is_none(),
786 "Expected no further chunks after cancellation, got {next_chunk:?}"
787 );
788 }
789
790 #[tokio::test]
791 async fn test_stream_pause_resume() {
792 let stream = create_mock_stream();
793
794 stream.pause();
796 assert!(stream.is_paused());
797
798 stream.resume();
800 assert!(!stream.is_paused());
801 }
802
803 #[tokio::test]
804 async fn test_stream_aggregates_reasoning_content() {
805 let mut stream = create_reasoning_stream();
806 while stream.next().await.is_some() {}
807
808 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
809
810 assert!(choice_items.iter().any(|item| matches!(
811 item,
812 AssistantContent::Reasoning(Reasoning {
813 id: Some(id),
814 content
815 }) if id == "rs_1"
816 && matches!(
817 content.first(),
818 Some(ReasoningContent::Text {
819 text,
820 signature: Some(signature)
821 }) if text == "step one" && signature == "sig_1"
822 )
823 )));
824 }
825
826 #[tokio::test]
827 async fn test_stream_reasoning_only_does_not_inject_empty_text() {
828 let mut stream = create_reasoning_only_stream();
829 while stream.next().await.is_some() {}
830
831 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
832 assert_eq!(choice_items.len(), 1);
833 assert!(matches!(
834 choice_items.first(),
835 Some(AssistantContent::Reasoning(Reasoning { id: Some(id), .. })) if id == "rs_only"
836 ));
837 }
838
839 #[tokio::test]
840 async fn test_stream_aggregates_assistant_items_in_arrival_order() {
841 let mut stream = create_interleaved_stream();
842 while stream.next().await.is_some() {}
843
844 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
845 assert_eq!(choice_items.len(), 3);
846 assert!(matches!(
847 choice_items.first(),
848 Some(AssistantContent::Reasoning(Reasoning { id: Some(id), .. })) if id == "rs_interleaved"
849 ));
850 assert!(matches!(
851 choice_items.get(1),
852 Some(AssistantContent::Text(Text { text, .. })) if text == "final-text"
853 ));
854 assert!(matches!(
855 choice_items.get(2),
856 Some(AssistantContent::ToolCall(ToolCall { id, .. })) if id == "tool_1"
857 ));
858 }
859
860 #[tokio::test]
861 async fn unknown_choice_reaches_consumer_but_not_aggregated_choice() {
862 let unknown = serde_json::json!({
863 "type": "web_search_call",
864 "id": "ws_1",
865 "status": "completed",
866 });
867 let yielded = unknown.clone();
868 let stream = stream! {
869 yield Ok(RawStreamingChoice::Unknown(yielded));
870 yield Ok(RawStreamingChoice::Message("done".to_string()));
871 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(1)));
872 };
873 let mut stream = StreamingCompletionResponse::stream(to_stream_result(stream));
874
875 let mut consumer_unknown = None;
876 let mut consumer_text = String::new();
877 while let Some(item) = stream.next().await {
878 match item.expect("stream item should be Ok") {
879 StreamedAssistantContent::Unknown(value) => consumer_unknown = Some(value),
880 StreamedAssistantContent::Text(text) => consumer_text.push_str(&text.text),
881 _ => {}
882 }
883 }
884
885 assert_eq!(consumer_unknown.as_ref(), Some(&unknown));
887 assert_eq!(consumer_text, "done");
888
889 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
892 assert_eq!(choice_items.len(), 1);
893 assert!(matches!(
894 choice_items.first(),
895 Some(AssistantContent::Text(Text { text, .. })) if text == "done"
896 ));
897 }
898
899 #[tokio::test]
900 async fn test_stream_keeps_non_contiguous_text_chunks_split_by_tool_call() {
901 let mut stream = create_text_tool_text_stream();
902 while stream.next().await.is_some() {}
903
904 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
905 assert_eq!(choice_items.len(), 3);
906 assert!(matches!(
907 choice_items.first(),
908 Some(AssistantContent::Text(Text { text, .. })) if text == "first"
909 ));
910 assert!(matches!(
911 choice_items.get(1),
912 Some(AssistantContent::ToolCall(ToolCall { id, .. })) if id == "tool_split"
913 ));
914 assert!(matches!(
915 choice_items.get(2),
916 Some(AssistantContent::Text(Text { text, .. })) if text == "second"
917 ));
918 }
919
920 #[tokio::test]
921 async fn test_stream_preserves_text_additional_params() {
922 let mut stream = create_text_metadata_stream();
923 while stream.next().await.is_some() {}
924
925 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
926 assert_eq!(choice_items.len(), 2);
927
928 let Some(AssistantContent::Text(Text {
929 text,
930 additional_params: Some(additional_params),
931 })) = choice_items.first()
932 else {
933 panic!("expected first text item with metadata");
934 };
935 assert_eq!(text, "first");
936 assert_eq!(
937 additional_params["citations"]
938 .as_array()
939 .expect("citations should be an array")
940 .len(),
941 2
942 );
943
944 let Some(AssistantContent::Text(Text {
945 text,
946 additional_params: Some(additional_params),
947 })) = choice_items.get(1)
948 else {
949 panic!("expected second text item with metadata");
950 };
951 assert_eq!(text, "second");
952 assert_eq!(additional_params["block"], 2);
953 }
954}
955
956#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
958#[serde(untagged)]
959pub enum StreamedAssistantContent<R> {
960 Text(Text),
962 ToolCall {
964 tool_call: ToolCall,
965 internal_call_id: String,
968 },
969 ToolCallDelta {
971 id: String,
973 internal_call_id: String,
975 content: ToolCallDeltaContent,
976 },
977 Reasoning(Reasoning),
979 ReasoningDelta {
981 id: Option<String>,
983 reasoning: String,
985 },
986 Final(R),
988 Unknown(serde_json::Value),
996}
997
998impl<R> StreamedAssistantContent<R>
999where
1000 R: Clone + Unpin,
1001{
1002 pub fn text(text: &str) -> Self {
1004 Self::Text(Text::new(text.to_string()))
1005 }
1006
1007 pub fn final_response(res: R) -> Self {
1009 Self::Final(res)
1010 }
1011}
1012
1013#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1015#[serde(untagged)]
1016pub enum StreamedUserContent {
1017 ToolResult {
1019 tool_result: ToolResult,
1020 internal_call_id: String,
1024 },
1025}
1026
1027impl StreamedUserContent {
1028 pub fn tool_result(tool_result: ToolResult, internal_call_id: String) -> Self {
1030 Self::ToolResult {
1031 tool_result,
1032 internal_call_id,
1033 }
1034 }
1035}