1use crate::completion::CompletionRequest;
4use crate::providers::anthropic::streaming::StreamingCompletionResponse;
5use crate::{
6 OneOrMany,
7 client::Provider,
8 completion::{self, CompletionError, GetTokenUsage},
9 http_client::HttpClientExt,
10 message::{self, DocumentMediaType, DocumentSourceKind, MessageError, MimeType, Reasoning},
11 one_or_many::string_or_one_or_many,
12 telemetry::{ProviderResponseExt, SpanCombinator},
13 wasm_compat::*,
14};
15use bytes::Bytes;
16use serde::{Deserialize, Serialize};
17use std::{convert::Infallible, str::FromStr};
18use tracing::{Instrument, Level, enabled, info_span};
19
20pub const CLAUDE_OPUS_4_6: &str = "claude-opus-4-6";
26pub const CLAUDE_OPUS_4_7: &str = "claude-opus-4-7";
28pub const CLAUDE_SONNET_4_6: &str = "claude-sonnet-4-6";
30pub const CLAUDE_HAIKU_4_5: &str = "claude-haiku-4-5";
32
33pub const ANTHROPIC_VERSION_2023_01_01: &str = "2023-01-01";
34pub const ANTHROPIC_VERSION_2023_06_01: &str = "2023-06-01";
35pub const ANTHROPIC_VERSION_LATEST: &str = ANTHROPIC_VERSION_2023_06_01;
36const EMPTY_RESPONSE_ERROR: &str = "Response contained no message or tool call (empty)";
37pub(crate) const ANTHROPIC_RAW_CONTENT_KEY: &str = "anthropic_content";
38
39pub trait AnthropicCompatibleProvider: Provider {
40 const PROVIDER_NAME: &'static str;
41
42 fn default_max_tokens(model: &str) -> Option<u64> {
43 let _ = model;
44 None
45 }
46}
47
48impl AnthropicCompatibleProvider for super::client::AnthropicExt {
49 const PROVIDER_NAME: &'static str = "anthropic";
50
51 fn default_max_tokens(model: &str) -> Option<u64> {
52 default_max_tokens_for_model(model)
53 }
54}
55
56#[derive(Debug, Deserialize, Serialize)]
57pub struct CompletionResponse {
58 pub content: Vec<Content>,
59 pub id: String,
60 pub model: String,
61 pub role: String,
62 pub stop_reason: Option<String>,
63 pub stop_sequence: Option<String>,
64 pub usage: Usage,
65}
66
67impl ProviderResponseExt for CompletionResponse {
68 type OutputMessage = Content;
69 type Usage = Usage;
70
71 fn get_response_id(&self) -> Option<String> {
72 Some(self.id.to_owned())
73 }
74
75 fn get_response_model_name(&self) -> Option<String> {
76 Some(self.model.to_owned())
77 }
78
79 fn get_output_messages(&self) -> Vec<Self::OutputMessage> {
80 self.content.clone()
81 }
82
83 fn get_text_response(&self) -> Option<String> {
84 let res = self
85 .content
86 .iter()
87 .filter_map(|x| {
88 if let Content::Text { text, .. } = x {
89 Some(text.as_str())
90 } else {
91 None
92 }
93 })
94 .collect::<String>();
95
96 if res.is_empty() { None } else { Some(res) }
97 }
98
99 fn get_usage(&self) -> Option<Self::Usage> {
100 Some(self.usage.clone())
101 }
102}
103
104#[derive(Clone, Debug, Deserialize, Serialize)]
105pub struct Usage {
106 pub input_tokens: u64,
107 pub cache_read_input_tokens: Option<u64>,
108 pub cache_creation_input_tokens: Option<u64>,
109 pub output_tokens: u64,
110}
111
112impl std::fmt::Display for Usage {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 write!(
115 f,
116 "Input tokens: {}\nCache read input tokens: {}\nCache creation input tokens: {}\nOutput tokens: {}",
117 self.input_tokens,
118 match self.cache_read_input_tokens {
119 Some(token) => token.to_string(),
120 None => "n/a".to_string(),
121 },
122 match self.cache_creation_input_tokens {
123 Some(token) => token.to_string(),
124 None => "n/a".to_string(),
125 },
126 self.output_tokens
127 )
128 }
129}
130
131impl GetTokenUsage for Usage {
132 fn token_usage(&self) -> Option<crate::completion::Usage> {
133 let mut usage = crate::completion::Usage::new();
134
135 usage.input_tokens = self.input_tokens;
136 usage.output_tokens = self.output_tokens;
137 usage.cached_input_tokens = self.cache_read_input_tokens.unwrap_or_default();
138 usage.cache_creation_input_tokens = self.cache_creation_input_tokens.unwrap_or_default();
139 usage.total_tokens = self.input_tokens
140 + self.cache_read_input_tokens.unwrap_or_default()
141 + self.cache_creation_input_tokens.unwrap_or_default()
142 + self.output_tokens;
143
144 Some(usage)
145 }
146}
147
148#[derive(Debug, Deserialize, Serialize)]
149pub struct ToolDefinition {
150 pub name: String,
151 pub description: Option<String>,
152 pub input_schema: serde_json::Value,
153 #[serde(skip_serializing_if = "Option::is_none")]
157 pub cache_control: Option<CacheControl>,
158}
159
160#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
166pub enum CacheTtl {
167 #[default]
169 #[serde(rename = "5m")]
170 FiveMinutes,
171 #[serde(rename = "1h")]
173 OneHour,
174}
175
176#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
181#[serde(tag = "type", rename_all = "snake_case")]
182pub enum CacheControl {
183 Ephemeral {
184 #[serde(skip_serializing_if = "Option::is_none")]
186 ttl: Option<CacheTtl>,
187 },
188}
189
190impl CacheControl {
191 pub fn ephemeral() -> Self {
193 Self::Ephemeral { ttl: None }
194 }
195
196 pub fn ephemeral_1h() -> Self {
198 Self::Ephemeral {
199 ttl: Some(CacheTtl::OneHour),
200 }
201 }
202}
203
204#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
206#[serde(tag = "type", rename_all = "snake_case")]
207pub enum SystemContent {
208 Text {
209 text: String,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 cache_control: Option<CacheControl>,
212 },
213}
214
215impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
216 type Error = CompletionError;
217
218 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
219 let content = response
220 .content
221 .iter()
222 .map(|content| content.clone().try_into())
223 .collect::<Result<Vec<_>, _>>()?;
224
225 let choice = if content.is_empty() {
226 if response.stop_reason.as_deref() == Some("end_turn") {
230 OneOrMany::one(completion::AssistantContent::text(""))
231 } else {
232 return Err(CompletionError::ResponseError(
233 EMPTY_RESPONSE_ERROR.to_owned(),
234 ));
235 }
236 } else {
237 OneOrMany::many(content)
238 .map_err(|_| CompletionError::ResponseError(EMPTY_RESPONSE_ERROR.to_owned()))?
239 };
240
241 let usage = completion::Usage {
242 input_tokens: response.usage.input_tokens,
243 output_tokens: response.usage.output_tokens,
244 total_tokens: response.usage.input_tokens
245 + response.usage.cache_read_input_tokens.unwrap_or(0)
246 + response.usage.cache_creation_input_tokens.unwrap_or(0)
247 + response.usage.output_tokens,
248 cached_input_tokens: response.usage.cache_read_input_tokens.unwrap_or(0),
249 cache_creation_input_tokens: response.usage.cache_creation_input_tokens.unwrap_or(0),
250 tool_use_prompt_tokens: 0,
251 reasoning_tokens: 0,
252 };
253
254 Ok(completion::CompletionResponse {
255 choice,
256 usage,
257 raw_response: response,
258 message_id: None,
259 })
260 }
261}
262
263#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
264pub struct Message {
265 pub role: Role,
266 #[serde(deserialize_with = "string_or_one_or_many")]
267 pub content: OneOrMany<Content>,
268}
269
270#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
271#[serde(rename_all = "lowercase")]
272pub enum Role {
273 User,
274 Assistant,
275}
276
277#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
278#[serde(tag = "type", rename_all = "snake_case")]
279pub enum Content {
280 Text {
281 text: String,
282 #[serde(default, skip_serializing_if = "Vec::is_empty")]
285 citations: Vec<Citation>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 cache_control: Option<CacheControl>,
288 },
289 Image {
290 source: ImageSource,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 cache_control: Option<CacheControl>,
293 },
294 ToolUse {
295 id: String,
296 name: String,
297 input: serde_json::Value,
298 },
299 ServerToolUse {
300 id: String,
301 name: String,
302 #[serde(default)]
303 input: serde_json::Value,
304 },
305 WebSearchToolResult {
306 tool_use_id: String,
307 content: serde_json::Value,
308 },
309 ToolResult {
310 tool_use_id: String,
311 #[serde(deserialize_with = "string_or_one_or_many")]
312 content: OneOrMany<ToolResultContent>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 is_error: Option<bool>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 cache_control: Option<CacheControl>,
317 },
318 Document {
319 source: DocumentSource,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 title: Option<String>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
327 context: Option<String>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
332 citations: Option<CitationsConfig>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 cache_control: Option<CacheControl>,
335 },
336 Thinking {
337 thinking: String,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 signature: Option<String>,
340 },
341 RedactedThinking {
342 data: String,
343 },
344}
345
346impl FromStr for Content {
347 type Err = Infallible;
348
349 fn from_str(s: &str) -> Result<Self, Self::Err> {
350 Ok(Content::Text {
351 text: s.to_owned(),
352 citations: Vec::new(),
353 cache_control: None,
354 })
355 }
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
370pub struct CitationsConfig {
371 pub enabled: bool,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq)]
396pub enum Citation {
397 CharLocation {
399 cited_text: String,
401 document_index: usize,
403 document_title: Option<String>,
405 start_char_index: usize,
407 end_char_index: usize,
409 },
410 PageLocation {
412 cited_text: String,
414 document_index: usize,
416 document_title: Option<String>,
418 start_page_number: u32,
420 end_page_number: u32,
422 },
423 ContentBlockLocation {
425 cited_text: String,
427 document_index: usize,
429 document_title: Option<String>,
431 start_block_index: usize,
433 end_block_index: usize,
435 },
436 SearchResultLocation {
438 cited_text: String,
440 source: String,
442 title: Option<String>,
444 search_result_index: usize,
447 start_block_index: usize,
449 end_block_index: usize,
451 },
452 WebSearchResultLocation {
454 cited_text: String,
456 url: String,
458 title: Option<String>,
460 encrypted_index: String,
463 },
464 Unknown(serde_json::Value),
467}
468
469#[derive(Deserialize)]
470struct CharLocationCitationFields {
471 cited_text: String,
472 document_index: usize,
473 #[serde(default)]
474 document_title: Option<String>,
475 start_char_index: usize,
476 end_char_index: usize,
477}
478
479#[derive(Deserialize)]
480struct PageLocationCitationFields {
481 cited_text: String,
482 document_index: usize,
483 #[serde(default)]
484 document_title: Option<String>,
485 start_page_number: u32,
486 end_page_number: u32,
487}
488
489#[derive(Deserialize)]
490struct ContentBlockLocationCitationFields {
491 cited_text: String,
492 document_index: usize,
493 #[serde(default)]
494 document_title: Option<String>,
495 start_block_index: usize,
496 end_block_index: usize,
497}
498
499#[derive(Deserialize)]
500struct SearchResultLocationCitationFields {
501 cited_text: String,
502 source: String,
503 #[serde(default)]
504 title: Option<String>,
505 search_result_index: usize,
506 start_block_index: usize,
507 end_block_index: usize,
508}
509
510#[derive(Deserialize)]
511struct WebSearchResultLocationCitationFields {
512 cited_text: String,
513 url: String,
514 title: Option<String>,
515 encrypted_index: String,
516}
517
518impl Serialize for Citation {
519 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
520 where
521 S: serde::Serializer,
522 {
523 let mut value = serde_json::Map::new();
524 match self {
525 Citation::CharLocation {
526 cited_text,
527 document_index,
528 document_title,
529 start_char_index,
530 end_char_index,
531 } => {
532 value.insert("type".into(), serde_json::json!("char_location"));
533 value.insert("cited_text".into(), serde_json::json!(cited_text));
534 value.insert("document_index".into(), serde_json::json!(document_index));
535 if let Some(document_title) = document_title {
536 value.insert("document_title".into(), serde_json::json!(document_title));
537 }
538 value.insert(
539 "start_char_index".into(),
540 serde_json::json!(start_char_index),
541 );
542 value.insert("end_char_index".into(), serde_json::json!(end_char_index));
543 }
544 Citation::PageLocation {
545 cited_text,
546 document_index,
547 document_title,
548 start_page_number,
549 end_page_number,
550 } => {
551 value.insert("type".into(), serde_json::json!("page_location"));
552 value.insert("cited_text".into(), serde_json::json!(cited_text));
553 value.insert("document_index".into(), serde_json::json!(document_index));
554 if let Some(document_title) = document_title {
555 value.insert("document_title".into(), serde_json::json!(document_title));
556 }
557 value.insert(
558 "start_page_number".into(),
559 serde_json::json!(start_page_number),
560 );
561 value.insert("end_page_number".into(), serde_json::json!(end_page_number));
562 }
563 Citation::ContentBlockLocation {
564 cited_text,
565 document_index,
566 document_title,
567 start_block_index,
568 end_block_index,
569 } => {
570 value.insert("type".into(), serde_json::json!("content_block_location"));
571 value.insert("cited_text".into(), serde_json::json!(cited_text));
572 value.insert("document_index".into(), serde_json::json!(document_index));
573 if let Some(document_title) = document_title {
574 value.insert("document_title".into(), serde_json::json!(document_title));
575 }
576 value.insert(
577 "start_block_index".into(),
578 serde_json::json!(start_block_index),
579 );
580 value.insert("end_block_index".into(), serde_json::json!(end_block_index));
581 }
582 Citation::SearchResultLocation {
583 cited_text,
584 source,
585 title,
586 search_result_index,
587 start_block_index,
588 end_block_index,
589 } => {
590 value.insert("type".into(), serde_json::json!("search_result_location"));
591 value.insert("cited_text".into(), serde_json::json!(cited_text));
592 value.insert("source".into(), serde_json::json!(source));
593 if let Some(title) = title {
594 value.insert("title".into(), serde_json::json!(title));
595 }
596 value.insert(
597 "search_result_index".into(),
598 serde_json::json!(search_result_index),
599 );
600 value.insert(
601 "start_block_index".into(),
602 serde_json::json!(start_block_index),
603 );
604 value.insert("end_block_index".into(), serde_json::json!(end_block_index));
605 }
606 Citation::WebSearchResultLocation {
607 cited_text,
608 url,
609 title,
610 encrypted_index,
611 } => {
612 value.insert(
613 "type".into(),
614 serde_json::json!("web_search_result_location"),
615 );
616 value.insert("cited_text".into(), serde_json::json!(cited_text));
617 value.insert("url".into(), serde_json::json!(url));
618 value.insert("title".into(), serde_json::json!(title));
619 value.insert("encrypted_index".into(), serde_json::json!(encrypted_index));
620 }
621 Citation::Unknown(raw) => return raw.serialize(serializer),
622 }
623
624 serde_json::Value::Object(value).serialize(serializer)
625 }
626}
627
628impl<'de> Deserialize<'de> for Citation {
629 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
630 where
631 D: serde::Deserializer<'de>,
632 {
633 let value = serde_json::Value::deserialize(deserializer)?;
634 let Some(citation_type) = value.get("type").and_then(serde_json::Value::as_str) else {
635 return Ok(Citation::Unknown(value));
636 };
637
638 match citation_type {
639 "char_location" => {
640 let fields: CharLocationCitationFields =
641 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
642 Ok(Citation::CharLocation {
643 cited_text: fields.cited_text,
644 document_index: fields.document_index,
645 document_title: fields.document_title,
646 start_char_index: fields.start_char_index,
647 end_char_index: fields.end_char_index,
648 })
649 }
650 "page_location" => {
651 let fields: PageLocationCitationFields =
652 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
653 Ok(Citation::PageLocation {
654 cited_text: fields.cited_text,
655 document_index: fields.document_index,
656 document_title: fields.document_title,
657 start_page_number: fields.start_page_number,
658 end_page_number: fields.end_page_number,
659 })
660 }
661 "content_block_location" => {
662 let fields: ContentBlockLocationCitationFields =
663 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
664 Ok(Citation::ContentBlockLocation {
665 cited_text: fields.cited_text,
666 document_index: fields.document_index,
667 document_title: fields.document_title,
668 start_block_index: fields.start_block_index,
669 end_block_index: fields.end_block_index,
670 })
671 }
672 "search_result_location" => {
673 let fields: SearchResultLocationCitationFields =
674 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
675 Ok(Citation::SearchResultLocation {
676 cited_text: fields.cited_text,
677 source: fields.source,
678 title: fields.title,
679 search_result_index: fields.search_result_index,
680 start_block_index: fields.start_block_index,
681 end_block_index: fields.end_block_index,
682 })
683 }
684 "web_search_result_location" => {
685 let fields: WebSearchResultLocationCitationFields =
686 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
687 Ok(Citation::WebSearchResultLocation {
688 cited_text: fields.cited_text,
689 url: fields.url,
690 title: fields.title,
691 encrypted_index: fields.encrypted_index,
692 })
693 }
694 _ => Ok(Citation::Unknown(value)),
695 }
696 }
697}
698
699type AnthropicDocParams = (Option<String>, Option<String>, Option<CitationsConfig>);
702
703fn extract_anthropic_doc_params(
711 additional_params: Option<serde_json::Value>,
712) -> Result<AnthropicDocParams, MessageError> {
713 let Some(value) = additional_params else {
714 return Ok((None, None, None));
715 };
716 let title = value
717 .get("title")
718 .and_then(|v| v.as_str())
719 .map(String::from);
720 let context = value
721 .get("context")
722 .and_then(|v| v.as_str())
723 .map(String::from);
724 let citations = value
725 .get("citations")
726 .cloned()
727 .map(serde_json::from_value::<CitationsConfig>)
728 .transpose()
729 .map_err(|e| {
730 MessageError::ConversionError(format!(
731 "Document `additional_params.citations` is not a valid CitationsConfig: {e}",
732 ))
733 })?;
734 Ok((title, context, citations))
735}
736
737pub fn anthropic_citations(text: &message::Text) -> Result<Vec<Citation>, serde_json::Error> {
765 match text
766 .additional_params
767 .as_ref()
768 .and_then(|v| v.get("citations"))
769 {
770 Some(c) => serde_json::from_value::<Vec<Citation>>(c.clone()),
771 None => Ok(Vec::new()),
772 }
773}
774
775fn extract_anthropic_text_citations(text: &message::Text) -> Result<Vec<Citation>, MessageError> {
776 anthropic_citations(text).map_err(|err| {
777 MessageError::ConversionError(format!(
778 "Text `additional_params.citations` is not valid Anthropic citations: {err}"
779 ))
780 })
781}
782
783fn anthropic_text_content_from_message_text(text: message::Text) -> Result<Content, MessageError> {
784 if let Some(raw_content) = extract_anthropic_raw_content(&text)? {
785 if !text.text.is_empty() {
786 return Err(MessageError::ConversionError(format!(
787 "Text `{ANTHROPIC_RAW_CONTENT_KEY}` metadata cannot be combined with non-empty text"
788 )));
789 }
790
791 return Ok(raw_content);
792 }
793
794 let citations = extract_anthropic_text_citations(&text)?;
795 Ok(Content::Text {
796 text: text.text,
797 citations,
798 cache_control: None,
799 })
800}
801
802fn extract_anthropic_raw_content(text: &message::Text) -> Result<Option<Content>, MessageError> {
803 let Some(raw_content) = text
804 .additional_params
805 .as_ref()
806 .and_then(|value| value.get(ANTHROPIC_RAW_CONTENT_KEY))
807 else {
808 return Ok(None);
809 };
810
811 let content = serde_json::from_value::<Content>(raw_content.clone()).map_err(|err| {
812 MessageError::ConversionError(format!(
813 "Text `{ANTHROPIC_RAW_CONTENT_KEY}` metadata is not valid Anthropic content: {err}"
814 ))
815 })?;
816
817 match content {
818 Content::ServerToolUse { .. } | Content::WebSearchToolResult { .. } => Ok(Some(content)),
819 _ => Err(MessageError::ConversionError(format!(
820 "Text `{ANTHROPIC_RAW_CONTENT_KEY}` metadata only supports Anthropic server_tool_use and web_search_tool_result blocks"
821 ))),
822 }
823}
824
825fn anthropic_raw_content_to_message_text(content: Content) -> Result<message::Text, MessageError> {
826 let raw_content = serde_json::to_value(content).map_err(|err| {
827 MessageError::ConversionError(format!("Failed to preserve Anthropic content block: {err}"))
828 })?;
829
830 Ok(message::Text {
831 text: String::new(),
832 additional_params: Some(serde_json::json!({
833 ANTHROPIC_RAW_CONTENT_KEY: raw_content
834 })),
835 })
836}
837
838fn anthropic_document_additional_params(
839 title: Option<String>,
840 context: Option<String>,
841 citations: Option<CitationsConfig>,
842) -> Result<Option<serde_json::Value>, MessageError> {
843 let mut params = serde_json::Map::new();
844
845 if let Some(title) = title {
846 params.insert("title".to_string(), serde_json::Value::String(title));
847 }
848 if let Some(context) = context {
849 params.insert("context".to_string(), serde_json::Value::String(context));
850 }
851 if let Some(citations) = citations {
852 params.insert(
853 "citations".to_string(),
854 serde_json::to_value(citations).map_err(|err| {
855 MessageError::ConversionError(format!(
856 "Failed to preserve Anthropic document citations metadata: {err}"
857 ))
858 })?,
859 );
860 }
861
862 Ok((!params.is_empty()).then_some(serde_json::Value::Object(params)))
863}
864
865#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
866#[serde(tag = "type", rename_all = "snake_case")]
867pub enum ToolResultContent {
868 Text { text: String },
869 Image { source: ImageSource },
870}
871
872impl FromStr for ToolResultContent {
873 type Err = Infallible;
874
875 fn from_str(s: &str) -> Result<Self, Self::Err> {
876 Ok(ToolResultContent::Text { text: s.to_owned() })
877 }
878}
879
880#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
888#[serde(tag = "type", rename_all = "snake_case")]
889pub enum ImageSource {
890 #[serde(rename = "base64")]
891 Base64 {
892 data: String,
893 media_type: ImageFormat,
894 },
895 #[serde(rename = "url")]
896 Url { url: String },
897}
898
899#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
907#[serde(tag = "type", rename_all = "snake_case")]
908pub enum DocumentSource {
909 Base64 {
910 data: String,
911 media_type: DocumentFormat,
912 },
913 Text {
914 data: String,
915 media_type: PlainTextMediaType,
916 },
917 Url {
918 url: String,
919 },
920 File {
921 file_id: String,
922 },
923}
924
925#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
926#[serde(rename_all = "lowercase")]
927pub enum ImageFormat {
928 #[serde(rename = "image/jpeg")]
929 JPEG,
930 #[serde(rename = "image/png")]
931 PNG,
932 #[serde(rename = "image/gif")]
933 GIF,
934 #[serde(rename = "image/webp")]
935 WEBP,
936}
937
938#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
945#[serde(rename_all = "lowercase")]
946pub enum DocumentFormat {
947 #[serde(rename = "application/pdf")]
948 PDF,
949}
950
951#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
957pub enum PlainTextMediaType {
958 #[serde(rename = "text/plain")]
959 Plain,
960}
961
962#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
963#[serde(rename_all = "lowercase")]
964pub enum SourceType {
965 BASE64,
966 URL,
967 TEXT,
968}
969
970impl From<String> for Content {
971 fn from(text: String) -> Self {
972 Content::Text {
973 text,
974 citations: Vec::new(),
975 cache_control: None,
976 }
977 }
978}
979
980impl From<String> for ToolResultContent {
981 fn from(text: String) -> Self {
982 ToolResultContent::Text { text }
983 }
984}
985
986impl TryFrom<message::ContentFormat> for SourceType {
987 type Error = MessageError;
988
989 fn try_from(format: message::ContentFormat) -> Result<Self, Self::Error> {
990 match format {
991 message::ContentFormat::Base64 => Ok(SourceType::BASE64),
992 message::ContentFormat::Url => Ok(SourceType::URL),
993 message::ContentFormat::String => Ok(SourceType::TEXT),
994 }
995 }
996}
997
998impl From<SourceType> for message::ContentFormat {
999 fn from(source_type: SourceType) -> Self {
1000 match source_type {
1001 SourceType::BASE64 => message::ContentFormat::Base64,
1002 SourceType::URL => message::ContentFormat::Url,
1003 SourceType::TEXT => message::ContentFormat::String,
1004 }
1005 }
1006}
1007
1008impl TryFrom<message::ImageMediaType> for ImageFormat {
1009 type Error = MessageError;
1010
1011 fn try_from(media_type: message::ImageMediaType) -> Result<Self, Self::Error> {
1012 Ok(match media_type {
1013 message::ImageMediaType::JPEG => ImageFormat::JPEG,
1014 message::ImageMediaType::PNG => ImageFormat::PNG,
1015 message::ImageMediaType::GIF => ImageFormat::GIF,
1016 message::ImageMediaType::WEBP => ImageFormat::WEBP,
1017 _ => {
1018 return Err(MessageError::ConversionError(
1019 format!("Unsupported image media type: {media_type:?}").to_owned(),
1020 ));
1021 }
1022 })
1023 }
1024}
1025
1026impl From<ImageFormat> for message::ImageMediaType {
1027 fn from(format: ImageFormat) -> Self {
1028 match format {
1029 ImageFormat::JPEG => message::ImageMediaType::JPEG,
1030 ImageFormat::PNG => message::ImageMediaType::PNG,
1031 ImageFormat::GIF => message::ImageMediaType::GIF,
1032 ImageFormat::WEBP => message::ImageMediaType::WEBP,
1033 }
1034 }
1035}
1036
1037impl TryFrom<DocumentMediaType> for DocumentFormat {
1038 type Error = MessageError;
1039 fn try_from(value: DocumentMediaType) -> Result<Self, Self::Error> {
1040 match value {
1041 DocumentMediaType::PDF => Ok(DocumentFormat::PDF),
1042 other => Err(MessageError::ConversionError(format!(
1043 "DocumentFormat only supports PDF for base64 sources, got: {}",
1044 other.to_mime_type()
1045 ))),
1046 }
1047 }
1048}
1049
1050impl TryFrom<message::AssistantContent> for Content {
1051 type Error = MessageError;
1052 fn try_from(text: message::AssistantContent) -> Result<Self, Self::Error> {
1053 match text {
1054 message::AssistantContent::Text(text) => anthropic_text_content_from_message_text(text),
1055 message::AssistantContent::Image(_) => Err(MessageError::ConversionError(
1056 "Anthropic currently doesn't support images.".to_string(),
1057 )),
1058 message::AssistantContent::ToolCall(message::ToolCall { id, function, .. }) => {
1059 Ok(Content::ToolUse {
1060 id,
1061 name: function.name,
1062 input: function.arguments,
1063 })
1064 }
1065 message::AssistantContent::Reasoning(reasoning) => Ok(Content::Thinking {
1066 thinking: reasoning.display_text(),
1067 signature: reasoning.first_signature().map(str::to_owned),
1068 }),
1069 }
1070 }
1071}
1072
1073fn anthropic_content_from_assistant_content(
1074 content: message::AssistantContent,
1075) -> Result<Vec<Content>, MessageError> {
1076 match content {
1077 message::AssistantContent::Text(text) => {
1078 Ok(vec![anthropic_text_content_from_message_text(text)?])
1079 }
1080 message::AssistantContent::Image(_) => Err(MessageError::ConversionError(
1081 "Anthropic currently doesn't support images.".to_string(),
1082 )),
1083 message::AssistantContent::ToolCall(message::ToolCall { id, function, .. }) => {
1084 Ok(vec![Content::ToolUse {
1085 id,
1086 name: function.name,
1087 input: function.arguments,
1088 }])
1089 }
1090 message::AssistantContent::Reasoning(reasoning) => {
1091 let mut converted = Vec::new();
1092 for block in reasoning.content {
1093 match block {
1094 message::ReasoningContent::Text { text, signature } => {
1095 converted.push(Content::Thinking {
1096 thinking: text,
1097 signature,
1098 });
1099 }
1100 message::ReasoningContent::Summary(summary) => {
1101 converted.push(Content::Thinking {
1102 thinking: summary,
1103 signature: None,
1104 });
1105 }
1106 message::ReasoningContent::Redacted { data }
1107 | message::ReasoningContent::Encrypted(data) => {
1108 converted.push(Content::RedactedThinking { data });
1109 }
1110 }
1111 }
1112
1113 if converted.is_empty() {
1114 return Err(MessageError::ConversionError(
1115 "Cannot convert empty reasoning content to Anthropic format".to_string(),
1116 ));
1117 }
1118
1119 Ok(converted)
1120 }
1121 }
1122}
1123
1124impl TryFrom<message::Message> for Message {
1125 type Error = MessageError;
1126
1127 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1128 Ok(match message {
1129 message::Message::User { content } => Message {
1130 role: Role::User,
1131 content: content.try_map(|content| match content {
1132 message::UserContent::Text(message::Text { text, .. }) => Ok(Content::Text {
1133 text,
1134 citations: Vec::new(),
1135 cache_control: None,
1136 }),
1137 message::UserContent::ToolResult(message::ToolResult {
1138 id, content, ..
1139 }) => Ok(Content::ToolResult {
1140 tool_use_id: id,
1141 content: content.try_map(|content| match content {
1142 message::ToolResultContent::Text(message::Text { text, .. }) => {
1143 Ok(ToolResultContent::Text { text })
1144 }
1145 message::ToolResultContent::Image(image) => {
1146 let DocumentSourceKind::Base64(data) = image.data else {
1147 return Err(MessageError::ConversionError(
1148 "Only base64 strings can be used with the Anthropic API"
1149 .to_string(),
1150 ));
1151 };
1152 let media_type =
1153 image.media_type.ok_or(MessageError::ConversionError(
1154 "Image media type is required".to_owned(),
1155 ))?;
1156 Ok(ToolResultContent::Image {
1157 source: ImageSource::Base64 {
1158 data,
1159 media_type: media_type.try_into()?,
1160 },
1161 })
1162 }
1163 })?,
1164 is_error: None,
1165 cache_control: None,
1166 }),
1167 message::UserContent::Image(message::Image {
1168 data, media_type, ..
1169 }) => {
1170 let source = match data {
1171 DocumentSourceKind::Base64(data) => {
1172 let media_type =
1173 media_type.ok_or(MessageError::ConversionError(
1174 "Image media type is required for Claude API".to_string(),
1175 ))?;
1176 ImageSource::Base64 {
1177 data,
1178 media_type: ImageFormat::try_from(media_type)?,
1179 }
1180 }
1181 DocumentSourceKind::Url(url) => ImageSource::Url { url },
1182 DocumentSourceKind::Unknown => {
1183 return Err(MessageError::ConversionError(
1184 "Image content has no body".into(),
1185 ));
1186 }
1187 doc => {
1188 return Err(MessageError::ConversionError(format!(
1189 "Unsupported document type: {doc:?}"
1190 )));
1191 }
1192 };
1193
1194 Ok(Content::Image {
1195 source,
1196 cache_control: None,
1197 })
1198 }
1199 message::UserContent::Document(message::Document {
1200 data,
1201 media_type,
1202 additional_params,
1203 }) => {
1204 let (title, context, citations) =
1205 extract_anthropic_doc_params(additional_params)?;
1206
1207 if let DocumentSourceKind::FileId(file_id) = data {
1208 return Ok(Content::Document {
1209 source: DocumentSource::File { file_id },
1210 title,
1211 context,
1212 citations,
1213 cache_control: None,
1214 });
1215 }
1216
1217 let media_type = media_type.ok_or(MessageError::ConversionError(
1218 "Document media type is required".to_string(),
1219 ))?;
1220
1221 let source = match media_type {
1222 DocumentMediaType::PDF => {
1223 let data = match data {
1224 DocumentSourceKind::Base64(data)
1225 | DocumentSourceKind::String(data) => data,
1226 _ => {
1227 return Err(MessageError::ConversionError(
1228 "Only base64 encoded data is supported for PDF documents".into(),
1229 ));
1230 }
1231 };
1232 DocumentSource::Base64 {
1233 data,
1234 media_type: DocumentFormat::PDF,
1235 }
1236 }
1237 DocumentMediaType::TXT => {
1238 let data = match data {
1239 DocumentSourceKind::String(data)
1240 | DocumentSourceKind::Base64(data) => data,
1241 _ => {
1242 return Err(MessageError::ConversionError(
1243 "Only string or base64 data is supported for plain text documents".into(),
1244 ));
1245 }
1246 };
1247 DocumentSource::Text {
1248 data,
1249 media_type: PlainTextMediaType::Plain,
1250 }
1251 }
1252 other => {
1253 return Err(MessageError::ConversionError(format!(
1254 "Anthropic only supports PDF and plain text documents, got: {}",
1255 other.to_mime_type()
1256 )));
1257 }
1258 };
1259
1260 Ok(Content::Document {
1261 source,
1262 title,
1263 context,
1264 citations,
1265 cache_control: None,
1266 })
1267 }
1268 message::UserContent::Audio { .. } => Err(MessageError::ConversionError(
1269 "Audio is not supported in Anthropic".to_owned(),
1270 )),
1271 message::UserContent::Video { .. } => Err(MessageError::ConversionError(
1272 "Video is not supported in Anthropic".to_owned(),
1273 )),
1274 })?,
1275 },
1276
1277 message::Message::System { content } => Message {
1278 role: Role::User,
1279 content: OneOrMany::one(Content::Text {
1280 text: content,
1281 citations: Vec::new(),
1282 cache_control: None,
1283 }),
1284 },
1285
1286 message::Message::Assistant { content, .. } => {
1287 let converted_content = content.into_iter().try_fold(
1288 Vec::new(),
1289 |mut accumulated, assistant_content| {
1290 accumulated
1291 .extend(anthropic_content_from_assistant_content(assistant_content)?);
1292 Ok::<Vec<Content>, MessageError>(accumulated)
1293 },
1294 )?;
1295
1296 Message {
1297 content: OneOrMany::many(converted_content).map_err(|_| {
1298 MessageError::ConversionError(
1299 "Assistant message did not contain Anthropic-compatible content"
1300 .to_owned(),
1301 )
1302 })?,
1303 role: Role::Assistant,
1304 }
1305 }
1306 })
1307 }
1308}
1309
1310impl TryFrom<Content> for message::AssistantContent {
1311 type Error = MessageError;
1312
1313 fn try_from(content: Content) -> Result<Self, Self::Error> {
1314 Ok(match content {
1315 Content::Text {
1316 text, citations, ..
1317 } => {
1318 let additional_params =
1323 (!citations.is_empty()).then(|| serde_json::json!({ "citations": citations }));
1324 message::AssistantContent::Text(message::Text {
1325 text,
1326 additional_params,
1327 })
1328 }
1329 Content::ToolUse { id, name, input } => {
1330 message::AssistantContent::tool_call(id, name, input)
1331 }
1332 raw @ (Content::ServerToolUse { .. } | Content::WebSearchToolResult { .. }) => {
1333 message::AssistantContent::Text(anthropic_raw_content_to_message_text(raw)?)
1334 }
1335 Content::Thinking {
1336 thinking,
1337 signature,
1338 } => message::AssistantContent::Reasoning(Reasoning::new_with_signature(
1339 &thinking, signature,
1340 )),
1341 Content::RedactedThinking { data } => {
1342 message::AssistantContent::Reasoning(Reasoning::redacted(data))
1343 }
1344 _ => {
1345 return Err(MessageError::ConversionError(
1346 "Content did not contain a message, tool call, or reasoning".to_owned(),
1347 ));
1348 }
1349 })
1350 }
1351}
1352
1353impl From<ToolResultContent> for message::ToolResultContent {
1354 fn from(content: ToolResultContent) -> Self {
1355 match content {
1356 ToolResultContent::Text { text, .. } => message::ToolResultContent::text(text),
1357 ToolResultContent::Image { source } => match source {
1358 ImageSource::Base64 { data, media_type } => {
1359 message::ToolResultContent::image_base64(data, Some(media_type.into()), None)
1360 }
1361 ImageSource::Url { url } => message::ToolResultContent::image_url(url, None, None),
1362 },
1363 }
1364 }
1365}
1366
1367impl TryFrom<Message> for message::Message {
1368 type Error = MessageError;
1369
1370 fn try_from(message: Message) -> Result<Self, Self::Error> {
1371 Ok(match message.role {
1372 Role::User => message::Message::User {
1373 content: message.content.try_map(|content| {
1374 Ok(match content {
1375 Content::Text { text, .. } => message::UserContent::text(text),
1376 Content::ToolResult {
1377 tool_use_id,
1378 content,
1379 ..
1380 } => message::UserContent::tool_result(
1381 tool_use_id,
1382 content.map(|content| content.into()),
1383 ),
1384 Content::Image { source, .. } => match source {
1385 ImageSource::Base64 { data, media_type } => {
1386 message::UserContent::Image(message::Image {
1387 data: DocumentSourceKind::Base64(data),
1388 media_type: Some(media_type.into()),
1389 detail: None,
1390 additional_params: None,
1391 })
1392 }
1393 ImageSource::Url { url } => {
1394 message::UserContent::Image(message::Image {
1395 data: DocumentSourceKind::Url(url),
1396 media_type: None,
1397 detail: None,
1398 additional_params: None,
1399 })
1400 }
1401 },
1402 Content::Document {
1403 source,
1404 title,
1405 context,
1406 citations,
1407 ..
1408 } => {
1409 let additional_params =
1410 anthropic_document_additional_params(title, context, citations)?;
1411
1412 match source {
1413 DocumentSource::Base64 { data, media_type } => {
1414 let rig_media_type = match media_type {
1415 DocumentFormat::PDF => message::DocumentMediaType::PDF,
1416 };
1417 message::UserContent::Document(message::Document {
1418 data: DocumentSourceKind::String(data),
1419 media_type: Some(rig_media_type),
1420 additional_params,
1421 })
1422 }
1423 DocumentSource::Text { data, .. } => {
1424 message::UserContent::Document(message::Document {
1425 data: DocumentSourceKind::String(data),
1426 media_type: Some(message::DocumentMediaType::TXT),
1427 additional_params,
1428 })
1429 }
1430 DocumentSource::Url { url } => {
1431 message::UserContent::Document(message::Document {
1432 data: DocumentSourceKind::Url(url),
1433 media_type: None,
1434 additional_params,
1435 })
1436 }
1437 DocumentSource::File { file_id } => {
1438 message::UserContent::Document(message::Document {
1439 data: DocumentSourceKind::FileId(file_id),
1440 media_type: None,
1441 additional_params,
1442 })
1443 }
1444 }
1445 }
1446 _ => {
1447 return Err(MessageError::ConversionError(
1448 "Unsupported content type for User role".to_owned(),
1449 ));
1450 }
1451 })
1452 })?,
1453 },
1454 Role::Assistant => message::Message::Assistant {
1455 id: None,
1456 content: message.content.try_map(|content| content.try_into())?,
1457 },
1458 })
1459 }
1460}
1461
1462#[doc(hidden)]
1463#[derive(Clone)]
1464pub struct GenericCompletionModel<Ext = super::client::AnthropicExt, T = reqwest::Client> {
1465 pub(crate) client: crate::client::Client<Ext, T>,
1466 pub model: String,
1467 pub default_max_tokens: Option<u64>,
1468 pub prompt_caching: bool,
1471 pub automatic_caching: bool,
1475 pub automatic_caching_ttl: Option<CacheTtl>,
1478}
1479
1480pub type CompletionModel<T = reqwest::Client> =
1485 GenericCompletionModel<super::client::AnthropicExt, T>;
1486
1487impl<Ext, T> GenericCompletionModel<Ext, T>
1488where
1489 T: HttpClientExt,
1490 Ext: AnthropicCompatibleProvider + Clone + 'static,
1491{
1492 pub fn new(client: crate::client::Client<Ext, T>, model: impl Into<String>) -> Self {
1493 let model = model.into();
1494 let default_max_tokens = Ext::default_max_tokens(&model);
1495
1496 Self {
1497 client,
1498 model,
1499 default_max_tokens,
1500 prompt_caching: false,
1501 automatic_caching: false,
1502 automatic_caching_ttl: None,
1503 }
1504 }
1505
1506 pub fn with_model(client: crate::client::Client<Ext, T>, model: &str) -> Self {
1507 Self {
1508 client,
1509 model: model.to_string(),
1510 default_max_tokens: Ext::default_max_tokens(model)
1511 .or_else(|| Some(default_max_tokens_with_fallback(model))),
1512 prompt_caching: false,
1513 automatic_caching: false,
1514 automatic_caching_ttl: None,
1515 }
1516 }
1517
1518 pub fn with_prompt_caching(mut self) -> Self {
1536 self.prompt_caching = true;
1537 self
1538 }
1539
1540 pub fn with_automatic_caching(mut self) -> Self {
1575 self.automatic_caching = true;
1576 self
1577 }
1578
1579 pub fn with_automatic_caching_1h(mut self) -> Self {
1591 self.automatic_caching = true;
1592 self.automatic_caching_ttl = Some(CacheTtl::OneHour);
1593 self
1594 }
1595}
1596
1597fn default_max_tokens_for_model(model: &str) -> Option<u64> {
1601 if model.starts_with("claude-opus-4-7") || model.starts_with("claude-opus-4-6") {
1602 Some(128_000)
1603 } else if model.starts_with("claude-opus-4")
1604 || model.starts_with("claude-sonnet-4")
1605 || model.starts_with("claude-haiku-4-5")
1606 {
1607 Some(64_000)
1608 } else {
1609 None
1610 }
1611}
1612
1613fn default_max_tokens_with_fallback(model: &str) -> u64 {
1614 default_max_tokens_for_model(model).unwrap_or(2_048)
1615}
1616
1617#[derive(Debug, Deserialize, Serialize)]
1618pub struct Metadata {
1619 user_id: Option<String>,
1620}
1621
1622#[derive(Default, Debug, Serialize, Deserialize)]
1623#[serde(tag = "type", rename_all = "snake_case")]
1624pub enum ToolChoice {
1625 #[default]
1626 Auto,
1627 Any,
1628 None,
1629 Tool {
1630 name: String,
1631 },
1632}
1633impl TryFrom<message::ToolChoice> for ToolChoice {
1634 type Error = CompletionError;
1635
1636 fn try_from(value: message::ToolChoice) -> Result<Self, Self::Error> {
1637 let res = match value {
1638 message::ToolChoice::Auto => Self::Auto,
1639 message::ToolChoice::None => Self::None,
1640 message::ToolChoice::Required => Self::Any,
1641 message::ToolChoice::Specific { function_names } => {
1642 if function_names.len() != 1 {
1643 return Err(CompletionError::ProviderError(
1644 "Only one tool may be specified to be used by Claude".into(),
1645 ));
1646 }
1647
1648 let Some(name) = function_names.into_iter().next() else {
1649 return Err(CompletionError::ProviderError(
1650 "Only one tool may be specified to be used by Claude".into(),
1651 ));
1652 };
1653
1654 Self::Tool { name }
1655 }
1656 };
1657
1658 Ok(res)
1659 }
1660}
1661
1662fn sanitize_schema(schema: &mut serde_json::Value) {
1668 use serde_json::Value;
1669
1670 if let Value::Object(obj) = schema {
1671 let is_object_schema = obj.get("type") == Some(&Value::String("object".to_string()))
1672 || obj.contains_key("properties");
1673
1674 if is_object_schema && !obj.contains_key("additionalProperties") {
1675 obj.insert("additionalProperties".to_string(), Value::Bool(false));
1676 }
1677
1678 if let Some(Value::Object(properties)) = obj.get("properties") {
1679 let prop_keys = properties.keys().cloned().map(Value::String).collect();
1680 obj.insert("required".to_string(), Value::Array(prop_keys));
1681 }
1682
1683 let is_numeric_schema = obj.get("type") == Some(&Value::String("integer".to_string()))
1685 || obj.get("type") == Some(&Value::String("number".to_string()));
1686
1687 if is_numeric_schema {
1688 for key in [
1689 "minimum",
1690 "maximum",
1691 "exclusiveMinimum",
1692 "exclusiveMaximum",
1693 "multipleOf",
1694 ] {
1695 obj.remove(key);
1696 }
1697 }
1698
1699 if let Some(defs) = obj.get_mut("$defs")
1700 && let Value::Object(defs_obj) = defs
1701 {
1702 for (_, def_schema) in defs_obj.iter_mut() {
1703 sanitize_schema(def_schema);
1704 }
1705 }
1706
1707 if let Some(properties) = obj.get_mut("properties")
1708 && let Value::Object(props) = properties
1709 {
1710 for (_, prop_value) in props.iter_mut() {
1711 sanitize_schema(prop_value);
1712 }
1713 }
1714
1715 if let Some(items) = obj.get_mut("items") {
1716 sanitize_schema(items);
1717 }
1718
1719 if let Some(one_of) = obj.remove("oneOf") {
1721 match obj.get_mut("anyOf") {
1722 Some(Value::Array(existing)) => {
1723 if let Value::Array(mut incoming) = one_of {
1724 existing.append(&mut incoming);
1725 }
1726 }
1727 _ => {
1728 obj.insert("anyOf".to_string(), one_of);
1729 }
1730 }
1731 }
1732
1733 for key in ["anyOf", "allOf"] {
1734 if let Some(variants) = obj.get_mut(key)
1735 && let Value::Array(variants_array) = variants
1736 {
1737 for variant in variants_array.iter_mut() {
1738 sanitize_schema(variant);
1739 }
1740 }
1741 }
1742 }
1743}
1744
1745#[derive(Debug, Deserialize, Serialize)]
1748#[serde(tag = "type", rename_all = "snake_case")]
1749enum OutputFormat {
1750 JsonSchema { schema: serde_json::Value },
1752}
1753
1754#[derive(Debug, Deserialize, Serialize)]
1756struct OutputConfig {
1757 format: OutputFormat,
1758}
1759
1760#[derive(Debug, Deserialize, Serialize)]
1761struct AnthropicCompletionRequest {
1762 model: String,
1763 messages: Vec<Message>,
1764 max_tokens: u64,
1765 #[serde(skip_serializing_if = "Vec::is_empty")]
1767 system: Vec<SystemContent>,
1768 #[serde(skip_serializing_if = "Option::is_none")]
1769 temperature: Option<f64>,
1770 #[serde(skip_serializing_if = "Option::is_none")]
1771 tool_choice: Option<ToolChoice>,
1772 #[serde(skip_serializing_if = "Vec::is_empty")]
1773 tools: Vec<serde_json::Value>,
1774 #[serde(skip_serializing_if = "Option::is_none")]
1775 output_config: Option<OutputConfig>,
1776 #[serde(flatten, skip_serializing_if = "Option::is_none")]
1777 additional_params: Option<serde_json::Value>,
1778 #[serde(skip_serializing_if = "Option::is_none")]
1782 cache_control: Option<CacheControl>,
1783}
1784
1785fn set_content_cache_control(content: &mut Content, value: Option<CacheControl>) {
1787 match content {
1788 Content::Text { cache_control, .. } => *cache_control = value,
1789 Content::Image { cache_control, .. } => *cache_control = value,
1790 Content::ToolResult { cache_control, .. } => *cache_control = value,
1791 Content::Document { cache_control, .. } => *cache_control = value,
1792 _ => {}
1793 }
1794}
1795
1796const MAX_CACHE_CONTROL_MARKERS: usize = 4;
1797
1798pub fn apply_cache_control(system: &mut [SystemContent], messages: &mut [Message]) {
1803 if let Some(SystemContent::Text { cache_control, .. }) = system.last_mut() {
1805 *cache_control = Some(CacheControl::ephemeral());
1806 }
1807
1808 for msg in messages.iter_mut() {
1810 for content in msg.content.iter_mut() {
1811 set_content_cache_control(content, None);
1812 }
1813 }
1814
1815 if let Some(last_msg) = messages.last_mut() {
1817 set_content_cache_control(last_msg.content.last_mut(), Some(CacheControl::ephemeral()));
1818 }
1819}
1820
1821fn final_cacheable_tool_idx(tools: &[serde_json::Value]) -> Option<usize> {
1822 tools.iter().rposition(|tool| {
1823 tool.as_object().is_some_and(|tool| {
1824 !matches!(
1825 tool.get("defer_loading"),
1826 Some(serde_json::Value::Bool(true))
1827 )
1828 })
1829 })
1830}
1831
1832fn tool_cache_control_count(tools: &[serde_json::Value]) -> usize {
1833 tools
1834 .iter()
1835 .filter(|tool| tool_cache_control_value(tool).is_some())
1836 .count()
1837}
1838
1839fn tool_cache_control_value(tool: &serde_json::Value) -> Option<&serde_json::Value> {
1840 tool.get("cache_control")
1841 .filter(|cache_control| !cache_control.is_null())
1842}
1843
1844fn normalize_tool_cache_control(tools: &mut [serde_json::Value]) {
1845 for tool in tools.iter_mut() {
1846 if let Some(tool) = tool.as_object_mut()
1847 && tool
1848 .get("cache_control")
1849 .is_some_and(serde_json::Value::is_null)
1850 {
1851 tool.remove("cache_control");
1852 }
1853 }
1854}
1855
1856fn build_cache_control(ttl: Option<CacheTtl>) -> CacheControl {
1857 CacheControl::Ephemeral { ttl }
1858}
1859
1860#[derive(Clone, Copy, PartialEq, Eq)]
1861enum CacheControlTtl {
1862 FiveMinutes,
1863 OneHour,
1864}
1865
1866fn cache_control_ttl(cache_control: &CacheControl) -> CacheControlTtl {
1867 match cache_control {
1868 CacheControl::Ephemeral {
1869 ttl: Some(CacheTtl::OneHour),
1870 } => CacheControlTtl::OneHour,
1871 CacheControl::Ephemeral { .. } => CacheControlTtl::FiveMinutes,
1872 }
1873}
1874
1875fn cache_control_ttl_from_json(cache_control: &serde_json::Value) -> CacheControlTtl {
1876 match cache_control.get("ttl") {
1877 Some(serde_json::Value::String(ttl)) if ttl == "1h" => CacheControlTtl::OneHour,
1878 _ => CacheControlTtl::FiveMinutes,
1879 }
1880}
1881
1882fn content_cache_control(content: &Content) -> Option<&CacheControl> {
1883 match content {
1884 Content::Text { cache_control, .. }
1885 | Content::Image { cache_control, .. }
1886 | Content::ToolResult { cache_control, .. }
1887 | Content::Document { cache_control, .. } => cache_control.as_ref(),
1888 _ => None,
1889 }
1890}
1891
1892fn validate_cache_control_ttl(
1893 ttl: CacheControlTtl,
1894 shorter_ttl_seen: &mut bool,
1895) -> Result<(), CompletionError> {
1896 match ttl {
1897 CacheControlTtl::OneHour if *shorter_ttl_seen => Err(CompletionError::RequestError(
1898 "Anthropic cache_control markers with ttl `1h` must appear before markers with \
1899 the default 5-minute TTL"
1900 .into(),
1901 )),
1902 CacheControlTtl::OneHour => Ok(()),
1903 CacheControlTtl::FiveMinutes => {
1904 *shorter_ttl_seen = true;
1905 Ok(())
1906 }
1907 }
1908}
1909
1910fn validate_cache_control_ttl_order(
1911 system: &[SystemContent],
1912 messages: &[Message],
1913 tools: &[serde_json::Value],
1914 top_level_cache_control: Option<&CacheControl>,
1915) -> Result<(), CompletionError> {
1916 let mut shorter_ttl_seen = false;
1917
1918 for tool in tools {
1919 if let Some(cache_control) = tool_cache_control_value(tool) {
1920 validate_cache_control_ttl(
1921 cache_control_ttl_from_json(cache_control),
1922 &mut shorter_ttl_seen,
1923 )?;
1924 }
1925 }
1926
1927 for SystemContent::Text { cache_control, .. } in system {
1928 if let Some(cache_control) = cache_control {
1929 validate_cache_control_ttl(cache_control_ttl(cache_control), &mut shorter_ttl_seen)?;
1930 }
1931 }
1932
1933 for message in messages {
1934 for content in message.content.iter() {
1935 if let Some(cache_control) = content_cache_control(content) {
1936 validate_cache_control_ttl(
1937 cache_control_ttl(cache_control),
1938 &mut shorter_ttl_seen,
1939 )?;
1940 }
1941 }
1942 }
1943
1944 if let Some(cache_control) = top_level_cache_control {
1945 validate_cache_control_ttl(cache_control_ttl(cache_control), &mut shorter_ttl_seen)?;
1946 }
1947
1948 Ok(())
1949}
1950
1951fn top_level_cache_control_ttl(cache_control: Option<&CacheControl>) -> Option<CacheTtl> {
1952 cache_control
1953 .map(|cache_control| match cache_control {
1954 CacheControl::Ephemeral { ttl } => ttl.clone(),
1955 })
1956 .unwrap_or_default()
1957}
1958
1959fn apply_tool_cache_control(
1961 tools: &mut [serde_json::Value],
1962 remaining_cache_markers: &mut usize,
1963 cache_control: &CacheControl,
1964) -> Result<(), CompletionError> {
1965 let Some(idx) = final_cacheable_tool_idx(tools) else {
1966 return Ok(());
1967 };
1968
1969 let Some(tool) = tools
1970 .get_mut(idx)
1971 .and_then(serde_json::Value::as_object_mut)
1972 else {
1973 return Ok(());
1974 };
1975
1976 if tool
1977 .get("cache_control")
1978 .is_some_and(|cache_control| !cache_control.is_null())
1979 {
1980 return Ok(());
1981 }
1982
1983 if *remaining_cache_markers == 0 {
1984 return Err(CompletionError::RequestError(
1985 "Anthropic manual prompt caching requires a cache_control marker on the final \
1986 non-deferred tool, but explicit tool markers exhaust the available cache point budget"
1987 .into(),
1988 ));
1989 }
1990
1991 tool.insert(
1992 "cache_control".to_string(),
1993 serde_json::to_value(cache_control)?,
1994 );
1995 *remaining_cache_markers -= 1;
1996
1997 Ok(())
1998}
1999
2000fn apply_system_cache_control(
2001 system: &mut [SystemContent],
2002 remaining_cache_markers: &mut usize,
2003 cache_control_value: &CacheControl,
2004) {
2005 if *remaining_cache_markers == 0 {
2006 return;
2007 }
2008
2009 if let Some(SystemContent::Text { cache_control, .. }) = system.last_mut()
2010 && cache_control.is_none()
2011 {
2012 *cache_control = Some(cache_control_value.clone());
2013 *remaining_cache_markers -= 1;
2014 }
2015}
2016
2017fn clear_message_cache_control(messages: &mut [Message]) {
2018 for msg in messages.iter_mut() {
2019 for content in msg.content.iter_mut() {
2020 set_content_cache_control(content, None);
2021 }
2022 }
2023}
2024
2025fn apply_message_cache_control(
2026 messages: &mut [Message],
2027 remaining_cache_markers: &mut usize,
2028 cache_control: &CacheControl,
2029) {
2030 clear_message_cache_control(messages);
2031
2032 if *remaining_cache_markers == 0 {
2033 return;
2034 }
2035
2036 if let Some(last_msg) = messages.last_mut() {
2037 set_content_cache_control(last_msg.content.last_mut(), Some(cache_control.clone()));
2038 *remaining_cache_markers -= 1;
2039 }
2040}
2041
2042pub(super) fn apply_prompt_cache_control(
2043 system: &mut [SystemContent],
2044 messages: &mut [Message],
2045 tools: &mut [serde_json::Value],
2046 prompt_caching: bool,
2047 top_level_cache_control: Option<&CacheControl>,
2048) -> Result<(), CompletionError> {
2049 normalize_tool_cache_control(tools);
2050
2051 let max_cache_markers = if top_level_cache_control.is_some() {
2052 MAX_CACHE_CONTROL_MARKERS - 1
2053 } else {
2054 MAX_CACHE_CONTROL_MARKERS
2055 };
2056 let tool_cache_markers = tool_cache_control_count(tools);
2057
2058 if tool_cache_markers > max_cache_markers {
2059 return Err(CompletionError::RequestError(
2060 format!(
2061 "Too many Anthropic tool `cache_control` markers: {tool_cache_markers} exceeds \
2062 the available prompt caching budget of {max_cache_markers}"
2063 )
2064 .into(),
2065 ));
2066 }
2067
2068 let mut remaining_cache_markers = max_cache_markers - tool_cache_markers;
2069
2070 if prompt_caching {
2071 let generated_cache_control =
2072 build_cache_control(top_level_cache_control_ttl(top_level_cache_control));
2073
2074 apply_tool_cache_control(
2075 tools,
2076 &mut remaining_cache_markers,
2077 &generated_cache_control,
2078 )?;
2079 apply_system_cache_control(
2080 system,
2081 &mut remaining_cache_markers,
2082 &generated_cache_control,
2083 );
2084
2085 if top_level_cache_control.is_some() {
2086 clear_message_cache_control(messages);
2087 } else {
2088 apply_message_cache_control(
2089 messages,
2090 &mut remaining_cache_markers,
2091 &generated_cache_control,
2092 );
2093 }
2094 }
2095
2096 validate_cache_control_ttl_order(system, messages, tools, top_level_cache_control)?;
2097
2098 Ok(())
2099}
2100
2101pub(super) fn extract_top_level_cache_control(
2102 additional_params: &mut serde_json::Value,
2103) -> Result<Option<CacheControl>, CompletionError> {
2104 if let Some(map) = additional_params.as_object_mut()
2105 && let Some(raw_cache_control) = map.remove("cache_control")
2106 {
2107 if raw_cache_control.is_null() {
2108 return Ok(None);
2109 }
2110
2111 return serde_json::from_value::<CacheControl>(raw_cache_control)
2112 .map(Some)
2113 .map_err(|err| {
2114 CompletionError::RequestError(
2115 format!("Invalid Anthropic `additional_params.cache_control` payload: {err}")
2116 .into(),
2117 )
2118 });
2119 }
2120
2121 Ok(None)
2122}
2123
2124pub(super) fn resolve_top_level_cache_control(
2125 automatic_caching: bool,
2126 automatic_caching_ttl: Option<CacheTtl>,
2127 additional_params: &mut serde_json::Value,
2128) -> Result<Option<CacheControl>, CompletionError> {
2129 let raw_cache_control = extract_top_level_cache_control(additional_params)?;
2130 let typed_cache_control = automatic_caching.then_some(CacheControl::Ephemeral {
2131 ttl: automatic_caching_ttl.clone(),
2132 });
2133
2134 match (typed_cache_control, raw_cache_control) {
2135 (Some(typed_cache_control), Some(raw_cache_control)) => {
2136 if automatic_caching_ttl.is_some()
2137 && cache_control_ttl(&typed_cache_control) != cache_control_ttl(&raw_cache_control)
2138 {
2139 return Err(CompletionError::RequestError(
2140 "Anthropic `additional_params.cache_control` conflicts with the typed \
2141 automatic caching TTL"
2142 .into(),
2143 ));
2144 }
2145
2146 Ok(Some(raw_cache_control))
2147 }
2148 (Some(typed_cache_control), None) => Ok(Some(typed_cache_control)),
2149 (None, raw_cache_control) => Ok(raw_cache_control),
2150 }
2151}
2152
2153pub(super) fn split_system_messages_from_history(
2154 history: Vec<message::Message>,
2155) -> (Vec<SystemContent>, Vec<message::Message>) {
2156 let mut system = Vec::new();
2157 let mut remaining = Vec::new();
2158
2159 for message in history {
2160 match message {
2161 message::Message::System { content } => {
2162 if !content.is_empty() {
2163 system.push(SystemContent::Text {
2164 text: content,
2165 cache_control: None,
2166 });
2167 }
2168 }
2169 other => remaining.push(other),
2170 }
2171 }
2172
2173 (system, remaining)
2174}
2175
2176pub struct AnthropicRequestParams<'a> {
2178 pub model: &'a str,
2179 pub request: CompletionRequest,
2180 pub prompt_caching: bool,
2181 pub automatic_caching: bool,
2183 pub automatic_caching_ttl: Option<CacheTtl>,
2185}
2186
2187impl TryFrom<AnthropicRequestParams<'_>> for AnthropicCompletionRequest {
2188 type Error = CompletionError;
2189
2190 fn try_from(params: AnthropicRequestParams<'_>) -> Result<Self, Self::Error> {
2191 let AnthropicRequestParams {
2192 model,
2193 request: mut req,
2194 prompt_caching,
2195 automatic_caching,
2196 automatic_caching_ttl,
2197 } = params;
2198
2199 let Some(max_tokens) = req.max_tokens else {
2201 return Err(CompletionError::RequestError(
2202 "`max_tokens` must be set for Anthropic".into(),
2203 ));
2204 };
2205
2206 let mut full_history = vec![];
2207 if let Some(docs) = req.normalized_documents() {
2208 full_history.push(docs);
2209 }
2210 full_history.extend(req.chat_history);
2211 let (history_system, full_history) = split_system_messages_from_history(full_history);
2212
2213 let mut messages = full_history
2214 .into_iter()
2215 .map(Message::try_from)
2216 .collect::<Result<Vec<Message>, _>>()?;
2217
2218 let mut additional_params_payload = req
2219 .additional_params
2220 .take()
2221 .unwrap_or(serde_json::Value::Null);
2222 let top_level_cache_control = resolve_top_level_cache_control(
2223 automatic_caching,
2224 automatic_caching_ttl,
2225 &mut additional_params_payload,
2226 )?;
2227 let mut tools = build_tool_definitions(req.tools, &mut additional_params_payload)?;
2228
2229 let mut system = if let Some(preamble) = req.preamble {
2231 if preamble.is_empty() {
2232 vec![]
2233 } else {
2234 vec![SystemContent::Text {
2235 text: preamble,
2236 cache_control: None,
2237 }]
2238 }
2239 } else {
2240 vec![]
2241 };
2242 system.extend(history_system);
2243
2244 apply_prompt_cache_control(
2245 &mut system,
2246 &mut messages,
2247 &mut tools,
2248 prompt_caching,
2249 top_level_cache_control.as_ref(),
2250 )?;
2251
2252 let output_config = if let Some(schema) = req.output_schema {
2253 let mut schema_value = schema.to_value();
2254 sanitize_schema(&mut schema_value);
2255 Some(OutputConfig {
2256 format: OutputFormat::JsonSchema {
2257 schema: schema_value,
2258 },
2259 })
2260 } else {
2261 None
2262 };
2263
2264 Ok(Self {
2265 model: model.to_string(),
2266 messages,
2267 max_tokens,
2268 system,
2269 temperature: req.temperature,
2270 tool_choice: req.tool_choice.and_then(|x| ToolChoice::try_from(x).ok()),
2271 tools,
2272 output_config,
2273 cache_control: top_level_cache_control,
2275 additional_params: if additional_params_payload.is_null() {
2276 None
2277 } else {
2278 Some(additional_params_payload)
2279 },
2280 })
2281 }
2282}
2283
2284pub(super) fn extract_tools_from_additional_params(
2285 additional_params: &mut serde_json::Value,
2286) -> Result<Vec<serde_json::Value>, CompletionError> {
2287 if let Some(map) = additional_params.as_object_mut()
2288 && let Some(raw_tools) = map.remove("tools")
2289 {
2290 return serde_json::from_value::<Vec<serde_json::Value>>(raw_tools).map_err(|err| {
2291 CompletionError::RequestError(
2292 format!("Invalid Anthropic `additional_params.tools` payload: {err}").into(),
2293 )
2294 });
2295 }
2296
2297 Ok(Vec::new())
2298}
2299
2300pub(super) fn build_tool_definitions(
2301 tools: Vec<completion::ToolDefinition>,
2302 additional_params_payload: &mut serde_json::Value,
2303) -> Result<Vec<serde_json::Value>, CompletionError> {
2304 let mut additional_tools = extract_tools_from_additional_params(additional_params_payload)?;
2305
2306 let mut tools = tools
2307 .into_iter()
2308 .map(|tool| ToolDefinition {
2309 name: tool.name,
2310 description: Some(tool.description),
2311 input_schema: tool.parameters,
2312 cache_control: None,
2313 })
2314 .map(serde_json::to_value)
2315 .collect::<Result<Vec<_>, _>>()?;
2316 tools.append(&mut additional_tools);
2317
2318 Ok(tools)
2319}
2320
2321impl<Ext, T> completion::CompletionModel for GenericCompletionModel<Ext, T>
2322where
2323 T: HttpClientExt + Clone + Default + WasmCompatSend + WasmCompatSync + 'static,
2324 Ext: AnthropicCompatibleProvider + Clone + WasmCompatSend + WasmCompatSync + 'static,
2325{
2326 type Response = CompletionResponse;
2327 type StreamingResponse = StreamingCompletionResponse;
2328 type Client = crate::client::Client<Ext, T>;
2329
2330 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
2331 Self::new(client.clone(), model.into())
2332 }
2333
2334 async fn completion(
2335 &self,
2336 mut completion_request: completion::CompletionRequest,
2337 ) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
2338 let request_model = completion_request
2339 .model
2340 .clone()
2341 .unwrap_or_else(|| self.model.clone());
2342 let span = if tracing::Span::current().is_disabled() {
2343 info_span!(
2344 target: "rig::completions",
2345 "chat",
2346 gen_ai.operation.name = "chat",
2347 gen_ai.provider.name = Ext::PROVIDER_NAME,
2348 gen_ai.request.model = &request_model,
2349 gen_ai.system_instructions = &completion_request.preamble,
2350 gen_ai.response.id = tracing::field::Empty,
2351 gen_ai.response.model = tracing::field::Empty,
2352 gen_ai.usage.output_tokens = tracing::field::Empty,
2353 gen_ai.usage.input_tokens = tracing::field::Empty,
2354 gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
2355 gen_ai.usage.cache_creation.input_tokens = tracing::field::Empty,
2356 )
2357 } else {
2358 tracing::Span::current()
2359 };
2360
2361 if completion_request.max_tokens.is_none() {
2363 if let Some(tokens) = self.default_max_tokens {
2364 completion_request.max_tokens = Some(tokens);
2365 } else {
2366 return Err(CompletionError::RequestError(
2367 "`max_tokens` must be set for Anthropic".into(),
2368 ));
2369 }
2370 }
2371
2372 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
2373 model: &request_model,
2374 request: completion_request,
2375 prompt_caching: self.prompt_caching,
2376 automatic_caching: self.automatic_caching,
2377 automatic_caching_ttl: self.automatic_caching_ttl.clone(),
2378 })?;
2379
2380 if enabled!(Level::TRACE) {
2381 tracing::trace!(
2382 target: "rig::completions",
2383 "Anthropic completion request: {}",
2384 serde_json::to_string_pretty(&request)?
2385 );
2386 }
2387
2388 async move {
2389 let request: Vec<u8> = serde_json::to_vec(&request)?;
2390
2391 let req = self
2392 .client
2393 .post("/v1/messages")?
2394 .body(request)
2395 .map_err(|e| CompletionError::HttpError(e.into()))?;
2396
2397 let response = self
2398 .client
2399 .send::<_, Bytes>(req)
2400 .await
2401 .map_err(CompletionError::HttpError)?;
2402
2403 if response.status().is_success() {
2404 match serde_json::from_slice::<ApiResponse<CompletionResponse>>(
2405 response
2406 .into_body()
2407 .await
2408 .map_err(CompletionError::HttpError)?
2409 .to_vec()
2410 .as_slice(),
2411 )? {
2412 ApiResponse::Message(completion) => {
2413 let span = tracing::Span::current();
2414 span.record_response_metadata(&completion);
2415 span.record_token_usage(&completion.usage);
2416 if enabled!(Level::TRACE) {
2417 tracing::trace!(
2418 target: "rig::completions",
2419 "Anthropic completion response: {}",
2420 serde_json::to_string_pretty(&completion)?
2421 );
2422 }
2423 completion.try_into()
2424 }
2425 ApiResponse::Error(ApiErrorResponse { message }) => {
2426 Err(CompletionError::ResponseError(message))
2427 }
2428 }
2429 } else {
2430 let text: String = String::from_utf8_lossy(
2431 &response
2432 .into_body()
2433 .await
2434 .map_err(CompletionError::HttpError)?,
2435 )
2436 .into();
2437 Err(CompletionError::ProviderError(text))
2438 }
2439 }
2440 .instrument(span)
2441 .await
2442 }
2443
2444 async fn stream(
2445 &self,
2446 request: CompletionRequest,
2447 ) -> Result<
2448 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
2449 CompletionError,
2450 > {
2451 GenericCompletionModel::stream(self, request).await
2452 }
2453}
2454
2455#[derive(Debug, Deserialize)]
2456struct ApiErrorResponse {
2457 message: String,
2458}
2459
2460#[derive(Debug, Deserialize)]
2461#[serde(tag = "type", rename_all = "snake_case")]
2462enum ApiResponse<T> {
2463 Message(T),
2464 Error(ApiErrorResponse),
2465}
2466
2467#[cfg(test)]
2468mod tests {
2469 use super::*;
2470 use serde_json::json;
2471 use serde_path_to_error::deserialize;
2472
2473 #[test]
2474 fn current_model_default_max_tokens_match_anthropic_limits() {
2475 assert_eq!(default_max_tokens_for_model(CLAUDE_OPUS_4_7), Some(128_000));
2476 assert_eq!(default_max_tokens_for_model(CLAUDE_OPUS_4_6), Some(128_000));
2477 assert_eq!(
2478 default_max_tokens_for_model(CLAUDE_SONNET_4_6),
2479 Some(64_000)
2480 );
2481 assert_eq!(default_max_tokens_for_model(CLAUDE_HAIKU_4_5), Some(64_000));
2482 }
2483
2484 #[test]
2485 fn unknown_model_uses_conservative_default_max_tokens_fallback() {
2486 assert_eq!(default_max_tokens_for_model("claude-unknown"), None);
2487 assert_eq!(default_max_tokens_with_fallback("claude-unknown"), 2_048);
2488 }
2489
2490 #[test]
2491 fn test_deserialize_message() {
2492 let assistant_message_json = r#"
2493 {
2494 "role": "assistant",
2495 "content": "\n\nHello there, how may I assist you today?"
2496 }
2497 "#;
2498
2499 let assistant_message_json2 = r#"
2500 {
2501 "role": "assistant",
2502 "content": [
2503 {
2504 "type": "text",
2505 "text": "\n\nHello there, how may I assist you today?"
2506 },
2507 {
2508 "type": "tool_use",
2509 "id": "toolu_01A09q90qw90lq917835lq9",
2510 "name": "get_weather",
2511 "input": {"location": "San Francisco, CA"}
2512 }
2513 ]
2514 }
2515 "#;
2516
2517 let user_message_json = r#"
2518 {
2519 "role": "user",
2520 "content": [
2521 {
2522 "type": "image",
2523 "source": {
2524 "type": "base64",
2525 "media_type": "image/jpeg",
2526 "data": "/9j/4AAQSkZJRg..."
2527 }
2528 },
2529 {
2530 "type": "text",
2531 "text": "What is in this image?"
2532 },
2533 {
2534 "type": "tool_result",
2535 "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
2536 "content": "15 degrees"
2537 }
2538 ]
2539 }
2540 "#;
2541
2542 let assistant_message: Message = {
2543 let jd = &mut serde_json::Deserializer::from_str(assistant_message_json);
2544 deserialize(jd).unwrap_or_else(|err| {
2545 panic!("Deserialization error at {}: {}", err.path(), err);
2546 })
2547 };
2548
2549 let assistant_message2: Message = {
2550 let jd = &mut serde_json::Deserializer::from_str(assistant_message_json2);
2551 deserialize(jd).unwrap_or_else(|err| {
2552 panic!("Deserialization error at {}: {}", err.path(), err);
2553 })
2554 };
2555
2556 let user_message: Message = {
2557 let jd = &mut serde_json::Deserializer::from_str(user_message_json);
2558 deserialize(jd).unwrap_or_else(|err| {
2559 panic!("Deserialization error at {}: {}", err.path(), err);
2560 })
2561 };
2562
2563 let Message { role, content } = assistant_message;
2564 assert_eq!(role, Role::Assistant);
2565 assert_eq!(
2566 content.first(),
2567 Content::Text {
2568 text: "\n\nHello there, how may I assist you today?".to_owned(),
2569 citations: Vec::new(),
2570 cache_control: None,
2571 }
2572 );
2573
2574 let Message { role, content } = assistant_message2;
2575 {
2576 assert_eq!(role, Role::Assistant);
2577 assert_eq!(content.len(), 2);
2578
2579 let mut iter = content.into_iter();
2580
2581 match iter.next().unwrap() {
2582 Content::Text { text, .. } => {
2583 assert_eq!(text, "\n\nHello there, how may I assist you today?");
2584 }
2585 _ => panic!("Expected text content"),
2586 }
2587
2588 match iter.next().unwrap() {
2589 Content::ToolUse { id, name, input } => {
2590 assert_eq!(id, "toolu_01A09q90qw90lq917835lq9");
2591 assert_eq!(name, "get_weather");
2592 assert_eq!(input, json!({"location": "San Francisco, CA"}));
2593 }
2594 _ => panic!("Expected tool use content"),
2595 }
2596
2597 assert_eq!(iter.next(), None);
2598 }
2599
2600 let Message { role, content } = user_message;
2601 {
2602 assert_eq!(role, Role::User);
2603 assert_eq!(content.len(), 3);
2604
2605 let mut iter = content.into_iter();
2606
2607 match iter.next().unwrap() {
2608 Content::Image { source, .. } => {
2609 assert_eq!(
2610 source,
2611 ImageSource::Base64 {
2612 data: "/9j/4AAQSkZJRg...".to_owned(),
2613 media_type: ImageFormat::JPEG,
2614 }
2615 );
2616 }
2617 _ => panic!("Expected image content"),
2618 }
2619
2620 match iter.next().unwrap() {
2621 Content::Text { text, .. } => {
2622 assert_eq!(text, "What is in this image?");
2623 }
2624 _ => panic!("Expected text content"),
2625 }
2626
2627 match iter.next().unwrap() {
2628 Content::ToolResult {
2629 tool_use_id,
2630 content,
2631 is_error,
2632 ..
2633 } => {
2634 assert_eq!(tool_use_id, "toolu_01A09q90qw90lq917835lq9");
2635 assert_eq!(
2636 content.first(),
2637 ToolResultContent::Text {
2638 text: "15 degrees".to_owned()
2639 }
2640 );
2641 assert_eq!(is_error, None);
2642 }
2643 _ => panic!("Expected tool result content"),
2644 }
2645
2646 assert_eq!(iter.next(), None);
2647 }
2648 }
2649
2650 #[test]
2651 fn test_message_to_message_conversion() {
2652 let user_message: Message = serde_json::from_str(
2653 r#"
2654 {
2655 "role": "user",
2656 "content": [
2657 {
2658 "type": "image",
2659 "source": {
2660 "type": "base64",
2661 "media_type": "image/jpeg",
2662 "data": "/9j/4AAQSkZJRg..."
2663 }
2664 },
2665 {
2666 "type": "text",
2667 "text": "What is in this image?"
2668 },
2669 {
2670 "type": "document",
2671 "source": {
2672 "type": "base64",
2673 "data": "base64_encoded_pdf_data",
2674 "media_type": "application/pdf"
2675 }
2676 }
2677 ]
2678 }
2679 "#,
2680 )
2681 .unwrap();
2682
2683 let assistant_message = Message {
2684 role: Role::Assistant,
2685 content: OneOrMany::one(Content::ToolUse {
2686 id: "toolu_01A09q90qw90lq917835lq9".to_string(),
2687 name: "get_weather".to_string(),
2688 input: json!({"location": "San Francisco, CA"}),
2689 }),
2690 };
2691
2692 let tool_message = Message {
2693 role: Role::User,
2694 content: OneOrMany::one(Content::ToolResult {
2695 tool_use_id: "toolu_01A09q90qw90lq917835lq9".to_string(),
2696 content: OneOrMany::one(ToolResultContent::Text {
2697 text: "15 degrees".to_string(),
2698 }),
2699 is_error: None,
2700 cache_control: None,
2701 }),
2702 };
2703
2704 let converted_user_message: message::Message = user_message.clone().try_into().unwrap();
2705 let converted_assistant_message: message::Message =
2706 assistant_message.clone().try_into().unwrap();
2707 let converted_tool_message: message::Message = tool_message.clone().try_into().unwrap();
2708
2709 match converted_user_message.clone() {
2710 message::Message::User { content } => {
2711 assert_eq!(content.len(), 3);
2712
2713 let mut iter = content.into_iter();
2714
2715 match iter.next().unwrap() {
2716 message::UserContent::Image(message::Image {
2717 data, media_type, ..
2718 }) => {
2719 assert_eq!(data, DocumentSourceKind::base64("/9j/4AAQSkZJRg..."));
2720 assert_eq!(media_type, Some(message::ImageMediaType::JPEG));
2721 }
2722 _ => panic!("Expected image content"),
2723 }
2724
2725 match iter.next().unwrap() {
2726 message::UserContent::Text(message::Text { text, .. }) => {
2727 assert_eq!(text, "What is in this image?");
2728 }
2729 _ => panic!("Expected text content"),
2730 }
2731
2732 match iter.next().unwrap() {
2733 message::UserContent::Document(message::Document {
2734 data, media_type, ..
2735 }) => {
2736 assert_eq!(
2737 data,
2738 DocumentSourceKind::String("base64_encoded_pdf_data".into())
2739 );
2740 assert_eq!(media_type, Some(message::DocumentMediaType::PDF));
2741 }
2742 _ => panic!("Expected document content"),
2743 }
2744
2745 assert_eq!(iter.next(), None);
2746 }
2747 _ => panic!("Expected user message"),
2748 }
2749
2750 match converted_tool_message.clone() {
2751 message::Message::User { content } => {
2752 let message::ToolResult { id, content, .. } = match content.first() {
2753 message::UserContent::ToolResult(tool_result) => tool_result,
2754 _ => panic!("Expected tool result content"),
2755 };
2756 assert_eq!(id, "toolu_01A09q90qw90lq917835lq9");
2757 match content.first() {
2758 message::ToolResultContent::Text(message::Text { text, .. }) => {
2759 assert_eq!(text, "15 degrees");
2760 }
2761 _ => panic!("Expected text content"),
2762 }
2763 }
2764 _ => panic!("Expected tool result content"),
2765 }
2766
2767 match converted_assistant_message.clone() {
2768 message::Message::Assistant { content, .. } => {
2769 assert_eq!(content.len(), 1);
2770
2771 match content.first() {
2772 message::AssistantContent::ToolCall(message::ToolCall {
2773 id, function, ..
2774 }) => {
2775 assert_eq!(id, "toolu_01A09q90qw90lq917835lq9");
2776 assert_eq!(function.name, "get_weather");
2777 assert_eq!(function.arguments, json!({"location": "San Francisco, CA"}));
2778 }
2779 _ => panic!("Expected tool call content"),
2780 }
2781 }
2782 _ => panic!("Expected assistant message"),
2783 }
2784
2785 let original_user_message: Message = converted_user_message.try_into().unwrap();
2786 let original_assistant_message: Message = converted_assistant_message.try_into().unwrap();
2787 let original_tool_message: Message = converted_tool_message.try_into().unwrap();
2788
2789 assert_eq!(user_message, original_user_message);
2790 assert_eq!(assistant_message, original_assistant_message);
2791 assert_eq!(tool_message, original_tool_message);
2792 }
2793
2794 #[test]
2795 fn test_content_format_conversion() {
2796 use crate::completion::message::ContentFormat;
2797
2798 let source_type: SourceType = ContentFormat::Url.try_into().unwrap();
2799 assert_eq!(source_type, SourceType::URL);
2800
2801 let content_format: ContentFormat = SourceType::URL.into();
2802 assert_eq!(content_format, ContentFormat::Url);
2803
2804 let source_type: SourceType = ContentFormat::Base64.try_into().unwrap();
2805 assert_eq!(source_type, SourceType::BASE64);
2806
2807 let content_format: ContentFormat = SourceType::BASE64.into();
2808 assert_eq!(content_format, ContentFormat::Base64);
2809
2810 let source_type: SourceType = ContentFormat::String.try_into().unwrap();
2811 assert_eq!(source_type, SourceType::TEXT);
2812
2813 let content_format: ContentFormat = SourceType::TEXT.into();
2814 assert_eq!(content_format, ContentFormat::String);
2815 }
2816
2817 #[test]
2818 fn test_cache_control_serialization() {
2819 let system = SystemContent::Text {
2821 text: "You are a helpful assistant.".to_string(),
2822 cache_control: Some(CacheControl::ephemeral()),
2823 };
2824 let json = serde_json::to_string(&system).unwrap();
2825 assert!(json.contains(r#""cache_control":{"type":"ephemeral"}"#));
2826 assert!(json.contains(r#""type":"text""#));
2827
2828 let system_no_cache = SystemContent::Text {
2830 text: "Hello".to_string(),
2831 cache_control: None,
2832 };
2833 let json_no_cache = serde_json::to_string(&system_no_cache).unwrap();
2834 assert!(!json_no_cache.contains("cache_control"));
2835
2836 let content = Content::Text {
2838 text: "Test message".to_string(),
2839 citations: Vec::new(),
2840 cache_control: Some(CacheControl::ephemeral()),
2841 };
2842 let json_content = serde_json::to_string(&content).unwrap();
2843 assert!(json_content.contains(r#""cache_control":{"type":"ephemeral"}"#));
2844
2845 let mut system_vec = vec![SystemContent::Text {
2847 text: "System prompt".to_string(),
2848 cache_control: None,
2849 }];
2850 let mut messages = vec![
2851 Message {
2852 role: Role::User,
2853 content: OneOrMany::one(Content::Text {
2854 text: "First message".to_string(),
2855 citations: Vec::new(),
2856 cache_control: None,
2857 }),
2858 },
2859 Message {
2860 role: Role::Assistant,
2861 content: OneOrMany::one(Content::Text {
2862 text: "Response".to_string(),
2863 citations: Vec::new(),
2864 cache_control: None,
2865 }),
2866 },
2867 ];
2868
2869 apply_cache_control(&mut system_vec, &mut messages);
2870
2871 match &system_vec[0] {
2873 SystemContent::Text { cache_control, .. } => {
2874 assert!(cache_control.is_some());
2875 }
2876 }
2877
2878 for content in messages[0].content.iter() {
2881 if let Content::Text { cache_control, .. } = content {
2882 assert!(cache_control.is_none());
2883 }
2884 }
2885
2886 for content in messages[1].content.iter() {
2888 if let Content::Text { cache_control, .. } = content {
2889 assert!(cache_control.is_some());
2890 }
2891 }
2892 }
2893
2894 fn generic_tool(name: &str) -> completion::ToolDefinition {
2895 completion::ToolDefinition {
2896 name: name.to_string(),
2897 description: format!("{name} description"),
2898 parameters: json!({
2899 "type": "object",
2900 "properties": {}
2901 }),
2902 }
2903 }
2904
2905 fn completion_request_with_tools(
2906 tools: Vec<completion::ToolDefinition>,
2907 additional_params: Option<serde_json::Value>,
2908 ) -> CompletionRequest {
2909 CompletionRequest {
2910 model: None,
2911 preamble: Some("System prompt".to_string()),
2912 chat_history: OneOrMany::one(message::Message::from("Hello")),
2913 documents: Vec::new(),
2914 tools,
2915 temperature: None,
2916 max_tokens: Some(64),
2917 tool_choice: None,
2918 additional_params,
2919 output_schema: None,
2920 }
2921 }
2922
2923 fn system_has_cache_control(value: &serde_json::Value) -> bool {
2924 value["system"]
2925 .as_array()
2926 .and_then(|blocks| blocks.last())
2927 .and_then(|block| block.get("cache_control"))
2928 .is_some()
2929 }
2930
2931 fn last_message_has_cache_control(value: &serde_json::Value) -> bool {
2932 value["messages"]
2933 .as_array()
2934 .and_then(|messages| messages.last())
2935 .and_then(|message| message["content"].as_array())
2936 .and_then(|content| content.last())
2937 .and_then(|content| content.get("cache_control"))
2938 .is_some()
2939 }
2940
2941 #[test]
2942 fn test_tool_definition_cache_control_serialization() {
2943 let tool = ToolDefinition {
2944 name: "cached_tool".to_string(),
2945 description: Some("Cached tool".to_string()),
2946 input_schema: json!({"type": "object"}),
2947 cache_control: Some(CacheControl::ephemeral()),
2948 };
2949
2950 let value = serde_json::to_value(tool).unwrap();
2951 assert_eq!(value["cache_control"]["type"], "ephemeral");
2952
2953 let tool_without_cache = ToolDefinition {
2954 name: "uncached_tool".to_string(),
2955 description: Some("Uncached tool".to_string()),
2956 input_schema: json!({"type": "object"}),
2957 cache_control: None,
2958 };
2959
2960 let value = serde_json::to_value(tool_without_cache).unwrap();
2961 assert!(value.get("cache_control").is_none());
2962 }
2963
2964 #[test]
2965 fn test_apply_tool_cache_control_marks_only_final_tool() {
2966 let mut tools = vec![
2967 json!({
2968 "name": "first_tool",
2969 "description": "First tool",
2970 "input_schema": {"type": "object"}
2971 }),
2972 json!({
2973 "name": "second_tool",
2974 "description": "Second tool",
2975 "input_schema": {"type": "object"}
2976 }),
2977 ];
2978
2979 let mut remaining_cache_markers = 4;
2980 apply_tool_cache_control(
2981 &mut tools,
2982 &mut remaining_cache_markers,
2983 &CacheControl::ephemeral(),
2984 )
2985 .unwrap();
2986
2987 assert!(tools[0].get("cache_control").is_none());
2988 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
2989 assert_eq!(remaining_cache_markers, 3);
2990 }
2991
2992 #[test]
2993 fn test_prompt_caching_skips_final_deferred_tool_in_request() {
2994 let request = completion_request_with_tools(
2995 Vec::new(),
2996 Some(json!({
2997 "tools": [
2998 {
2999 "name": "regular_tool",
3000 "description": "Regular tool",
3001 "input_schema": {"type": "object"}
3002 },
3003 {
3004 "name": "deferred_tool",
3005 "description": "Deferred tool",
3006 "input_schema": {"type": "object"},
3007 "defer_loading": true
3008 }
3009 ]
3010 })),
3011 );
3012
3013 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3014 model: "claude-sonnet-4-6",
3015 request,
3016 prompt_caching: true,
3017 automatic_caching: false,
3018 automatic_caching_ttl: None,
3019 })
3020 .unwrap();
3021
3022 let value = serde_json::to_value(request).unwrap();
3023 let tools = value["tools"].as_array().unwrap();
3024 assert_eq!(tools[0]["name"], "regular_tool");
3025 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3026 assert_eq!(tools[1]["name"], "deferred_tool");
3027 assert!(tools[1].get("cache_control").is_none());
3028 }
3029
3030 #[test]
3031 fn test_prompt_caching_preserves_existing_final_tool_cache_control() {
3032 let request = completion_request_with_tools(
3033 Vec::new(),
3034 Some(json!({
3035 "tools": [{
3036 "name": "cached_tool",
3037 "description": "Cached tool",
3038 "input_schema": {"type": "object"},
3039 "cache_control": {"type": "ephemeral", "ttl": "1h"}
3040 }]
3041 })),
3042 );
3043
3044 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3045 model: "claude-sonnet-4-6",
3046 request,
3047 prompt_caching: true,
3048 automatic_caching: false,
3049 automatic_caching_ttl: None,
3050 })
3051 .unwrap();
3052
3053 let value = serde_json::to_value(request).unwrap();
3054 let tools = value["tools"].as_array().unwrap();
3055 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3056 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
3057 }
3058
3059 #[test]
3060 fn test_prompt_caching_all_deferred_tools_do_not_receive_cache_control() {
3061 let request = completion_request_with_tools(
3062 Vec::new(),
3063 Some(json!({
3064 "tools": [
3065 {
3066 "name": "first_deferred_tool",
3067 "description": "First deferred tool",
3068 "input_schema": {"type": "object"},
3069 "defer_loading": true
3070 },
3071 {
3072 "name": "second_deferred_tool",
3073 "description": "Second deferred tool",
3074 "input_schema": {"type": "object"},
3075 "defer_loading": true
3076 }
3077 ]
3078 })),
3079 );
3080
3081 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3082 model: "claude-sonnet-4-6",
3083 request,
3084 prompt_caching: true,
3085 automatic_caching: false,
3086 automatic_caching_ttl: None,
3087 })
3088 .unwrap();
3089
3090 let value = serde_json::to_value(request).unwrap();
3091 let tools = value["tools"].as_array().unwrap();
3092 assert!(tools[0].get("cache_control").is_none());
3093 assert!(tools[1].get("cache_control").is_none());
3094 }
3095
3096 #[test]
3097 fn test_prompt_caching_preserves_earlier_tool_cache_control() {
3098 let request = completion_request_with_tools(
3099 Vec::new(),
3100 Some(json!({
3101 "tools": [
3102 {
3103 "name": "earlier_tool",
3104 "description": "Earlier tool",
3105 "input_schema": {"type": "object"},
3106 "cache_control": {"type": "ephemeral", "ttl": "1h"}
3107 },
3108 {
3109 "name": "later_tool",
3110 "description": "Later tool",
3111 "input_schema": {"type": "object"}
3112 }
3113 ]
3114 })),
3115 );
3116
3117 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3118 model: "claude-sonnet-4-6",
3119 request,
3120 prompt_caching: true,
3121 automatic_caching: false,
3122 automatic_caching_ttl: None,
3123 })
3124 .unwrap();
3125
3126 let value = serde_json::to_value(request).unwrap();
3127 let tools = value["tools"].as_array().unwrap();
3128 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3129 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
3130 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3131 }
3132
3133 #[test]
3134 fn test_prompt_caching_deferred_marker_does_not_suppress_loaded_tool_marker() {
3135 let request = completion_request_with_tools(
3136 Vec::new(),
3137 Some(json!({
3138 "tools": [
3139 {
3140 "name": "regular_tool",
3141 "description": "Regular tool",
3142 "input_schema": {"type": "object"}
3143 },
3144 {
3145 "name": "deferred_cached_tool",
3146 "description": "Deferred cached tool",
3147 "input_schema": {"type": "object"},
3148 "defer_loading": true,
3149 "cache_control": {"type": "ephemeral"}
3150 }
3151 ]
3152 })),
3153 );
3154
3155 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3156 model: "claude-sonnet-4-6",
3157 request,
3158 prompt_caching: true,
3159 automatic_caching: false,
3160 automatic_caching_ttl: None,
3161 })
3162 .unwrap();
3163
3164 let value = serde_json::to_value(request).unwrap();
3165 let tools = value["tools"].as_array().unwrap();
3166 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3167 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3168 }
3169
3170 #[test]
3171 fn test_prompt_caching_errors_when_tool_cache_control_ttl_order_is_invalid() {
3172 let request = completion_request_with_tools(
3173 Vec::new(),
3174 Some(json!({
3175 "tools": [
3176 {
3177 "name": "first_cached_tool",
3178 "description": "First cached tool",
3179 "input_schema": {"type": "object"},
3180 "cache_control": {"type": "ephemeral"}
3181 },
3182 {
3183 "name": "second_cached_tool",
3184 "description": "Second cached tool",
3185 "input_schema": {"type": "object"},
3186 "cache_control": {"type": "ephemeral", "ttl": "1h"}
3187 }
3188 ]
3189 })),
3190 );
3191
3192 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3193 model: "claude-sonnet-4-6",
3194 request,
3195 prompt_caching: true,
3196 automatic_caching: false,
3197 automatic_caching_ttl: None,
3198 })
3199 .unwrap_err();
3200
3201 assert!(err.to_string().contains("ttl `1h`"));
3202 }
3203
3204 #[test]
3205 fn test_prompt_caching_preserves_valid_mixed_ttl_tool_cache_controls() {
3206 let request = completion_request_with_tools(
3207 Vec::new(),
3208 Some(json!({
3209 "tools": [
3210 {
3211 "name": "first_cached_tool",
3212 "description": "First cached tool",
3213 "input_schema": {"type": "object"},
3214 "cache_control": {"type": "ephemeral", "ttl": "1h"}
3215 },
3216 {
3217 "name": "second_cached_tool",
3218 "description": "Second cached tool",
3219 "input_schema": {"type": "object"},
3220 "cache_control": {"type": "ephemeral"}
3221 }
3222 ]
3223 })),
3224 );
3225
3226 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3227 model: "claude-sonnet-4-6",
3228 request,
3229 prompt_caching: true,
3230 automatic_caching: false,
3231 automatic_caching_ttl: None,
3232 })
3233 .unwrap();
3234
3235 let value = serde_json::to_value(request).unwrap();
3236 let tools = value["tools"].as_array().unwrap();
3237 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3238 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
3239 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3240 assert!(tools[1]["cache_control"].get("ttl").is_none());
3241 }
3242
3243 #[test]
3244 fn test_prompt_caching_preserves_deferred_tool_cache_control() {
3245 let request = completion_request_with_tools(
3246 Vec::new(),
3247 Some(json!({
3248 "tools": [{
3249 "name": "deferred_cached_tool",
3250 "description": "Deferred cached tool",
3251 "input_schema": {"type": "object"},
3252 "defer_loading": true,
3253 "cache_control": {"type": "ephemeral"}
3254 }]
3255 })),
3256 );
3257
3258 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3259 model: "claude-sonnet-4-6",
3260 request,
3261 prompt_caching: true,
3262 automatic_caching: false,
3263 automatic_caching_ttl: None,
3264 })
3265 .unwrap();
3266
3267 let value = serde_json::to_value(request).unwrap();
3268 let tools = value["tools"].as_array().unwrap();
3269 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3270 }
3271
3272 #[test]
3273 fn test_prompt_caching_budget_preserves_three_tool_markers_and_skips_message() {
3274 let request = completion_request_with_tools(
3275 Vec::new(),
3276 Some(json!({
3277 "tools": [
3278 {
3279 "name": "first_cached_tool",
3280 "description": "First cached tool",
3281 "input_schema": {"type": "object"},
3282 "cache_control": {"type": "ephemeral"}
3283 },
3284 {
3285 "name": "second_cached_tool",
3286 "description": "Second cached tool",
3287 "input_schema": {"type": "object"},
3288 "cache_control": {"type": "ephemeral"}
3289 },
3290 {
3291 "name": "third_cached_tool",
3292 "description": "Third cached tool",
3293 "input_schema": {"type": "object"},
3294 "cache_control": {"type": "ephemeral"}
3295 }
3296 ]
3297 })),
3298 );
3299
3300 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3301 model: "claude-sonnet-4-6",
3302 request,
3303 prompt_caching: true,
3304 automatic_caching: false,
3305 automatic_caching_ttl: None,
3306 })
3307 .unwrap();
3308
3309 let value = serde_json::to_value(request).unwrap();
3310 let tools = value["tools"].as_array().unwrap();
3311 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3312 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3313 assert_eq!(tools[2]["cache_control"]["type"], "ephemeral");
3314 assert!(system_has_cache_control(&value));
3315 assert!(!last_message_has_cache_control(&value));
3316 }
3317
3318 #[test]
3319 fn test_prompt_caching_errors_when_explicit_tool_markers_exceed_budget() {
3320 let request = completion_request_with_tools(
3321 Vec::new(),
3322 Some(json!({
3323 "tools": [
3324 {
3325 "name": "first_cached_tool",
3326 "description": "First cached tool",
3327 "input_schema": {"type": "object"},
3328 "cache_control": {"type": "ephemeral"}
3329 },
3330 {
3331 "name": "second_cached_tool",
3332 "description": "Second cached tool",
3333 "input_schema": {"type": "object"},
3334 "cache_control": {"type": "ephemeral"}
3335 },
3336 {
3337 "name": "third_cached_tool",
3338 "description": "Third cached tool",
3339 "input_schema": {"type": "object"},
3340 "cache_control": {"type": "ephemeral"}
3341 },
3342 {
3343 "name": "fourth_cached_tool",
3344 "description": "Fourth cached tool",
3345 "input_schema": {"type": "object"},
3346 "cache_control": {"type": "ephemeral"}
3347 },
3348 {
3349 "name": "fifth_cached_tool",
3350 "description": "Fifth cached tool",
3351 "input_schema": {"type": "object"},
3352 "cache_control": {"type": "ephemeral"}
3353 }
3354 ]
3355 })),
3356 );
3357
3358 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3359 model: "claude-sonnet-4-6",
3360 request,
3361 prompt_caching: true,
3362 automatic_caching: false,
3363 automatic_caching_ttl: None,
3364 })
3365 .unwrap_err();
3366
3367 assert!(err.to_string().contains("Too many Anthropic tool"));
3368 }
3369
3370 #[test]
3371 fn test_prompt_caching_errors_when_final_tool_marker_has_no_budget() {
3372 let request = completion_request_with_tools(
3373 Vec::new(),
3374 Some(json!({
3375 "tools": [
3376 {
3377 "name": "first_cached_tool",
3378 "description": "First cached tool",
3379 "input_schema": {"type": "object"},
3380 "cache_control": {"type": "ephemeral"}
3381 },
3382 {
3383 "name": "second_cached_tool",
3384 "description": "Second cached tool",
3385 "input_schema": {"type": "object"},
3386 "cache_control": {"type": "ephemeral"}
3387 },
3388 {
3389 "name": "third_cached_tool",
3390 "description": "Third cached tool",
3391 "input_schema": {"type": "object"},
3392 "cache_control": {"type": "ephemeral"}
3393 },
3394 {
3395 "name": "fourth_cached_tool",
3396 "description": "Fourth cached tool",
3397 "input_schema": {"type": "object"},
3398 "cache_control": {"type": "ephemeral"}
3399 },
3400 {
3401 "name": "final_uncached_tool",
3402 "description": "Final uncached tool",
3403 "input_schema": {"type": "object"}
3404 }
3405 ]
3406 })),
3407 );
3408
3409 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3410 model: "claude-sonnet-4-6",
3411 request,
3412 prompt_caching: true,
3413 automatic_caching: false,
3414 automatic_caching_ttl: None,
3415 })
3416 .unwrap_err();
3417
3418 assert!(err.to_string().contains("final non-deferred tool"));
3419 }
3420
3421 #[test]
3422 fn test_prompt_caching_replaces_null_final_tool_cache_control() {
3423 let request = completion_request_with_tools(
3424 Vec::new(),
3425 Some(json!({
3426 "tools": [{
3427 "name": "final_tool",
3428 "description": "Final tool",
3429 "input_schema": {"type": "object"},
3430 "cache_control": null
3431 }]
3432 })),
3433 );
3434
3435 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3436 model: "claude-sonnet-4-6",
3437 request,
3438 prompt_caching: true,
3439 automatic_caching: false,
3440 automatic_caching_ttl: None,
3441 })
3442 .unwrap();
3443
3444 let value = serde_json::to_value(request).unwrap();
3445 let tools = value["tools"].as_array().unwrap();
3446 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3447 }
3448
3449 #[test]
3450 fn test_prompt_caching_ignores_null_tool_cache_control_when_budgeting() {
3451 let request = completion_request_with_tools(
3452 Vec::new(),
3453 Some(json!({
3454 "tools": [
3455 {
3456 "name": "first_null_tool",
3457 "description": "First null tool",
3458 "input_schema": {"type": "object"},
3459 "cache_control": null
3460 },
3461 {
3462 "name": "second_null_tool",
3463 "description": "Second null tool",
3464 "input_schema": {"type": "object"},
3465 "cache_control": null
3466 },
3467 {
3468 "name": "third_null_tool",
3469 "description": "Third null tool",
3470 "input_schema": {"type": "object"},
3471 "cache_control": null
3472 },
3473 {
3474 "name": "fourth_null_tool",
3475 "description": "Fourth null tool",
3476 "input_schema": {"type": "object"},
3477 "cache_control": null
3478 },
3479 {
3480 "name": "final_uncached_tool",
3481 "description": "Final uncached tool",
3482 "input_schema": {"type": "object"}
3483 }
3484 ]
3485 })),
3486 );
3487
3488 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3489 model: "claude-sonnet-4-6",
3490 request,
3491 prompt_caching: true,
3492 automatic_caching: false,
3493 automatic_caching_ttl: None,
3494 })
3495 .unwrap();
3496
3497 let value = serde_json::to_value(request).unwrap();
3498 let tools = value["tools"].as_array().unwrap();
3499 assert!(tools[0].get("cache_control").is_none());
3500 assert!(tools[1].get("cache_control").is_none());
3501 assert!(tools[2].get("cache_control").is_none());
3502 assert!(tools[3].get("cache_control").is_none());
3503 assert_eq!(tools[4]["cache_control"]["type"], "ephemeral");
3504 }
3505
3506 #[test]
3507 fn test_prompt_caching_preserves_non_null_provider_tool_cache_control_escape_hatch() {
3508 let request = completion_request_with_tools(
3509 Vec::new(),
3510 Some(json!({
3511 "tools": [{
3512 "name": "provider_tool",
3513 "description": "Provider tool",
3514 "input_schema": {"type": "object"},
3515 "cache_control": {"type": "provider_specific"}
3516 }]
3517 })),
3518 );
3519
3520 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3521 model: "claude-sonnet-4-6",
3522 request,
3523 prompt_caching: true,
3524 automatic_caching: false,
3525 automatic_caching_ttl: None,
3526 })
3527 .unwrap();
3528
3529 let value = serde_json::to_value(request).unwrap();
3530 let tools = value["tools"].as_array().unwrap();
3531 assert_eq!(tools[0]["cache_control"]["type"], "provider_specific");
3532 }
3533
3534 #[test]
3535 fn test_prompt_caching_automatic_mode_uses_reduced_marker_budget() {
3536 let request = completion_request_with_tools(
3537 Vec::new(),
3538 Some(json!({
3539 "tools": [
3540 {
3541 "name": "first_cached_tool",
3542 "description": "First cached tool",
3543 "input_schema": {"type": "object"},
3544 "cache_control": {"type": "ephemeral"}
3545 },
3546 {
3547 "name": "second_cached_tool",
3548 "description": "Second cached tool",
3549 "input_schema": {"type": "object"},
3550 "cache_control": {"type": "ephemeral"}
3551 },
3552 {
3553 "name": "third_cached_tool",
3554 "description": "Third cached tool",
3555 "input_schema": {"type": "object"},
3556 "cache_control": {"type": "ephemeral"}
3557 }
3558 ]
3559 })),
3560 );
3561
3562 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3563 model: "claude-sonnet-4-6",
3564 request,
3565 prompt_caching: true,
3566 automatic_caching: true,
3567 automatic_caching_ttl: None,
3568 })
3569 .unwrap();
3570
3571 let value = serde_json::to_value(request).unwrap();
3572 let tools = value["tools"].as_array().unwrap();
3573 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3574 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3575 assert_eq!(tools[2]["cache_control"]["type"], "ephemeral");
3576 assert_eq!(value["cache_control"]["type"], "ephemeral");
3577 assert!(!system_has_cache_control(&value));
3578 assert!(!last_message_has_cache_control(&value));
3579 }
3580
3581 #[test]
3582 fn test_prompt_caching_automatic_mode_errors_when_final_tool_marker_has_no_budget() {
3583 let request = completion_request_with_tools(
3584 Vec::new(),
3585 Some(json!({
3586 "tools": [
3587 {
3588 "name": "first_cached_tool",
3589 "description": "First cached tool",
3590 "input_schema": {"type": "object"},
3591 "cache_control": {"type": "ephemeral"}
3592 },
3593 {
3594 "name": "second_cached_tool",
3595 "description": "Second cached tool",
3596 "input_schema": {"type": "object"},
3597 "cache_control": {"type": "ephemeral"}
3598 },
3599 {
3600 "name": "third_cached_tool",
3601 "description": "Third cached tool",
3602 "input_schema": {"type": "object"},
3603 "cache_control": {"type": "ephemeral"}
3604 },
3605 {
3606 "name": "final_uncached_tool",
3607 "description": "Final uncached tool",
3608 "input_schema": {"type": "object"}
3609 }
3610 ]
3611 })),
3612 );
3613
3614 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3615 model: "claude-sonnet-4-6",
3616 request,
3617 prompt_caching: true,
3618 automatic_caching: true,
3619 automatic_caching_ttl: None,
3620 })
3621 .unwrap_err();
3622
3623 assert!(err.to_string().contains("final non-deferred tool"));
3624 }
3625
3626 #[test]
3627 fn test_automatic_caching_errors_when_explicit_tool_markers_exhaust_budget() {
3628 let request = completion_request_with_tools(
3629 Vec::new(),
3630 Some(json!({
3631 "tools": [
3632 {
3633 "name": "first_cached_tool",
3634 "description": "First cached tool",
3635 "input_schema": {"type": "object"},
3636 "cache_control": {"type": "ephemeral"}
3637 },
3638 {
3639 "name": "second_cached_tool",
3640 "description": "Second cached tool",
3641 "input_schema": {"type": "object"},
3642 "cache_control": {"type": "ephemeral"}
3643 },
3644 {
3645 "name": "third_cached_tool",
3646 "description": "Third cached tool",
3647 "input_schema": {"type": "object"},
3648 "cache_control": {"type": "ephemeral"}
3649 },
3650 {
3651 "name": "fourth_cached_tool",
3652 "description": "Fourth cached tool",
3653 "input_schema": {"type": "object"},
3654 "cache_control": {"type": "ephemeral"}
3655 }
3656 ]
3657 })),
3658 );
3659
3660 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3661 model: "claude-sonnet-4-6",
3662 request,
3663 prompt_caching: false,
3664 automatic_caching: true,
3665 automatic_caching_ttl: None,
3666 })
3667 .unwrap_err();
3668
3669 assert!(err.to_string().contains("Too many Anthropic tool"));
3670 }
3671
3672 #[test]
3673 fn test_automatic_caching_1h_errors_with_explicit_five_minute_tool_marker() {
3674 let request = completion_request_with_tools(
3675 Vec::new(),
3676 Some(json!({
3677 "tools": [{
3678 "name": "cached_tool",
3679 "description": "Cached tool",
3680 "input_schema": {"type": "object"},
3681 "cache_control": {"type": "ephemeral"}
3682 }]
3683 })),
3684 );
3685
3686 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3687 model: "claude-sonnet-4-6",
3688 request,
3689 prompt_caching: false,
3690 automatic_caching: true,
3691 automatic_caching_ttl: Some(CacheTtl::OneHour),
3692 })
3693 .unwrap_err();
3694
3695 assert!(err.to_string().contains("ttl `1h`"));
3696 }
3697
3698 #[test]
3699 fn test_prompt_and_automatic_caching_1h_uses_1h_generated_markers() {
3700 let request = completion_request_with_tools(vec![generic_tool("cached_tool")], None);
3701
3702 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3703 model: "claude-sonnet-4-6",
3704 request,
3705 prompt_caching: true,
3706 automatic_caching: true,
3707 automatic_caching_ttl: Some(CacheTtl::OneHour),
3708 })
3709 .unwrap();
3710
3711 let value = serde_json::to_value(request).unwrap();
3712 let tools = value["tools"].as_array().unwrap();
3713 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3714 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
3715 assert_eq!(
3716 value["system"]
3717 .as_array()
3718 .and_then(|blocks| blocks.last())
3719 .and_then(|block| block["cache_control"].get("ttl")),
3720 Some(&json!("1h"))
3721 );
3722 assert_eq!(value["cache_control"]["ttl"], "1h");
3723 assert!(!last_message_has_cache_control(&value));
3724 }
3725
3726 #[test]
3727 fn test_prompt_and_raw_top_level_automatic_caching_1h_uses_1h_generated_markers() {
3728 let request = completion_request_with_tools(
3729 vec![generic_tool("cached_tool")],
3730 Some(json!({
3731 "cache_control": {"type": "ephemeral", "ttl": "1h"},
3732 "metadata": {"source": "test"}
3733 })),
3734 );
3735
3736 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3737 model: "claude-sonnet-4-6",
3738 request,
3739 prompt_caching: true,
3740 automatic_caching: true,
3741 automatic_caching_ttl: None,
3742 })
3743 .unwrap();
3744
3745 let value = serde_json::to_value(request).unwrap();
3746 let tools = value["tools"].as_array().unwrap();
3747 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3748 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
3749 assert_eq!(
3750 value["system"]
3751 .as_array()
3752 .and_then(|blocks| blocks.last())
3753 .and_then(|block| block["cache_control"].get("ttl")),
3754 Some(&json!("1h"))
3755 );
3756 assert_eq!(value["cache_control"]["ttl"], "1h");
3757 assert_eq!(value["metadata"]["source"], "test");
3758 assert!(!last_message_has_cache_control(&value));
3759 }
3760
3761 #[test]
3762 fn test_prompt_caching_uses_raw_top_level_cache_control_ttl() {
3763 let request = completion_request_with_tools(
3764 vec![generic_tool("cached_tool")],
3765 Some(json!({
3766 "cache_control": {"type": "ephemeral", "ttl": "1h"},
3767 "metadata": {"source": "raw-cache-control"}
3768 })),
3769 );
3770
3771 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3772 model: "claude-sonnet-4-6",
3773 request,
3774 prompt_caching: true,
3775 automatic_caching: false,
3776 automatic_caching_ttl: None,
3777 })
3778 .unwrap();
3779
3780 let value = serde_json::to_value(request).unwrap();
3781 let tools = value["tools"].as_array().unwrap();
3782 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3783 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
3784 assert_eq!(
3785 value["system"]
3786 .as_array()
3787 .and_then(|blocks| blocks.last())
3788 .and_then(|block| block["cache_control"].get("ttl")),
3789 Some(&json!("1h"))
3790 );
3791 assert_eq!(value["cache_control"]["ttl"], "1h");
3792 assert_eq!(value["metadata"]["source"], "raw-cache-control");
3793 assert!(!last_message_has_cache_control(&value));
3794 }
3795
3796 #[test]
3797 fn test_raw_top_level_automatic_caching_reduces_marker_budget() {
3798 let request = completion_request_with_tools(
3799 Vec::new(),
3800 Some(json!({
3801 "cache_control": {"type": "ephemeral"},
3802 "tools": [
3803 {
3804 "name": "first_cached_tool",
3805 "description": "First cached tool",
3806 "input_schema": {"type": "object"},
3807 "cache_control": {"type": "ephemeral"}
3808 },
3809 {
3810 "name": "second_cached_tool",
3811 "description": "Second cached tool",
3812 "input_schema": {"type": "object"},
3813 "cache_control": {"type": "ephemeral"}
3814 },
3815 {
3816 "name": "third_cached_tool",
3817 "description": "Third cached tool",
3818 "input_schema": {"type": "object"},
3819 "cache_control": {"type": "ephemeral"}
3820 },
3821 {
3822 "name": "fourth_cached_tool",
3823 "description": "Fourth cached tool",
3824 "input_schema": {"type": "object"},
3825 "cache_control": {"type": "ephemeral"}
3826 }
3827 ]
3828 })),
3829 );
3830
3831 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3832 model: "claude-sonnet-4-6",
3833 request,
3834 prompt_caching: false,
3835 automatic_caching: false,
3836 automatic_caching_ttl: None,
3837 })
3838 .unwrap_err();
3839
3840 assert!(err.to_string().contains("Too many Anthropic tool"));
3841 }
3842
3843 #[test]
3844 fn test_raw_top_level_automatic_caching_1h_errors_after_explicit_five_minute_tool_marker() {
3845 let request = completion_request_with_tools(
3846 Vec::new(),
3847 Some(json!({
3848 "cache_control": {"type": "ephemeral", "ttl": "1h"},
3849 "tools": [{
3850 "name": "cached_tool",
3851 "description": "Cached tool",
3852 "input_schema": {"type": "object"},
3853 "cache_control": {"type": "ephemeral"}
3854 }]
3855 })),
3856 );
3857
3858 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3859 model: "claude-sonnet-4-6",
3860 request,
3861 prompt_caching: false,
3862 automatic_caching: false,
3863 automatic_caching_ttl: None,
3864 })
3865 .unwrap_err();
3866
3867 assert!(err.to_string().contains("ttl `1h`"));
3868 }
3869
3870 #[test]
3871 fn test_typed_automatic_caching_ttl_errors_on_conflicting_raw_top_level_ttl() {
3872 let request = completion_request_with_tools(
3873 Vec::new(),
3874 Some(json!({
3875 "cache_control": {"type": "ephemeral"}
3876 })),
3877 );
3878
3879 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3880 model: "claude-sonnet-4-6",
3881 request,
3882 prompt_caching: false,
3883 automatic_caching: true,
3884 automatic_caching_ttl: Some(CacheTtl::OneHour),
3885 })
3886 .unwrap_err();
3887
3888 assert!(
3889 err.to_string()
3890 .contains("conflicts with the typed automatic caching TTL")
3891 );
3892 }
3893
3894 #[test]
3895 fn test_prompt_caching_marks_final_tool_in_request() {
3896 let request = completion_request_with_tools(
3897 vec![generic_tool("first_tool"), generic_tool("second_tool")],
3898 None,
3899 );
3900
3901 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3902 model: "claude-sonnet-4-6",
3903 request,
3904 prompt_caching: true,
3905 automatic_caching: false,
3906 automatic_caching_ttl: None,
3907 })
3908 .unwrap();
3909
3910 let value = serde_json::to_value(request).unwrap();
3911 let tools = value["tools"].as_array().unwrap();
3912 assert_eq!(tools.len(), 2);
3913 assert!(tools[0].get("cache_control").is_none());
3914 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3915 }
3916
3917 #[test]
3918 fn test_prompt_caching_marks_final_additional_tool_in_request() {
3919 let request = completion_request_with_tools(
3920 vec![generic_tool("rig_tool")],
3921 Some(json!({
3922 "tools": [{
3923 "name": "provider_tool",
3924 "description": "Provider tool",
3925 "input_schema": {"type": "object"}
3926 }],
3927 "metadata": {"source": "test"}
3928 })),
3929 );
3930
3931 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3932 model: "claude-sonnet-4-6",
3933 request,
3934 prompt_caching: true,
3935 automatic_caching: false,
3936 automatic_caching_ttl: None,
3937 })
3938 .unwrap();
3939
3940 let value = serde_json::to_value(request).unwrap();
3941 let tools = value["tools"].as_array().unwrap();
3942 assert_eq!(tools.len(), 2);
3943 assert!(tools[0].get("cache_control").is_none());
3944 assert_eq!(tools[1]["name"], "provider_tool");
3945 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3946 assert_eq!(value["metadata"]["source"], "test");
3947 }
3948
3949 #[test]
3950 fn test_prompt_caching_without_tools_omits_tools() {
3951 let request = completion_request_with_tools(Vec::new(), None);
3952
3953 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3954 model: "claude-sonnet-4-6",
3955 request,
3956 prompt_caching: true,
3957 automatic_caching: false,
3958 automatic_caching_ttl: None,
3959 })
3960 .unwrap();
3961
3962 let value = serde_json::to_value(request).unwrap();
3963 assert!(value.get("tools").is_none());
3964 }
3965
3966 #[test]
3967 fn test_plaintext_document_serialization() {
3968 let content = Content::Document {
3969 source: DocumentSource::Text {
3970 data: "Hello, world!".to_string(),
3971 media_type: PlainTextMediaType::Plain,
3972 },
3973 title: None,
3974 context: None,
3975 citations: None,
3976 cache_control: None,
3977 };
3978
3979 let json = serde_json::to_value(&content).unwrap();
3980 assert_eq!(json["type"], "document");
3981 assert_eq!(json["source"]["type"], "text");
3982 assert_eq!(json["source"]["media_type"], "text/plain");
3983 assert_eq!(json["source"]["data"], "Hello, world!");
3984 }
3985
3986 #[test]
3987 fn test_plaintext_document_deserialization() {
3988 let json = r#"
3989 {
3990 "type": "document",
3991 "source": {
3992 "type": "text",
3993 "media_type": "text/plain",
3994 "data": "Hello, world!"
3995 }
3996 }
3997 "#;
3998
3999 let content: Content = serde_json::from_str(json).unwrap();
4000 match content {
4001 Content::Document {
4002 source,
4003 cache_control,
4004 ..
4005 } => {
4006 assert_eq!(
4007 source,
4008 DocumentSource::Text {
4009 data: "Hello, world!".to_string(),
4010 media_type: PlainTextMediaType::Plain,
4011 }
4012 );
4013 assert_eq!(cache_control, None);
4014 }
4015 _ => panic!("Expected Document content"),
4016 }
4017 }
4018
4019 #[test]
4020 fn test_base64_pdf_document_serialization() {
4021 let content = Content::Document {
4022 source: DocumentSource::Base64 {
4023 data: "base64data".to_string(),
4024 media_type: DocumentFormat::PDF,
4025 },
4026 title: None,
4027 context: None,
4028 citations: None,
4029 cache_control: None,
4030 };
4031
4032 let json = serde_json::to_value(&content).unwrap();
4033 assert_eq!(json["type"], "document");
4034 assert_eq!(json["source"]["type"], "base64");
4035 assert_eq!(json["source"]["media_type"], "application/pdf");
4036 assert_eq!(json["source"]["data"], "base64data");
4037 }
4038
4039 #[test]
4040 fn test_base64_pdf_document_deserialization() {
4041 let json = r#"
4042 {
4043 "type": "document",
4044 "source": {
4045 "type": "base64",
4046 "media_type": "application/pdf",
4047 "data": "base64data"
4048 }
4049 }
4050 "#;
4051
4052 let content: Content = serde_json::from_str(json).unwrap();
4053 match content {
4054 Content::Document { source, .. } => {
4055 assert_eq!(
4056 source,
4057 DocumentSource::Base64 {
4058 data: "base64data".to_string(),
4059 media_type: DocumentFormat::PDF,
4060 }
4061 );
4062 }
4063 _ => panic!("Expected Document content"),
4064 }
4065 }
4066
4067 #[test]
4068 fn test_file_id_document_serialization() {
4069 let content = Content::Document {
4070 source: DocumentSource::File {
4071 file_id: "file_abc".to_string(),
4072 },
4073 title: None,
4074 context: None,
4075 citations: None,
4076 cache_control: None,
4077 };
4078
4079 let json = serde_json::to_value(&content).unwrap();
4080 assert_eq!(json["type"], "document");
4081 assert_eq!(json["source"]["type"], "file");
4082 assert_eq!(json["source"]["file_id"], "file_abc");
4083 }
4084
4085 #[test]
4086 fn test_file_id_document_deserialization() {
4087 let json = r#"
4088 {
4089 "type": "document",
4090 "source": {
4091 "type": "file",
4092 "file_id": "file_abc"
4093 }
4094 }
4095 "#;
4096
4097 let content: Content = serde_json::from_str(json).unwrap();
4098 match content {
4099 Content::Document { source, .. } => {
4100 assert_eq!(
4101 source,
4102 DocumentSource::File {
4103 file_id: "file_abc".to_string(),
4104 }
4105 );
4106 }
4107 _ => panic!("Expected Document content"),
4108 }
4109 }
4110
4111 #[test]
4112 fn test_file_id_rig_to_anthropic_conversion() {
4113 use crate::completion::message as msg;
4114
4115 let rig_message = msg::Message::User {
4116 content: OneOrMany::one(msg::UserContent::Document(msg::Document {
4117 data: DocumentSourceKind::FileId("file_abc".to_string()),
4118 media_type: None,
4119 additional_params: None,
4120 })),
4121 };
4122
4123 let anthropic_message: Message = rig_message.try_into().unwrap();
4124 assert_eq!(anthropic_message.role, Role::User);
4125
4126 let mut iter = anthropic_message.content.into_iter();
4127 match iter.next().unwrap() {
4128 Content::Document { source, .. } => {
4129 assert_eq!(
4130 source,
4131 DocumentSource::File {
4132 file_id: "file_abc".to_string(),
4133 }
4134 );
4135 }
4136 other => panic!("Expected Document content, got: {other:?}"),
4137 }
4138 }
4139
4140 #[test]
4141 fn test_file_id_anthropic_to_rig_conversion() {
4142 use crate::completion::message as msg;
4143
4144 let anthropic_message = Message {
4145 role: Role::User,
4146 content: OneOrMany::one(Content::Document {
4147 source: DocumentSource::File {
4148 file_id: "file_abc".to_string(),
4149 },
4150 title: None,
4151 context: None,
4152 citations: None,
4153 cache_control: None,
4154 }),
4155 };
4156
4157 let rig_message: msg::Message = anthropic_message.try_into().unwrap();
4158 match rig_message {
4159 msg::Message::User { content } => {
4160 let mut iter = content.into_iter();
4161 match iter.next().unwrap() {
4162 msg::UserContent::Document(msg::Document {
4163 data, media_type, ..
4164 }) => {
4165 assert_eq!(data, DocumentSourceKind::FileId("file_abc".to_string()));
4166 assert_eq!(media_type, None);
4167 }
4168 other => panic!("Expected Document content, got: {other:?}"),
4169 }
4170 }
4171 _ => panic!("Expected User message"),
4172 }
4173 }
4174
4175 #[test]
4176 fn test_plaintext_rig_to_anthropic_conversion() {
4177 use crate::completion::message as msg;
4178
4179 let rig_message = msg::Message::User {
4180 content: OneOrMany::one(msg::UserContent::document(
4181 "Some plain text content".to_string(),
4182 Some(msg::DocumentMediaType::TXT),
4183 )),
4184 };
4185
4186 let anthropic_message: Message = rig_message.try_into().unwrap();
4187 assert_eq!(anthropic_message.role, Role::User);
4188
4189 let mut iter = anthropic_message.content.into_iter();
4190 match iter.next().unwrap() {
4191 Content::Document { source, .. } => {
4192 assert_eq!(
4193 source,
4194 DocumentSource::Text {
4195 data: "Some plain text content".to_string(),
4196 media_type: PlainTextMediaType::Plain,
4197 }
4198 );
4199 }
4200 other => panic!("Expected Document content, got: {other:?}"),
4201 }
4202 }
4203
4204 #[test]
4205 fn test_plaintext_anthropic_to_rig_conversion() {
4206 use crate::completion::message as msg;
4207
4208 let anthropic_message = Message {
4209 role: Role::User,
4210 content: OneOrMany::one(Content::Document {
4211 source: DocumentSource::Text {
4212 data: "Some plain text content".to_string(),
4213 media_type: PlainTextMediaType::Plain,
4214 },
4215 title: None,
4216 context: None,
4217 citations: None,
4218 cache_control: None,
4219 }),
4220 };
4221
4222 let rig_message: msg::Message = anthropic_message.try_into().unwrap();
4223 match rig_message {
4224 msg::Message::User { content } => {
4225 let mut iter = content.into_iter();
4226 match iter.next().unwrap() {
4227 msg::UserContent::Document(msg::Document {
4228 data, media_type, ..
4229 }) => {
4230 assert_eq!(
4231 data,
4232 DocumentSourceKind::String("Some plain text content".into())
4233 );
4234 assert_eq!(media_type, Some(msg::DocumentMediaType::TXT));
4235 }
4236 other => panic!("Expected Document content, got: {other:?}"),
4237 }
4238 }
4239 _ => panic!("Expected User message"),
4240 }
4241 }
4242
4243 #[test]
4244 fn test_plaintext_roundtrip_rig_to_anthropic_and_back() {
4245 use crate::completion::message as msg;
4246
4247 let original = msg::Message::User {
4248 content: OneOrMany::one(msg::UserContent::document(
4249 "Round trip text".to_string(),
4250 Some(msg::DocumentMediaType::TXT),
4251 )),
4252 };
4253
4254 let anthropic: Message = original.clone().try_into().unwrap();
4255 let back: msg::Message = anthropic.try_into().unwrap();
4256
4257 match (&original, &back) {
4258 (
4259 msg::Message::User {
4260 content: orig_content,
4261 },
4262 msg::Message::User {
4263 content: back_content,
4264 },
4265 ) => match (orig_content.first(), back_content.first()) {
4266 (
4267 msg::UserContent::Document(msg::Document {
4268 media_type: orig_mt,
4269 ..
4270 }),
4271 msg::UserContent::Document(msg::Document {
4272 media_type: back_mt,
4273 ..
4274 }),
4275 ) => {
4276 assert_eq!(orig_mt, back_mt);
4277 }
4278 _ => panic!("Expected Document content in both"),
4279 },
4280 _ => panic!("Expected User messages"),
4281 }
4282 }
4283
4284 #[test]
4285 fn test_unsupported_document_type_returns_error() {
4286 use crate::completion::message as msg;
4287
4288 let rig_message = msg::Message::User {
4289 content: OneOrMany::one(msg::UserContent::Document(msg::Document {
4290 data: DocumentSourceKind::String("data".into()),
4291 media_type: Some(msg::DocumentMediaType::HTML),
4292 additional_params: None,
4293 })),
4294 };
4295
4296 let result: Result<Message, _> = rig_message.try_into();
4297 assert!(result.is_err());
4298 let err = result.unwrap_err().to_string();
4299 assert!(
4300 err.contains("Anthropic only supports PDF and plain text documents"),
4301 "Unexpected error: {err}"
4302 );
4303 }
4304
4305 #[test]
4306 fn test_plaintext_document_url_source_returns_error() {
4307 use crate::completion::message as msg;
4308
4309 let rig_message = msg::Message::User {
4310 content: OneOrMany::one(msg::UserContent::Document(msg::Document {
4311 data: DocumentSourceKind::Url("https://example.com/doc.txt".into()),
4312 media_type: Some(msg::DocumentMediaType::TXT),
4313 additional_params: None,
4314 })),
4315 };
4316
4317 let result: Result<Message, _> = rig_message.try_into();
4318 assert!(result.is_err());
4319 let err = result.unwrap_err().to_string();
4320 assert!(
4321 err.contains("Only string or base64 data is supported for plain text documents"),
4322 "Unexpected error: {err}"
4323 );
4324 }
4325
4326 #[test]
4327 fn test_plaintext_document_with_cache_control() {
4328 let content = Content::Document {
4329 source: DocumentSource::Text {
4330 data: "cached text".to_string(),
4331 media_type: PlainTextMediaType::Plain,
4332 },
4333 title: None,
4334 context: None,
4335 citations: None,
4336 cache_control: Some(CacheControl::ephemeral()),
4337 };
4338
4339 let json = serde_json::to_value(&content).unwrap();
4340 assert_eq!(json["source"]["type"], "text");
4341 assert_eq!(json["source"]["media_type"], "text/plain");
4342 assert_eq!(json["cache_control"]["type"], "ephemeral");
4343 }
4344
4345 #[test]
4346 fn test_message_with_plaintext_document_deserialization() {
4347 let json = r#"
4348 {
4349 "role": "user",
4350 "content": [
4351 {
4352 "type": "document",
4353 "source": {
4354 "type": "text",
4355 "media_type": "text/plain",
4356 "data": "Hello from a text file"
4357 }
4358 },
4359 {
4360 "type": "text",
4361 "text": "Summarize this document."
4362 }
4363 ]
4364 }
4365 "#;
4366
4367 let message: Message = serde_json::from_str(json).unwrap();
4368 assert_eq!(message.role, Role::User);
4369 assert_eq!(message.content.len(), 2);
4370
4371 let mut iter = message.content.into_iter();
4372
4373 match iter.next().unwrap() {
4374 Content::Document { source, .. } => {
4375 assert_eq!(
4376 source,
4377 DocumentSource::Text {
4378 data: "Hello from a text file".to_string(),
4379 media_type: PlainTextMediaType::Plain,
4380 }
4381 );
4382 }
4383 _ => panic!("Expected Document content"),
4384 }
4385
4386 match iter.next().unwrap() {
4387 Content::Text { text, .. } => {
4388 assert_eq!(text, "Summarize this document.");
4389 }
4390 _ => panic!("Expected Text content"),
4391 }
4392 }
4393
4394 #[test]
4395 fn test_assistant_reasoning_multiblock_to_anthropic_content() {
4396 let reasoning = message::Reasoning {
4397 id: None,
4398 content: vec![
4399 message::ReasoningContent::Text {
4400 text: "step one".to_string(),
4401 signature: Some("sig-1".to_string()),
4402 },
4403 message::ReasoningContent::Summary("summary".to_string()),
4404 message::ReasoningContent::Text {
4405 text: "step two".to_string(),
4406 signature: Some("sig-2".to_string()),
4407 },
4408 message::ReasoningContent::Redacted {
4409 data: "redacted block".to_string(),
4410 },
4411 ],
4412 };
4413
4414 let msg = message::Message::Assistant {
4415 id: None,
4416 content: OneOrMany::one(message::AssistantContent::Reasoning(reasoning)),
4417 };
4418 let converted: Message = msg.try_into().expect("convert assistant message");
4419 let converted_content = converted.content.iter().cloned().collect::<Vec<_>>();
4420
4421 assert_eq!(converted.role, Role::Assistant);
4422 assert_eq!(converted_content.len(), 4);
4423 assert!(matches!(
4424 converted_content.first(),
4425 Some(Content::Thinking { thinking, signature: Some(signature) })
4426 if thinking == "step one" && signature == "sig-1"
4427 ));
4428 assert!(matches!(
4429 converted_content.get(1),
4430 Some(Content::Thinking { thinking, signature: None }) if thinking == "summary"
4431 ));
4432 assert!(matches!(
4433 converted_content.get(2),
4434 Some(Content::Thinking { thinking, signature: Some(signature) })
4435 if thinking == "step two" && signature == "sig-2"
4436 ));
4437 assert!(matches!(
4438 converted_content.get(3),
4439 Some(Content::RedactedThinking { data }) if data == "redacted block"
4440 ));
4441 }
4442
4443 #[test]
4444 fn test_redacted_thinking_content_to_assistant_reasoning() {
4445 let content = Content::RedactedThinking {
4446 data: "opaque-redacted".to_string(),
4447 };
4448 let converted: message::AssistantContent =
4449 content.try_into().expect("convert redacted thinking");
4450
4451 assert!(matches!(
4452 converted,
4453 message::AssistantContent::Reasoning(message::Reasoning { content, .. })
4454 if matches!(
4455 content.first(),
4456 Some(message::ReasoningContent::Redacted { data }) if data == "opaque-redacted"
4457 )
4458 ));
4459 }
4460
4461 #[test]
4462 fn test_assistant_encrypted_reasoning_maps_to_redacted_thinking() {
4463 let reasoning = message::Reasoning {
4464 id: None,
4465 content: vec![message::ReasoningContent::Encrypted(
4466 "ciphertext".to_string(),
4467 )],
4468 };
4469 let msg = message::Message::Assistant {
4470 id: None,
4471 content: OneOrMany::one(message::AssistantContent::Reasoning(reasoning)),
4472 };
4473
4474 let converted: Message = msg.try_into().expect("convert assistant message");
4475 let converted_content = converted.content.iter().cloned().collect::<Vec<_>>();
4476
4477 assert_eq!(converted_content.len(), 1);
4478 assert!(matches!(
4479 converted_content.first(),
4480 Some(Content::RedactedThinking { data }) if data == "ciphertext"
4481 ));
4482 }
4483
4484 #[test]
4485 fn empty_end_turn_response_normalizes_to_empty_text_choice() {
4486 let response = CompletionResponse {
4487 content: vec![],
4488 id: "msg_123".to_string(),
4489 model: CLAUDE_SONNET_4_6.to_string(),
4490 role: "assistant".to_string(),
4491 stop_reason: Some("end_turn".to_string()),
4492 stop_sequence: None,
4493 usage: Usage {
4494 input_tokens: 7,
4495 cache_read_input_tokens: None,
4496 cache_creation_input_tokens: None,
4497 output_tokens: 2,
4498 },
4499 };
4500
4501 let parsed: completion::CompletionResponse<CompletionResponse> = response
4502 .try_into()
4503 .expect("empty end_turn should not error");
4504
4505 assert_eq!(parsed.choice.len(), 1);
4506 assert!(matches!(
4507 parsed.choice.first(),
4508 completion::AssistantContent::Text(text) if text.text.is_empty()
4509 ));
4510 }
4511
4512 #[test]
4513 fn empty_non_end_turn_response_still_errors() {
4514 let response = CompletionResponse {
4515 content: vec![],
4516 id: "msg_123".to_string(),
4517 model: CLAUDE_SONNET_4_6.to_string(),
4518 role: "assistant".to_string(),
4519 stop_reason: Some("tool_use".to_string()),
4520 stop_sequence: None,
4521 usage: Usage {
4522 input_tokens: 7,
4523 cache_read_input_tokens: None,
4524 cache_creation_input_tokens: None,
4525 output_tokens: 2,
4526 },
4527 };
4528
4529 let err = completion::CompletionResponse::<CompletionResponse>::try_from(response)
4530 .expect_err("empty non-end_turn should remain an error");
4531
4532 assert!(matches!(
4533 err,
4534 CompletionError::ResponseError(message) if message == EMPTY_RESPONSE_ERROR
4535 ));
4536 }
4537
4538 #[test]
4539 fn test_tool_result_content_in_message_roundtrip() {
4540 let message_json = r#"{
4541 "role": "user",
4542 "content": [
4543 {
4544 "type": "tool_result",
4545 "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
4546 "content": [
4547 {
4548 "type": "text",
4549 "text": "Here is the screenshot:"
4550 },
4551 {
4552 "type": "image",
4553 "source": {
4554 "type": "base64",
4555 "media_type": "image/png",
4556 "data": "iVBORw0KGgo..."
4557 }
4558 }
4559 ]
4560 }
4561 ]
4562 }"#;
4563
4564 let message: Message = serde_json::from_str(message_json).unwrap();
4565 let serialized = serde_json::to_value(&message).unwrap();
4566
4567 let tool_result = &serialized["content"][0];
4568 assert_eq!(tool_result["type"], "tool_result");
4569
4570 let image_content = &tool_result["content"][1];
4571 assert_eq!(image_content["type"], "image");
4572 assert_eq!(image_content["source"]["type"], "base64");
4573 assert_eq!(image_content["source"]["media_type"], "image/png");
4574 assert_eq!(image_content["source"]["data"], "iVBORw0KGgo...");
4575 }
4576
4577 #[test]
4582 fn document_serializes_citations_and_metadata() {
4583 let doc = Content::Document {
4584 source: DocumentSource::Text {
4585 data: "hello".into(),
4586 media_type: PlainTextMediaType::Plain,
4587 },
4588 title: Some("My Doc".into()),
4589 context: None,
4590 citations: Some(CitationsConfig { enabled: true }),
4591 cache_control: None,
4592 };
4593 let value = serde_json::to_value(&doc).unwrap();
4594 assert_eq!(value["citations"]["enabled"], true);
4595 assert_eq!(value["title"], "My Doc");
4596 assert!(
4597 value.get("context").is_none(),
4598 "context should be skipped when None"
4599 );
4600 }
4601
4602 #[test]
4603 fn text_serializes_without_citations_when_empty() {
4604 let content = Content::Text {
4605 text: "hello".into(),
4606 citations: Vec::new(),
4607 cache_control: None,
4608 };
4609 let value = serde_json::to_value(&content).unwrap();
4610 assert!(
4611 value.get("citations").is_none(),
4612 "empty citations vec must be skipped"
4613 );
4614 }
4615
4616 #[test]
4617 fn text_deserializes_char_location_citation() {
4618 let value = json!({
4619 "type": "text",
4620 "text": "the grass is green",
4621 "citations": [{
4622 "type": "char_location",
4623 "cited_text": "The grass is green.",
4624 "document_index": 0,
4625 "document_title": "Example",
4626 "start_char_index": 0,
4627 "end_char_index": 20
4628 }]
4629 });
4630 let parsed: Content = serde_json::from_value(value).unwrap();
4631 let Content::Text { citations, .. } = parsed else {
4632 panic!("expected Content::Text");
4633 };
4634 assert_eq!(citations.len(), 1);
4635 let Citation::CharLocation {
4636 start_char_index,
4637 end_char_index,
4638 ..
4639 } = &citations[0]
4640 else {
4641 panic!("expected CharLocation");
4642 };
4643 assert_eq!(*start_char_index, 0);
4644 assert_eq!(*end_char_index, 20);
4645 }
4646
4647 #[test]
4648 fn text_deserializes_search_result_location_citation() {
4649 let value = json!({
4650 "type": "text",
4651 "text": "API keys are required.",
4652 "citations": [{
4653 "type": "search_result_location",
4654 "cited_text": "All API requests must include an API key.",
4655 "source": "https://docs.example.com/api-reference",
4656 "title": "API Reference",
4657 "search_result_index": 0,
4658 "start_block_index": 0,
4659 "end_block_index": 1
4660 }]
4661 });
4662
4663 let parsed: Content = serde_json::from_value(value).unwrap();
4664 let Content::Text { citations, .. } = parsed else {
4665 panic!("expected Content::Text");
4666 };
4667
4668 assert!(matches!(
4669 &citations[0],
4670 Citation::SearchResultLocation {
4671 source,
4672 title: Some(title),
4673 search_result_index: 0,
4674 start_block_index: 0,
4675 end_block_index: 1,
4676 ..
4677 } if source == "https://docs.example.com/api-reference" && title == "API Reference"
4678 ));
4679 }
4680
4681 #[test]
4682 fn text_deserializes_web_search_result_location_citation() {
4683 let value = json!({
4684 "type": "text",
4685 "text": "Claude Shannon worked at Bell Labs.",
4686 "citations": [{
4687 "type": "web_search_result_location",
4688 "cited_text": "Claude Shannon was a mathematician.",
4689 "url": "https://example.com/shannon",
4690 "title": "Claude Shannon",
4691 "encrypted_index": "encrypted-reference"
4692 }]
4693 });
4694
4695 let parsed: Content = serde_json::from_value(value).unwrap();
4696 let Content::Text { citations, .. } = parsed else {
4697 panic!("expected Content::Text");
4698 };
4699
4700 assert!(matches!(
4701 &citations[0],
4702 Citation::WebSearchResultLocation {
4703 url,
4704 title,
4705 encrypted_index,
4706 ..
4707 } if url == "https://example.com/shannon"
4708 && title.as_deref() == Some("Claude Shannon")
4709 && encrypted_index == "encrypted-reference"
4710 ));
4711 }
4712
4713 #[test]
4714 fn text_deserializes_web_search_result_location_citation_with_null_title() {
4715 let value = json!({
4716 "type": "text",
4717 "text": "Claude Shannon worked at Bell Labs.",
4718 "citations": [{
4719 "type": "web_search_result_location",
4720 "cited_text": "Claude Shannon was a mathematician.",
4721 "url": "https://example.com/shannon",
4722 "title": null,
4723 "encrypted_index": "encrypted-reference"
4724 }]
4725 });
4726
4727 let parsed: Content = serde_json::from_value(value).unwrap();
4728 let Content::Text { citations, .. } = parsed else {
4729 panic!("expected Content::Text");
4730 };
4731
4732 let Citation::WebSearchResultLocation { title, .. } = &citations[0] else {
4733 panic!("expected WebSearchResultLocation");
4734 };
4735 assert_eq!(title, &None);
4736
4737 let serialized = serde_json::to_value(&citations[0]).unwrap();
4738 assert!(serialized.get("title").is_some());
4739 assert!(serialized["title"].is_null());
4740 }
4741
4742 #[test]
4743 fn web_search_response_preserves_raw_blocks_and_citations() {
4744 let value = json!({
4745 "id": "msg_web_search",
4746 "model": CLAUDE_SONNET_4_6,
4747 "role": "assistant",
4748 "stop_reason": "end_turn",
4749 "stop_sequence": null,
4750 "usage": {
4751 "input_tokens": 10,
4752 "output_tokens": 20
4753 },
4754 "content": [
4755 {
4756 "type": "server_tool_use",
4757 "id": "srvtoolu_01",
4758 "name": "web_search",
4759 "input": {
4760 "query": "claude shannon birth date"
4761 }
4762 },
4763 {
4764 "type": "web_search_tool_result",
4765 "tool_use_id": "srvtoolu_01",
4766 "content": [
4767 {
4768 "type": "web_search_result",
4769 "url": "https://example.com/shannon",
4770 "title": "Claude Shannon",
4771 "encrypted_content": "encrypted-content",
4772 "page_age": "April 30, 2025"
4773 }
4774 ]
4775 },
4776 {
4777 "type": "text",
4778 "text": "Claude Shannon was born on April 30, 1916.",
4779 "citations": [{
4780 "type": "web_search_result_location",
4781 "cited_text": "Claude Shannon was born on April 30, 1916.",
4782 "url": "https://example.com/shannon",
4783 "title": "Claude Shannon",
4784 "encrypted_index": "encrypted-index"
4785 }]
4786 }
4787 ]
4788 });
4789
4790 let response: CompletionResponse = serde_json::from_value(value).unwrap();
4791 let converted: completion::CompletionResponse<CompletionResponse> =
4792 response.try_into().unwrap();
4793 assert_eq!(converted.choice.len(), 3);
4794 assert_eq!(
4795 converted.raw_response.get_text_response().as_deref(),
4796 Some("Claude Shannon was born on April 30, 1916.")
4797 );
4798
4799 let items = converted.choice.iter().collect::<Vec<_>>();
4800 let message::AssistantContent::Text(server_tool_use) = items[0] else {
4801 panic!("expected raw server_tool_use metadata");
4802 };
4803 assert_eq!(server_tool_use.text, "");
4804 assert_eq!(
4805 server_tool_use.additional_params.as_ref().unwrap()[ANTHROPIC_RAW_CONTENT_KEY]["type"],
4806 "server_tool_use"
4807 );
4808
4809 let message::AssistantContent::Text(web_search_result) = items[1] else {
4810 panic!("expected raw web_search_tool_result metadata");
4811 };
4812 assert_eq!(
4813 web_search_result.additional_params.as_ref().unwrap()[ANTHROPIC_RAW_CONTENT_KEY]["content"]
4814 [0]["encrypted_content"],
4815 "encrypted-content"
4816 );
4817
4818 let message::AssistantContent::Text(answer) = items[2] else {
4819 panic!("expected text answer");
4820 };
4821 let citations = anthropic_citations(answer).unwrap();
4822 assert!(matches!(
4823 citations.first(),
4824 Some(Citation::WebSearchResultLocation {
4825 encrypted_index,
4826 ..
4827 }) if encrypted_index == "encrypted-index"
4828 ));
4829
4830 let round_trip: Message = message::Message::Assistant {
4831 id: converted.message_id.clone(),
4832 content: converted.choice,
4833 }
4834 .try_into()
4835 .unwrap();
4836
4837 let round_trip_items = round_trip.content.iter().collect::<Vec<_>>();
4838 assert!(matches!(
4839 round_trip_items.first(),
4840 Some(Content::ServerToolUse { id, name, input })
4841 if id == "srvtoolu_01"
4842 && name == "web_search"
4843 && input["query"] == "claude shannon birth date"
4844 ));
4845 assert!(matches!(
4846 round_trip_items.get(1),
4847 Some(Content::WebSearchToolResult {
4848 tool_use_id,
4849 content
4850 }) if tool_use_id == "srvtoolu_01"
4851 && content[0]["encrypted_content"] == "encrypted-content"
4852 ));
4853 }
4854
4855 #[test]
4856 fn web_search_tool_result_error_object_is_preserved_raw() {
4857 let value = json!({
4858 "id": "msg_web_search_error",
4859 "model": CLAUDE_SONNET_4_6,
4860 "role": "assistant",
4861 "stop_reason": "end_turn",
4862 "stop_sequence": null,
4863 "usage": {
4864 "input_tokens": 10,
4865 "output_tokens": 2
4866 },
4867 "content": [{
4868 "type": "web_search_tool_result",
4869 "tool_use_id": "srvtoolu_01",
4870 "content": {
4871 "type": "web_search_tool_result_error",
4872 "error_code": "max_uses_exceeded"
4873 }
4874 }]
4875 });
4876
4877 let response: CompletionResponse = serde_json::from_value(value).unwrap();
4878 let converted: completion::CompletionResponse<CompletionResponse> =
4879 response.try_into().unwrap();
4880 let message::AssistantContent::Text(web_search_result) = converted.choice.first() else {
4881 panic!("expected raw web_search_tool_result metadata");
4882 };
4883
4884 let raw_content =
4885 &web_search_result.additional_params.as_ref().unwrap()[ANTHROPIC_RAW_CONTENT_KEY];
4886 assert_eq!(raw_content["type"], "web_search_tool_result");
4887 assert_eq!(raw_content["content"]["error_code"], "max_uses_exceeded");
4888 assert_eq!(
4889 raw_content["content"]["type"],
4890 "web_search_tool_result_error"
4891 );
4892
4893 let round_trip: Message = message::Message::Assistant {
4894 id: converted.message_id,
4895 content: converted.choice,
4896 }
4897 .try_into()
4898 .unwrap();
4899
4900 assert!(matches!(
4901 round_trip.content.first(),
4902 Content::WebSearchToolResult {
4903 tool_use_id,
4904 content
4905 } if tool_use_id == "srvtoolu_01"
4906 && content["error_code"] == "max_uses_exceeded"
4907 ));
4908 }
4909
4910 #[test]
4911 fn text_deserializes_unknown_citation_without_failing() {
4912 let value = json!({
4913 "type": "text",
4914 "text": "future citation",
4915 "citations": [{
4916 "type": "future_location",
4917 "cited_text": "future text",
4918 "new_field": "kept"
4919 }]
4920 });
4921
4922 let parsed: Content = serde_json::from_value(value).unwrap();
4923 let Content::Text { citations, .. } = parsed else {
4924 panic!("expected Content::Text");
4925 };
4926
4927 assert!(matches!(
4928 &citations[0],
4929 Citation::Unknown(raw)
4930 if raw["type"] == "future_location" && raw["new_field"] == "kept"
4931 ));
4932 }
4933
4934 #[test]
4935 fn page_location_citation_roundtrips() {
4936 let citation = Citation::PageLocation {
4937 cited_text: "Water is essential for life.".into(),
4938 document_index: 1,
4939 document_title: Some("PDF Doc".into()),
4940 start_page_number: 5,
4941 end_page_number: 6,
4942 };
4943 let value = serde_json::to_value(&citation).unwrap();
4944 assert_eq!(value["type"], "page_location");
4945 assert_eq!(value["start_page_number"], 5);
4946 let back: Citation = serde_json::from_value(value).unwrap();
4947 assert_eq!(back, citation);
4948 }
4949
4950 #[test]
4951 fn content_block_location_citation_roundtrips() {
4952 let citation = Citation::ContentBlockLocation {
4953 cited_text: "These are important findings.".into(),
4954 document_index: 2,
4955 document_title: None,
4956 start_block_index: 0,
4957 end_block_index: 1,
4958 };
4959 let value = serde_json::to_value(&citation).unwrap();
4960 assert_eq!(value["type"], "content_block_location");
4961 assert!(value.get("document_title").is_none());
4962 let back: Citation = serde_json::from_value(value).unwrap();
4963 assert_eq!(back, citation);
4964 }
4965
4966 #[test]
4967 fn anthropic_citations_extracts_from_additional_params() {
4968 let text = message::Text {
4969 text: "the grass is green".into(),
4970 additional_params: Some(json!({
4971 "citations": [{
4972 "type": "char_location",
4973 "cited_text": "The grass is green.",
4974 "document_index": 0,
4975 "start_char_index": 0,
4976 "end_char_index": 20
4977 }]
4978 })),
4979 };
4980 let citations = anthropic_citations(&text).unwrap();
4981 assert_eq!(citations.len(), 1);
4982 }
4983
4984 #[test]
4985 fn anthropic_citations_returns_empty_when_absent() {
4986 let text = message::Text::new("hello".to_string());
4987 assert!(anthropic_citations(&text).unwrap().is_empty());
4988 }
4989
4990 #[test]
4991 fn content_text_with_citations_survives_assistant_conversion() {
4992 let content = Content::Text {
4993 text: "the grass is green".into(),
4994 citations: vec![Citation::CharLocation {
4995 cited_text: "The grass is green.".into(),
4996 document_index: 0,
4997 document_title: None,
4998 start_char_index: 0,
4999 end_char_index: 20,
5000 }],
5001 cache_control: None,
5002 };
5003 let assistant: message::AssistantContent = content.try_into().unwrap();
5004 let message::AssistantContent::Text(text) = assistant else {
5005 panic!("expected text variant");
5006 };
5007 let recovered = anthropic_citations(&text).unwrap();
5008 assert_eq!(recovered.len(), 1);
5009 }
5010
5011 #[test]
5012 fn provider_text_response_concatenates_text_blocks_without_inserted_newlines() {
5013 let response = CompletionResponse {
5014 content: vec![
5015 Content::Text {
5016 text: "According to the document, ".into(),
5017 citations: Vec::new(),
5018 cache_control: None,
5019 },
5020 Content::Text {
5021 text: "the grass is green".into(),
5022 citations: Vec::new(),
5023 cache_control: None,
5024 },
5025 Content::Text {
5026 text: " and the sky is blue.".into(),
5027 citations: Vec::new(),
5028 cache_control: None,
5029 },
5030 ],
5031 id: "msg_1".into(),
5032 model: "claude-test".into(),
5033 role: "assistant".into(),
5034 stop_reason: Some("end_turn".into()),
5035 stop_sequence: None,
5036 usage: Usage {
5037 input_tokens: 1,
5038 cache_read_input_tokens: None,
5039 cache_creation_input_tokens: None,
5040 output_tokens: 1,
5041 },
5042 };
5043
5044 assert_eq!(
5045 response.get_text_response().as_deref(),
5046 Some("According to the document, the grass is green and the sky is blue.")
5047 );
5048 }
5049
5050 #[test]
5051 fn assistant_text_citations_survive_anthropic_request_conversion() {
5052 let assistant = message::Message::Assistant {
5053 id: None,
5054 content: OneOrMany::one(message::AssistantContent::Text(message::Text {
5055 text: "the grass is green".into(),
5056 additional_params: Some(json!({
5057 "citations": [{
5058 "type": "char_location",
5059 "cited_text": "The grass is green.",
5060 "document_index": 0,
5061 "start_char_index": 0,
5062 "end_char_index": 20
5063 }]
5064 })),
5065 })),
5066 };
5067
5068 let converted: Message = assistant.try_into().unwrap();
5069 let Content::Text {
5070 citations, text, ..
5071 } = converted.content.first()
5072 else {
5073 panic!("expected assistant text content");
5074 };
5075
5076 assert_eq!(text, "the grass is green");
5077 assert_eq!(
5078 citations,
5079 vec![Citation::CharLocation {
5080 cited_text: "The grass is green.".into(),
5081 document_index: 0,
5082 document_title: None,
5083 start_char_index: 0,
5084 end_char_index: 20,
5085 }]
5086 );
5087 }
5088
5089 #[test]
5090 fn assistant_text_invalid_known_citations_are_rejected_for_anthropic_request_conversion() {
5091 let text = message::AssistantContent::Text(message::Text {
5092 text: "bad citation".into(),
5093 additional_params: Some(json!({
5094 "citations": [{
5095 "type": "char_location",
5096 "cited_text": "bad"
5097 }]
5098 })),
5099 });
5100
5101 let result = Content::try_from(text);
5102
5103 assert!(
5104 result.is_err(),
5105 "invalid Anthropic citation metadata should not be silently dropped"
5106 );
5107 }
5108
5109 #[test]
5110 fn document_additional_params_forward_to_anthropic_document() {
5111 let doc = message::UserContent::Document(message::Document {
5112 data: message::DocumentSourceKind::String("Hello world.".into()),
5113 media_type: Some(message::DocumentMediaType::TXT),
5114 additional_params: Some(json!({
5115 "title": "Doc1",
5116 "context": "ctx",
5117 "citations": { "enabled": true }
5118 })),
5119 });
5120 let msg = message::Message::User {
5121 content: OneOrMany::one(doc),
5122 };
5123 let converted: Message = msg.try_into().unwrap();
5124 let block = converted.content.first();
5125 let Content::Document {
5126 title,
5127 context,
5128 citations,
5129 ..
5130 } = block
5131 else {
5132 panic!("expected Content::Document");
5133 };
5134 assert_eq!(title.as_deref(), Some("Doc1"));
5135 assert_eq!(context.as_deref(), Some("ctx"));
5136 assert_eq!(citations, Some(CitationsConfig { enabled: true }));
5137 }
5138
5139 fn assert_reverse_document_metadata(
5140 source: DocumentSource,
5141 expected_data: DocumentSourceKind,
5142 expected_media_type: Option<message::DocumentMediaType>,
5143 ) -> message::Message {
5144 let provider_message = Message {
5145 role: Role::User,
5146 content: OneOrMany::one(Content::Document {
5147 source,
5148 title: Some("Doc1".into()),
5149 context: Some("ctx".into()),
5150 citations: Some(CitationsConfig { enabled: true }),
5151 cache_control: None,
5152 }),
5153 };
5154
5155 let generic: message::Message = provider_message.try_into().unwrap();
5156 let message::Message::User { content } = &generic else {
5157 panic!("expected generic user message");
5158 };
5159 let message::UserContent::Document(document) = content.first() else {
5160 panic!("expected generic document");
5161 };
5162
5163 assert_eq!(document.data, expected_data);
5164 assert_eq!(document.media_type, expected_media_type);
5165 let additional_params = document
5166 .additional_params
5167 .as_ref()
5168 .expect("expected Anthropic document metadata");
5169 assert_eq!(additional_params["title"], "Doc1");
5170 assert_eq!(additional_params["context"], "ctx");
5171 assert_eq!(additional_params["citations"]["enabled"], true);
5172
5173 generic
5174 }
5175
5176 #[test]
5177 fn anthropic_document_metadata_survives_reverse_conversion_for_all_sources() {
5178 assert_reverse_document_metadata(
5179 DocumentSource::Text {
5180 data: "Hello world.".into(),
5181 media_type: PlainTextMediaType::Plain,
5182 },
5183 DocumentSourceKind::String("Hello world.".into()),
5184 Some(message::DocumentMediaType::TXT),
5185 );
5186 assert_reverse_document_metadata(
5187 DocumentSource::Base64 {
5188 data: "base64-pdf".into(),
5189 media_type: DocumentFormat::PDF,
5190 },
5191 DocumentSourceKind::String("base64-pdf".into()),
5192 Some(message::DocumentMediaType::PDF),
5193 );
5194 assert_reverse_document_metadata(
5195 DocumentSource::Url {
5196 url: "https://example.com/doc.pdf".into(),
5197 },
5198 DocumentSourceKind::Url("https://example.com/doc.pdf".into()),
5199 None,
5200 );
5201 assert_reverse_document_metadata(
5202 DocumentSource::File {
5203 file_id: "file_abc".into(),
5204 },
5205 DocumentSourceKind::FileId("file_abc".into()),
5206 None,
5207 );
5208 }
5209
5210 #[test]
5211 fn anthropic_document_metadata_survives_reverse_round_trip() {
5212 let provider_message = Message {
5213 role: Role::User,
5214 content: OneOrMany::one(Content::Document {
5215 source: DocumentSource::Text {
5216 data: "Hello world.".into(),
5217 media_type: PlainTextMediaType::Plain,
5218 },
5219 title: Some("Doc1".into()),
5220 context: Some("ctx".into()),
5221 citations: Some(CitationsConfig { enabled: true }),
5222 cache_control: None,
5223 }),
5224 };
5225
5226 let generic: message::Message = provider_message.try_into().unwrap();
5227 let message::Message::User { content } = &generic else {
5228 panic!("expected generic user message");
5229 };
5230 let message::UserContent::Document(document) = content.first() else {
5231 panic!("expected generic document");
5232 };
5233 let additional_params = document
5234 .additional_params
5235 .as_ref()
5236 .expect("expected Anthropic document metadata");
5237 assert_eq!(additional_params["title"], "Doc1");
5238 assert_eq!(additional_params["context"], "ctx");
5239 assert_eq!(additional_params["citations"]["enabled"], true);
5240
5241 let round_trip: Message = generic.try_into().unwrap();
5242 let Content::Document {
5243 title,
5244 context,
5245 citations,
5246 ..
5247 } = round_trip.content.first()
5248 else {
5249 panic!("expected Anthropic document");
5250 };
5251 assert_eq!(title.as_deref(), Some("Doc1"));
5252 assert_eq!(context.as_deref(), Some("ctx"));
5253 assert_eq!(citations, Some(CitationsConfig { enabled: true }));
5254 }
5255
5256 #[test]
5257 fn anthropic_document_empty_metadata_stays_none_on_reverse_conversion() {
5258 let provider_message = Message {
5259 role: Role::User,
5260 content: OneOrMany::one(Content::Document {
5261 source: DocumentSource::Text {
5262 data: "Hello world.".into(),
5263 media_type: PlainTextMediaType::Plain,
5264 },
5265 title: None,
5266 context: None,
5267 citations: None,
5268 cache_control: None,
5269 }),
5270 };
5271
5272 let generic: message::Message = provider_message.try_into().unwrap();
5273 let message::Message::User { content } = &generic else {
5274 panic!("expected generic user message");
5275 };
5276 let message::UserContent::Document(document) = content.first() else {
5277 panic!("expected generic document");
5278 };
5279
5280 assert_eq!(document.additional_params, None);
5281 }
5282}