1use crate::OneOrMany;
12use crate::agent::prompt_request::streaming::StreamingPromptRequest;
13use crate::completion::{
14 CompletionError, CompletionModel, CompletionRequestBuilder, CompletionResponse, GetTokenUsage,
15 Message, Usage,
16};
17use crate::message::{
18 AssistantContent, Reasoning, ReasoningContent, Text, ToolCall, ToolFunction, ToolResult,
19};
20use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
21use futures::stream::{AbortHandle, Abortable};
22use futures::{Stream, StreamExt};
23use serde::{Deserialize, Serialize};
24use std::future::Future;
25use std::pin::Pin;
26use std::sync::atomic::AtomicBool;
27use std::task::{Context, Poll};
28use tokio::sync::watch;
29
30pub struct PauseControl {
32 pub(crate) paused_tx: watch::Sender<bool>,
33 pub(crate) paused_rx: watch::Receiver<bool>,
34}
35
36impl PauseControl {
37 pub fn new() -> Self {
39 let (paused_tx, paused_rx) = watch::channel(false);
40 Self {
41 paused_tx,
42 paused_rx,
43 }
44 }
45
46 pub fn pause(&self) {
48 let _ = self.paused_tx.send(true);
49 }
50
51 pub fn resume(&self) {
53 let _ = self.paused_tx.send(false);
54 }
55
56 pub fn is_paused(&self) -> bool {
58 *self.paused_rx.borrow()
59 }
60}
61
62impl Default for PauseControl {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
70pub enum ToolCallDeltaContent {
71 Name(String),
73 Delta(String),
75}
76
77#[derive(Debug, Clone)]
79pub enum RawStreamingChoice<R>
80where
81 R: Clone,
82{
83 Message(String),
85
86 TextStart {
92 additional_params: Option<serde_json::Value>,
94 },
95
96 TextAdditionalParams(serde_json::Value),
101
102 ToolCall(RawStreamingToolCall),
104 ToolCallDelta {
106 id: String,
108 internal_call_id: String,
110 content: ToolCallDeltaContent,
111 },
112 Reasoning {
114 id: Option<String>,
116 content: ReasoningContent,
118 },
119 ReasoningDelta {
121 id: Option<String>,
123 reasoning: String,
125 },
126
127 FinalResponse(R),
130
131 MessageId(String),
134
135 Unknown(serde_json::Value),
142}
143
144#[derive(Debug, Clone)]
146pub struct RawStreamingToolCall {
147 pub id: String,
149 pub internal_call_id: String,
151 pub call_id: Option<String>,
153 pub name: String,
155 pub arguments: serde_json::Value,
157 pub signature: Option<String>,
159 pub additional_params: Option<serde_json::Value>,
161}
162
163impl RawStreamingToolCall {
164 pub fn empty() -> Self {
166 Self {
167 id: String::new(),
168 internal_call_id: crate::id::generate(),
169 call_id: None,
170 name: String::new(),
171 arguments: serde_json::Value::Null,
172 signature: None,
173 additional_params: None,
174 }
175 }
176
177 pub fn new(id: String, name: String, arguments: serde_json::Value) -> Self {
179 Self {
180 id,
181 internal_call_id: crate::id::generate(),
182 call_id: None,
183 name,
184 arguments,
185 signature: None,
186 additional_params: None,
187 }
188 }
189
190 pub fn with_internal_call_id(mut self, internal_call_id: String) -> Self {
192 self.internal_call_id = internal_call_id;
193 self
194 }
195
196 pub fn with_call_id(mut self, call_id: String) -> Self {
198 self.call_id = Some(call_id);
199 self
200 }
201
202 pub fn with_signature(mut self, signature: Option<String>) -> Self {
204 self.signature = signature;
205 self
206 }
207
208 pub fn with_additional_params(mut self, additional_params: Option<serde_json::Value>) -> Self {
210 self.additional_params = additional_params;
211 self
212 }
213}
214
215impl From<RawStreamingToolCall> for ToolCall {
216 fn from(tool_call: RawStreamingToolCall) -> Self {
217 ToolCall {
218 id: tool_call.id,
219 call_id: tool_call.call_id,
220 function: ToolFunction {
221 name: tool_call.name,
222 arguments: tool_call.arguments,
223 },
224 signature: tool_call.signature,
225 additional_params: tool_call.additional_params,
226 }
227 }
228}
229
230#[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
231pub type StreamingResult<R> =
233 Pin<Box<dyn Stream<Item = Result<RawStreamingChoice<R>, CompletionError>> + Send>>;
234
235#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
236pub type StreamingResult<R> =
238 Pin<Box<dyn Stream<Item = Result<RawStreamingChoice<R>, CompletionError>>>>;
239
240pub struct StreamingCompletionResponse<R>
244where
245 R: Clone + Unpin + GetTokenUsage,
246{
247 pub(crate) inner: Abortable<StreamingResult<R>>,
248 pub(crate) abort_handle: AbortHandle,
249 pub(crate) pause_control: PauseControl,
250 assistant_items: Vec<AssistantContent>,
251 text_item_index: Option<usize>,
252 reasoning_item_index: Option<usize>,
253 pub choice: OneOrMany<AssistantContent>,
256 pub response: Option<R>,
259 pub final_response_yielded: AtomicBool,
260 pub message_id: Option<String>,
262}
263
264impl<R> StreamingCompletionResponse<R>
265where
266 R: Clone + Unpin + GetTokenUsage,
267{
268 pub fn stream(inner: StreamingResult<R>) -> StreamingCompletionResponse<R> {
270 let (abort_handle, abort_registration) = AbortHandle::new_pair();
271 let abortable_stream = Abortable::new(inner, abort_registration);
272 let pause_control = PauseControl::new();
273 Self {
274 inner: abortable_stream,
275 abort_handle,
276 pause_control,
277 assistant_items: vec![],
278 text_item_index: None,
279 reasoning_item_index: None,
280 choice: OneOrMany::one(AssistantContent::text("")),
281 response: None,
282 final_response_yielded: AtomicBool::new(false),
283 message_id: None,
284 }
285 }
286
287 pub fn cancel(&self) {
289 self.abort_handle.abort();
290 }
291
292 pub fn pause(&self) {
294 self.pause_control.pause();
295 }
296
297 pub fn resume(&self) {
299 self.pause_control.resume();
300 }
301
302 pub fn is_paused(&self) -> bool {
304 self.pause_control.is_paused()
305 }
306
307 pub fn usage(&self) -> Usage {
314 self.response.token_usage()
315 }
316
317 fn append_text_chunk(&mut self, text: &str) {
318 if let Some(index) = self.text_item_index
319 && let Some(AssistantContent::Text(existing_text)) = self.assistant_items.get_mut(index)
320 {
321 existing_text.text.push_str(text);
322 return;
323 }
324
325 self.assistant_items
326 .push(AssistantContent::text(text.to_owned()));
327 self.text_item_index = Some(self.assistant_items.len() - 1);
328 }
329
330 fn append_text_additional_params(&mut self, additional_params: serde_json::Value) {
331 if additional_params.is_null() {
332 return;
333 }
334
335 let index = if let Some(index) = self.text_item_index
336 && matches!(
337 self.assistant_items.get(index),
338 Some(AssistantContent::Text(_))
339 ) {
340 index
341 } else {
342 self.assistant_items.push(AssistantContent::text(""));
343 let index = self.assistant_items.len() - 1;
344 self.text_item_index = Some(index);
345 index
346 };
347
348 let Some(AssistantContent::Text(text)) = self.assistant_items.get_mut(index) else {
349 return;
350 };
351
352 match text.additional_params.as_mut() {
353 Some(existing) => merge_text_additional_params(existing, additional_params),
354 None => text.additional_params = Some(additional_params),
355 }
356 }
357
358 fn append_reasoning_chunk(&mut self, id: &Option<String>, text: &str) {
362 if let Some(index) = self.reasoning_item_index
363 && let Some(AssistantContent::Reasoning(existing)) = self.assistant_items.get_mut(index)
364 && let Some(ReasoningContent::Text {
365 text: existing_text,
366 ..
367 }) = existing.content.last_mut()
368 {
369 existing_text.push_str(text);
370 return;
371 }
372
373 self.assistant_items
374 .push(AssistantContent::Reasoning(Reasoning {
375 id: id.clone(),
376 content: vec![ReasoningContent::Text {
377 text: text.to_string(),
378 signature: None,
379 }],
380 }));
381 self.reasoning_item_index = Some(self.assistant_items.len() - 1);
382 }
383}
384
385fn merge_text_additional_params(existing: &mut serde_json::Value, incoming: serde_json::Value) {
386 match (existing, incoming) {
387 (serde_json::Value::Object(existing_map), serde_json::Value::Object(incoming_map)) => {
388 for (key, incoming_value) in incoming_map {
389 match existing_map.get_mut(&key) {
390 Some(existing_value) => match (existing_value, incoming_value) {
391 (
392 serde_json::Value::Array(existing_array),
393 serde_json::Value::Array(mut incoming_array),
394 ) => existing_array.append(&mut incoming_array),
395 (existing_value, incoming_value) => {
396 merge_text_additional_params(existing_value, incoming_value);
397 }
398 },
399 None => {
400 existing_map.insert(key, incoming_value);
401 }
402 }
403 }
404 }
405 (existing, incoming) => {
406 *existing = incoming;
407 }
408 }
409}
410
411impl<R> From<StreamingCompletionResponse<R>> for CompletionResponse<Option<R>>
412where
413 R: Clone + Unpin + GetTokenUsage,
414{
415 fn from(value: StreamingCompletionResponse<R>) -> CompletionResponse<Option<R>> {
416 CompletionResponse {
417 choice: value.choice,
418 usage: value.response.token_usage(),
422 raw_response: value.response,
423 message_id: value.message_id,
424 }
425 }
426}
427
428impl<R> Stream for StreamingCompletionResponse<R>
429where
430 R: Clone + Unpin + GetTokenUsage,
431{
432 type Item = Result<StreamedAssistantContent<R>, CompletionError>;
433
434 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
435 let stream = self.get_mut();
436
437 if stream.is_paused() {
438 cx.waker().wake_by_ref();
439 return Poll::Pending;
440 }
441
442 match Pin::new(&mut stream.inner).poll_next(cx) {
443 Poll::Pending => Poll::Pending,
444 Poll::Ready(None) => {
445 if stream.assistant_items.is_empty() {
448 stream.assistant_items.push(AssistantContent::text(""));
449 }
450
451 if let Some(choice) =
452 OneOrMany::from_iter_optional(std::mem::take(&mut stream.assistant_items))
453 {
454 stream.choice = choice;
455 }
456
457 Poll::Ready(None)
458 }
459 Poll::Ready(Some(Err(err))) => {
460 if matches!(err, CompletionError::ProviderError(ref e) if e.to_string().contains("aborted"))
461 {
462 return Poll::Ready(None); }
464 Poll::Ready(Some(Err(err)))
465 }
466 Poll::Ready(Some(Ok(choice))) => match choice {
467 RawStreamingChoice::Message(text) => {
468 stream.reasoning_item_index = None;
469 stream.append_text_chunk(&text);
470 Poll::Ready(Some(Ok(StreamedAssistantContent::text(&text))))
471 }
472 RawStreamingChoice::TextStart { additional_params } => {
473 stream.reasoning_item_index = None;
474 stream.text_item_index = None;
475 if let Some(additional_params) = additional_params {
476 stream.append_text_additional_params(additional_params);
477 }
478 stream.poll_next_unpin(cx)
479 }
480 RawStreamingChoice::TextAdditionalParams(additional_params) => {
481 stream.append_text_additional_params(additional_params);
482 stream.poll_next_unpin(cx)
483 }
484 RawStreamingChoice::ToolCallDelta {
485 id,
486 internal_call_id,
487 content,
488 } => Poll::Ready(Some(Ok(StreamedAssistantContent::ToolCallDelta {
489 id,
490 internal_call_id,
491 content,
492 }))),
493 RawStreamingChoice::Reasoning { id, content } => {
494 let reasoning = Reasoning {
495 id,
496 content: vec![content],
497 };
498 stream.text_item_index = None;
499 stream.reasoning_item_index = None;
501 stream
502 .assistant_items
503 .push(AssistantContent::Reasoning(reasoning.clone()));
504 Poll::Ready(Some(Ok(StreamedAssistantContent::Reasoning(reasoning))))
505 }
506 RawStreamingChoice::ReasoningDelta { id, reasoning } => {
507 stream.text_item_index = None;
508 stream.append_reasoning_chunk(&id, &reasoning);
509 Poll::Ready(Some(Ok(StreamedAssistantContent::ReasoningDelta {
510 id,
511 reasoning,
512 })))
513 }
514 RawStreamingChoice::ToolCall(raw_tool_call) => {
515 let internal_call_id = raw_tool_call.internal_call_id.clone();
516 let tool_call: ToolCall = raw_tool_call.into();
517 stream.text_item_index = None;
518 stream.reasoning_item_index = None;
519 stream
520 .assistant_items
521 .push(AssistantContent::ToolCall(tool_call.clone()));
522 Poll::Ready(Some(Ok(StreamedAssistantContent::ToolCall {
523 tool_call,
524 internal_call_id,
525 })))
526 }
527 RawStreamingChoice::FinalResponse(response) => {
528 if stream
529 .final_response_yielded
530 .load(std::sync::atomic::Ordering::SeqCst)
531 {
532 stream.poll_next_unpin(cx)
533 } else {
534 stream.response = Some(response.clone());
536 stream
537 .final_response_yielded
538 .store(true, std::sync::atomic::Ordering::SeqCst);
539 let final_response = StreamedAssistantContent::final_response(response);
540 Poll::Ready(Some(Ok(final_response)))
541 }
542 }
543 RawStreamingChoice::MessageId(id) => {
544 stream.message_id = Some(id);
545 stream.poll_next_unpin(cx)
546 }
547 RawStreamingChoice::Unknown(value) => {
548 Poll::Ready(Some(Ok(StreamedAssistantContent::Unknown(value))))
552 }
553 },
554 }
555 }
556}
557
558pub trait StreamingPrompt<M, R>
564where
565 M: CompletionModel + 'static,
566 <M as CompletionModel>::StreamingResponse: WasmCompatSend,
567 R: Clone + Unpin + GetTokenUsage,
568{
569 fn stream_prompt(
574 &self,
575 prompt: impl Into<Message> + WasmCompatSend,
576 ) -> StreamingPromptRequest<M>;
577}
578
579pub trait StreamingChat<M, R>: WasmCompatSend + WasmCompatSync
585where
586 M: CompletionModel + 'static,
587 <M as CompletionModel>::StreamingResponse: WasmCompatSend,
588 R: Clone + Unpin + GetTokenUsage,
589{
590 fn stream_chat<I, T>(
615 &self,
616 prompt: impl Into<Message> + WasmCompatSend,
617 chat_history: I,
618 ) -> StreamingPromptRequest<M>
619 where
620 I: IntoIterator<Item = T> + WasmCompatSend,
621 T: Into<Message>;
622}
623
624pub trait StreamingCompletion<M: CompletionModel> {
626 fn stream_completion<I, T>(
628 &self,
629 prompt: impl Into<Message> + WasmCompatSend,
630 chat_history: I,
631 ) -> impl Future<Output = Result<CompletionRequestBuilder<M>, CompletionError>>
632 where
633 I: IntoIterator<Item = T> + WasmCompatSend,
634 T: Into<Message>;
635}
636
637#[cfg(test)]
639mod tests {
640 use std::time::Duration;
641
642 use super::*;
643 use crate::test_utils::MockResponse;
644 use async_stream::stream;
645 use tokio::time::sleep;
646
647 #[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
648 fn to_stream_result(
649 stream: impl futures::Stream<Item = Result<RawStreamingChoice<MockResponse>, CompletionError>>
650 + Send
651 + 'static,
652 ) -> StreamingResult<MockResponse> {
653 Box::pin(stream)
654 }
655
656 #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
657 fn to_stream_result(
658 stream: impl futures::Stream<Item = Result<RawStreamingChoice<MockResponse>, CompletionError>>
659 + 'static,
660 ) -> StreamingResult<MockResponse> {
661 Box::pin(stream)
662 }
663
664 fn create_mock_stream() -> StreamingCompletionResponse<MockResponse> {
665 let stream = stream! {
666 yield Ok(RawStreamingChoice::Message("hello 1".to_string()));
667 sleep(Duration::from_millis(100)).await;
668 yield Ok(RawStreamingChoice::Message("hello 2".to_string()));
669 sleep(Duration::from_millis(100)).await;
670 yield Ok(RawStreamingChoice::Message("hello 3".to_string()));
671 sleep(Duration::from_millis(100)).await;
672 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(15)));
673 };
674
675 StreamingCompletionResponse::stream(to_stream_result(stream))
676 }
677
678 fn create_reasoning_stream() -> StreamingCompletionResponse<MockResponse> {
679 let stream = stream! {
680 yield Ok(RawStreamingChoice::Reasoning {
681 id: Some("rs_1".to_string()),
682 content: ReasoningContent::Text {
683 text: "step one".to_string(),
684 signature: Some("sig_1".to_string()),
685 },
686 });
687 yield Ok(RawStreamingChoice::Message("final answer".to_string()));
688 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(5)));
689 };
690
691 StreamingCompletionResponse::stream(to_stream_result(stream))
692 }
693
694 fn create_reasoning_only_stream() -> StreamingCompletionResponse<MockResponse> {
695 let stream = stream! {
696 yield Ok(RawStreamingChoice::Reasoning {
697 id: Some("rs_only".to_string()),
698 content: ReasoningContent::Summary("hidden summary".to_string()),
699 });
700 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(2)));
701 };
702
703 StreamingCompletionResponse::stream(to_stream_result(stream))
704 }
705
706 fn create_interleaved_stream() -> StreamingCompletionResponse<MockResponse> {
707 let stream = stream! {
708 yield Ok(RawStreamingChoice::Reasoning {
709 id: Some("rs_interleaved".to_string()),
710 content: ReasoningContent::Text {
711 text: "chain-of-thought".to_string(),
712 signature: None,
713 },
714 });
715 yield Ok(RawStreamingChoice::Message("final-text".to_string()));
716 yield Ok(RawStreamingChoice::ToolCall(
717 RawStreamingToolCall::new(
718 "tool_1".to_string(),
719 "mock_tool".to_string(),
720 serde_json::json!({"arg": 1}),
721 ),
722 ));
723 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(3)));
724 };
725
726 StreamingCompletionResponse::stream(to_stream_result(stream))
727 }
728
729 fn create_text_tool_text_stream() -> StreamingCompletionResponse<MockResponse> {
730 let stream = stream! {
731 yield Ok(RawStreamingChoice::Message("first".to_string()));
732 yield Ok(RawStreamingChoice::ToolCall(
733 RawStreamingToolCall::new(
734 "tool_split".to_string(),
735 "mock_tool".to_string(),
736 serde_json::json!({"arg": "x"}),
737 ),
738 ));
739 yield Ok(RawStreamingChoice::Message("second".to_string()));
740 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(3)));
741 };
742
743 StreamingCompletionResponse::stream(to_stream_result(stream))
744 }
745
746 fn create_text_metadata_stream() -> StreamingCompletionResponse<MockResponse> {
747 let stream = stream! {
748 yield Ok(RawStreamingChoice::TextStart {
749 additional_params: None,
750 });
751 yield Ok(RawStreamingChoice::Message("first".to_string()));
752 yield Ok(RawStreamingChoice::TextAdditionalParams(serde_json::json!({
753 "citations": [{
754 "type": "char_location",
755 "cited_text": "First citation.",
756 "document_index": 0,
757 "start_char_index": 0,
758 "end_char_index": 15
759 }]
760 })));
761 yield Ok(RawStreamingChoice::TextAdditionalParams(serde_json::json!({
762 "citations": [{
763 "type": "char_location",
764 "cited_text": "Second citation.",
765 "document_index": 0,
766 "start_char_index": 16,
767 "end_char_index": 32
768 }]
769 })));
770 yield Ok(RawStreamingChoice::TextStart {
771 additional_params: Some(serde_json::json!({
772 "block": 2
773 })),
774 });
775 yield Ok(RawStreamingChoice::Message("second".to_string()));
776 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(3)));
777 };
778
779 StreamingCompletionResponse::stream(to_stream_result(stream))
780 }
781
782 #[tokio::test]
783 async fn into_completion_response_derives_usage_from_final_response() {
784 let mut stream = create_mock_stream();
785
786 while stream.next().await.is_some() {}
788
789 assert_eq!(stream.usage().total_tokens, 15);
791
792 let response: CompletionResponse<Option<MockResponse>> = stream.into();
794 assert_eq!(response.usage.total_tokens, 15);
795 }
796
797 #[tokio::test]
798 async fn usage_is_zero_sentinel_before_final_response() {
799 let stream = StreamingCompletionResponse::stream(to_stream_result(stream! {
801 yield Ok(RawStreamingChoice::Message("no final response".to_string()));
802 }));
803 assert_eq!(stream.usage().total_tokens, 0);
804 }
805
806 #[tokio::test]
807 async fn test_stream_cancellation() {
808 let mut stream = create_mock_stream();
809
810 println!("Response: ");
811 let mut chunk_count = 0;
812 while let Some(chunk) = stream.next().await {
813 match chunk {
814 Ok(StreamedAssistantContent::Text(text)) => {
815 print!("{}", text.text);
816 std::io::Write::flush(&mut std::io::stdout()).unwrap();
817 chunk_count += 1;
818 }
819 Ok(StreamedAssistantContent::ToolCall {
820 tool_call,
821 internal_call_id,
822 }) => {
823 println!("\nTool Call: {tool_call:?}, internal_call_id={internal_call_id:?}");
824 chunk_count += 1;
825 }
826 Ok(StreamedAssistantContent::ToolCallDelta {
827 id,
828 internal_call_id,
829 content,
830 }) => {
831 println!(
832 "\nTool Call delta: id={id:?}, internal_call_id={internal_call_id:?}, content={content:?}"
833 );
834 chunk_count += 1;
835 }
836 Ok(StreamedAssistantContent::Final(res)) => {
837 println!("\nFinal response: {res:?}");
838 }
839 Ok(StreamedAssistantContent::Reasoning(reasoning)) => {
840 let reasoning = reasoning.display_text();
841 print!("{reasoning}");
842 std::io::Write::flush(&mut std::io::stdout()).unwrap();
843 }
844 Ok(StreamedAssistantContent::ReasoningDelta { reasoning, .. }) => {
845 println!("Reasoning delta: {reasoning}");
846 chunk_count += 1;
847 }
848 Ok(StreamedAssistantContent::Unknown(value)) => {
849 println!("\nUnknown item: {value:?}");
850 chunk_count += 1;
851 }
852 Err(e) => {
853 eprintln!("Error: {e:?}");
854 break;
855 }
856 }
857
858 if chunk_count >= 2 {
859 println!("\nCancelling stream...");
860 stream.cancel();
861 println!("Stream cancelled.");
862 break;
863 }
864 }
865
866 let next_chunk = stream.next().await;
867 assert!(
868 next_chunk.is_none(),
869 "Expected no further chunks after cancellation, got {next_chunk:?}"
870 );
871 }
872
873 #[tokio::test]
874 async fn test_stream_pause_resume() {
875 let stream = create_mock_stream();
876
877 stream.pause();
879 assert!(stream.is_paused());
880
881 stream.resume();
883 assert!(!stream.is_paused());
884 }
885
886 #[tokio::test]
887 async fn test_stream_aggregates_reasoning_content() {
888 let mut stream = create_reasoning_stream();
889 while stream.next().await.is_some() {}
890
891 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
892
893 assert!(choice_items.iter().any(|item| matches!(
894 item,
895 AssistantContent::Reasoning(Reasoning {
896 id: Some(id),
897 content
898 }) if id == "rs_1"
899 && matches!(
900 content.first(),
901 Some(ReasoningContent::Text {
902 text,
903 signature: Some(signature)
904 }) if text == "step one" && signature == "sig_1"
905 )
906 )));
907 }
908
909 #[tokio::test]
910 async fn test_stream_reasoning_only_does_not_inject_empty_text() {
911 let mut stream = create_reasoning_only_stream();
912 while stream.next().await.is_some() {}
913
914 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
915 assert_eq!(choice_items.len(), 1);
916 assert!(matches!(
917 choice_items.first(),
918 Some(AssistantContent::Reasoning(Reasoning { id: Some(id), .. })) if id == "rs_only"
919 ));
920 }
921
922 #[tokio::test]
923 async fn test_stream_aggregates_assistant_items_in_arrival_order() {
924 let mut stream = create_interleaved_stream();
925 while stream.next().await.is_some() {}
926
927 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
928 assert_eq!(choice_items.len(), 3);
929 assert!(matches!(
930 choice_items.first(),
931 Some(AssistantContent::Reasoning(Reasoning { id: Some(id), .. })) if id == "rs_interleaved"
932 ));
933 assert!(matches!(
934 choice_items.get(1),
935 Some(AssistantContent::Text(Text { text, .. })) if text == "final-text"
936 ));
937 assert!(matches!(
938 choice_items.get(2),
939 Some(AssistantContent::ToolCall(ToolCall { id, .. })) if id == "tool_1"
940 ));
941 }
942
943 #[tokio::test]
944 async fn unknown_choice_reaches_consumer_but_not_aggregated_choice() {
945 let unknown = serde_json::json!({
946 "type": "web_search_call",
947 "id": "ws_1",
948 "status": "completed",
949 });
950 let yielded = unknown.clone();
951 let stream = stream! {
952 yield Ok(RawStreamingChoice::Unknown(yielded));
953 yield Ok(RawStreamingChoice::Message("done".to_string()));
954 yield Ok(RawStreamingChoice::FinalResponse(MockResponse::with_total_tokens(1)));
955 };
956 let mut stream = StreamingCompletionResponse::stream(to_stream_result(stream));
957
958 let mut consumer_unknown = None;
959 let mut consumer_text = String::new();
960 while let Some(item) = stream.next().await {
961 match item.expect("stream item should be Ok") {
962 StreamedAssistantContent::Unknown(value) => consumer_unknown = Some(value),
963 StreamedAssistantContent::Text(text) => consumer_text.push_str(&text.text),
964 _ => {}
965 }
966 }
967
968 assert_eq!(consumer_unknown.as_ref(), Some(&unknown));
970 assert_eq!(consumer_text, "done");
971
972 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
975 assert_eq!(choice_items.len(), 1);
976 assert!(matches!(
977 choice_items.first(),
978 Some(AssistantContent::Text(Text { text, .. })) if text == "done"
979 ));
980 }
981
982 #[tokio::test]
983 async fn test_stream_keeps_non_contiguous_text_chunks_split_by_tool_call() {
984 let mut stream = create_text_tool_text_stream();
985 while stream.next().await.is_some() {}
986
987 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
988 assert_eq!(choice_items.len(), 3);
989 assert!(matches!(
990 choice_items.first(),
991 Some(AssistantContent::Text(Text { text, .. })) if text == "first"
992 ));
993 assert!(matches!(
994 choice_items.get(1),
995 Some(AssistantContent::ToolCall(ToolCall { id, .. })) if id == "tool_split"
996 ));
997 assert!(matches!(
998 choice_items.get(2),
999 Some(AssistantContent::Text(Text { text, .. })) if text == "second"
1000 ));
1001 }
1002
1003 #[tokio::test]
1004 async fn test_stream_preserves_text_additional_params() {
1005 let mut stream = create_text_metadata_stream();
1006 while stream.next().await.is_some() {}
1007
1008 let choice_items: Vec<AssistantContent> = stream.choice.clone().into_iter().collect();
1009 assert_eq!(choice_items.len(), 2);
1010
1011 let Some(AssistantContent::Text(Text {
1012 text,
1013 additional_params: Some(additional_params),
1014 })) = choice_items.first()
1015 else {
1016 panic!("expected first text item with metadata");
1017 };
1018 assert_eq!(text, "first");
1019 assert_eq!(
1020 additional_params["citations"]
1021 .as_array()
1022 .expect("citations should be an array")
1023 .len(),
1024 2
1025 );
1026
1027 let Some(AssistantContent::Text(Text {
1028 text,
1029 additional_params: Some(additional_params),
1030 })) = choice_items.get(1)
1031 else {
1032 panic!("expected second text item with metadata");
1033 };
1034 assert_eq!(text, "second");
1035 assert_eq!(additional_params["block"], 2);
1036 }
1037}
1038
1039#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1041#[serde(untagged)]
1042pub enum StreamedAssistantContent<R> {
1043 Text(Text),
1045 ToolCall {
1047 tool_call: ToolCall,
1048 internal_call_id: String,
1051 },
1052 ToolCallDelta {
1054 id: String,
1056 internal_call_id: String,
1058 content: ToolCallDeltaContent,
1059 },
1060 Reasoning(Reasoning),
1062 ReasoningDelta {
1064 id: Option<String>,
1066 reasoning: String,
1068 },
1069 Final(R),
1071 Unknown(serde_json::Value),
1079}
1080
1081impl<R> StreamedAssistantContent<R>
1082where
1083 R: Clone + Unpin,
1084{
1085 pub fn text(text: &str) -> Self {
1087 Self::Text(Text::new(text.to_string()))
1088 }
1089
1090 pub fn final_response(res: R) -> Self {
1092 Self::Final(res)
1093 }
1094}
1095
1096#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1098#[serde(untagged)]
1099pub enum StreamedUserContent {
1100 ToolResult {
1102 tool_result: ToolResult,
1103 internal_call_id: String,
1107 },
1108}
1109
1110impl StreamedUserContent {
1111 pub fn tool_result(tool_result: ToolResult, internal_call_id: String) -> Self {
1113 Self::ToolResult {
1114 tool_result,
1115 internal_call_id,
1116 }
1117 }
1118}