1use serde::{Deserialize, Serialize};
2use serde_json::{Value, json};
3
4use super::completion::{
5 AnthropicCompatibleProvider, AnthropicCompletionRequest, Content, GenericCompletionModel,
6 Usage, anthropic_usage_totals, map_finish_reason,
7};
8use crate::completion::{CompletionError, CompletionRequest};
9use crate::http_client::sse::GenericEventSource;
10use crate::http_client::{self, HttpClientExt};
11use crate::message::ReasoningContent;
12use crate::providers::internal::adapter::{AdapterOutput, WireAdapter, WireFrame};
13use crate::providers::internal::sse_transport::{
14 OpenLog, SseTransportOptions, open_wire_stream, skip_blank_frames,
15};
16use crate::providers::internal::wire::{self, WireEvent};
17use crate::streaming::{
18 self, MintKind, RawStreamingChoice, RawStreamingResult, StreamFinal, StreamPartId,
19 ToolCallDeltaContent, ToolInputEnd, UnparseableToolInput,
20};
21use crate::telemetry::{CompletionOperation, SpanCombinator};
22use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
23use std::collections::HashMap;
24
25fn streaming_body(request: &AnthropicCompletionRequest) -> Result<Value, CompletionError> {
34 let mut body = serde_json::to_value(request)?;
35 if let Some(map) = body.as_object_mut() {
36 map.insert("stream".to_string(), Value::Bool(true));
39
40 if map.contains_key("tools") {
50 map.entry("tool_choice")
51 .or_insert_with(|| json!({ "type": "auto" }));
52 } else {
53 map.remove("tool_choice");
54 }
55 }
56
57 Ok(body)
58}
59
60const KNOWN_EVENT_TYPES: &[&str] = &[
70 "message_start",
71 "content_block_start",
72 "content_block_delta",
73 "content_block_stop",
74 "message_delta",
75 "message_stop",
76 "ping",
77 "error",
78];
79
80#[derive(Debug, Deserialize)]
81#[serde(tag = "type", rename_all = "snake_case")]
82pub enum StreamingEvent {
83 MessageStart {
84 #[serde(default)]
88 message: Option<MessageStart>,
89 },
90 ContentBlockStart {
91 index: usize,
92 content_block: Content,
93 },
94 ContentBlockDelta {
95 index: usize,
96 delta: ContentDelta,
97 },
98 ContentBlockStop {
99 index: usize,
100 },
101 MessageDelta {
102 delta: MessageDelta,
103 usage: PartialUsage,
104 },
105 MessageStop,
106 Ping,
108 Error {
114 error: serde_json::Value,
115 },
116}
117
118#[derive(Debug, Deserialize)]
119pub struct MessageStart {
120 pub id: String,
121 pub role: String,
122 pub content: Vec<Content>,
123 pub model: String,
124 pub stop_reason: Option<String>,
125 pub stop_sequence: Option<String>,
126 pub usage: Usage,
127}
128
129#[derive(Debug)]
130pub enum ContentDelta {
131 TextDelta {
132 text: String,
133 },
134 InputJsonDelta {
135 partial_json: String,
136 },
137 ThinkingDelta {
138 thinking: String,
139 },
140 SignatureDelta {
141 signature: String,
142 },
143 CitationsDelta {
144 citation: super::completion::Citation,
145 },
146 Unknown(serde_json::Value),
154}
155
156impl<'de> Deserialize<'de> for ContentDelta {
165 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
166 where
167 D: serde::Deserializer<'de>,
168 {
169 let value = serde_json::Value::deserialize(deserializer)?;
170 if !value.is_object() {
175 return Err(serde::de::Error::custom("content delta must be an object"));
176 }
177 let str_field = |tag: &str, field: &str| -> Result<String, D::Error> {
178 value
179 .get(field)
180 .and_then(serde_json::Value::as_str)
181 .map(ToOwned::to_owned)
182 .ok_or_else(|| {
183 serde::de::Error::custom(format!(
184 "`{tag}` content delta is missing a string `{field}` field"
185 ))
186 })
187 };
188 match value.get("type").cloned() {
189 Some(serde_json::Value::String(tag)) => match tag.as_str() {
190 "text_delta" => Ok(Self::TextDelta {
191 text: str_field("text_delta", "text")?,
192 }),
193 "input_json_delta" => Ok(Self::InputJsonDelta {
194 partial_json: str_field("input_json_delta", "partial_json")?,
195 }),
196 "thinking_delta" => Ok(Self::ThinkingDelta {
197 thinking: str_field("thinking_delta", "thinking")?,
198 }),
199 "signature_delta" => Ok(Self::SignatureDelta {
200 signature: str_field("signature_delta", "signature")?,
201 }),
202 "citations_delta" => {
203 let citation = value.get("citation").cloned().ok_or_else(|| {
204 serde::de::Error::custom(
205 "`citations_delta` content delta is missing a `citation` field",
206 )
207 })?;
208 Ok(Self::CitationsDelta {
209 citation: serde_json::from_value(citation)
210 .map_err(serde::de::Error::custom)?,
211 })
212 }
213 _ => Ok(Self::Unknown(value)),
214 },
215 Some(_) => Err(serde::de::Error::custom(
216 "content delta `type` must be a string",
217 )),
218 None => Err(serde::de::Error::custom(
223 "content delta is missing a `type` field",
224 )),
225 }
226 }
227}
228
229#[derive(Debug, Deserialize)]
230pub struct MessageDelta {
231 pub stop_reason: Option<String>,
232 pub stop_sequence: Option<String>,
233}
234
235#[derive(Debug, Deserialize, Clone, Serialize, Default)]
236pub struct PartialUsage {
237 pub output_tokens: usize,
238 #[serde(default)]
239 pub input_tokens: Option<usize>,
240 #[serde(default)]
241 pub cache_creation_input_tokens: Option<u64>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub cache_creation: Option<super::completion::CacheCreation>,
247 #[serde(default)]
248 pub cache_read_input_tokens: Option<u64>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub output_tokens_details: Option<super::completion::OutputTokensDetails>,
255}
256
257impl From<&PartialUsage> for crate::completion::Usage {
258 fn from(value: &PartialUsage) -> crate::completion::Usage {
259 anthropic_usage_totals(
260 value.input_tokens.unwrap_or_default() as u64,
261 value.output_tokens as u64,
262 value.cache_read_input_tokens,
263 value.cache_creation_input_tokens,
264 value.output_tokens_details,
265 )
266 }
267}
268
269impl From<PartialUsage> for crate::completion::Usage {
270 fn from(value: PartialUsage) -> crate::completion::Usage {
271 (&value).into()
272 }
273}
274
275struct ServerToolUseState {
280 name: String,
281 id: String,
282 initial_input: Value,
283 input_json: String,
284}
285
286#[derive(Default)]
287struct ThinkingState {
288 signature: String,
294 initial_signature: String,
302}
303
304impl ThinkingState {
305 fn into_signature(self) -> Option<String> {
308 let signature = if self.signature.is_empty() {
309 self.initial_signature
310 } else {
311 self.signature
312 };
313 (!signature.is_empty()).then_some(signature)
314 }
315}
316
317#[derive(Default)]
324struct AnthropicAdapter {
325 current_tool_call: Option<String>,
327 server_tool_uses: HashMap<usize, ServerToolUseState>,
328 current_thinking: Option<ThinkingState>,
329 input_tokens: u64,
330 cache_creation: Option<super::completion::CacheCreation>,
333 message_id: Option<String>,
334 response_model: Option<String>,
335 failed: bool,
339}
340
341impl WireAdapter for AnthropicAdapter {
342 type Frame = WireFrame;
343 type Event = StreamingEvent;
344 type Response = StreamingCompletionResponse;
345
346 fn classify(&self, frame: WireFrame) -> WireEvent<StreamingEvent> {
347 wire::classify_tagged_frame(&frame.as_str(), "type", |event_type| {
348 KNOWN_EVENT_TYPES.contains(&event_type)
349 })
350 }
351
352 fn interpret(&mut self, event: StreamingEvent, out: &mut AdapterOutput<Self::Response>) {
353 if self.failed {
354 return;
355 }
356
357 match &event {
358 StreamingEvent::MessageStart { message } => {
359 let Some(message) = message else { return };
362 self.input_tokens = message.usage.input_tokens;
363 self.cache_creation = message.usage.cache_creation.clone();
364 self.message_id = Some(message.id.clone());
365 self.response_model = Some(message.model.clone());
366
367 let span = tracing::Span::current();
368 span.record("gen_ai.response.id", &message.id);
369 span.record("gen_ai.response.model", &message.model);
370 return;
371 }
372 StreamingEvent::MessageDelta { delta, usage } => {
373 let Some(reason) = delta.stop_reason.as_ref() else {
376 return;
377 };
378 let usage = PartialUsage {
420 output_tokens: usage.output_tokens,
421 input_tokens: usage
422 .input_tokens
423 .filter(|tokens| *tokens > 0)
424 .or_else(|| usize::try_from(self.input_tokens).ok()),
425 cache_creation_input_tokens: usage.cache_creation_input_tokens,
426 cache_creation: usage
427 .cache_creation
428 .clone()
429 .or_else(|| self.cache_creation.clone()),
430 cache_read_input_tokens: usage.cache_read_input_tokens,
431 output_tokens_details: usage.output_tokens_details,
437 };
438
439 let span = tracing::Span::current();
440 span.record_token_usage(&crate::completion::Usage::from(&usage));
441 out.push(Ok(RawStreamingChoice::FinalResponse(
442 StreamingCompletionResponse {
443 usage,
444 stop_reason: Some(reason.clone()),
445 stop_sequence: delta.stop_sequence.clone(),
449 message_id: self.message_id.clone(),
450 model: self.response_model.clone(),
451 provider_request_id: None,
454 },
455 )));
456 return;
457 }
458 StreamingEvent::Error { error } => {
459 self.failed = true;
465 let body = serde_json::json!({ "type": "error", "error": error }).to_string();
466 out.push(Err(crate::provider_response::completion_error_from_body(
467 body,
468 )));
469 return;
470 }
471 _ => {}
472 }
473
474 if let Some(result) = handle_event(
475 &event,
476 &mut self.current_tool_call,
477 &mut self.server_tool_uses,
478 &mut self.current_thinking,
479 ) {
480 out.push(result);
481 }
482 }
483
484 fn finish(&mut self, _out: &mut AdapterOutput<Self::Response>) {
485 }
488
489 fn is_finished(&self) -> bool {
490 self.failed
495 }
496}
497
498#[derive(Clone, Debug, Default, Deserialize, Serialize)]
505pub struct StreamingCompletionResponse {
506 pub usage: PartialUsage,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub stop_reason: Option<String>,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub stop_sequence: Option<String>,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
525 pub message_id: Option<String>,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub model: Option<String>,
529 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub provider_request_id: Option<String>,
534}
535
536impl From<(&str, StreamingCompletionResponse)> for StreamFinal {
542 fn from((provider, response): (&str, StreamingCompletionResponse)) -> Self {
543 StreamFinal::new(provider, crate::completion::Usage::from(&response.usage))
544 .with_optional_finish_reason(response.stop_reason.as_deref().map(map_finish_reason))
545 .with_optional_message_id(response.message_id)
546 .with_optional_provider_request_id(response.provider_request_id)
547 .with_optional_model(response.model)
548 }
549}
550
551impl<Ext, T> GenericCompletionModel<Ext, T>
552where
553 T: HttpClientExt + Clone + Default + 'static,
554 Ext: AnthropicCompatibleProvider + Clone + WasmCompatSend + WasmCompatSync + 'static,
555{
556 pub async fn raw_stream(
565 &self,
566 completion_request: CompletionRequest,
567 ) -> Result<RawStreamingResult<StreamingCompletionResponse>, CompletionError> {
568 let (span, request) =
569 self.prepare_request(completion_request, CompletionOperation::ChatStreaming)?;
570
571 let body = streaming_body(&request)?;
575 crate::providers::internal::trace_json(
576 crate::providers::internal::LogTarget::Completions,
577 "Anthropic completion request",
578 &body,
579 );
580
581 let body: Vec<u8> = serde_json::to_vec(&body)?;
582
583 let req = self
584 .client
585 .post("/v1/messages")?
586 .body(body)
587 .map_err(http_client::Error::Protocol)?;
588
589 let event_source = GenericEventSource::new(self.client.clone(), req);
590 let (event_source, request_id_slot) = match Ext::REQUEST_ID_HEADER {
591 Some(header) => {
592 let (event_source, slot) = event_source.capture_request_id(header);
593 (event_source, Some(slot))
594 }
595 None => (event_source, None),
596 };
597
598 let stream = open_wire_stream(
602 event_source,
603 SseTransportOptions {
604 open_log: OpenLog::Silent,
605 stream_ended_is_error: true,
606 log_transport_errors: false,
607 },
608 skip_blank_frames,
609 AnthropicAdapter::default(),
610 span,
611 );
612 Ok(
613 crate::providers::internal::sse_transport::stamp_terminal_request_id(
614 stream,
615 request_id_slot,
616 Ext::REQUEST_ID_HEADER,
617 |response, id| response.provider_request_id = Some(id),
618 ),
619 )
620 }
621
622 pub(crate) async fn stream(
623 &self,
624 completion_request: CompletionRequest,
625 ) -> Result<streaming::StreamingCompletionResponse, CompletionError> {
626 let stream = self.raw_stream(completion_request).await?;
627 let normalized = streaming::normalize_stream(stream, |response| {
628 Ok(StreamFinal::from((Ext::PROVIDER_NAME, response)))
629 });
630
631 Ok(streaming::StreamingCompletionResponse::stream(
632 Ext::PROVIDER_NAME,
633 normalized,
634 ))
635 }
636}
637
638fn handle_event(
639 event: &StreamingEvent,
640 current_tool_call: &mut Option<String>,
641 server_tool_uses: &mut HashMap<usize, ServerToolUseState>,
642 current_thinking: &mut Option<ThinkingState>,
643) -> Option<Result<RawStreamingChoice<StreamingCompletionResponse>, CompletionError>> {
644 match event {
645 StreamingEvent::ContentBlockDelta { index, delta } => match delta {
646 ContentDelta::TextDelta { text } => {
647 if current_tool_call.is_none() {
648 return Some(Ok(RawStreamingChoice::Message(text.clone())));
649 }
650 None
651 }
652 ContentDelta::InputJsonDelta { partial_json } => {
653 if let Some(server_tool_use) = server_tool_uses.get_mut(index) {
654 server_tool_use.input_json.push_str(partial_json);
655 return None;
656 }
657
658 if let Some(id) = current_tool_call {
659 return Some(Ok(RawStreamingChoice::ToolCallDelta {
662 id: StreamPartId::wire(id.clone()),
663 content: ToolCallDeltaContent::Delta(partial_json.clone()),
664 }));
665 }
666 None
667 }
668 ContentDelta::ThinkingDelta { thinking } => {
669 current_thinking.get_or_insert_with(ThinkingState::default);
670
671 Some(Ok(RawStreamingChoice::ReasoningDelta {
672 id: MintKind::Block.for_wire_index(*index as u64),
675 provider_id: None,
676 reasoning: thinking.clone(),
677 }))
678 }
679 ContentDelta::SignatureDelta { signature } => {
680 current_thinking
681 .get_or_insert_with(ThinkingState::default)
682 .signature
683 .push_str(signature);
684
685 None
689 }
690 ContentDelta::CitationsDelta { citation } => {
691 crate::message::AdditionalParams::from_entries([("citations", json!([citation]))])
692 .map(|params| Ok(RawStreamingChoice::TextAdditionalParams(params)))
693 }
694 ContentDelta::Unknown(value) => {
695 tracing::warn!(
699 delta_type = value.get("type").and_then(serde_json::Value::as_str),
700 "skipping unrecognized Anthropic content delta type"
701 );
702 None
703 }
704 },
705 StreamingEvent::ContentBlockStart {
706 index,
707 content_block,
708 } => match content_block {
709 Content::Text {
714 text: _,
715 citations,
716 cache_control: _,
717 } => {
718 let additional_params = crate::message::AdditionalParams::from_entries(
719 (!citations.is_empty()).then(|| ("citations", json!(citations))),
720 );
721 Some(Ok(RawStreamingChoice::TextStart {
722 id: MintKind::Block.for_wire_index(*index as u64),
725 additional_params,
726 }))
727 }
728 Content::ServerToolUse { id, name, input } => {
729 server_tool_uses.insert(
730 *index,
731 ServerToolUseState {
732 name: name.clone(),
733 id: id.clone(),
734 initial_input: input.clone(),
735 input_json: String::new(),
736 },
737 );
738 None
739 }
740 raw @ (Content::WebSearchToolResult { .. }
741 | Content::CodeExecutionToolResult { .. }) => Some(Ok(RawStreamingChoice::TextStart {
742 id: MintKind::Block.for_wire_index(*index as u64),
743 additional_params: crate::message::AdditionalParams::from_entries([(
744 super::completion::ANTHROPIC_RAW_CONTENT_KEY,
745 json!(raw),
746 )]),
747 })),
748 Content::ToolUse { id, name, .. } => {
749 *current_tool_call = Some(id.clone());
750 Some(Ok(RawStreamingChoice::ToolCallDelta {
751 id: StreamPartId::wire(id.clone()),
752 content: ToolCallDeltaContent::Name(name.clone()),
753 }))
754 }
755 Content::Thinking {
756 thinking,
757 signature,
758 } => {
759 *current_thinking = Some(ThinkingState {
766 signature: String::new(),
767 initial_signature: signature.clone().unwrap_or_default(),
768 });
769 (!thinking.is_empty()).then(|| {
772 Ok(RawStreamingChoice::ReasoningDelta {
773 id: MintKind::Block.for_wire_index(*index as u64),
774 provider_id: None,
775 reasoning: thinking.clone(),
776 })
777 })
778 }
779 Content::RedactedThinking { data } => Some(Ok(RawStreamingChoice::Reasoning {
780 id: MintKind::Block.for_wire_index(*index as u64),
782 provider_id: None,
783 content: ReasoningContent::Redacted { data: data.clone() },
784 })),
785 _ => None,
787 },
788 StreamingEvent::ContentBlockStop { index } => {
789 if let Some(thinking_state) = Option::take(current_thinking) {
798 return Some(Ok(RawStreamingChoice::ReasoningEnd {
805 id: MintKind::Block.for_wire_index(*index as u64),
806 reasoning: None,
807 signature: thinking_state.into_signature(),
808 wire_sent: true,
811 }));
812 }
813
814 if let Some(server_tool_use) = server_tool_uses.remove(index) {
815 let input = if server_tool_use.input_json.is_empty() {
816 if server_tool_use.initial_input.is_null() {
817 json!({})
818 } else {
819 server_tool_use.initial_input
820 }
821 } else {
822 match serde_json::from_str(&server_tool_use.input_json) {
823 Ok(json_value) => json_value,
824 Err(e) => return Some(Err(CompletionError::from(e))),
825 }
826 };
827
828 return Some(Ok(RawStreamingChoice::TextStart {
829 id: MintKind::Block.for_wire_index(*index as u64),
830 additional_params: crate::message::AdditionalParams::from_entries([(
831 super::completion::ANTHROPIC_RAW_CONTENT_KEY,
832 json!(Content::ServerToolUse {
833 id: server_tool_use.id,
834 name: server_tool_use.name,
835 input,
836 }),
837 )]),
838 }));
839 }
840
841 Option::take(current_tool_call).map(|id| {
845 Ok(RawStreamingChoice::ToolInputEnd(ToolInputEnd::new(
846 id,
847 UnparseableToolInput::Error,
848 )))
849 })
850 }
851 StreamingEvent::MessageStart { .. }
854 | StreamingEvent::MessageDelta { .. }
855 | StreamingEvent::MessageStop
856 | StreamingEvent::Ping
857 | StreamingEvent::Error { .. } => None,
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use super::super::completion::{
864 AnthropicRequestParams, CLAUDE_OPUS_4_8, CacheControl, CacheTtl, Message, SystemContent,
865 apply_prompt_cache_control, build_tool_definitions, resolve_top_level_cache_control,
866 };
867 use super::*;
868 use crate::completion::Message as RigMessage;
869 use crate::completion::request::Document as RigDocument;
870 use crate::streaming::RawStreamingToolCall;
871 use async_stream::stream;
872 use futures::StreamExt;
873
874 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
878 fn to_stream_result(
879 stream: impl futures::Stream<
880 Item = Result<RawStreamingChoice<StreamingCompletionResponse>, CompletionError>,
881 > + Send
882 + 'static,
883 ) -> crate::streaming::StreamingResult {
884 crate::streaming::normalize_stream(Box::pin(stream), |response| {
885 Ok(StreamFinal::from(("anthropic", response)))
886 })
887 }
888
889 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
890 fn to_stream_result(
891 stream: impl futures::Stream<
892 Item = Result<RawStreamingChoice<StreamingCompletionResponse>, CompletionError>,
893 > + 'static,
894 ) -> crate::streaming::StreamingResult {
895 crate::streaming::normalize_stream(Box::pin(stream), |response| {
896 Ok(StreamFinal::from(("anthropic", response)))
897 })
898 }
899
900 fn built_streaming_body(
904 model: &str,
905 request: CompletionRequest,
906 strict_tools: bool,
907 ) -> Result<Value, CompletionError> {
908 let typed = AnthropicCompletionRequest::try_from_params::<
909 crate::providers::anthropic::client::AnthropicExt,
910 >(
911 AnthropicRequestParams {
912 model,
913 request,
914 prompt_caching: false,
915 automatic_caching: false,
916 automatic_caching_ttl: None,
917 static_prefix_cache_ttl: None,
918 },
919 strict_tools,
920 )?;
921
922 streaming_body(&typed)
923 }
924
925 #[test]
926 fn test_streaming_tool_build_marks_final_combined_tool() {
927 let mut additional_params = json!({
928 "tools": [{
929 "name": "provider_tool",
930 "description": "Provider tool",
931 "input_schema": {"type": "object"}
932 }]
933 });
934
935 let mut tools =
936 build_tool_definitions::<crate::providers::anthropic::client::AnthropicExt>(
937 vec![crate::completion::ToolDefinition {
938 name: "rig_tool".to_string(),
939 description: "Rig tool".to_string(),
940 parameters: json!({"type": "object", "properties": {}}),
941 }],
942 &mut additional_params,
943 false,
944 )
945 .unwrap();
946 let mut system: Vec<SystemContent> = Vec::new();
947 let mut messages: Vec<Message> = Vec::new();
948 apply_prompt_cache_control(&mut system, &mut messages, &mut tools, true, None, None)
949 .unwrap();
950
951 assert_eq!(tools.len(), 2);
952 assert!(tools[0].get("cache_control").is_none());
953 assert_eq!(tools[1]["name"], "provider_tool");
954 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
955 }
956
957 #[test]
958 fn streaming_request_keeps_documents_after_leading_system_messages() {
959 let request = CompletionRequest {
960 model: None,
961 preamble: None,
962 chat_history: vec![
963 RigMessage::system("System prompt"),
964 RigMessage::assistant("Earlier assistant turn"),
965 RigMessage::system("Mid-conversation instruction"),
966 RigMessage::user("Prompt"),
967 ],
968 documents: vec![RigDocument {
969 id: "doc1".to_string(),
970 text: "Document text.".to_string(),
971 additional_props: Default::default(),
972 }],
973 tools: vec![],
974 temperature: None,
975 max_tokens: Some(64),
976 tool_choice: None,
977 additional_params: None,
978 output_schema: None,
979 record_telemetry_content: false,
980 };
981
982 let body = built_streaming_body(CLAUDE_OPUS_4_8, request, false)
983 .expect("streaming request body should build");
984
985 assert_eq!(body["system"][0]["text"], "System prompt");
986 assert_eq!(body["system"][1]["text"], "Mid-conversation instruction");
987 let messages = body["messages"]
988 .as_array()
989 .expect("messages should be array");
990 assert_eq!(messages.len(), 3);
991 assert_eq!(messages[0]["role"], "user");
992 assert!(
993 messages[0].to_string().contains("<file id: doc1>"),
994 "document message should follow top-level system: {messages:?}"
995 );
996 assert_eq!(messages[1]["role"], "assistant");
997 assert_eq!(messages[2]["role"], "user");
998 assert_eq!(
999 messages
1000 .iter()
1001 .filter(|message| message.to_string().contains("<file id: doc1>"))
1002 .count(),
1003 1,
1004 "document message should appear exactly once: {messages:?}"
1005 );
1006 }
1007
1008 #[test]
1009 fn streaming_body_is_blocking_body_plus_stream_flag_and_carries_output_schema() {
1010 let schema: schemars::Schema = serde_json::from_value(json!({
1011 "title": "WeatherResponse",
1012 "type": "object",
1013 "properties": { "city": { "type": "string" } }
1014 }))
1015 .expect("schema should deserialize");
1016
1017 let request = CompletionRequest {
1018 model: None,
1019 preamble: Some("You are helpful".to_string()),
1020 chat_history: vec![RigMessage::user("What's the weather?")],
1021 documents: vec![],
1022 tools: vec![],
1023 temperature: Some(0.5),
1024 max_tokens: Some(64),
1025 tool_choice: None,
1026 additional_params: None,
1027 output_schema: Some(schema),
1028 record_telemetry_content: false,
1029 };
1030
1031 let streaming_body = built_streaming_body(CLAUDE_OPUS_4_8, request.clone(), false)
1032 .expect("streaming request body should build");
1033
1034 assert_eq!(streaming_body["stream"], serde_json::Value::Bool(true));
1036
1037 assert_eq!(
1041 streaming_body["output_config"]["format"]["type"],
1042 "json_schema"
1043 );
1044 assert!(
1045 streaming_body["output_config"]["format"]["schema"].is_object(),
1046 "streaming body must carry the structured-output schema: {streaming_body}"
1047 );
1048
1049 let blocking = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
1053 model: CLAUDE_OPUS_4_8,
1054 request,
1055 prompt_caching: false,
1056 automatic_caching: false,
1057 automatic_caching_ttl: None,
1058 static_prefix_cache_ttl: None,
1059 })
1060 .expect("blocking request body should build");
1061 let mut expected = serde_json::to_value(&blocking).expect("serialize blocking body");
1062 expected
1063 .as_object_mut()
1064 .expect("body is an object")
1065 .insert("stream".to_string(), serde_json::Value::Bool(true));
1066
1067 assert_eq!(streaming_body, expected);
1068 }
1069
1070 #[test]
1071 fn streaming_body_keeps_explicit_tool_choice_auto_when_tools_present_but_unset() {
1072 let request = CompletionRequest {
1073 model: None,
1074 preamble: None,
1075 chat_history: vec![RigMessage::user("Add 2 and 3")],
1076 documents: vec![],
1077 tools: vec![crate::completion::ToolDefinition {
1078 name: "add".to_string(),
1079 description: "Add x and y".to_string(),
1080 parameters: json!({
1081 "type": "object",
1082 "properties": { "x": { "type": "integer" } }
1083 }),
1084 }],
1085 temperature: None,
1086 max_tokens: Some(64),
1087 tool_choice: None,
1088 additional_params: None,
1089 output_schema: None,
1090 record_telemetry_content: false,
1091 };
1092
1093 let body = built_streaming_body(CLAUDE_OPUS_4_8, request, false)
1094 .expect("streaming request body should build");
1095
1096 assert_eq!(body["tool_choice"], json!({ "type": "auto" }));
1100 assert!(body["tools"].is_array());
1101 }
1102
1103 #[test]
1104 fn streaming_body_applies_strict_tool_opt_in() {
1105 let request = CompletionRequest {
1106 model: None,
1107 preamble: None,
1108 chat_history: vec![RigMessage::user("Look this up")],
1109 documents: vec![],
1110 tools: vec![crate::completion::ToolDefinition {
1111 name: "lookup".to_string(),
1112 description: "Look up a value".to_string(),
1113 parameters: json!({
1114 "type": "object",
1115 "properties": { "query": { "type": "string" } },
1116 "required": ["query"]
1117 }),
1118 }],
1119 temperature: None,
1120 max_tokens: Some(64),
1121 tool_choice: None,
1122 additional_params: None,
1123 output_schema: None,
1124 record_telemetry_content: false,
1125 };
1126
1127 let body = built_streaming_body(CLAUDE_OPUS_4_8, request, true)
1128 .expect("streaming request body should build");
1129
1130 assert_eq!(body["tools"][0]["strict"], true);
1131 assert_eq!(
1132 body["tools"][0]["input_schema"]["additionalProperties"],
1133 false
1134 );
1135 assert_eq!(
1136 body["tools"][0]["input_schema"]["required"],
1137 json!(["query"])
1138 );
1139 }
1140
1141 #[test]
1142 fn streaming_body_drops_tool_choice_when_no_tools_are_advertised() {
1143 let request = CompletionRequest {
1148 model: None,
1149 preamble: None,
1150 chat_history: vec![RigMessage::user("Hi")],
1151 documents: vec![],
1152 tools: vec![],
1153 temperature: None,
1154 max_tokens: Some(64),
1155 tool_choice: Some(crate::message::ToolChoice::Auto),
1156 additional_params: None,
1157 output_schema: None,
1158 record_telemetry_content: false,
1159 };
1160
1161 let body = built_streaming_body(CLAUDE_OPUS_4_8, request, false)
1162 .expect("streaming request body should build");
1163
1164 assert!(
1165 body.get("tool_choice").is_none(),
1166 "tool_choice must be omitted when no tools are advertised: {body}"
1167 );
1168 assert!(body.get("tools").is_none());
1169 }
1170
1171 #[test]
1172 fn test_streaming_prompt_cache_control_uses_raw_top_level_ttl() {
1173 let mut additional_params = json!({
1174 "cache_control": {"type": "ephemeral", "ttl": "1h"}
1175 });
1176 let top_level_cache_control =
1177 resolve_top_level_cache_control(false, None, &mut additional_params).unwrap();
1178 let mut tools =
1179 build_tool_definitions::<crate::providers::anthropic::client::AnthropicExt>(
1180 vec![crate::completion::ToolDefinition {
1181 name: "rig_tool".to_string(),
1182 description: "Rig tool".to_string(),
1183 parameters: json!({"type": "object", "properties": {}}),
1184 }],
1185 &mut additional_params,
1186 false,
1187 )
1188 .unwrap();
1189 let mut system = vec![SystemContent::Text {
1190 text: "System prompt".to_string(),
1191 cache_control: None,
1192 }];
1193 let mut messages: Vec<Message> = Vec::new();
1194
1195 apply_prompt_cache_control(
1196 &mut system,
1197 &mut messages,
1198 &mut tools,
1199 true,
1200 None,
1201 top_level_cache_control.as_ref(),
1202 )
1203 .unwrap();
1204
1205 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
1206 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
1207 match &system[0] {
1208 SystemContent::Text {
1209 cache_control: Some(CacheControl::Ephemeral { ttl }),
1210 ..
1211 } => assert_eq!(ttl.as_ref(), Some(&CacheTtl::OneHour)),
1212 other => panic!("expected system cache_control, got {other:?}"),
1213 }
1214 assert!(additional_params.get("cache_control").is_none());
1215 }
1216
1217 fn handle_event(
1218 event: &StreamingEvent,
1219 current_tool_call: &mut Option<String>,
1220 current_thinking: &mut Option<ThinkingState>,
1221 ) -> Option<Result<RawStreamingChoice<StreamingCompletionResponse>, CompletionError>> {
1222 let mut server_tool_uses = HashMap::new();
1223 super::handle_event(
1224 event,
1225 current_tool_call,
1226 &mut server_tool_uses,
1227 current_thinking,
1228 )
1229 }
1230
1231 #[test]
1232 fn test_thinking_delta_deserialization() {
1233 let json = r#"{"type": "thinking_delta", "thinking": "Let me think about this..."}"#;
1234 let delta: ContentDelta = serde_json::from_str(json).unwrap();
1235
1236 match delta {
1237 ContentDelta::ThinkingDelta { thinking } => {
1238 assert_eq!(thinking, "Let me think about this...");
1239 }
1240 _ => panic!("Expected ThinkingDelta variant"),
1241 }
1242 }
1243
1244 #[test]
1245 fn test_signature_delta_deserialization() {
1246 let json = r#"{"type": "signature_delta", "signature": "abc123def456"}"#;
1247 let delta: ContentDelta = serde_json::from_str(json).unwrap();
1248
1249 match delta {
1250 ContentDelta::SignatureDelta { signature } => {
1251 assert_eq!(signature, "abc123def456");
1252 }
1253 _ => panic!("Expected SignatureDelta variant"),
1254 }
1255 }
1256
1257 #[test]
1258 fn test_thinking_delta_streaming_event_deserialization() {
1259 let json = r#"{
1260 "type": "content_block_delta",
1261 "index": 0,
1262 "delta": {
1263 "type": "thinking_delta",
1264 "thinking": "First, I need to understand the problem."
1265 }
1266 }"#;
1267
1268 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1269
1270 match event {
1271 StreamingEvent::ContentBlockDelta { index, delta } => {
1272 assert_eq!(index, 0);
1273 match delta {
1274 ContentDelta::ThinkingDelta { thinking } => {
1275 assert_eq!(thinking, "First, I need to understand the problem.");
1276 }
1277 _ => panic!("Expected ThinkingDelta"),
1278 }
1279 }
1280 _ => panic!("Expected ContentBlockDelta event"),
1281 }
1282 }
1283
1284 #[test]
1285 fn test_signature_delta_streaming_event_deserialization() {
1286 let json = r#"{
1287 "type": "content_block_delta",
1288 "index": 0,
1289 "delta": {
1290 "type": "signature_delta",
1291 "signature": "ErUBCkYICBgCIkCaGbqC85F4"
1292 }
1293 }"#;
1294
1295 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1296
1297 match event {
1298 StreamingEvent::ContentBlockDelta { index, delta } => {
1299 assert_eq!(index, 0);
1300 match delta {
1301 ContentDelta::SignatureDelta { signature } => {
1302 assert_eq!(signature, "ErUBCkYICBgCIkCaGbqC85F4");
1303 }
1304 _ => panic!("Expected SignatureDelta"),
1305 }
1306 }
1307 _ => panic!("Expected ContentBlockDelta event"),
1308 }
1309 }
1310
1311 #[test]
1312 fn test_handle_thinking_delta_event() {
1313 let event = StreamingEvent::ContentBlockDelta {
1314 index: 0,
1315 delta: ContentDelta::ThinkingDelta {
1316 thinking: "Analyzing the request...".to_string(),
1317 },
1318 };
1319
1320 let mut tool_call_state = None;
1321 let mut thinking_state = None;
1322 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1323
1324 assert!(result.is_some());
1325 let choice = result.unwrap().unwrap();
1326
1327 match choice {
1328 RawStreamingChoice::ReasoningDelta { id, reasoning, .. } => {
1329 assert_eq!(id, crate::streaming::MintKind::Block.for_wire_index(0));
1330 assert_eq!(reasoning, "Analyzing the request...");
1331 }
1332 _ => panic!("Expected ReasoningDelta choice"),
1333 }
1334
1335 assert!(thinking_state.is_some());
1338 }
1339
1340 #[test]
1341 fn test_handle_signature_delta_event() {
1342 let event = StreamingEvent::ContentBlockDelta {
1343 index: 0,
1344 delta: ContentDelta::SignatureDelta {
1345 signature: "test_signature".to_string(),
1346 },
1347 };
1348
1349 let mut tool_call_state = None;
1350 let mut thinking_state = None;
1351 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1352
1353 assert!(result.is_none());
1355
1356 assert!(thinking_state.is_some());
1358 assert_eq!(thinking_state.unwrap().signature, "test_signature");
1359 }
1360
1361 #[test]
1362 fn test_handle_redacted_thinking_content_block_start_event() {
1363 let event = StreamingEvent::ContentBlockStart {
1364 index: 0,
1365 content_block: Content::RedactedThinking {
1366 data: "redacted_blob".to_string(),
1367 },
1368 };
1369 let mut tool_call_state = None;
1370 let mut thinking_state = None;
1371 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1372
1373 assert!(result.is_some());
1374 match result.unwrap().unwrap() {
1375 RawStreamingChoice::Reasoning {
1376 content: ReasoningContent::Redacted { data },
1377 ..
1378 } => {
1379 assert_eq!(data, "redacted_blob");
1380 }
1381 _ => panic!("Expected Redacted reasoning chunk"),
1382 }
1383 }
1384
1385 #[test]
1392 fn signature_only_thinking_block_survives_content_block_stop() {
1393 let mut tool_call_state = None;
1394 let mut thinking_state = None;
1395
1396 let start = StreamingEvent::ContentBlockStart {
1397 index: 0,
1398 content_block: Content::Thinking {
1399 thinking: String::new(),
1400 signature: Some(String::new()),
1401 },
1402 };
1403 assert!(handle_event(&start, &mut tool_call_state, &mut thinking_state).is_none());
1404
1405 let signature = StreamingEvent::ContentBlockDelta {
1406 index: 0,
1407 delta: ContentDelta::SignatureDelta {
1408 signature: "the_whole_signature".to_string(),
1409 },
1410 };
1411 assert!(handle_event(&signature, &mut tool_call_state, &mut thinking_state).is_none());
1412
1413 let stop = StreamingEvent::ContentBlockStop { index: 0 };
1414 let result = handle_event(&stop, &mut tool_call_state, &mut thinking_state)
1415 .expect("signature-only thinking block must not be dropped")
1416 .expect("thinking block should not be an error");
1417
1418 match result {
1419 RawStreamingChoice::ReasoningEnd { id, signature, .. } => {
1420 assert_eq!(id, crate::streaming::MintKind::Block.for_wire_index(0));
1421 assert_eq!(signature.as_deref(), Some("the_whole_signature"));
1422 }
1423 other => panic!("Expected a signed lifecycle end, got {other:?}"),
1424 }
1425 }
1426
1427 #[test]
1430 fn signature_delivered_only_on_content_block_start_is_kept() {
1431 let mut tool_call_state = None;
1432 let mut thinking_state = None;
1433
1434 let start = StreamingEvent::ContentBlockStart {
1435 index: 0,
1436 content_block: Content::Thinking {
1437 thinking: String::new(),
1438 signature: Some("up_front_signature".to_string()),
1439 },
1440 };
1441 assert!(handle_event(&start, &mut tool_call_state, &mut thinking_state).is_none());
1442
1443 let stop = StreamingEvent::ContentBlockStop { index: 0 };
1444 match handle_event(&stop, &mut tool_call_state, &mut thinking_state)
1445 .expect("an up-front signature must not be dropped")
1446 .expect("thinking block should not be an error")
1447 {
1448 RawStreamingChoice::ReasoningEnd { signature, .. } => {
1449 assert_eq!(signature.as_deref(), Some("up_front_signature"));
1450 }
1451 other => panic!("Expected a signed lifecycle end, got {other:?}"),
1452 }
1453 }
1454
1455 #[test]
1459 fn signature_deltas_supersede_the_opening_signature() {
1460 let mut tool_call_state = None;
1461 let mut thinking_state = None;
1462
1463 let start = StreamingEvent::ContentBlockStart {
1464 index: 0,
1465 content_block: Content::Thinking {
1466 thinking: String::new(),
1467 signature: Some("opening".to_string()),
1468 },
1469 };
1470 assert!(handle_event(&start, &mut tool_call_state, &mut thinking_state).is_none());
1471
1472 for fragment in ["delta_", "assembled"] {
1473 let signature = StreamingEvent::ContentBlockDelta {
1474 index: 0,
1475 delta: ContentDelta::SignatureDelta {
1476 signature: fragment.to_string(),
1477 },
1478 };
1479 assert!(handle_event(&signature, &mut tool_call_state, &mut thinking_state).is_none());
1480 }
1481
1482 let stop = StreamingEvent::ContentBlockStop { index: 0 };
1483 match handle_event(&stop, &mut tool_call_state, &mut thinking_state)
1484 .expect("thinking block should be restated")
1485 .expect("thinking block should not be an error")
1486 {
1487 RawStreamingChoice::ReasoningEnd { signature, .. } => {
1488 assert_eq!(signature.as_deref(), Some("delta_assembled"))
1489 }
1490 other => panic!("Expected a signed lifecycle end, got {other:?}"),
1491 }
1492 }
1493
1494 #[test]
1497 fn thinking_block_start_text_streams_as_the_first_delta() {
1498 let mut tool_call_state = None;
1499 let mut thinking_state = None;
1500
1501 let start = StreamingEvent::ContentBlockStart {
1502 index: 2,
1503 content_block: Content::Thinking {
1504 thinking: "opening ".to_string(),
1505 signature: None,
1506 },
1507 };
1508 match handle_event(&start, &mut tool_call_state, &mut thinking_state)
1512 .expect("the opening text streams")
1513 .expect("not an error")
1514 {
1515 RawStreamingChoice::ReasoningDelta { id, reasoning, .. } => {
1516 assert_eq!(id, crate::streaming::MintKind::Block.for_wire_index(2));
1517 assert_eq!(reasoning, "opening ");
1518 }
1519 other => panic!("Expected the opening delta, got {other:?}"),
1520 }
1521
1522 let delta = StreamingEvent::ContentBlockDelta {
1523 index: 2,
1524 delta: ContentDelta::ThinkingDelta {
1525 thinking: "rest".to_string(),
1526 },
1527 };
1528 assert!(handle_event(&delta, &mut tool_call_state, &mut thinking_state).is_some());
1529
1530 let stop = StreamingEvent::ContentBlockStop { index: 2 };
1531 match handle_event(&stop, &mut tool_call_state, &mut thinking_state)
1532 .expect("the stop emits the lifecycle end")
1533 .expect("not an error")
1534 {
1535 RawStreamingChoice::ReasoningEnd {
1536 id,
1537 reasoning: None,
1538 signature: None,
1539 wire_sent: true,
1540 } => {
1541 assert_eq!(id, crate::streaming::MintKind::Block.for_wire_index(2));
1542 }
1543 other => panic!("Expected a bare lifecycle end, got {other:?}"),
1544 }
1545 }
1546
1547 #[test]
1549 fn wholly_empty_thinking_block_is_dropped() {
1550 let mut tool_call_state = None;
1551 let mut thinking_state = None;
1552
1553 let start = StreamingEvent::ContentBlockStart {
1554 index: 0,
1555 content_block: Content::Thinking {
1556 thinking: String::new(),
1557 signature: None,
1558 },
1559 };
1560 assert!(handle_event(&start, &mut tool_call_state, &mut thinking_state).is_none());
1561
1562 let stop = StreamingEvent::ContentBlockStop { index: 0 };
1563 match handle_event(&stop, &mut tool_call_state, &mut thinking_state)
1567 .expect("the stop emits the lifecycle end")
1568 .expect("not an error")
1569 {
1570 RawStreamingChoice::ReasoningEnd {
1571 reasoning: None,
1572 signature: None,
1573 ..
1574 } => {}
1575 other => panic!("Expected a bare lifecycle end, got {other:?}"),
1576 }
1577 }
1578
1579 #[test]
1580 fn test_handle_text_delta_event() {
1581 let event = StreamingEvent::ContentBlockDelta {
1582 index: 0,
1583 delta: ContentDelta::TextDelta {
1584 text: "Hello, world!".to_string(),
1585 },
1586 };
1587
1588 let mut tool_call_state = None;
1589 let mut thinking_state = None;
1590 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1591
1592 assert!(result.is_some());
1593 let choice = result.unwrap().unwrap();
1594
1595 match choice {
1596 RawStreamingChoice::Message(text) => {
1597 assert_eq!(text, "Hello, world!");
1598 }
1599 _ => panic!("Expected Message choice"),
1600 }
1601 }
1602
1603 #[test]
1604 fn test_handle_text_block_start_event() {
1605 let event = StreamingEvent::ContentBlockStart {
1606 index: 0,
1607 content_block: Content::Text {
1608 text: String::new(),
1609 citations: Vec::new(),
1610 cache_control: None,
1611 },
1612 };
1613
1614 let mut tool_call_state = None;
1615 let mut thinking_state = None;
1616 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1617
1618 assert!(result.is_some());
1619 let choice = result.unwrap().unwrap();
1620 assert!(matches!(
1621 choice,
1622 RawStreamingChoice::TextStart {
1623 additional_params: None,
1624 ..
1625 }
1626 ));
1627 }
1628
1629 #[test]
1630 fn test_thinking_delta_does_not_interfere_with_tool_calls() {
1631 let event = StreamingEvent::ContentBlockDelta {
1633 index: 0,
1634 delta: ContentDelta::ThinkingDelta {
1635 thinking: "Thinking while tool is active...".to_string(),
1636 },
1637 };
1638
1639 let mut tool_call_state = Some("tool_123".to_string());
1640 let mut thinking_state = None;
1641
1642 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1643
1644 assert!(result.is_some());
1645 let choice = result.unwrap().unwrap();
1646
1647 match choice {
1648 RawStreamingChoice::ReasoningDelta { reasoning, .. } => {
1649 assert_eq!(reasoning, "Thinking while tool is active...");
1650 }
1651 _ => panic!("Expected ReasoningDelta choice"),
1652 }
1653
1654 assert!(tool_call_state.is_some());
1656 }
1657
1658 #[test]
1659 fn test_handle_input_json_delta_event() {
1660 let event = StreamingEvent::ContentBlockDelta {
1661 index: 0,
1662 delta: ContentDelta::InputJsonDelta {
1663 partial_json: "{\"arg\":\"value".to_string(),
1664 },
1665 };
1666
1667 let mut tool_call_state = Some("tool_123".to_string());
1668 let mut thinking_state = None;
1669
1670 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
1671
1672 assert!(result.is_some());
1674 let choice = result.unwrap().unwrap();
1675
1676 match choice {
1677 RawStreamingChoice::ToolCallDelta { id, content } => {
1678 assert_eq!(id, crate::streaming::StreamPartId::wire("tool_123"));
1679 match content {
1680 ToolCallDeltaContent::Delta(delta) => assert_eq!(delta, "{\"arg\":\"value"),
1681 _ => panic!("Expected Delta content"),
1682 }
1683 }
1684 _ => panic!("Expected ToolCallDelta choice, got {:?}", choice),
1685 }
1686
1687 assert!(tool_call_state.is_some());
1690 }
1691
1692 #[test]
1693 fn test_tool_call_accumulation_with_multiple_deltas() {
1694 let mut tool_call_state = Some("tool_123".to_string());
1695 let mut thinking_state = None;
1696
1697 let event1 = StreamingEvent::ContentBlockDelta {
1699 index: 0,
1700 delta: ContentDelta::InputJsonDelta {
1701 partial_json: "{\"location\":".to_string(),
1702 },
1703 };
1704 let result1 = handle_event(&event1, &mut tool_call_state, &mut thinking_state);
1705 assert!(result1.is_some());
1706
1707 let event2 = StreamingEvent::ContentBlockDelta {
1709 index: 0,
1710 delta: ContentDelta::InputJsonDelta {
1711 partial_json: "\"Paris\",".to_string(),
1712 },
1713 };
1714 let result2 = handle_event(&event2, &mut tool_call_state, &mut thinking_state);
1715 assert!(result2.is_some());
1716
1717 let event3 = StreamingEvent::ContentBlockDelta {
1719 index: 0,
1720 delta: ContentDelta::InputJsonDelta {
1721 partial_json: "\"temp\":\"20C\"}".to_string(),
1722 },
1723 };
1724 let result3 = handle_event(&event3, &mut tool_call_state, &mut thinking_state);
1725 assert!(result3.is_some());
1726
1727 assert!(tool_call_state.is_some());
1728
1729 let stop_event = StreamingEvent::ContentBlockStop { index: 0 };
1734 let final_result = handle_event(&stop_event, &mut tool_call_state, &mut thinking_state);
1735 assert!(final_result.is_some());
1736
1737 match final_result.unwrap().unwrap() {
1738 RawStreamingChoice::ToolInputEnd(end) => {
1739 assert_eq!(end.id, crate::streaming::StreamPartId::wire("tool_123"));
1740 assert!(matches!(
1741 end.on_unparseable,
1742 crate::streaming::UnparseableToolInput::Error
1743 ));
1744 }
1745 other => panic!("Expected ToolInputEnd, got {:?}", other),
1746 }
1747
1748 assert!(tool_call_state.is_none());
1750 }
1751
1752 #[test]
1753 fn test_citations_delta_streaming_event_deserialization() {
1754 let json = r#"{
1755 "type": "content_block_delta",
1756 "index": 0,
1757 "delta": {
1758 "type": "citations_delta",
1759 "citation": {
1760 "type": "char_location",
1761 "cited_text": "The grass is green.",
1762 "document_index": 0,
1763 "document_title": "Example",
1764 "start_char_index": 0,
1765 "end_char_index": 20
1766 }
1767 }
1768 }"#;
1769
1770 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1771 let StreamingEvent::ContentBlockDelta { index, delta } = event else {
1772 panic!("expected ContentBlockDelta");
1773 };
1774 assert_eq!(index, 0);
1775 let ContentDelta::CitationsDelta { citation } = delta else {
1776 panic!("expected CitationsDelta");
1777 };
1778 let crate::providers::anthropic::completion::Citation::CharLocation(citation) = citation
1779 else {
1780 panic!("expected CharLocation");
1781 };
1782 assert_eq!(citation.start_char_index, 0);
1783 assert_eq!(citation.end_char_index, 20);
1784 }
1785
1786 #[test]
1787 fn test_search_result_citations_delta_streaming_event_deserialization() {
1788 let json = r#"{
1789 "type": "content_block_delta",
1790 "index": 0,
1791 "delta": {
1792 "type": "citations_delta",
1793 "citation": {
1794 "type": "search_result_location",
1795 "cited_text": "API requests require a key.",
1796 "source": "https://docs.example.com/api-reference",
1797 "title": "API Reference",
1798 "search_result_index": 0,
1799 "start_block_index": 0,
1800 "end_block_index": 1
1801 }
1802 }
1803 }"#;
1804
1805 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1806 let StreamingEvent::ContentBlockDelta { delta, .. } = event else {
1807 panic!("expected ContentBlockDelta");
1808 };
1809 let ContentDelta::CitationsDelta { citation } = delta else {
1810 panic!("expected CitationsDelta");
1811 };
1812 assert!(matches!(
1813 citation,
1814 crate::providers::anthropic::completion::Citation::SearchResultLocation(
1815 crate::providers::anthropic::completion::SearchResultLocationCitation {
1816 search_result_index: 0,
1817 start_block_index: 0,
1818 end_block_index: 1,
1819 ..
1820 }
1821 )
1822 ));
1823 }
1824
1825 #[test]
1826 fn test_web_search_result_citations_delta_streaming_event_deserialization() {
1827 let json = r#"{
1828 "type": "content_block_delta",
1829 "index": 0,
1830 "delta": {
1831 "type": "citations_delta",
1832 "citation": {
1833 "type": "web_search_result_location",
1834 "cited_text": "Claude Shannon was a mathematician.",
1835 "url": "https://example.com/shannon",
1836 "title": "Claude Shannon",
1837 "encrypted_index": "encrypted-reference"
1838 }
1839 }
1840 }"#;
1841
1842 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1843 let StreamingEvent::ContentBlockDelta { delta, .. } = event else {
1844 panic!("expected ContentBlockDelta");
1845 };
1846 let ContentDelta::CitationsDelta { citation } = delta else {
1847 panic!("expected CitationsDelta");
1848 };
1849 assert!(matches!(
1850 citation,
1851 crate::providers::anthropic::completion::Citation::WebSearchResultLocation(ref citation)
1852 if citation.url == "https://example.com/shannon"
1853 && citation.encrypted_index == "encrypted-reference"
1854 ));
1855 }
1856
1857 #[test]
1858 fn test_web_search_result_citations_delta_allows_null_title() {
1859 let json = r#"{
1860 "type": "content_block_delta",
1861 "index": 0,
1862 "delta": {
1863 "type": "citations_delta",
1864 "citation": {
1865 "type": "web_search_result_location",
1866 "cited_text": "Claude Shannon was a mathematician.",
1867 "url": "https://example.com/shannon",
1868 "title": null,
1869 "encrypted_index": "encrypted-reference"
1870 }
1871 }
1872 }"#;
1873
1874 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1875 let StreamingEvent::ContentBlockDelta { delta, .. } = event else {
1876 panic!("expected ContentBlockDelta");
1877 };
1878 let ContentDelta::CitationsDelta { citation } = delta else {
1879 panic!("expected CitationsDelta");
1880 };
1881 assert!(matches!(
1882 citation,
1883 crate::providers::anthropic::completion::Citation::WebSearchResultLocation(
1884 crate::providers::anthropic::completion::WebSearchResultLocationCitation {
1885 title: None,
1886 ..
1887 }
1888 )
1889 ));
1890 }
1891
1892 #[test]
1893 fn test_text_content_block_start_allows_null_citations() {
1894 let json = r#"{
1899 "type": "content_block_start",
1900 "index": 0,
1901 "content_block": {
1902 "type": "text",
1903 "text": "",
1904 "citations": null
1905 }
1906 }"#;
1907
1908 let event: StreamingEvent = serde_json::from_str(json).unwrap();
1909 let StreamingEvent::ContentBlockStart { content_block, .. } = event else {
1910 panic!("expected ContentBlockStart");
1911 };
1912 let Content::Text {
1913 text, citations, ..
1914 } = content_block
1915 else {
1916 panic!("expected text content block");
1917 };
1918 assert_eq!(text, "");
1919 assert!(citations.is_empty());
1920 }
1921
1922 #[test]
1923 fn test_web_search_content_block_start_events_deserialize() {
1924 let server_tool_use = r#"{
1925 "type": "content_block_start",
1926 "index": 1,
1927 "content_block": {
1928 "type": "server_tool_use",
1929 "id": "srvtoolu_01",
1930 "name": "web_search",
1931 "input": {
1932 "query": "claude shannon birth date"
1933 }
1934 }
1935 }"#;
1936 let event: StreamingEvent = serde_json::from_str(server_tool_use).unwrap();
1937 assert!(matches!(
1938 event,
1939 StreamingEvent::ContentBlockStart {
1940 content_block: Content::ServerToolUse {
1941 ref id,
1942 ref name,
1943 ref input
1944 },
1945 ..
1946 } if id == "srvtoolu_01"
1947 && name == "web_search"
1948 && input["query"] == "claude shannon birth date"
1949 ));
1950
1951 let web_search_tool_result = r#"{
1952 "type": "content_block_start",
1953 "index": 2,
1954 "content_block": {
1955 "type": "web_search_tool_result",
1956 "tool_use_id": "srvtoolu_01",
1957 "content": [{
1958 "type": "web_search_result",
1959 "url": "https://example.com/shannon",
1960 "title": "Claude Shannon",
1961 "encrypted_content": "encrypted-content"
1962 }]
1963 }
1964 }"#;
1965 let event: StreamingEvent = serde_json::from_str(web_search_tool_result).unwrap();
1966 assert!(matches!(
1967 event,
1968 StreamingEvent::ContentBlockStart {
1969 content_block: Content::WebSearchToolResult {
1970 ref tool_use_id,
1971 ref content
1972 },
1973 ..
1974 } if tool_use_id == "srvtoolu_01"
1975 && content[0]["encrypted_content"] == "encrypted-content"
1976 ));
1977 }
1978
1979 #[test]
1980 fn test_code_execution_tool_result_block_is_preserved() {
1981 let event: StreamingEvent = serde_json::from_value(serde_json::json!({
1982 "type": "content_block_start",
1983 "index": 1,
1984 "content_block": {
1985 "type": "code_execution_tool_result",
1986 "tool_use_id": "srvtoolu_01",
1987 "content": {
1988 "type": "code_execution_result",
1989 "return_code": 0,
1990 "stdout": "42\n",
1991 "stderr": "",
1992 "content": []
1993 }
1994 }
1995 }))
1996 .unwrap();
1997 let mut tool_call_state = None;
1998 let mut server_tool_uses = HashMap::new();
1999 let mut thinking_state = None;
2000
2001 let choice = super::handle_event(
2002 &event,
2003 &mut tool_call_state,
2004 &mut server_tool_uses,
2005 &mut thinking_state,
2006 )
2007 .expect("code_execution_tool_result block should produce raw metadata")
2008 .unwrap();
2009
2010 let RawStreamingChoice::TextStart {
2011 id,
2012 additional_params: Some(additional_params),
2013 } = choice
2014 else {
2015 panic!("expected text-start metadata for code_execution_tool_result");
2016 };
2017 assert_eq!(id, crate::streaming::MintKind::Block.for_wire_index(1));
2018 assert_eq!(
2019 additional_params[crate::providers::anthropic::completion::ANTHROPIC_RAW_CONTENT_KEY]["type"],
2020 "code_execution_tool_result"
2021 );
2022 assert_eq!(
2023 additional_params[crate::providers::anthropic::completion::ANTHROPIC_RAW_CONTENT_KEY]["content"]
2024 ["stdout"],
2025 "42\n"
2026 );
2027 }
2028
2029 #[tokio::test]
2030 async fn test_streaming_web_search_blocks_are_preserved_on_final_choice() {
2031 let raw_stream = stream! {
2032 let mut tool_call_state = None;
2033 let mut server_tool_uses = HashMap::new();
2034 let mut thinking_state = None;
2035
2036 let server_tool_use_start = super::handle_event(
2037 &StreamingEvent::ContentBlockStart {
2038 index: 0,
2039 content_block: Content::ServerToolUse {
2040 id: "srvtoolu_01".to_string(),
2041 name: "web_search".to_string(),
2042 input: serde_json::Value::Null,
2043 },
2044 },
2045 &mut tool_call_state,
2046 &mut server_tool_uses,
2047 &mut thinking_state,
2048 );
2049 assert!(
2050 server_tool_use_start.is_none(),
2051 "server_tool_use start should be accumulated until its input JSON is complete"
2052 );
2053
2054 let server_tool_use_delta = super::handle_event(
2055 &StreamingEvent::ContentBlockDelta {
2056 index: 0,
2057 delta: ContentDelta::InputJsonDelta {
2058 partial_json: r#"{"query":"claude shannon birth date"}"#.to_string(),
2059 },
2060 },
2061 &mut tool_call_state,
2062 &mut server_tool_uses,
2063 &mut thinking_state,
2064 );
2065 assert!(
2066 server_tool_use_delta.is_none(),
2067 "server_tool_use input JSON should not be emitted as a Rig tool-call delta"
2068 );
2069
2070 yield super::handle_event(
2071 &StreamingEvent::ContentBlockStop { index: 0 },
2072 &mut tool_call_state,
2073 &mut server_tool_uses,
2074 &mut thinking_state,
2075 )
2076 .expect("server_tool_use stop should produce completed raw metadata");
2077
2078 yield super::handle_event(
2079 &StreamingEvent::ContentBlockStart {
2080 index: 1,
2081 content_block: Content::WebSearchToolResult {
2082 tool_use_id: "srvtoolu_01".to_string(),
2083 content: serde_json::json!([{
2084 "type": "web_search_result",
2085 "url": "https://example.com/shannon",
2086 "title": "Claude Shannon",
2087 "encrypted_content": "encrypted-content"
2088 }]),
2089 },
2090 },
2091 &mut tool_call_state,
2092 &mut server_tool_uses,
2093 &mut thinking_state,
2094 )
2095 .expect("web_search_tool_result block should produce raw metadata");
2096
2097 yield super::handle_event(
2098 &StreamingEvent::ContentBlockStart {
2099 index: 2,
2100 content_block: Content::Text {
2101 text: String::new(),
2102 citations: Vec::new(),
2103 cache_control: None,
2104 },
2105 },
2106 &mut tool_call_state,
2107 &mut server_tool_uses,
2108 &mut thinking_state,
2109 )
2110 .expect("text block start should produce a raw choice");
2111
2112 yield super::handle_event(
2113 &StreamingEvent::ContentBlockDelta {
2114 index: 2,
2115 delta: ContentDelta::TextDelta {
2116 text: "Claude Shannon was born on April 30, 1916.".to_string(),
2117 },
2118 },
2119 &mut tool_call_state,
2120 &mut server_tool_uses,
2121 &mut thinking_state,
2122 )
2123 .expect("text delta should produce a raw choice");
2124
2125 yield super::handle_event(
2126 &StreamingEvent::ContentBlockDelta {
2127 index: 2,
2128 delta: ContentDelta::CitationsDelta {
2129 citation: crate::providers::anthropic::completion::Citation::WebSearchResultLocation(
2130 crate::providers::anthropic::completion::WebSearchResultLocationCitation {
2131 cited_text: "Claude Shannon was born on April 30, 1916."
2132 .to_string(),
2133 url: "https://example.com/shannon".to_string(),
2134 title: Some("Claude Shannon".to_string()),
2135 encrypted_index: "encrypted-index".to_string(),
2136 },
2137 ),
2138 },
2139 },
2140 &mut tool_call_state,
2141 &mut server_tool_uses,
2142 &mut thinking_state,
2143 )
2144 .expect("citation delta should produce a raw choice");
2145
2146 yield Ok(RawStreamingChoice::FinalResponse(StreamingCompletionResponse::default()));
2147 };
2148
2149 let mut stream = crate::streaming::StreamingCompletionResponse::stream(
2150 "anthropic",
2151 to_stream_result(raw_stream),
2152 );
2153 while stream.next().await.is_some() {}
2154
2155 let choice_items: Vec<crate::message::AssistantContent> =
2156 stream.choice.clone().into_iter().collect();
2157 assert_eq!(choice_items.len(), 3);
2158 assert!(
2159 choice_items
2160 .iter()
2161 .all(|item| !matches!(item, crate::message::AssistantContent::ToolCall(_))),
2162 "provider-owned web-search blocks must not become Rig client tool calls"
2163 );
2164
2165 let Some(crate::message::AssistantContent::Text(server_tool_use)) = choice_items.first()
2166 else {
2167 panic!("expected raw server_tool_use metadata");
2168 };
2169 assert_eq!(
2170 server_tool_use.additional_params.as_ref().unwrap()
2171 [crate::providers::anthropic::completion::ANTHROPIC_RAW_CONTENT_KEY]["type"],
2172 "server_tool_use"
2173 );
2174 assert_eq!(
2175 server_tool_use.additional_params.as_ref().unwrap()
2176 [crate::providers::anthropic::completion::ANTHROPIC_RAW_CONTENT_KEY]["input"]["query"],
2177 "claude shannon birth date"
2178 );
2179
2180 let Some(crate::message::AssistantContent::Text(web_search_result)) = choice_items.get(1)
2181 else {
2182 panic!("expected raw web_search_tool_result metadata");
2183 };
2184 assert_eq!(
2185 web_search_result.additional_params.as_ref().unwrap()
2186 [crate::providers::anthropic::completion::ANTHROPIC_RAW_CONTENT_KEY]["content"][0]
2187 ["encrypted_content"],
2188 "encrypted-content"
2189 );
2190
2191 let Some(crate::message::AssistantContent::Text(answer)) = choice_items.get(2) else {
2192 panic!("expected answer text");
2193 };
2194 assert_eq!(answer.text, "Claude Shannon was born on April 30, 1916.");
2195 let citations = crate::providers::anthropic::completion::anthropic_citations(answer)
2196 .expect("expected preserved citations");
2197 assert!(matches!(
2198 citations.first(),
2199 Some(crate::providers::anthropic::completion::Citation::WebSearchResultLocation(citation))
2200 if citation.encrypted_index == "encrypted-index"
2201 ));
2202 }
2203
2204 #[test]
2205 fn test_handle_citations_delta_event_preserves_metadata() {
2206 let event = StreamingEvent::ContentBlockDelta {
2207 index: 0,
2208 delta: ContentDelta::CitationsDelta {
2209 citation: crate::providers::anthropic::completion::Citation::CharLocation(
2210 crate::providers::anthropic::completion::CharLocationCitation {
2211 cited_text: "The grass is green.".to_string(),
2212 document_index: 0,
2213 document_title: Some("Example".to_string()),
2214 start_char_index: 0,
2215 end_char_index: 20,
2216 },
2217 ),
2218 },
2219 };
2220
2221 let mut tool_call_state = None;
2222 let mut thinking_state = None;
2223 let result = handle_event(&event, &mut tool_call_state, &mut thinking_state);
2224
2225 assert!(result.is_some());
2226 let choice = result.unwrap().unwrap();
2227 let RawStreamingChoice::TextAdditionalParams(additional_params) = choice else {
2228 panic!("expected TextAdditionalParams choice");
2229 };
2230 assert_eq!(additional_params["citations"][0]["type"], "char_location");
2231 }
2232
2233 #[tokio::test]
2234 async fn test_streaming_citation_deltas_are_preserved_on_final_text() {
2235 let citation = crate::providers::anthropic::completion::Citation::CharLocation(
2236 crate::providers::anthropic::completion::CharLocationCitation {
2237 cited_text: "The grass is green.".to_string(),
2238 document_index: 0,
2239 document_title: Some("Example".to_string()),
2240 start_char_index: 0,
2241 end_char_index: 20,
2242 },
2243 );
2244
2245 let raw_stream = stream! {
2246 let mut tool_call_state = None;
2247 let mut thinking_state = None;
2248
2249 yield handle_event(
2250 &StreamingEvent::ContentBlockStart {
2251 index: 0,
2252 content_block: Content::Text {
2253 text: String::new(),
2254 citations: Vec::new(),
2255 cache_control: None,
2256 },
2257 },
2258 &mut tool_call_state,
2259 &mut thinking_state,
2260 )
2261 .expect("text block start should produce a raw choice");
2262
2263 yield handle_event(
2264 &StreamingEvent::ContentBlockDelta {
2265 index: 0,
2266 delta: ContentDelta::TextDelta {
2267 text: "the grass is green".to_string(),
2268 },
2269 },
2270 &mut tool_call_state,
2271 &mut thinking_state,
2272 )
2273 .expect("text delta should produce a raw choice");
2274
2275 yield handle_event(
2276 &StreamingEvent::ContentBlockDelta {
2277 index: 0,
2278 delta: ContentDelta::CitationsDelta {
2279 citation: crate::providers::anthropic::completion::Citation::CharLocation(
2280 crate::providers::anthropic::completion::CharLocationCitation {
2281 cited_text: "The grass is green.".to_string(),
2282 document_index: 0,
2283 document_title: Some("Example".to_string()),
2284 start_char_index: 0,
2285 end_char_index: 20,
2286 },
2287 ),
2288 },
2289 },
2290 &mut tool_call_state,
2291 &mut thinking_state,
2292 )
2293 .expect("citation delta should produce a raw choice");
2294
2295 yield Ok(RawStreamingChoice::FinalResponse(StreamingCompletionResponse::default()));
2296 };
2297
2298 let mut stream = crate::streaming::StreamingCompletionResponse::stream(
2299 "anthropic",
2300 to_stream_result(raw_stream),
2301 );
2302 while stream.next().await.is_some() {}
2303
2304 let choice_items: Vec<crate::message::AssistantContent> =
2305 stream.choice.clone().into_iter().collect();
2306 let Some(crate::message::AssistantContent::Text(text)) = choice_items.first() else {
2307 panic!("expected accumulated text item");
2308 };
2309
2310 assert_eq!(text.text, "the grass is green");
2311 let citations = crate::providers::anthropic::completion::anthropic_citations(text).unwrap();
2312 assert_eq!(citations, vec![citation]);
2313 }
2314
2315 #[test]
2324 fn classify_dispatches_on_the_known_event_list() {
2325 let adapter = AnthropicAdapter::default();
2326
2327 let frame =
2328 WireFrame::Text(r#"{"type":"something_new_from_anthropic","field":"x"}"#.into());
2329 assert!(matches!(
2330 adapter.classify(frame),
2331 crate::providers::internal::wire::WireEvent::Unknown { event_type, .. }
2332 if event_type == "something_new_from_anthropic"
2333 ));
2334
2335 let frame = WireFrame::Text(r#"{"type":"ping"}"#.into());
2336 assert!(matches!(
2337 adapter.classify(frame),
2338 crate::providers::internal::wire::WireEvent::Known(StreamingEvent::Ping)
2339 ));
2340
2341 let frame = WireFrame::Text("{not json".into());
2342 assert!(matches!(
2343 adapter.classify(frame),
2344 crate::providers::internal::wire::WireEvent::Corrupt(_)
2345 ));
2346 }
2347
2348 #[test]
2353 fn novel_nested_delta_type_is_a_known_noop() {
2354 let adapter = AnthropicAdapter::default();
2355 let frame = WireFrame::Text(
2356 r#"{"type":"content_block_delta","index":0,"delta":{"type":"banana_delta","x":1}}"#
2357 .into(),
2358 );
2359 let crate::providers::internal::wire::WireEvent::Known(event) = adapter.classify(frame)
2360 else {
2361 panic!("a novel nested delta type must stay a Known event");
2362 };
2363
2364 let mut adapter = AnthropicAdapter::default();
2365 let mut out = Vec::new();
2366 adapter.interpret(event, &mut out);
2367 assert!(out.is_empty(), "an unmodeled nested delta is a no-op");
2368 }
2369
2370 #[test]
2377 fn per_ttl_cache_creation_split_carries_from_message_start_to_terminal() {
2378 let mut adapter = AnthropicAdapter::default();
2379 let mut out = Vec::new();
2380
2381 let start = WireFrame::Text(
2382 r#"{"type":"message_start","message":{"id":"msg_1","role":"assistant","content":[],"model":"claude-sonnet-4-6","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"output_tokens":1,"cache_creation_input_tokens":9702,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_1h_input_tokens":9366,"ephemeral_5m_input_tokens":336}}}}"#
2383 .into(),
2384 );
2385 let crate::providers::internal::wire::WireEvent::Known(event) = adapter.classify(start)
2386 else {
2387 panic!("message_start must classify Known");
2388 };
2389 adapter.interpret(event, &mut out);
2390
2391 let delta = WireFrame::Text(
2392 r#"{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":7,"input_tokens":3,"cache_creation_input_tokens":9702,"cache_read_input_tokens":0}}"#
2393 .into(),
2394 );
2395 let crate::providers::internal::wire::WireEvent::Known(event) = adapter.classify(delta)
2396 else {
2397 panic!("message_delta must classify Known");
2398 };
2399 adapter.interpret(event, &mut out);
2400
2401 let terminal = out
2402 .iter()
2403 .find_map(|item| match item {
2404 Ok(crate::streaming::RawStreamingChoice::FinalResponse(response)) => {
2405 Some(response.clone())
2406 }
2407 _ => None,
2408 })
2409 .expect("terminal message_delta must yield a final response");
2410 let split = terminal
2411 .usage
2412 .cache_creation
2413 .expect("terminal usage must carry the message_start cache_creation split");
2414 assert_eq!(split.ephemeral_1h_input_tokens, 9366);
2415 assert_eq!(split.ephemeral_5m_input_tokens, 336);
2416 assert_eq!(terminal.usage.cache_creation_input_tokens, Some(9702));
2417 }
2418
2419 #[test]
2425 fn delta_missing_its_type_is_corrupt_not_skipped() {
2426 let adapter = AnthropicAdapter::default();
2427 let frame = WireFrame::Text(
2428 r#"{"type":"content_block_delta","index":0,"delta":{"text":"hello"}}"#.into(),
2429 );
2430 assert!(matches!(
2431 adapter.classify(frame),
2432 crate::providers::internal::wire::WireEvent::Corrupt(_)
2433 ));
2434 }
2435
2436 #[test]
2440 fn known_nested_delta_tag_with_defective_payload_is_corrupt() {
2441 let adapter = AnthropicAdapter::default();
2442 let frame = WireFrame::Text(
2443 r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":42}}"#
2444 .into(),
2445 );
2446 assert!(matches!(
2447 adapter.classify(frame),
2448 crate::providers::internal::wire::WireEvent::Corrupt(_)
2449 ));
2450 }
2451
2452 #[test]
2457 fn top_level_error_event_surfaces_as_a_provider_error() {
2458 let adapter = AnthropicAdapter::default();
2459 let frame = WireFrame::Text(
2460 r#"{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}"#.into(),
2461 );
2462 let crate::providers::internal::wire::WireEvent::Known(event) = adapter.classify(frame)
2463 else {
2464 panic!("the error envelope must classify as a Known event");
2465 };
2466
2467 let mut adapter = AnthropicAdapter::default();
2468 let mut out = Vec::new();
2469 adapter.interpret(event, &mut out);
2470
2471 assert_eq!(out.len(), 1, "the error envelope maps to one error item");
2472 let Some(Err(error)) = out.pop() else {
2473 panic!("the error envelope must surface as an Err item");
2474 };
2475 let body = error
2476 .provider_response_body()
2477 .expect("the provider's error payload must be preserved");
2478 assert!(
2479 body.contains("overloaded_error") && body.contains("Overloaded"),
2480 "the full envelope must survive into the error body, got: {body}"
2481 );
2482 }
2483
2484 #[test]
2487 fn message_start_with_null_message_is_a_known_noop() {
2488 let adapter = AnthropicAdapter::default();
2489 let frame = WireFrame::Text(r#"{"type":"message_start","message":null}"#.into());
2490 let crate::providers::internal::wire::WireEvent::Known(event) = adapter.classify(frame)
2491 else {
2492 panic!("null-message message_start must stay a known event");
2493 };
2494
2495 let mut adapter = AnthropicAdapter::default();
2496 let mut out = Vec::new();
2497 adapter.interpret(event, &mut out);
2498 assert!(out.is_empty(), "a message-less message_start is a no-op");
2499 }
2500
2501 #[tokio::test]
2502 async fn terminal_record_normalizes_stop_reason_usage_and_metadata() {
2503 let raw_stream = stream! {
2504 yield Ok(RawStreamingChoice::Message("hi".to_string()));
2505 yield Ok(RawStreamingChoice::FinalResponse(StreamingCompletionResponse {
2506 usage: PartialUsage {
2507 output_tokens: 5,
2508 input_tokens: Some(3),
2509 cache_creation_input_tokens: None,
2510 cache_creation: None,
2511 cache_read_input_tokens: Some(2),
2512 output_tokens_details: None,
2513 },
2514 stop_reason: Some("max_tokens".to_string()),
2515 stop_sequence: None,
2516 message_id: Some("msg_1".to_string()),
2517 model: Some(CLAUDE_OPUS_4_8.to_string()),
2518 provider_request_id: None,
2519 }));
2520 };
2521
2522 let mut stream = crate::streaming::StreamingCompletionResponse::stream(
2523 "anthropic",
2524 to_stream_result(raw_stream),
2525 );
2526 while stream.next().await.is_some() {}
2527
2528 let terminal = stream.response.expect("expected a terminal record");
2529 assert_eq!(terminal.provider, "anthropic");
2530 assert_eq!(terminal.message_id.as_deref(), Some("msg_1"));
2531 assert_eq!(terminal.model.as_deref(), Some(CLAUDE_OPUS_4_8));
2532 assert_eq!(
2533 terminal.finish_reason,
2534 Some(crate::completion::FinishReason::Length)
2535 );
2536 assert_eq!(terminal.usage.input_tokens, 3);
2537 assert_eq!(terminal.usage.output_tokens, 5);
2538 assert_eq!(terminal.usage.cached_input_tokens, 2);
2539 assert_eq!(terminal.usage.total_tokens, 10);
2540 }
2541
2542 #[tokio::test]
2543 async fn terminal_record_upgrades_end_turn_to_tool_calls_after_a_streamed_tool_call() {
2544 let raw_stream = stream! {
2548 yield Ok(RawStreamingChoice::ToolCall(RawStreamingToolCall::new(
2549 "toolu_1".to_string(),
2550 "add".to_string(),
2551 json!({"x": 1}),
2552 )));
2553 yield Ok(RawStreamingChoice::FinalResponse(StreamingCompletionResponse {
2554 stop_reason: Some("end_turn".to_string()),
2555 ..Default::default()
2556 }));
2557 };
2558
2559 let mut stream = crate::streaming::StreamingCompletionResponse::stream(
2560 "anthropic",
2561 to_stream_result(raw_stream),
2562 );
2563 while stream.next().await.is_some() {}
2564
2565 let terminal = stream.response.expect("expected a terminal record");
2566 assert_eq!(
2567 terminal.finish_reason,
2568 Some(crate::completion::FinishReason::ToolCalls)
2569 );
2570 }
2571
2572 #[tokio::test]
2573 async fn unknown_stop_reason_survives_onto_the_terminal_record() {
2574 let raw_stream = stream! {
2575 yield Ok(RawStreamingChoice::FinalResponse(StreamingCompletionResponse {
2576 stop_reason: Some("pause_turn".to_string()),
2577 ..Default::default()
2578 }));
2579 };
2580
2581 let mut stream = crate::streaming::StreamingCompletionResponse::stream(
2582 "anthropic",
2583 to_stream_result(raw_stream),
2584 );
2585 while stream.next().await.is_some() {}
2586
2587 let terminal = stream.response.expect("expected a terminal record");
2588 assert_eq!(
2589 terminal.finish_reason,
2590 Some(crate::completion::FinishReason::Other(
2591 "pause_turn".to_owned()
2592 ))
2593 );
2594 }
2595
2596 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
2597 mod terminal_emission {
2598 use super::super::super::completion::CLAUDE_SONNET_4_6;
2599 use crate::client::CompletionClient;
2600 use crate::completion::CompletionModel as _;
2601 use crate::providers::anthropic::Client;
2602 use crate::streaming::StreamedAssistantContent;
2603 use crate::test_utils::MockStreamingClient;
2604 use futures::StreamExt;
2605
2606 const MESSAGE_START: &str = r#"{"type":"message_start","message":{"id":"msg_1","role":"assistant","content":[],"model":"claude-sonnet-4-6","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":5,"output_tokens":0}}}"#;
2607 const TEXT_START: &str =
2608 r#"{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#;
2609 const TEXT_DELTA: &str =
2610 r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}"#;
2611 const MESSAGE_DELTA: &str = r#"{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":3}}"#;
2612
2613 fn sse(frames: &[&str]) -> bytes::Bytes {
2614 bytes::Bytes::from(
2615 frames
2616 .iter()
2617 .map(|frame| format!("data: {frame}\n\n"))
2618 .collect::<String>(),
2619 )
2620 }
2621
2622 async fn collect(
2623 sse_bytes: bytes::Bytes,
2624 ) -> (
2625 Vec<String>,
2626 bool,
2627 bool,
2628 crate::streaming::StreamingCompletionResponse,
2629 ) {
2630 let client = Client::builder()
2631 .api_key("test-key")
2632 .http_client(MockStreamingClient { sse_bytes })
2633 .build()
2634 .expect("build client");
2635 let model = client.completion_model(CLAUDE_SONNET_4_6);
2636 let request = model.completion_request("hello").build();
2637 let mut stream = crate::completion::CompletionModel::stream(&model, request)
2638 .await
2639 .expect("stream should open");
2640
2641 let mut texts = Vec::new();
2642 let mut saw_error = false;
2643 let mut saw_terminal = false;
2644 while let Some(item) = stream.next().await {
2645 match item {
2646 Ok(StreamedAssistantContent::Text(text)) => texts.push(text.text),
2647 Ok(StreamedAssistantContent::Final(_)) => saw_terminal = true,
2648 Ok(_) => {}
2649 Err(_) => saw_error = true,
2650 }
2651 }
2652 (texts, saw_error, saw_terminal, stream)
2653 }
2654
2655 #[tokio::test]
2656 async fn truncated_stream_yields_content_but_no_terminal_record() {
2657 let (texts, saw_error, saw_terminal, stream) =
2658 collect(sse(&[MESSAGE_START, TEXT_START, TEXT_DELTA])).await;
2659
2660 assert_eq!(texts, ["hi"]);
2661 assert!(!saw_error);
2662 assert!(
2663 !saw_terminal,
2664 "EOF without message_delta must not synthesize a terminal record"
2665 );
2666 assert!(stream.response.is_none());
2667 }
2668
2669 #[tokio::test]
2670 async fn errored_stream_forwards_the_error_and_no_terminal_record() {
2671 use crate::test_utils::SequencedStreamingHttpClient;
2672
2673 let client = Client::builder()
2677 .api_key("test-key")
2678 .http_client(SequencedStreamingHttpClient::new(vec![
2679 Ok(sse(&[MESSAGE_START, TEXT_START, TEXT_DELTA])),
2680 Err(crate::http_client::Error::InvalidStatusCodeWithMessage(
2681 http::StatusCode::BAD_GATEWAY,
2682 "connection reset".to_string(),
2683 )),
2684 ]))
2685 .build()
2686 .expect("build client");
2687 let model = client.completion_model(CLAUDE_SONNET_4_6);
2688 let request = model.completion_request("hello").build();
2689 let mut stream = crate::completion::CompletionModel::stream(&model, request)
2690 .await
2691 .expect("stream should open");
2692
2693 let mut texts = Vec::new();
2694 let mut saw_error = false;
2695 let mut saw_terminal = false;
2696 while let Some(item) = stream.next().await {
2697 match item {
2698 Ok(StreamedAssistantContent::Text(text)) => texts.push(text.text),
2699 Ok(StreamedAssistantContent::Final(_)) => saw_terminal = true,
2700 Ok(_) => {}
2701 Err(_) => saw_error = true,
2702 }
2703 }
2704
2705 assert_eq!(texts, ["hi"]);
2706 assert!(saw_error, "the transport failure must reach the consumer");
2707 assert!(
2708 !saw_terminal,
2709 "a failed stream must not synthesize a terminal record"
2710 );
2711 assert!(stream.response.is_none());
2712 }
2713
2714 #[tokio::test]
2715 async fn provider_error_event_stops_the_stream_before_a_later_terminal() {
2716 const ERROR_EVENT: &str =
2722 r#"{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}"#;
2723 let (texts, saw_error, saw_terminal, stream) = collect(sse(&[
2724 MESSAGE_START,
2725 TEXT_START,
2726 TEXT_DELTA,
2727 ERROR_EVENT,
2728 MESSAGE_DELTA,
2729 ]))
2730 .await;
2731
2732 assert_eq!(texts, ["hi"]);
2733 assert!(saw_error, "the provider error must reach the consumer");
2734 assert!(
2735 !saw_terminal,
2736 "a message_delta after an in-band provider error must not read as a completed turn"
2737 );
2738 assert!(stream.response.is_none());
2739 }
2740
2741 #[tokio::test]
2753 async fn input_tokens_prefer_the_terminal_delta_and_fall_back_to_message_start() {
2754 fn message_start(input_tokens: usize) -> String {
2755 format!(
2756 r#"{{"type":"message_start","message":{{"id":"msg_1","role":"assistant","content":[],"model":"claude-sonnet-4-6","stop_reason":null,"stop_sequence":null,"usage":{{"input_tokens":{input_tokens},"output_tokens":0}}}}}}"#
2757 )
2758 }
2759 fn message_delta(input_tokens: usize) -> String {
2760 format!(
2761 r#"{{"type":"message_delta","delta":{{"stop_reason":"end_turn","stop_sequence":null}},"usage":{{"input_tokens":{input_tokens},"output_tokens":3}}}}"#
2762 )
2763 }
2764
2765 for (start, delta, expected, case) in [
2766 (
2770 message_start(0),
2771 message_delta(9),
2772 9,
2773 "a gateway reporting the prompt size on message_delta must reach the consumer",
2774 ),
2775 (
2780 message_start(5),
2781 MESSAGE_DELTA.to_owned(),
2782 5,
2783 "a delta without input_tokens falls back to message_start",
2784 ),
2785 (
2787 message_start(5),
2788 message_delta(5),
2789 5,
2790 "agreeing frames report that count",
2791 ),
2792 (
2795 message_start(5),
2796 message_delta(0),
2797 5,
2798 "a zero on the delta must not erase the message_start count",
2799 ),
2800 ] {
2801 let (_texts, _saw_error, saw_terminal, stream) =
2802 collect(sse(&[&start, TEXT_START, TEXT_DELTA, &delta])).await;
2803
2804 assert!(saw_terminal, "{case}: the turn must complete");
2805 let terminal = stream.response.expect("terminal record");
2806 assert_eq!(terminal.usage.input_tokens, expected, "{case}");
2807 }
2808 }
2809
2810 #[tokio::test]
2811 async fn malformed_frame_then_eof_yields_error_and_no_terminal_record() {
2812 let (texts, saw_error, saw_terminal, stream) =
2813 collect(sse(&[MESSAGE_START, TEXT_START, TEXT_DELTA, "{not json"])).await;
2814
2815 assert_eq!(texts, ["hi"]);
2816 assert!(saw_error, "the malformed frame must reach the consumer");
2817 assert!(
2818 !saw_terminal,
2819 "a parse error followed by EOF must not read as a completed turn"
2820 );
2821 assert!(stream.response.is_none());
2822 }
2823
2824 #[tokio::test]
2825 async fn malformed_frame_then_real_terminal_still_completes_the_stream() {
2826 let (texts, saw_error, saw_terminal, stream) = collect(sse(&[
2827 MESSAGE_START,
2828 TEXT_START,
2829 TEXT_DELTA,
2830 "{not json",
2831 MESSAGE_DELTA,
2832 ]))
2833 .await;
2834
2835 assert_eq!(texts, ["hi"]);
2836 assert!(saw_error, "the malformed frame must reach the consumer");
2837 assert!(
2838 saw_terminal,
2839 "a genuine message_delta after a parse error still completes the stream"
2840 );
2841 let terminal = stream.response.expect("terminal record");
2842 assert_eq!(
2843 terminal.finish_reason,
2844 Some(crate::completion::FinishReason::Stop)
2845 );
2846 assert_eq!(terminal.message_id.as_deref(), Some("msg_1"));
2847 }
2848
2849 #[tokio::test]
2858 async fn terminal_raw_round_trips_into_the_terminal_type() {
2859 const STOP_SEQUENCE_DELTA: &str = r#"{"type":"message_delta","delta":{"stop_reason":"stop_sequence","stop_sequence":"alpha"},"usage":{"output_tokens":3}}"#;
2860
2861 let client = Client::builder()
2862 .api_key("test-key")
2863 .http_client(MockStreamingClient {
2864 sse_bytes: sse(&[MESSAGE_START, TEXT_START, TEXT_DELTA, STOP_SEQUENCE_DELTA]),
2865 })
2866 .build()
2867 .expect("build client");
2868 let model = client.completion_model(CLAUDE_SONNET_4_6);
2869 let request = model.completion_request("hello").build();
2870 let mut stream = crate::completion::CompletionModel::stream(&model, request)
2871 .await
2872 .expect("stream should open");
2873 while let Some(item) = stream.next().await {
2874 item.expect("stream item");
2875 }
2876 let terminal = stream.response.expect("terminal record");
2877
2878 let raw = &terminal.raw;
2879 let typed: super::super::StreamingCompletionResponse =
2880 serde_json::from_value(raw.clone()).expect("raw must deserialize");
2881 assert_eq!(
2882 serde_json::to_value(&typed).expect("re-serialize"),
2883 *raw,
2884 "the capture must be exactly what the terminal type serializes to"
2885 );
2886 assert_eq!(typed.stop_reason.as_deref(), Some("stop_sequence"));
2887 assert_eq!(typed.stop_sequence.as_deref(), Some("alpha"));
2888 assert_eq!(typed.message_id.as_deref(), Some("msg_1"));
2889
2890 let renormalized = crate::streaming::StreamFinal::from(("anthropic", typed));
2893 assert_eq!(terminal.identity(), renormalized.identity());
2894 assert_eq!(terminal.finish_reason, renormalized.finish_reason);
2895 assert_eq!(terminal.model, renormalized.model);
2896 assert_eq!(terminal.usage, renormalized.usage);
2897 assert_eq!(
2898 terminal.finish_reason,
2899 Some(crate::completion::FinishReason::Stop)
2900 );
2901 assert_eq!(terminal.usage.output_tokens, 3);
2902 }
2903 }
2904}