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