1use async_stream::stream;
2use futures::StreamExt;
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5use tracing::{Level, enabled, info_span};
6use tracing_futures::Instrument;
7
8use super::completion::{
9 AnthropicCompatibleProvider, AnthropicCompletionRequest, AnthropicRequestParams, CacheTtl,
10 Content, GenericCompletionModel, Usage,
11};
12use crate::completion::{CompletionError, CompletionRequest, GetTokenUsage};
13use crate::http_client::sse::{Event, GenericEventSource};
14use crate::http_client::{self, HttpClientExt};
15use crate::message::ReasoningContent;
16use crate::streaming::{
17 self, RawStreamingChoice, RawStreamingToolCall, StreamingResult, ToolCallDeltaContent,
18};
19use crate::telemetry::SpanCombinator;
20use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
21use std::collections::HashMap;
22
23fn create_streaming_request_body(
34 request_model: String,
35 mut completion_request: CompletionRequest,
36 max_tokens: u64,
37 prompt_caching: bool,
38 automatic_caching: bool,
39 automatic_caching_ttl: Option<CacheTtl>,
40) -> Result<Value, CompletionError> {
41 completion_request.max_tokens = Some(max_tokens);
44
45 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
46 model: &request_model,
47 request: completion_request,
48 prompt_caching,
49 automatic_caching,
50 automatic_caching_ttl,
51 })?;
52
53 let mut body = serde_json::to_value(&request)?;
54 if let Some(map) = body.as_object_mut() {
55 map.insert("stream".to_string(), Value::Bool(true));
58
59 if map.contains_key("tools") {
69 map.entry("tool_choice")
70 .or_insert_with(|| json!({ "type": "auto" }));
71 } else {
72 map.remove("tool_choice");
73 }
74 }
75
76 Ok(body)
77}
78
79#[derive(Debug, Deserialize)]
80#[serde(tag = "type", rename_all = "snake_case")]
81pub enum StreamingEvent {
82 MessageStart {
83 message: MessageStart,
84 },
85 ContentBlockStart {
86 index: usize,
87 content_block: Content,
88 },
89 ContentBlockDelta {
90 index: usize,
91 delta: ContentDelta,
92 },
93 ContentBlockStop {
94 index: usize,
95 },
96 MessageDelta {
97 delta: MessageDelta,
98 usage: PartialUsage,
99 },
100 MessageStop,
101 Ping,
102 #[serde(other)]
103 Unknown,
104}
105
106#[derive(Debug, Deserialize)]
107pub struct MessageStart {
108 pub id: String,
109 pub role: String,
110 pub content: Vec<Content>,
111 pub model: String,
112 pub stop_reason: Option<String>,
113 pub stop_sequence: Option<String>,
114 pub usage: Usage,
115}
116
117#[derive(Debug, Deserialize)]
118#[serde(tag = "type", rename_all = "snake_case")]
119pub enum ContentDelta {
120 TextDelta {
121 text: String,
122 },
123 InputJsonDelta {
124 partial_json: String,
125 },
126 ThinkingDelta {
127 thinking: String,
128 },
129 SignatureDelta {
130 signature: String,
131 },
132 CitationsDelta {
133 citation: super::completion::Citation,
134 },
135 #[serde(other)]
139 Unknown,
140}
141
142#[derive(Debug, Deserialize)]
143pub struct MessageDelta {
144 pub stop_reason: Option<String>,
145 pub stop_sequence: Option<String>,
146}
147
148#[derive(Debug, Deserialize, Clone, Serialize, Default)]
149pub struct PartialUsage {
150 pub output_tokens: usize,
151 #[serde(default)]
152 pub input_tokens: Option<usize>,
153 #[serde(default)]
154 pub cache_creation_input_tokens: Option<u64>,
155 #[serde(default)]
156 pub cache_read_input_tokens: Option<u64>,
157}
158
159impl GetTokenUsage for PartialUsage {
160 fn token_usage(&self) -> crate::completion::Usage {
161 let mut usage = crate::completion::Usage::new();
162
163 usage.input_tokens = self.input_tokens.unwrap_or_default() as u64;
164 usage.output_tokens = self.output_tokens as u64;
165 usage.cached_input_tokens = self.cache_read_input_tokens.unwrap_or(0);
166 usage.cache_creation_input_tokens = self.cache_creation_input_tokens.unwrap_or(0);
167 usage.total_tokens = usage.input_tokens
168 + usage.cached_input_tokens
169 + usage.cache_creation_input_tokens
170 + usage.output_tokens;
171 usage
172 }
173}
174
175#[derive(Default)]
176struct ToolCallState {
177 name: String,
178 id: String,
179 internal_call_id: String,
180 input_json: String,
181}
182
183struct ServerToolUseState {
184 name: String,
185 id: String,
186 initial_input: Value,
187 input_json: String,
188}
189
190#[derive(Default)]
191struct ThinkingState {
192 thinking: String,
193 signature: String,
194}
195
196#[derive(Clone, Debug, Deserialize, Serialize)]
197pub struct StreamingCompletionResponse {
198 pub usage: PartialUsage,
199}
200
201impl GetTokenUsage for StreamingCompletionResponse {
202 fn token_usage(&self) -> crate::completion::Usage {
203 let mut usage = crate::completion::Usage::new();
204 usage.input_tokens = self.usage.input_tokens.unwrap_or(0) as u64;
205 usage.output_tokens = self.usage.output_tokens as u64;
206 usage.cached_input_tokens = self.usage.cache_read_input_tokens.unwrap_or(0);
207 usage.cache_creation_input_tokens = self.usage.cache_creation_input_tokens.unwrap_or(0);
208 usage.total_tokens = usage.input_tokens
209 + usage.cached_input_tokens
210 + usage.cache_creation_input_tokens
211 + usage.output_tokens;
212
213 usage
214 }
215}
216
217impl<Ext, T> GenericCompletionModel<Ext, T>
218where
219 T: HttpClientExt + Clone + Default + 'static,
220 Ext: AnthropicCompatibleProvider + Clone + WasmCompatSend + WasmCompatSync + 'static,
221{
222 pub(crate) async fn stream(
223 &self,
224 completion_request: CompletionRequest,
225 ) -> Result<streaming::StreamingCompletionResponse<StreamingCompletionResponse>, CompletionError>
226 {
227 let request_model = completion_request
228 .model
229 .clone()
230 .unwrap_or_else(|| self.model.clone());
231 let span = if tracing::Span::current().is_disabled() {
232 info_span!(
233 target: "rig::completions",
234 "chat_streaming",
235 gen_ai.operation.name = "chat_streaming",
236 gen_ai.provider.name = Ext::PROVIDER_NAME,
237 gen_ai.request.model = &request_model,
238 gen_ai.system_instructions = &completion_request.preamble,
239 gen_ai.response.id = tracing::field::Empty,
240 gen_ai.response.model = &request_model,
241 gen_ai.usage.output_tokens = tracing::field::Empty,
242 gen_ai.usage.input_tokens = tracing::field::Empty,
243 gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
244 gen_ai.usage.cache_creation.input_tokens = tracing::field::Empty,
245 gen_ai.input.messages = tracing::field::Empty,
246 gen_ai.output.messages = tracing::field::Empty,
247 )
248 } else {
249 tracing::Span::current()
250 };
251 let max_tokens = if let Some(tokens) = completion_request.max_tokens {
252 tokens
253 } else if let Some(tokens) = self.default_max_tokens {
254 tokens
255 } else {
256 return Err(CompletionError::RequestError(
257 "`max_tokens` must be set for Anthropic".into(),
258 ));
259 };
260
261 let body = create_streaming_request_body(
262 request_model,
263 completion_request,
264 max_tokens,
265 self.prompt_caching,
266 self.automatic_caching,
267 self.automatic_caching_ttl.clone(),
268 )?;
269
270 if enabled!(Level::TRACE) {
271 tracing::trace!(
272 target: "rig::completions",
273 "Anthropic completion request: {}",
274 serde_json::to_string_pretty(&body)?
275 );
276 }
277
278 let body: Vec<u8> = serde_json::to_vec(&body)?;
279
280 let req = self
281 .client
282 .post("/v1/messages")?
283 .body(body)
284 .map_err(http_client::Error::Protocol)?;
285
286 let stream = GenericEventSource::new(self.client.clone(), req);
287
288 let stream: StreamingResult<StreamingCompletionResponse> = Box::pin(stream! {
290 let mut current_tool_call: Option<ToolCallState> = None;
291 let mut server_tool_uses: HashMap<usize, ServerToolUseState> = HashMap::new();
292 let mut current_thinking: Option<ThinkingState> = None;
293 let mut sse_stream = Box::pin(stream);
294 let mut input_tokens = 0;
295 let mut final_usage = None;
296
297 let mut text_content = String::new();
298
299 while let Some(sse_result) = sse_stream.next().await {
300 match sse_result {
301 Ok(Event::Open) => {}
302 Ok(Event::Message(sse)) => {
303 match serde_json::from_str::<StreamingEvent>(&sse.data) {
305 Ok(event) => {
306 match &event {
307 StreamingEvent::MessageStart { message } => {
308 input_tokens = message.usage.input_tokens;
309
310 let span = tracing::Span::current();
311 span.record("gen_ai.response.id", &message.id);
312 span.record("gen_ai.response.model", &message.model);
313 },
314 StreamingEvent::MessageDelta { delta, usage } => {
315 if delta.stop_reason.is_some() {
316 let usage = PartialUsage {
320 output_tokens: usage.output_tokens,
321 input_tokens: usize::try_from(input_tokens).ok(),
322 cache_creation_input_tokens: usage.cache_creation_input_tokens,
323 cache_read_input_tokens: usage.cache_read_input_tokens
324 };
325
326 let span = tracing::Span::current();
327 span.record_token_usage(&usage);
328 final_usage = Some(usage);
329 break;
330 }
331 }
332 _ => {}
333 }
334
335 if let Some(result) = handle_event(
336 &event,
337 &mut current_tool_call,
338 &mut server_tool_uses,
339 &mut current_thinking,
340 ) {
341 if let Ok(RawStreamingChoice::Message(ref text)) = result {
342 text_content += text;
343 }
344 yield result;
345 }
346 },
347 Err(e) => {
348 if !sse.data.trim().is_empty() {
349 yield Err(CompletionError::ResponseError(
350 format!("Failed to parse JSON: {} (Data: {})", e, sse.data)
351 ));
352 }
353 }
354 }
355 },
356 Err(e) => {
357 yield Err(CompletionError::from_stream_transport(e));
358 break;
359 }
360 }
361 }
362
363 sse_stream.close();
365
366 yield Ok(RawStreamingChoice::FinalResponse(StreamingCompletionResponse {
367 usage: final_usage.unwrap_or_default()
368 }))
369 }.instrument(span));
370
371 Ok(streaming::StreamingCompletionResponse::stream(stream))
372 }
373}
374
375fn handle_event(
376 event: &StreamingEvent,
377 current_tool_call: &mut Option<ToolCallState>,
378 server_tool_uses: &mut HashMap<usize, ServerToolUseState>,
379 current_thinking: &mut Option<ThinkingState>,
380) -> Option<Result<RawStreamingChoice<StreamingCompletionResponse>, CompletionError>> {
381 match event {
382 StreamingEvent::ContentBlockDelta { index, delta } => match delta {
383 ContentDelta::TextDelta { text } => {
384 if current_tool_call.is_none() {
385 return Some(Ok(RawStreamingChoice::Message(text.clone())));
386 }
387 None
388 }
389 ContentDelta::InputJsonDelta { partial_json } => {
390 if let Some(server_tool_use) = server_tool_uses.get_mut(index) {
391 server_tool_use.input_json.push_str(partial_json);
392 return None;
393 }
394
395 if let Some(tool_call) = current_tool_call {
396 tool_call.input_json.push_str(partial_json);
397 return Some(Ok(RawStreamingChoice::ToolCallDelta {
399 id: tool_call.id.clone(),
400 internal_call_id: tool_call.internal_call_id.clone(),
401 content: ToolCallDeltaContent::Delta(partial_json.clone()),
402 }));
403 }
404 None
405 }
406 ContentDelta::ThinkingDelta { thinking } => {
407 current_thinking
408 .get_or_insert_with(ThinkingState::default)
409 .thinking
410 .push_str(thinking);
411
412 Some(Ok(RawStreamingChoice::ReasoningDelta {
413 id: None,
414 reasoning: thinking.clone(),
415 }))
416 }
417 ContentDelta::SignatureDelta { signature } => {
418 current_thinking
419 .get_or_insert_with(ThinkingState::default)
420 .signature
421 .push_str(signature);
422
423 None
425 }
426 ContentDelta::CitationsDelta { citation } => {
427 Some(Ok(RawStreamingChoice::TextAdditionalParams(json!({
428 "citations": [citation]
429 }))))
430 }
431 ContentDelta::Unknown => None,
432 },
433 StreamingEvent::ContentBlockStart {
434 index,
435 content_block,
436 } => match content_block {
437 Content::Text { citations, .. } => {
438 let additional_params = (!citations.is_empty()).then(|| {
439 json!({
440 "citations": citations
441 })
442 });
443 Some(Ok(RawStreamingChoice::TextStart { additional_params }))
444 }
445 Content::ServerToolUse { id, name, input } => {
446 server_tool_uses.insert(
447 *index,
448 ServerToolUseState {
449 name: name.clone(),
450 id: id.clone(),
451 initial_input: input.clone(),
452 input_json: String::new(),
453 },
454 );
455 None
456 }
457 raw @ Content::WebSearchToolResult { .. } => Some(Ok(RawStreamingChoice::TextStart {
458 additional_params: Some(json!({
459 super::completion::ANTHROPIC_RAW_CONTENT_KEY: raw
460 })),
461 })),
462 Content::ToolUse { id, name, .. } => {
463 let internal_call_id = crate::id::generate();
464 *current_tool_call = Some(ToolCallState {
465 name: name.clone(),
466 id: id.clone(),
467 internal_call_id: internal_call_id.clone(),
468 input_json: String::new(),
469 });
470 Some(Ok(RawStreamingChoice::ToolCallDelta {
471 id: id.clone(),
472 internal_call_id,
473 content: ToolCallDeltaContent::Name(name.clone()),
474 }))
475 }
476 Content::Thinking { .. } => {
477 *current_thinking = Some(ThinkingState::default());
478 None
479 }
480 Content::RedactedThinking { data } => Some(Ok(RawStreamingChoice::Reasoning {
481 id: None,
482 content: ReasoningContent::Redacted { data: data.clone() },
483 })),
484 _ => None,
486 },
487 StreamingEvent::ContentBlockStop { index } => {
488 if let Some(thinking_state) = Option::take(current_thinking)
489 && !thinking_state.thinking.is_empty()
490 {
491 let signature = if thinking_state.signature.is_empty() {
492 None
493 } else {
494 Some(thinking_state.signature)
495 };
496
497 return Some(Ok(RawStreamingChoice::Reasoning {
498 id: None,
499 content: ReasoningContent::Text {
500 text: thinking_state.thinking,
501 signature,
502 },
503 }));
504 }
505
506 if let Some(server_tool_use) = server_tool_uses.remove(index) {
507 let input = if server_tool_use.input_json.is_empty() {
508 if server_tool_use.initial_input.is_null() {
509 json!({})
510 } else {
511 server_tool_use.initial_input
512 }
513 } else {
514 match serde_json::from_str(&server_tool_use.input_json) {
515 Ok(json_value) => json_value,
516 Err(e) => return Some(Err(CompletionError::from(e))),
517 }
518 };
519
520 return Some(Ok(RawStreamingChoice::TextStart {
521 additional_params: Some(json!({
522 super::completion::ANTHROPIC_RAW_CONTENT_KEY: Content::ServerToolUse {
523 id: server_tool_use.id,
524 name: server_tool_use.name,
525 input,
526 }
527 })),
528 }));
529 }
530
531 if let Some(tool_call) = Option::take(current_tool_call) {
532 let json_str = if tool_call.input_json.is_empty() {
533 "{}"
534 } else {
535 &tool_call.input_json
536 };
537 match serde_json::from_str(json_str) {
538 Ok(json_value) => {
539 let raw_tool_call =
540 RawStreamingToolCall::new(tool_call.id, tool_call.name, json_value)
541 .with_internal_call_id(tool_call.internal_call_id);
542 Some(Ok(RawStreamingChoice::ToolCall(raw_tool_call)))
543 }
544 Err(e) => Some(Err(CompletionError::from(e))),
545 }
546 } else {
547 None
548 }
549 }
550 StreamingEvent::MessageStart { .. }
552 | StreamingEvent::MessageDelta { .. }
553 | StreamingEvent::MessageStop
554 | StreamingEvent::Ping
555 | StreamingEvent::Unknown => None,
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use super::super::completion::{
562 CLAUDE_OPUS_4_8, CacheControl, CacheTtl, Message, SystemContent,
563 apply_prompt_cache_control, build_tool_definitions, resolve_top_level_cache_control,
564 };
565 use super::*;
566 use crate::OneOrMany;
567 use crate::completion::Message as RigMessage;
568 use crate::completion::request::Document as RigDocument;
569 use async_stream::stream;
570 use futures::StreamExt;
571
572 #[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
573 fn to_stream_result(
574 stream: impl futures::Stream<
575 Item = Result<RawStreamingChoice<StreamingCompletionResponse>, CompletionError>,
576 > + Send
577 + 'static,
578 ) -> crate::streaming::StreamingResult<StreamingCompletionResponse> {
579 Box::pin(stream)
580 }
581
582 #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
583 fn to_stream_result(
584 stream: impl futures::Stream<
585 Item = Result<RawStreamingChoice<StreamingCompletionResponse>, CompletionError>,
586 > + 'static,
587 ) -> crate::streaming::StreamingResult<StreamingCompletionResponse> {
588 Box::pin(stream)
589 }
590
591 #[test]
592 fn test_streaming_tool_build_marks_final_combined_tool() {
593 let mut additional_params = json!({
594 "tools": [{
595 "name": "provider_tool",
596 "description": "Provider tool",
597 "input_schema": {"type": "object"}
598 }]
599 });
600
601 let mut tools = build_tool_definitions(
602 vec![crate::completion::ToolDefinition {
603 name: "rig_tool".to_string(),
604 description: "Rig tool".to_string(),
605 parameters: json!({"type": "object", "properties": {}}),
606 }],
607 &mut additional_params,
608 )
609 .unwrap();
610 let mut system: Vec<SystemContent> = Vec::new();
611 let mut messages: Vec<Message> = Vec::new();
612 apply_prompt_cache_control(&mut system, &mut messages, &mut tools, true, None).unwrap();
613
614 assert_eq!(tools.len(), 2);
615 assert!(tools[0].get("cache_control").is_none());
616 assert_eq!(tools[1]["name"], "provider_tool");
617 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
618 }
619
620 #[test]
621 fn streaming_request_keeps_documents_after_leading_system_messages() {
622 let request = CompletionRequest {
623 model: None,
624 preamble: None,
625 chat_history: OneOrMany::many(vec![
626 RigMessage::system("System prompt"),
627 RigMessage::assistant("Earlier assistant turn"),
628 RigMessage::system("Mid-conversation instruction"),
629 RigMessage::user("Prompt"),
630 ])
631 .unwrap(),
632 documents: vec![RigDocument {
633 id: "doc1".to_string(),
634 text: "Document text.".to_string(),
635 additional_props: Default::default(),
636 }],
637 tools: vec![],
638 temperature: None,
639 max_tokens: Some(64),
640 tool_choice: None,
641 additional_params: None,
642 output_schema: None,
643 };
644
645 let body = create_streaming_request_body(
646 CLAUDE_OPUS_4_8.to_string(),
647 request,
648 64,
649 false,
650 false,
651 None,
652 )
653 .expect("streaming request body should build");
654
655 assert_eq!(body["system"][0]["text"], "System prompt");
656 assert_eq!(body["system"][1]["text"], "Mid-conversation instruction");
657 let messages = body["messages"]
658 .as_array()
659 .expect("messages should be array");
660 assert_eq!(messages.len(), 3);
661 assert_eq!(messages[0]["role"], "user");
662 assert!(
663 messages[0].to_string().contains("<file id: doc1>"),
664 "document message should follow top-level system: {messages:?}"
665 );
666 assert_eq!(messages[1]["role"], "assistant");
667 assert_eq!(messages[2]["role"], "user");
668 assert_eq!(
669 messages
670 .iter()
671 .filter(|message| message.to_string().contains("<file id: doc1>"))
672 .count(),
673 1,
674 "document message should appear exactly once: {messages:?}"
675 );
676 }
677
678 #[test]
679 fn streaming_body_is_blocking_body_plus_stream_flag_and_carries_output_schema() {
680 let schema: schemars::Schema = serde_json::from_value(json!({
681 "title": "WeatherResponse",
682 "type": "object",
683 "properties": { "city": { "type": "string" } }
684 }))
685 .expect("schema should deserialize");
686
687 let request = CompletionRequest {
688 model: None,
689 preamble: Some("You are helpful".to_string()),
690 chat_history: OneOrMany::one(RigMessage::user("What's the weather?")),
691 documents: vec![],
692 tools: vec![],
693 temperature: Some(0.5),
694 max_tokens: Some(64),
695 tool_choice: None,
696 additional_params: None,
697 output_schema: Some(schema),
698 };
699
700 let streaming_body = create_streaming_request_body(
701 CLAUDE_OPUS_4_8.to_string(),
702 request.clone(),
703 64,
704 false,
705 false,
706 None,
707 )
708 .expect("streaming request body should build");
709
710 assert_eq!(streaming_body["stream"], serde_json::Value::Bool(true));
712
713 assert_eq!(
717 streaming_body["output_config"]["format"]["type"],
718 "json_schema"
719 );
720 assert!(
721 streaming_body["output_config"]["format"]["schema"].is_object(),
722 "streaming body must carry the structured-output schema: {streaming_body}"
723 );
724
725 let blocking = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
729 model: CLAUDE_OPUS_4_8,
730 request,
731 prompt_caching: false,
732 automatic_caching: false,
733 automatic_caching_ttl: None,
734 })
735 .expect("blocking request body should build");
736 let mut expected = serde_json::to_value(&blocking).expect("serialize blocking body");
737 expected
738 .as_object_mut()
739 .expect("body is an object")
740 .insert("stream".to_string(), serde_json::Value::Bool(true));
741
742 assert_eq!(streaming_body, expected);
743 }
744
745 #[test]
746 fn streaming_body_keeps_explicit_tool_choice_auto_when_tools_present_but_unset() {
747 let request = CompletionRequest {
748 model: None,
749 preamble: None,
750 chat_history: OneOrMany::one(RigMessage::user("Add 2 and 3")),
751 documents: vec![],
752 tools: vec![crate::completion::ToolDefinition {
753 name: "add".to_string(),
754 description: "Add x and y".to_string(),
755 parameters: json!({
756 "type": "object",
757 "properties": { "x": { "type": "integer" } }
758 }),
759 }],
760 temperature: None,
761 max_tokens: Some(64),
762 tool_choice: None,
763 additional_params: None,
764 output_schema: None,
765 };
766
767 let body = create_streaming_request_body(
768 CLAUDE_OPUS_4_8.to_string(),
769 request,
770 64,
771 false,
772 false,
773 None,
774 )
775 .expect("streaming request body should build");
776
777 assert_eq!(body["tool_choice"], json!({ "type": "auto" }));
781 assert!(body["tools"].is_array());
782 }
783
784 #[test]
785 fn streaming_body_drops_tool_choice_when_no_tools_are_advertised() {
786 let request = CompletionRequest {
791 model: None,
792 preamble: None,
793 chat_history: OneOrMany::one(RigMessage::user("Hi")),
794 documents: vec![],
795 tools: vec![],
796 temperature: None,
797 max_tokens: Some(64),
798 tool_choice: Some(crate::message::ToolChoice::Auto),
799 additional_params: None,
800 output_schema: None,
801 };
802
803 let body = create_streaming_request_body(
804 CLAUDE_OPUS_4_8.to_string(),
805 request,
806 64,
807 false,
808 false,
809 None,
810 )
811 .expect("streaming request body should build");
812
813 assert!(
814 body.get("tool_choice").is_none(),
815 "tool_choice must be omitted when no tools are advertised: {body}"
816 );
817 assert!(body.get("tools").is_none());
818 }
819
820 #[test]
821 fn test_streaming_prompt_cache_control_uses_raw_top_level_ttl() {
822 let mut additional_params = json!({
823 "cache_control": {"type": "ephemeral", "ttl": "1h"}
824 });
825 let top_level_cache_control =
826 resolve_top_level_cache_control(false, None, &mut additional_params).unwrap();
827 let mut tools = build_tool_definitions(
828 vec![crate::completion::ToolDefinition {
829 name: "rig_tool".to_string(),
830 description: "Rig tool".to_string(),
831 parameters: json!({"type": "object", "properties": {}}),
832 }],
833 &mut additional_params,
834 )
835 .unwrap();
836 let mut system = vec![SystemContent::Text {
837 text: "System prompt".to_string(),
838 cache_control: None,
839 }];
840 let mut messages: Vec<Message> = Vec::new();
841
842 apply_prompt_cache_control(
843 &mut system,
844 &mut messages,
845 &mut tools,
846 true,
847 top_level_cache_control.as_ref(),
848 )
849 .unwrap();
850
851 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
852 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
853 match &system[0] {
854 SystemContent::Text {
855 cache_control: Some(CacheControl::Ephemeral { ttl }),
856 ..
857 } => assert_eq!(ttl.as_ref(), Some(&CacheTtl::OneHour)),
858 other => panic!("expected system cache_control, got {other:?}"),
859 }
860 assert!(additional_params.get("cache_control").is_none());
861 }
862
863 fn handle_event(
864 event: &StreamingEvent,
865 current_tool_call: &mut Option<ToolCallState>,
866 current_thinking: &mut Option<ThinkingState>,
867 ) -> Option<Result<RawStreamingChoice<StreamingCompletionResponse>, CompletionError>> {
868 let mut server_tool_uses = HashMap::new();
869 super::handle_event(
870 event,
871 current_tool_call,
872 &mut server_tool_uses,
873 current_thinking,
874 )
875 }
876
877 #[test]
878 fn test_thinking_delta_deserialization() {
879 let json = r#"{"type": "thinking_delta", "thinking": "Let me think about this..."}"#;
880 let delta: ContentDelta = serde_json::from_str(json).unwrap();
881
882 match delta {
883 ContentDelta::ThinkingDelta { thinking } => {
884 assert_eq!(thinking, "Let me think about this...");
885 }
886 _ => panic!("Expected ThinkingDelta variant"),
887 }
888 }
889
890 #[test]
891 fn test_signature_delta_deserialization() {
892 let json = r#"{"type": "signature_delta", "signature": "abc123def456"}"#;
893 let delta: ContentDelta = serde_json::from_str(json).unwrap();
894
895 match delta {
896 ContentDelta::SignatureDelta { signature } => {
897 assert_eq!(signature, "abc123def456");
898 }
899 _ => panic!("Expected SignatureDelta variant"),
900 }
901 }
902
903 #[test]
904 fn test_thinking_delta_streaming_event_deserialization() {
905 let json = r#"{
906 "type": "content_block_delta",
907 "index": 0,
908 "delta": {
909 "type": "thinking_delta",
910 "thinking": "First, I need to understand the problem."
911 }
912 }"#;
913
914 let event: StreamingEvent = serde_json::from_str(json).unwrap();
915
916 match event {
917 StreamingEvent::ContentBlockDelta { index, delta } => {
918 assert_eq!(index, 0);
919 match delta {
920 ContentDelta::ThinkingDelta { thinking } => {
921 assert_eq!(thinking, "First, I need to understand the problem.");
922 }
923 _ => panic!("Expected ThinkingDelta"),
924 }
925 }
926 _ => panic!("Expected ContentBlockDelta event"),
927 }
928 }
929
930 #[test]
931 fn test_signature_delta_streaming_event_deserialization() {
932 let json = r#"{
933 "type": "content_block_delta",
934 "index": 0,
935 "delta": {
936 "type": "signature_delta",
937 "signature": "ErUBCkYICBgCIkCaGbqC85F4"
938 }
939 }"#;
940
941 let event: StreamingEvent = serde_json::from_str(json).unwrap();
942
943 match event {
944 StreamingEvent::ContentBlockDelta { index, delta } => {
945 assert_eq!(index, 0);
946 match delta {
947 ContentDelta::SignatureDelta { signature } => {
948 assert_eq!(signature, "ErUBCkYICBgCIkCaGbqC85F4");
949 }
950 _ => panic!("Expected SignatureDelta"),
951 }
952 }
953 _ => panic!("Expected ContentBlockDelta event"),
954 }
955 }
956
957 #[test]
958 fn test_handle_thinking_delta_event() {
959 let event = StreamingEvent::ContentBlockDelta {
960 index: 0,
961 delta: ContentDelta::ThinkingDelta {
962 thinking: "Analyzing the request...".to_string(),
963 },
964 };
965
966 let mut tool_call_state = None;
967 let mut thinking_state = None;
968 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
969
970 assert!(result.is_some());
971 let choice = result.unwrap().unwrap();
972
973 match choice {
974 RawStreamingChoice::ReasoningDelta { id, reasoning, .. } => {
975 assert_eq!(id, None);
976 assert_eq!(reasoning, "Analyzing the request...");
977 }
978 _ => panic!("Expected ReasoningDelta choice"),
979 }
980
981 assert!(thinking_state.is_some());
983 assert_eq!(thinking_state.unwrap().thinking, "Analyzing the request...");
984 }
985
986 #[test]
987 fn test_handle_signature_delta_event() {
988 let event = StreamingEvent::ContentBlockDelta {
989 index: 0,
990 delta: ContentDelta::SignatureDelta {
991 signature: "test_signature".to_string(),
992 },
993 };
994
995 let mut tool_call_state = None;
996 let mut thinking_state = None;
997 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
998
999 assert!(result.is_none());
1001
1002 assert!(thinking_state.is_some());
1004 assert_eq!(thinking_state.unwrap().signature, "test_signature");
1005 }
1006
1007 #[test]
1008 fn test_handle_redacted_thinking_content_block_start_event() {
1009 let event = StreamingEvent::ContentBlockStart {
1010 index: 0,
1011 content_block: Content::RedactedThinking {
1012 data: "redacted_blob".to_string(),
1013 },
1014 };
1015 let mut tool_call_state = None;
1016 let mut thinking_state = None;
1017 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1018
1019 assert!(result.is_some());
1020 match result.unwrap().unwrap() {
1021 RawStreamingChoice::Reasoning {
1022 content: ReasoningContent::Redacted { data },
1023 ..
1024 } => {
1025 assert_eq!(data, "redacted_blob");
1026 }
1027 _ => panic!("Expected Redacted reasoning chunk"),
1028 }
1029 }
1030
1031 #[test]
1032 fn test_handle_text_delta_event() {
1033 let event = StreamingEvent::ContentBlockDelta {
1034 index: 0,
1035 delta: ContentDelta::TextDelta {
1036 text: "Hello, world!".to_string(),
1037 },
1038 };
1039
1040 let mut tool_call_state = None;
1041 let mut thinking_state = None;
1042 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1043
1044 assert!(result.is_some());
1045 let choice = result.unwrap().unwrap();
1046
1047 match choice {
1048 RawStreamingChoice::Message(text) => {
1049 assert_eq!(text, "Hello, world!");
1050 }
1051 _ => panic!("Expected Message choice"),
1052 }
1053 }
1054
1055 #[test]
1056 fn test_handle_text_block_start_event() {
1057 let event = StreamingEvent::ContentBlockStart {
1058 index: 0,
1059 content_block: Content::Text {
1060 text: String::new(),
1061 citations: Vec::new(),
1062 cache_control: None,
1063 },
1064 };
1065
1066 let mut tool_call_state = None;
1067 let mut thinking_state = None;
1068 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1069
1070 assert!(result.is_some());
1071 let choice = result.unwrap().unwrap();
1072 assert!(matches!(
1073 choice,
1074 RawStreamingChoice::TextStart {
1075 additional_params: None
1076 }
1077 ));
1078 }
1079
1080 #[test]
1081 fn test_thinking_delta_does_not_interfere_with_tool_calls() {
1082 let event = StreamingEvent::ContentBlockDelta {
1084 index: 0,
1085 delta: ContentDelta::ThinkingDelta {
1086 thinking: "Thinking while tool is active...".to_string(),
1087 },
1088 };
1089
1090 let mut tool_call_state = Some(ToolCallState {
1091 name: "test_tool".to_string(),
1092 id: "tool_123".to_string(),
1093 internal_call_id: crate::id::generate(),
1094 input_json: String::new(),
1095 });
1096 let mut thinking_state = None;
1097
1098 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1099
1100 assert!(result.is_some());
1101 let choice = result.unwrap().unwrap();
1102
1103 match choice {
1104 RawStreamingChoice::ReasoningDelta { reasoning, .. } => {
1105 assert_eq!(reasoning, "Thinking while tool is active...");
1106 }
1107 _ => panic!("Expected ReasoningDelta choice"),
1108 }
1109
1110 assert!(tool_call_state.is_some());
1112 }
1113
1114 #[test]
1115 fn test_handle_input_json_delta_event() {
1116 let event = StreamingEvent::ContentBlockDelta {
1117 index: 0,
1118 delta: ContentDelta::InputJsonDelta {
1119 partial_json: "{\"arg\":\"value".to_string(),
1120 },
1121 };
1122
1123 let mut tool_call_state = Some(ToolCallState {
1124 name: "test_tool".to_string(),
1125 id: "tool_123".to_string(),
1126 internal_call_id: crate::id::generate(),
1127 input_json: String::new(),
1128 });
1129 let mut thinking_state = None;
1130
1131 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1132
1133 assert!(result.is_some());
1135 let choice = result.unwrap().unwrap();
1136
1137 match choice {
1138 RawStreamingChoice::ToolCallDelta {
1139 id,
1140 internal_call_id: _,
1141 content,
1142 } => {
1143 assert_eq!(id, "tool_123");
1144 match content {
1145 ToolCallDeltaContent::Delta(delta) => assert_eq!(delta, "{\"arg\":\"value"),
1146 _ => panic!("Expected Delta content"),
1147 }
1148 }
1149 _ => panic!("Expected ToolCallDelta choice, got {:?}", choice),
1150 }
1151
1152 assert!(tool_call_state.is_some());
1154 let state = tool_call_state.unwrap();
1155 assert_eq!(state.input_json, "{\"arg\":\"value");
1156 }
1157
1158 #[test]
1159 fn test_tool_call_accumulation_with_multiple_deltas() {
1160 let mut tool_call_state = Some(ToolCallState {
1161 name: "test_tool".to_string(),
1162 id: "tool_123".to_string(),
1163 internal_call_id: crate::id::generate(),
1164 input_json: String::new(),
1165 });
1166 let mut thinking_state = None;
1167
1168 let event1 = StreamingEvent::ContentBlockDelta {
1170 index: 0,
1171 delta: ContentDelta::InputJsonDelta {
1172 partial_json: "{\"location\":".to_string(),
1173 },
1174 };
1175 let result1 = handle_event(&event1, &mut tool_call_state, &mut thinking_state);
1176 assert!(result1.is_some());
1177
1178 let event2 = StreamingEvent::ContentBlockDelta {
1180 index: 0,
1181 delta: ContentDelta::InputJsonDelta {
1182 partial_json: "\"Paris\",".to_string(),
1183 },
1184 };
1185 let result2 = handle_event(&event2, &mut tool_call_state, &mut thinking_state);
1186 assert!(result2.is_some());
1187
1188 let event3 = StreamingEvent::ContentBlockDelta {
1190 index: 0,
1191 delta: ContentDelta::InputJsonDelta {
1192 partial_json: "\"temp\":\"20C\"}".to_string(),
1193 },
1194 };
1195 let result3 = handle_event(&event3, &mut tool_call_state, &mut thinking_state);
1196 assert!(result3.is_some());
1197
1198 assert!(tool_call_state.is_some());
1200 let state = tool_call_state.as_ref().unwrap();
1201 assert_eq!(
1202 state.input_json,
1203 "{\"location\":\"Paris\",\"temp\":\"20C\"}"
1204 );
1205
1206 let stop_event = StreamingEvent::ContentBlockStop { index: 0 };
1208 let final_result = handle_event(&stop_event, &mut tool_call_state, &mut thinking_state);
1209 assert!(final_result.is_some());
1210
1211 match final_result.unwrap().unwrap() {
1212 RawStreamingChoice::ToolCall(RawStreamingToolCall {
1213 id,
1214 name,
1215 arguments,
1216 ..
1217 }) => {
1218 assert_eq!(id, "tool_123");
1219 assert_eq!(name, "test_tool");
1220 assert_eq!(
1221 arguments.get("location").unwrap().as_str().unwrap(),
1222 "Paris"
1223 );
1224 assert_eq!(arguments.get("temp").unwrap().as_str().unwrap(), "20C");
1225 }
1226 other => panic!("Expected ToolCall, got {:?}", other),
1227 }
1228
1229 assert!(tool_call_state.is_none());
1231 }
1232
1233 #[test]
1234 fn test_citations_delta_streaming_event_deserialization() {
1235 let json = r#"{
1236 "type": "content_block_delta",
1237 "index": 0,
1238 "delta": {
1239 "type": "citations_delta",
1240 "citation": {
1241 "type": "char_location",
1242 "cited_text": "The grass is green.",
1243 "document_index": 0,
1244 "document_title": "Example",
1245 "start_char_index": 0,
1246 "end_char_index": 20
1247 }
1248 }
1249 }"#;
1250
1251 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1252 let StreamingEvent::ContentBlockDelta { index, delta } = event else {
1253 panic!("expected ContentBlockDelta");
1254 };
1255 assert_eq!(index, 0);
1256 let ContentDelta::CitationsDelta { citation } = delta else {
1257 panic!("expected CitationsDelta");
1258 };
1259 let crate::providers::anthropic::completion::Citation::CharLocation {
1260 start_char_index,
1261 end_char_index,
1262 ..
1263 } = citation
1264 else {
1265 panic!("expected CharLocation");
1266 };
1267 assert_eq!(start_char_index, 0);
1268 assert_eq!(end_char_index, 20);
1269 }
1270
1271 #[test]
1272 fn test_search_result_citations_delta_streaming_event_deserialization() {
1273 let json = r#"{
1274 "type": "content_block_delta",
1275 "index": 0,
1276 "delta": {
1277 "type": "citations_delta",
1278 "citation": {
1279 "type": "search_result_location",
1280 "cited_text": "API requests require a key.",
1281 "source": "https://docs.example.com/api-reference",
1282 "title": "API Reference",
1283 "search_result_index": 0,
1284 "start_block_index": 0,
1285 "end_block_index": 1
1286 }
1287 }
1288 }"#;
1289
1290 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1291 let StreamingEvent::ContentBlockDelta { delta, .. } = event else {
1292 panic!("expected ContentBlockDelta");
1293 };
1294 let ContentDelta::CitationsDelta { citation } = delta else {
1295 panic!("expected CitationsDelta");
1296 };
1297 assert!(matches!(
1298 citation,
1299 crate::providers::anthropic::completion::Citation::SearchResultLocation {
1300 search_result_index: 0,
1301 start_block_index: 0,
1302 end_block_index: 1,
1303 ..
1304 }
1305 ));
1306 }
1307
1308 #[test]
1309 fn test_web_search_result_citations_delta_streaming_event_deserialization() {
1310 let json = r#"{
1311 "type": "content_block_delta",
1312 "index": 0,
1313 "delta": {
1314 "type": "citations_delta",
1315 "citation": {
1316 "type": "web_search_result_location",
1317 "cited_text": "Claude Shannon was a mathematician.",
1318 "url": "https://example.com/shannon",
1319 "title": "Claude Shannon",
1320 "encrypted_index": "encrypted-reference"
1321 }
1322 }
1323 }"#;
1324
1325 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1326 let StreamingEvent::ContentBlockDelta { delta, .. } = event else {
1327 panic!("expected ContentBlockDelta");
1328 };
1329 let ContentDelta::CitationsDelta { citation } = delta else {
1330 panic!("expected CitationsDelta");
1331 };
1332 assert!(matches!(
1333 citation,
1334 crate::providers::anthropic::completion::Citation::WebSearchResultLocation {
1335 ref url,
1336 ref encrypted_index,
1337 ..
1338 } if url == "https://example.com/shannon"
1339 && encrypted_index == "encrypted-reference"
1340 ));
1341 }
1342
1343 #[test]
1344 fn test_web_search_result_citations_delta_allows_null_title() {
1345 let json = r#"{
1346 "type": "content_block_delta",
1347 "index": 0,
1348 "delta": {
1349 "type": "citations_delta",
1350 "citation": {
1351 "type": "web_search_result_location",
1352 "cited_text": "Claude Shannon was a mathematician.",
1353 "url": "https://example.com/shannon",
1354 "title": null,
1355 "encrypted_index": "encrypted-reference"
1356 }
1357 }
1358 }"#;
1359
1360 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1361 let StreamingEvent::ContentBlockDelta { delta, .. } = event else {
1362 panic!("expected ContentBlockDelta");
1363 };
1364 let ContentDelta::CitationsDelta { citation } = delta else {
1365 panic!("expected CitationsDelta");
1366 };
1367 assert!(matches!(
1368 citation,
1369 crate::providers::anthropic::completion::Citation::WebSearchResultLocation {
1370 title: None,
1371 ..
1372 }
1373 ));
1374 }
1375
1376 #[test]
1377 fn test_text_content_block_start_allows_null_citations() {
1378 let json = r#"{
1383 "type": "content_block_start",
1384 "index": 0,
1385 "content_block": {
1386 "type": "text",
1387 "text": "",
1388 "citations": null
1389 }
1390 }"#;
1391
1392 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1393 let StreamingEvent::ContentBlockStart { content_block, .. } = event else {
1394 panic!("expected ContentBlockStart");
1395 };
1396 let Content::Text {
1397 text, citations, ..
1398 } = content_block
1399 else {
1400 panic!("expected text content block");
1401 };
1402 assert_eq!(text, "");
1403 assert!(citations.is_empty());
1404 }
1405
1406 #[test]
1407 fn test_web_search_content_block_start_events_deserialize() {
1408 let server_tool_use = r#"{
1409 "type": "content_block_start",
1410 "index": 1,
1411 "content_block": {
1412 "type": "server_tool_use",
1413 "id": "srvtoolu_01",
1414 "name": "web_search",
1415 "input": {
1416 "query": "claude shannon birth date"
1417 }
1418 }
1419 }"#;
1420 let event: StreamingEvent = serde_json::from_str(server_tool_use).unwrap();
1421 assert!(matches!(
1422 event,
1423 StreamingEvent::ContentBlockStart {
1424 content_block: Content::ServerToolUse {
1425 ref id,
1426 ref name,
1427 ref input
1428 },
1429 ..
1430 } if id == "srvtoolu_01"
1431 && name == "web_search"
1432 && input["query"] == "claude shannon birth date"
1433 ));
1434
1435 let web_search_tool_result = r#"{
1436 "type": "content_block_start",
1437 "index": 2,
1438 "content_block": {
1439 "type": "web_search_tool_result",
1440 "tool_use_id": "srvtoolu_01",
1441 "content": [{
1442 "type": "web_search_result",
1443 "url": "https://example.com/shannon",
1444 "title": "Claude Shannon",
1445 "encrypted_content": "encrypted-content"
1446 }]
1447 }
1448 }"#;
1449 let event: StreamingEvent = serde_json::from_str(web_search_tool_result).unwrap();
1450 assert!(matches!(
1451 event,
1452 StreamingEvent::ContentBlockStart {
1453 content_block: Content::WebSearchToolResult {
1454 ref tool_use_id,
1455 ref content
1456 },
1457 ..
1458 } if tool_use_id == "srvtoolu_01"
1459 && content[0]["encrypted_content"] == "encrypted-content"
1460 ));
1461 }
1462
1463 #[tokio::test]
1464 async fn test_streaming_web_search_blocks_are_preserved_on_final_choice() {
1465 let raw_stream = stream! {
1466 let mut tool_call_state = None;
1467 let mut server_tool_uses = HashMap::new();
1468 let mut thinking_state = None;
1469
1470 let server_tool_use_start = super::handle_event(
1471 &StreamingEvent::ContentBlockStart {
1472 index: 0,
1473 content_block: Content::ServerToolUse {
1474 id: "srvtoolu_01".to_string(),
1475 name: "web_search".to_string(),
1476 input: serde_json::Value::Null,
1477 },
1478 },
1479 &mut tool_call_state,
1480 &mut server_tool_uses,
1481 &mut thinking_state,
1482 );
1483 assert!(
1484 server_tool_use_start.is_none(),
1485 "server_tool_use start should be accumulated until its input JSON is complete"
1486 );
1487
1488 let server_tool_use_delta = super::handle_event(
1489 &StreamingEvent::ContentBlockDelta {
1490 index: 0,
1491 delta: ContentDelta::InputJsonDelta {
1492 partial_json: r#"{"query":"claude shannon birth date"}"#.to_string(),
1493 },
1494 },
1495 &mut tool_call_state,
1496 &mut server_tool_uses,
1497 &mut thinking_state,
1498 );
1499 assert!(
1500 server_tool_use_delta.is_none(),
1501 "server_tool_use input JSON should not be emitted as a Rig tool-call delta"
1502 );
1503
1504 yield super::handle_event(
1505 &StreamingEvent::ContentBlockStop { index: 0 },
1506 &mut tool_call_state,
1507 &mut server_tool_uses,
1508 &mut thinking_state,
1509 )
1510 .expect("server_tool_use stop should produce completed raw metadata");
1511
1512 yield super::handle_event(
1513 &StreamingEvent::ContentBlockStart {
1514 index: 1,
1515 content_block: Content::WebSearchToolResult {
1516 tool_use_id: "srvtoolu_01".to_string(),
1517 content: serde_json::json!([{
1518 "type": "web_search_result",
1519 "url": "https://example.com/shannon",
1520 "title": "Claude Shannon",
1521 "encrypted_content": "encrypted-content"
1522 }]),
1523 },
1524 },
1525 &mut tool_call_state,
1526 &mut server_tool_uses,
1527 &mut thinking_state,
1528 )
1529 .expect("web_search_tool_result block should produce raw metadata");
1530
1531 yield super::handle_event(
1532 &StreamingEvent::ContentBlockStart {
1533 index: 2,
1534 content_block: Content::Text {
1535 text: String::new(),
1536 citations: Vec::new(),
1537 cache_control: None,
1538 },
1539 },
1540 &mut tool_call_state,
1541 &mut server_tool_uses,
1542 &mut thinking_state,
1543 )
1544 .expect("text block start should produce a raw choice");
1545
1546 yield super::handle_event(
1547 &StreamingEvent::ContentBlockDelta {
1548 index: 2,
1549 delta: ContentDelta::TextDelta {
1550 text: "Claude Shannon was born on April 30, 1916.".to_string(),
1551 },
1552 },
1553 &mut tool_call_state,
1554 &mut server_tool_uses,
1555 &mut thinking_state,
1556 )
1557 .expect("text delta should produce a raw choice");
1558
1559 yield super::handle_event(
1560 &StreamingEvent::ContentBlockDelta {
1561 index: 2,
1562 delta: ContentDelta::CitationsDelta {
1563 citation: crate::providers::anthropic::completion::Citation::WebSearchResultLocation {
1564 cited_text: "Claude Shannon was born on April 30, 1916.".to_string(),
1565 url: "https://example.com/shannon".to_string(),
1566 title: Some("Claude Shannon".to_string()),
1567 encrypted_index: "encrypted-index".to_string(),
1568 },
1569 },
1570 },
1571 &mut tool_call_state,
1572 &mut server_tool_uses,
1573 &mut thinking_state,
1574 )
1575 .expect("citation delta should produce a raw choice");
1576
1577 yield Ok(RawStreamingChoice::FinalResponse(StreamingCompletionResponse {
1578 usage: PartialUsage::default(),
1579 }));
1580 };
1581
1582 let mut stream =
1583 crate::streaming::StreamingCompletionResponse::stream(to_stream_result(raw_stream));
1584 while stream.next().await.is_some() {}
1585
1586 let choice_items: Vec<crate::message::AssistantContent> =
1587 stream.choice.clone().into_iter().collect();
1588 assert_eq!(choice_items.len(), 3);
1589 assert!(
1590 choice_items
1591 .iter()
1592 .all(|item| !matches!(item, crate::message::AssistantContent::ToolCall(_))),
1593 "provider-owned web-search blocks must not become Rig client tool calls"
1594 );
1595
1596 let Some(crate::message::AssistantContent::Text(server_tool_use)) = choice_items.first()
1597 else {
1598 panic!("expected raw server_tool_use metadata");
1599 };
1600 assert_eq!(
1601 server_tool_use.additional_params.as_ref().unwrap()
1602 [crate::providers::anthropic::completion::ANTHROPIC_RAW_CONTENT_KEY]["type"],
1603 "server_tool_use"
1604 );
1605 assert_eq!(
1606 server_tool_use.additional_params.as_ref().unwrap()
1607 [crate::providers::anthropic::completion::ANTHROPIC_RAW_CONTENT_KEY]["input"]["query"],
1608 "claude shannon birth date"
1609 );
1610
1611 let Some(crate::message::AssistantContent::Text(web_search_result)) = choice_items.get(1)
1612 else {
1613 panic!("expected raw web_search_tool_result metadata");
1614 };
1615 assert_eq!(
1616 web_search_result.additional_params.as_ref().unwrap()
1617 [crate::providers::anthropic::completion::ANTHROPIC_RAW_CONTENT_KEY]["content"][0]
1618 ["encrypted_content"],
1619 "encrypted-content"
1620 );
1621
1622 let Some(crate::message::AssistantContent::Text(answer)) = choice_items.get(2) else {
1623 panic!("expected answer text");
1624 };
1625 assert_eq!(answer.text, "Claude Shannon was born on April 30, 1916.");
1626 let citations = crate::providers::anthropic::completion::anthropic_citations(answer)
1627 .expect("expected preserved citations");
1628 assert!(matches!(
1629 citations.first(),
1630 Some(crate::providers::anthropic::completion::Citation::WebSearchResultLocation {
1631 encrypted_index,
1632 ..
1633 }) if encrypted_index == "encrypted-index"
1634 ));
1635 }
1636
1637 #[test]
1638 fn test_handle_citations_delta_event_preserves_metadata() {
1639 let event = StreamingEvent::ContentBlockDelta {
1640 index: 0,
1641 delta: ContentDelta::CitationsDelta {
1642 citation: crate::providers::anthropic::completion::Citation::CharLocation {
1643 cited_text: "The grass is green.".to_string(),
1644 document_index: 0,
1645 document_title: Some("Example".to_string()),
1646 start_char_index: 0,
1647 end_char_index: 20,
1648 },
1649 },
1650 };
1651
1652 let mut tool_call_state = None;
1653 let mut thinking_state = None;
1654 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1655
1656 assert!(result.is_some());
1657 let choice = result.unwrap().unwrap();
1658 let RawStreamingChoice::TextAdditionalParams(additional_params) = choice else {
1659 panic!("expected TextAdditionalParams choice");
1660 };
1661 assert_eq!(additional_params["citations"][0]["type"], "char_location");
1662 }
1663
1664 #[tokio::test]
1665 async fn test_streaming_citation_deltas_are_preserved_on_final_text() {
1666 let citation = crate::providers::anthropic::completion::Citation::CharLocation {
1667 cited_text: "The grass is green.".to_string(),
1668 document_index: 0,
1669 document_title: Some("Example".to_string()),
1670 start_char_index: 0,
1671 end_char_index: 20,
1672 };
1673
1674 let raw_stream = stream! {
1675 let mut tool_call_state = None;
1676 let mut thinking_state = None;
1677
1678 yield handle_event(
1679 &StreamingEvent::ContentBlockStart {
1680 index: 0,
1681 content_block: Content::Text {
1682 text: String::new(),
1683 citations: Vec::new(),
1684 cache_control: None,
1685 },
1686 },
1687 &mut tool_call_state,
1688 &mut thinking_state,
1689 )
1690 .expect("text block start should produce a raw choice");
1691
1692 yield handle_event(
1693 &StreamingEvent::ContentBlockDelta {
1694 index: 0,
1695 delta: ContentDelta::TextDelta {
1696 text: "the grass is green".to_string(),
1697 },
1698 },
1699 &mut tool_call_state,
1700 &mut thinking_state,
1701 )
1702 .expect("text delta should produce a raw choice");
1703
1704 yield handle_event(
1705 &StreamingEvent::ContentBlockDelta {
1706 index: 0,
1707 delta: ContentDelta::CitationsDelta {
1708 citation: crate::providers::anthropic::completion::Citation::CharLocation {
1709 cited_text: "The grass is green.".to_string(),
1710 document_index: 0,
1711 document_title: Some("Example".to_string()),
1712 start_char_index: 0,
1713 end_char_index: 20,
1714 },
1715 },
1716 },
1717 &mut tool_call_state,
1718 &mut thinking_state,
1719 )
1720 .expect("citation delta should produce a raw choice");
1721
1722 yield Ok(RawStreamingChoice::FinalResponse(StreamingCompletionResponse {
1723 usage: PartialUsage::default(),
1724 }));
1725 };
1726
1727 let mut stream =
1728 crate::streaming::StreamingCompletionResponse::stream(to_stream_result(raw_stream));
1729 while stream.next().await.is_some() {}
1730
1731 let choice_items: Vec<crate::message::AssistantContent> =
1732 stream.choice.clone().into_iter().collect();
1733 let Some(crate::message::AssistantContent::Text(text)) = choice_items.first() else {
1734 panic!("expected accumulated text item");
1735 };
1736
1737 assert_eq!(text.text, "the grass is green");
1738 let citations = crate::providers::anthropic::completion::anthropic_citations(text).unwrap();
1739 assert_eq!(citations, vec![citation]);
1740 }
1741
1742 #[test]
1743 fn test_unknown_content_delta_falls_back() {
1744 let json = r#"{"type": "something_new_from_anthropic", "field": "x"}"#;
1745 let delta: ContentDelta = serde_json::from_str(json).unwrap();
1746 assert!(matches!(delta, ContentDelta::Unknown));
1747 }
1748}