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) -> 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 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 let chat_history = req.chat_history_with_documents();
2269
2270 let Some(max_tokens) = req.max_tokens else {
2272 return Err(CompletionError::RequestError(
2273 "`max_tokens` must be set for Anthropic".into(),
2274 ));
2275 };
2276
2277 let (history_system, chat_history) = split_system_messages_from_history(
2278 chat_history,
2279 supports_mid_conversation_system_messages(model),
2280 );
2281 let mut full_history = vec![];
2282 full_history.extend(chat_history);
2283
2284 let mut messages = full_history
2285 .into_iter()
2286 .map(Message::try_from)
2287 .collect::<Result<Vec<Message>, _>>()?;
2288
2289 let mut additional_params_payload = req
2290 .additional_params
2291 .take()
2292 .unwrap_or(serde_json::Value::Null);
2293 let top_level_cache_control = resolve_top_level_cache_control(
2294 automatic_caching,
2295 automatic_caching_ttl,
2296 &mut additional_params_payload,
2297 )?;
2298 let mut tools = build_tool_definitions(req.tools, &mut additional_params_payload)?;
2299
2300 let mut system = if let Some(preamble) = req.preamble {
2302 if preamble.is_empty() {
2303 vec![]
2304 } else {
2305 vec![SystemContent::Text {
2306 text: preamble,
2307 cache_control: None,
2308 }]
2309 }
2310 } else {
2311 vec![]
2312 };
2313 system.extend(history_system);
2314
2315 apply_prompt_cache_control(
2316 &mut system,
2317 &mut messages,
2318 &mut tools,
2319 prompt_caching,
2320 top_level_cache_control.as_ref(),
2321 )?;
2322
2323 let output_config = if let Some(schema) = req.output_schema {
2324 let mut schema_value = schema.to_value();
2325 sanitize_schema(&mut schema_value);
2326 Some(OutputConfig {
2327 format: OutputFormat::JsonSchema {
2328 schema: schema_value,
2329 },
2330 })
2331 } else {
2332 None
2333 };
2334
2335 Ok(Self {
2336 model: model.to_string(),
2337 messages,
2338 max_tokens,
2339 system,
2340 temperature: req.temperature,
2341 tool_choice: req.tool_choice.and_then(|x| ToolChoice::try_from(x).ok()),
2342 tools,
2343 output_config,
2344 cache_control: top_level_cache_control,
2346 additional_params: if additional_params_payload.is_null() {
2347 None
2348 } else {
2349 Some(additional_params_payload)
2350 },
2351 })
2352 }
2353}
2354
2355pub(super) fn extract_tools_from_additional_params(
2356 additional_params: &mut serde_json::Value,
2357) -> Result<Vec<serde_json::Value>, CompletionError> {
2358 if let Some(map) = additional_params.as_object_mut()
2359 && let Some(raw_tools) = map.remove("tools")
2360 {
2361 return serde_json::from_value::<Vec<serde_json::Value>>(raw_tools).map_err(|err| {
2362 CompletionError::RequestError(
2363 format!("Invalid Anthropic `additional_params.tools` payload: {err}").into(),
2364 )
2365 });
2366 }
2367
2368 Ok(Vec::new())
2369}
2370
2371pub(super) fn build_tool_definitions(
2372 tools: Vec<completion::ToolDefinition>,
2373 additional_params_payload: &mut serde_json::Value,
2374) -> Result<Vec<serde_json::Value>, CompletionError> {
2375 let mut additional_tools = extract_tools_from_additional_params(additional_params_payload)?;
2376
2377 let mut tools = tools
2378 .into_iter()
2379 .map(|tool| ToolDefinition {
2380 name: tool.name,
2381 description: Some(tool.description),
2382 input_schema: tool.parameters,
2383 cache_control: None,
2384 })
2385 .map(serde_json::to_value)
2386 .collect::<Result<Vec<_>, _>>()?;
2387 tools.append(&mut additional_tools);
2388
2389 Ok(tools)
2390}
2391
2392impl<Ext, T> completion::CompletionModel for GenericCompletionModel<Ext, T>
2393where
2394 T: HttpClientExt + Clone + Default + WasmCompatSend + WasmCompatSync + 'static,
2395 Ext: AnthropicCompatibleProvider + Clone + WasmCompatSend + WasmCompatSync + 'static,
2396{
2397 type Response = CompletionResponse;
2398 type StreamingResponse = StreamingCompletionResponse;
2399 type Client = crate::client::Client<Ext, T>;
2400
2401 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
2402 Self::new(client.clone(), model.into())
2403 }
2404
2405 async fn completion(
2406 &self,
2407 mut completion_request: completion::CompletionRequest,
2408 ) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
2409 let request_model = completion_request
2410 .model
2411 .clone()
2412 .unwrap_or_else(|| self.model.clone());
2413 let span = if tracing::Span::current().is_disabled() {
2414 info_span!(
2415 target: "rig::completions",
2416 "chat",
2417 gen_ai.operation.name = "chat",
2418 gen_ai.provider.name = Ext::PROVIDER_NAME,
2419 gen_ai.request.model = &request_model,
2420 gen_ai.system_instructions = &completion_request.preamble,
2421 gen_ai.response.id = tracing::field::Empty,
2422 gen_ai.response.model = tracing::field::Empty,
2423 gen_ai.usage.output_tokens = tracing::field::Empty,
2424 gen_ai.usage.input_tokens = tracing::field::Empty,
2425 gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
2426 gen_ai.usage.cache_creation.input_tokens = tracing::field::Empty,
2427 )
2428 } else {
2429 tracing::Span::current()
2430 };
2431
2432 if completion_request.max_tokens.is_none() {
2434 if let Some(tokens) = self.default_max_tokens {
2435 completion_request.max_tokens = Some(tokens);
2436 } else {
2437 return Err(CompletionError::RequestError(
2438 "`max_tokens` must be set for Anthropic".into(),
2439 ));
2440 }
2441 }
2442
2443 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
2444 model: &request_model,
2445 request: completion_request,
2446 prompt_caching: self.prompt_caching,
2447 automatic_caching: self.automatic_caching,
2448 automatic_caching_ttl: self.automatic_caching_ttl.clone(),
2449 })?;
2450
2451 if enabled!(Level::TRACE) {
2452 tracing::trace!(
2453 target: "rig::completions",
2454 "Anthropic completion request: {}",
2455 serde_json::to_string_pretty(&request)?
2456 );
2457 }
2458
2459 async move {
2460 let request: Vec<u8> = serde_json::to_vec(&request)?;
2461
2462 let req = self
2463 .client
2464 .post("/v1/messages")?
2465 .body(request)
2466 .map_err(|e| CompletionError::HttpError(e.into()))?;
2467
2468 let response = self
2469 .client
2470 .send::<_, Bytes>(req)
2471 .await
2472 .map_err(CompletionError::HttpError)?;
2473
2474 if response.status().is_success() {
2475 match serde_json::from_slice::<ApiResponse<CompletionResponse>>(
2476 response
2477 .into_body()
2478 .await
2479 .map_err(CompletionError::HttpError)?
2480 .to_vec()
2481 .as_slice(),
2482 )? {
2483 ApiResponse::Message(completion) => {
2484 let span = tracing::Span::current();
2485 span.record_response_metadata(&completion);
2486 span.record_token_usage(&completion.usage);
2487 if enabled!(Level::TRACE) {
2488 tracing::trace!(
2489 target: "rig::completions",
2490 "Anthropic completion response: {}",
2491 serde_json::to_string_pretty(&completion)?
2492 );
2493 }
2494 completion.try_into()
2495 }
2496 ApiResponse::Error(ApiErrorResponse { message }) => {
2497 Err(CompletionError::ResponseError(message))
2498 }
2499 }
2500 } else {
2501 let text: String = String::from_utf8_lossy(
2502 &response
2503 .into_body()
2504 .await
2505 .map_err(CompletionError::HttpError)?,
2506 )
2507 .into();
2508 Err(CompletionError::ProviderError(text))
2509 }
2510 }
2511 .instrument(span)
2512 .await
2513 }
2514
2515 async fn stream(
2516 &self,
2517 request: CompletionRequest,
2518 ) -> Result<
2519 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
2520 CompletionError,
2521 > {
2522 GenericCompletionModel::stream(self, request).await
2523 }
2524}
2525
2526#[derive(Debug, Deserialize)]
2527struct ApiErrorResponse {
2528 message: String,
2529}
2530
2531#[derive(Debug, Deserialize)]
2532#[serde(tag = "type", rename_all = "snake_case")]
2533enum ApiResponse<T> {
2534 Message(T),
2535 Error(ApiErrorResponse),
2536}
2537
2538#[cfg(test)]
2539mod tests {
2540 use super::*;
2541 use serde_json::json;
2542 use serde_path_to_error::deserialize;
2543
2544 #[test]
2545 fn current_model_default_max_tokens_match_anthropic_limits() {
2546 assert_eq!(default_max_tokens_for_model(CLAUDE_OPUS_4_8), Some(128_000));
2547 assert_eq!(default_max_tokens_for_model(CLAUDE_OPUS_4_7), Some(128_000));
2548 assert_eq!(default_max_tokens_for_model(CLAUDE_OPUS_4_6), Some(128_000));
2549 assert_eq!(
2550 default_max_tokens_for_model(CLAUDE_SONNET_4_6),
2551 Some(64_000)
2552 );
2553 assert_eq!(default_max_tokens_for_model(CLAUDE_HAIKU_4_5), Some(64_000));
2554 }
2555
2556 #[test]
2557 fn unknown_model_uses_conservative_default_max_tokens_fallback() {
2558 assert_eq!(default_max_tokens_for_model("claude-unknown"), None);
2559 assert_eq!(default_max_tokens_with_fallback("claude-unknown"), 2_048);
2560 }
2561
2562 #[test]
2563 fn system_role_message_deserializes_and_round_trips() {
2564 let message: Message = serde_json::from_str(
2565 r#"
2566 {
2567 "role": "system",
2568 "content": "From now on, require explicit type annotations."
2569 }
2570 "#,
2571 )
2572 .unwrap();
2573
2574 assert_eq!(message.role, Role::System);
2575
2576 let generic: message::Message = message.try_into().unwrap();
2577 assert_eq!(
2578 generic,
2579 message::Message::System {
2580 content: "From now on, require explicit type annotations.".to_string()
2581 }
2582 );
2583
2584 let provider: Message = generic.try_into().unwrap();
2585 assert_eq!(provider.role, Role::System);
2586 }
2587
2588 #[test]
2589 fn test_deserialize_message() {
2590 let assistant_message_json = r#"
2591 {
2592 "role": "assistant",
2593 "content": "\n\nHello there, how may I assist you today?"
2594 }
2595 "#;
2596
2597 let assistant_message_json2 = r#"
2598 {
2599 "role": "assistant",
2600 "content": [
2601 {
2602 "type": "text",
2603 "text": "\n\nHello there, how may I assist you today?"
2604 },
2605 {
2606 "type": "tool_use",
2607 "id": "toolu_01A09q90qw90lq917835lq9",
2608 "name": "get_weather",
2609 "input": {"location": "San Francisco, CA"}
2610 }
2611 ]
2612 }
2613 "#;
2614
2615 let user_message_json = r#"
2616 {
2617 "role": "user",
2618 "content": [
2619 {
2620 "type": "image",
2621 "source": {
2622 "type": "base64",
2623 "media_type": "image/jpeg",
2624 "data": "/9j/4AAQSkZJRg..."
2625 }
2626 },
2627 {
2628 "type": "text",
2629 "text": "What is in this image?"
2630 },
2631 {
2632 "type": "tool_result",
2633 "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
2634 "content": "15 degrees"
2635 }
2636 ]
2637 }
2638 "#;
2639
2640 let assistant_message: Message = {
2641 let jd = &mut serde_json::Deserializer::from_str(assistant_message_json);
2642 deserialize(jd).unwrap_or_else(|err| {
2643 panic!("Deserialization error at {}: {}", err.path(), err);
2644 })
2645 };
2646
2647 let assistant_message2: Message = {
2648 let jd = &mut serde_json::Deserializer::from_str(assistant_message_json2);
2649 deserialize(jd).unwrap_or_else(|err| {
2650 panic!("Deserialization error at {}: {}", err.path(), err);
2651 })
2652 };
2653
2654 let user_message: Message = {
2655 let jd = &mut serde_json::Deserializer::from_str(user_message_json);
2656 deserialize(jd).unwrap_or_else(|err| {
2657 panic!("Deserialization error at {}: {}", err.path(), err);
2658 })
2659 };
2660
2661 let Message { role, content } = assistant_message;
2662 assert_eq!(role, Role::Assistant);
2663 assert_eq!(
2664 content.first(),
2665 Content::Text {
2666 text: "\n\nHello there, how may I assist you today?".to_owned(),
2667 citations: Vec::new(),
2668 cache_control: None,
2669 }
2670 );
2671
2672 let Message { role, content } = assistant_message2;
2673 {
2674 assert_eq!(role, Role::Assistant);
2675 assert_eq!(content.len(), 2);
2676
2677 let mut iter = content.into_iter();
2678
2679 match iter.next().unwrap() {
2680 Content::Text { text, .. } => {
2681 assert_eq!(text, "\n\nHello there, how may I assist you today?");
2682 }
2683 _ => panic!("Expected text content"),
2684 }
2685
2686 match iter.next().unwrap() {
2687 Content::ToolUse { id, name, input } => {
2688 assert_eq!(id, "toolu_01A09q90qw90lq917835lq9");
2689 assert_eq!(name, "get_weather");
2690 assert_eq!(input, json!({"location": "San Francisco, CA"}));
2691 }
2692 _ => panic!("Expected tool use content"),
2693 }
2694
2695 assert_eq!(iter.next(), None);
2696 }
2697
2698 let Message { role, content } = user_message;
2699 {
2700 assert_eq!(role, Role::User);
2701 assert_eq!(content.len(), 3);
2702
2703 let mut iter = content.into_iter();
2704
2705 match iter.next().unwrap() {
2706 Content::Image { source, .. } => {
2707 assert_eq!(
2708 source,
2709 ImageSource::Base64 {
2710 data: "/9j/4AAQSkZJRg...".to_owned(),
2711 media_type: ImageFormat::JPEG,
2712 }
2713 );
2714 }
2715 _ => panic!("Expected image content"),
2716 }
2717
2718 match iter.next().unwrap() {
2719 Content::Text { text, .. } => {
2720 assert_eq!(text, "What is in this image?");
2721 }
2722 _ => panic!("Expected text content"),
2723 }
2724
2725 match iter.next().unwrap() {
2726 Content::ToolResult {
2727 tool_use_id,
2728 content,
2729 is_error,
2730 ..
2731 } => {
2732 assert_eq!(tool_use_id, "toolu_01A09q90qw90lq917835lq9");
2733 assert_eq!(
2734 content.first(),
2735 ToolResultContent::Text {
2736 text: "15 degrees".to_owned()
2737 }
2738 );
2739 assert_eq!(is_error, None);
2740 }
2741 _ => panic!("Expected tool result content"),
2742 }
2743
2744 assert_eq!(iter.next(), None);
2745 }
2746 }
2747
2748 #[test]
2749 fn test_message_to_message_conversion() {
2750 let user_message: Message = serde_json::from_str(
2751 r#"
2752 {
2753 "role": "user",
2754 "content": [
2755 {
2756 "type": "image",
2757 "source": {
2758 "type": "base64",
2759 "media_type": "image/jpeg",
2760 "data": "/9j/4AAQSkZJRg..."
2761 }
2762 },
2763 {
2764 "type": "text",
2765 "text": "What is in this image?"
2766 },
2767 {
2768 "type": "document",
2769 "source": {
2770 "type": "base64",
2771 "data": "base64_encoded_pdf_data",
2772 "media_type": "application/pdf"
2773 }
2774 }
2775 ]
2776 }
2777 "#,
2778 )
2779 .unwrap();
2780
2781 let assistant_message = Message {
2782 role: Role::Assistant,
2783 content: OneOrMany::one(Content::ToolUse {
2784 id: "toolu_01A09q90qw90lq917835lq9".to_string(),
2785 name: "get_weather".to_string(),
2786 input: json!({"location": "San Francisco, CA"}),
2787 }),
2788 };
2789
2790 let tool_message = Message {
2791 role: Role::User,
2792 content: OneOrMany::one(Content::ToolResult {
2793 tool_use_id: "toolu_01A09q90qw90lq917835lq9".to_string(),
2794 content: OneOrMany::one(ToolResultContent::Text {
2795 text: "15 degrees".to_string(),
2796 }),
2797 is_error: None,
2798 cache_control: None,
2799 }),
2800 };
2801
2802 let converted_user_message: message::Message = user_message.clone().try_into().unwrap();
2803 let converted_assistant_message: message::Message =
2804 assistant_message.clone().try_into().unwrap();
2805 let converted_tool_message: message::Message = tool_message.clone().try_into().unwrap();
2806
2807 match converted_user_message.clone() {
2808 message::Message::User { content } => {
2809 assert_eq!(content.len(), 3);
2810
2811 let mut iter = content.into_iter();
2812
2813 match iter.next().unwrap() {
2814 message::UserContent::Image(message::Image {
2815 data, media_type, ..
2816 }) => {
2817 assert_eq!(data, DocumentSourceKind::base64("/9j/4AAQSkZJRg..."));
2818 assert_eq!(media_type, Some(message::ImageMediaType::JPEG));
2819 }
2820 _ => panic!("Expected image content"),
2821 }
2822
2823 match iter.next().unwrap() {
2824 message::UserContent::Text(message::Text { text, .. }) => {
2825 assert_eq!(text, "What is in this image?");
2826 }
2827 _ => panic!("Expected text content"),
2828 }
2829
2830 match iter.next().unwrap() {
2831 message::UserContent::Document(message::Document {
2832 data, media_type, ..
2833 }) => {
2834 assert_eq!(
2835 data,
2836 DocumentSourceKind::String("base64_encoded_pdf_data".into())
2837 );
2838 assert_eq!(media_type, Some(message::DocumentMediaType::PDF));
2839 }
2840 _ => panic!("Expected document content"),
2841 }
2842
2843 assert_eq!(iter.next(), None);
2844 }
2845 _ => panic!("Expected user message"),
2846 }
2847
2848 match converted_tool_message.clone() {
2849 message::Message::User { content } => {
2850 let message::ToolResult { id, content, .. } = match content.first() {
2851 message::UserContent::ToolResult(tool_result) => tool_result,
2852 _ => panic!("Expected tool result content"),
2853 };
2854 assert_eq!(id, "toolu_01A09q90qw90lq917835lq9");
2855 match content.first() {
2856 message::ToolResultContent::Text(message::Text { text, .. }) => {
2857 assert_eq!(text, "15 degrees");
2858 }
2859 _ => panic!("Expected text content"),
2860 }
2861 }
2862 _ => panic!("Expected tool result content"),
2863 }
2864
2865 match converted_assistant_message.clone() {
2866 message::Message::Assistant { content, .. } => {
2867 assert_eq!(content.len(), 1);
2868
2869 match content.first() {
2870 message::AssistantContent::ToolCall(message::ToolCall {
2871 id, function, ..
2872 }) => {
2873 assert_eq!(id, "toolu_01A09q90qw90lq917835lq9");
2874 assert_eq!(function.name, "get_weather");
2875 assert_eq!(function.arguments, json!({"location": "San Francisco, CA"}));
2876 }
2877 _ => panic!("Expected tool call content"),
2878 }
2879 }
2880 _ => panic!("Expected assistant message"),
2881 }
2882
2883 let original_user_message: Message = converted_user_message.try_into().unwrap();
2884 let original_assistant_message: Message = converted_assistant_message.try_into().unwrap();
2885 let original_tool_message: Message = converted_tool_message.try_into().unwrap();
2886
2887 assert_eq!(user_message, original_user_message);
2888 assert_eq!(assistant_message, original_assistant_message);
2889 assert_eq!(tool_message, original_tool_message);
2890 }
2891
2892 #[test]
2893 fn test_content_format_conversion() {
2894 use crate::completion::message::ContentFormat;
2895
2896 let source_type: SourceType = ContentFormat::Url.try_into().unwrap();
2897 assert_eq!(source_type, SourceType::URL);
2898
2899 let content_format: ContentFormat = SourceType::URL.into();
2900 assert_eq!(content_format, ContentFormat::Url);
2901
2902 let source_type: SourceType = ContentFormat::Base64.try_into().unwrap();
2903 assert_eq!(source_type, SourceType::BASE64);
2904
2905 let content_format: ContentFormat = SourceType::BASE64.into();
2906 assert_eq!(content_format, ContentFormat::Base64);
2907
2908 let source_type: SourceType = ContentFormat::String.try_into().unwrap();
2909 assert_eq!(source_type, SourceType::TEXT);
2910
2911 let content_format: ContentFormat = SourceType::TEXT.into();
2912 assert_eq!(content_format, ContentFormat::String);
2913 }
2914
2915 #[test]
2916 fn test_cache_control_serialization() {
2917 let system = SystemContent::Text {
2919 text: "You are a helpful assistant.".to_string(),
2920 cache_control: Some(CacheControl::ephemeral()),
2921 };
2922 let json = serde_json::to_string(&system).unwrap();
2923 assert!(json.contains(r#""cache_control":{"type":"ephemeral"}"#));
2924 assert!(json.contains(r#""type":"text""#));
2925
2926 let system_no_cache = SystemContent::Text {
2928 text: "Hello".to_string(),
2929 cache_control: None,
2930 };
2931 let json_no_cache = serde_json::to_string(&system_no_cache).unwrap();
2932 assert!(!json_no_cache.contains("cache_control"));
2933
2934 let content = Content::Text {
2936 text: "Test message".to_string(),
2937 citations: Vec::new(),
2938 cache_control: Some(CacheControl::ephemeral()),
2939 };
2940 let json_content = serde_json::to_string(&content).unwrap();
2941 assert!(json_content.contains(r#""cache_control":{"type":"ephemeral"}"#));
2942
2943 let mut system_vec = vec![SystemContent::Text {
2945 text: "System prompt".to_string(),
2946 cache_control: None,
2947 }];
2948 let mut messages = vec![
2949 Message {
2950 role: Role::User,
2951 content: OneOrMany::one(Content::Text {
2952 text: "First message".to_string(),
2953 citations: Vec::new(),
2954 cache_control: None,
2955 }),
2956 },
2957 Message {
2958 role: Role::Assistant,
2959 content: OneOrMany::one(Content::Text {
2960 text: "Response".to_string(),
2961 citations: Vec::new(),
2962 cache_control: None,
2963 }),
2964 },
2965 ];
2966
2967 apply_cache_control(&mut system_vec, &mut messages);
2968
2969 match &system_vec[0] {
2971 SystemContent::Text { cache_control, .. } => {
2972 assert!(cache_control.is_some());
2973 }
2974 }
2975
2976 for content in messages[0].content.iter() {
2979 if let Content::Text { cache_control, .. } = content {
2980 assert!(cache_control.is_none());
2981 }
2982 }
2983
2984 for content in messages[1].content.iter() {
2986 if let Content::Text { cache_control, .. } = content {
2987 assert!(cache_control.is_some());
2988 }
2989 }
2990 }
2991
2992 fn generic_tool(name: &str) -> completion::ToolDefinition {
2993 completion::ToolDefinition {
2994 name: name.to_string(),
2995 description: format!("{name} description"),
2996 parameters: json!({
2997 "type": "object",
2998 "properties": {}
2999 }),
3000 }
3001 }
3002
3003 fn completion_request_with_tools(
3004 tools: Vec<completion::ToolDefinition>,
3005 additional_params: Option<serde_json::Value>,
3006 ) -> CompletionRequest {
3007 CompletionRequest {
3008 model: None,
3009 preamble: Some("System prompt".to_string()),
3010 chat_history: OneOrMany::one(message::Message::from("Hello")),
3011 documents: Vec::new(),
3012 tools,
3013 temperature: None,
3014 max_tokens: Some(64),
3015 tool_choice: None,
3016 additional_params,
3017 output_schema: None,
3018 }
3019 }
3020
3021 fn completion_request_with_history(
3022 chat_history: Vec<message::Message>,
3023 preamble: Option<String>,
3024 ) -> CompletionRequest {
3025 CompletionRequest {
3026 model: None,
3027 preamble,
3028 chat_history: OneOrMany::many(chat_history).unwrap(),
3029 documents: Vec::new(),
3030 tools: Vec::new(),
3031 temperature: None,
3032 max_tokens: Some(64),
3033 tool_choice: None,
3034 additional_params: None,
3035 output_schema: None,
3036 }
3037 }
3038
3039 fn system_has_cache_control(value: &serde_json::Value) -> bool {
3040 value["system"]
3041 .as_array()
3042 .and_then(|blocks| blocks.last())
3043 .and_then(|block| block.get("cache_control"))
3044 .is_some()
3045 }
3046
3047 fn last_message_has_cache_control(value: &serde_json::Value) -> bool {
3048 value["messages"]
3049 .as_array()
3050 .and_then(|messages| messages.last())
3051 .and_then(|message| message["content"].as_array())
3052 .and_then(|content| content.last())
3053 .and_then(|content| content.get("cache_control"))
3054 .is_some()
3055 }
3056
3057 #[test]
3058 fn opus_4_8_preserves_mid_conversation_system_message() {
3059 let request = completion_request_with_history(
3060 vec![
3061 message::Message::System {
3062 content: "Global history instruction.".to_string(),
3063 },
3064 message::Message::from("Review this code."),
3065 message::Message::System {
3066 content: "From now on, require explicit type annotations.".to_string(),
3067 },
3068 ],
3069 Some("Top-level instruction.".to_string()),
3070 );
3071
3072 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3073 model: CLAUDE_OPUS_4_8,
3074 request,
3075 prompt_caching: false,
3076 automatic_caching: false,
3077 automatic_caching_ttl: None,
3078 })
3079 .unwrap();
3080
3081 let value = serde_json::to_value(request).unwrap();
3082 assert_eq!(value["system"][0]["text"], "Top-level instruction.");
3083 assert_eq!(value["system"][1]["text"], "Global history instruction.");
3084
3085 let messages = value["messages"].as_array().unwrap();
3086 assert_eq!(messages.len(), 2);
3087 assert_eq!(messages[0]["role"], "user");
3088 assert_eq!(messages[1]["role"], "system");
3089 assert_eq!(
3090 messages[1]["content"][0]["text"],
3091 "From now on, require explicit type annotations."
3092 );
3093 }
3094
3095 #[test]
3096 fn opus_4_8_preserves_mid_conversation_system_message_before_assistant_turn() {
3097 let request = completion_request_with_history(
3098 vec![
3099 message::Message::user("Review this code."),
3100 message::Message::System {
3101 content: "From now on, require explicit type annotations.".to_string(),
3102 },
3103 message::Message::assistant("I will enforce explicit type annotations."),
3104 ],
3105 None,
3106 );
3107
3108 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3109 model: CLAUDE_OPUS_4_8,
3110 request,
3111 prompt_caching: false,
3112 automatic_caching: false,
3113 automatic_caching_ttl: None,
3114 })
3115 .unwrap();
3116
3117 let value = serde_json::to_value(request).unwrap();
3118 let messages = value["messages"].as_array().unwrap();
3119 assert_eq!(messages.len(), 3);
3120 assert_eq!(messages[0]["role"], "user");
3121 assert_eq!(messages[1]["role"], "system");
3122 assert_eq!(messages[2]["role"], "assistant");
3123 assert!(value.get("system").is_none());
3124 }
3125
3126 #[test]
3127 fn opus_4_8_hoists_leading_system_message_when_documents_are_present() {
3128 let mut request = completion_request_with_history(
3129 vec![
3130 message::Message::System {
3131 content: "Global history instruction.".to_string(),
3132 },
3133 message::Message::assistant("Acknowledged."),
3134 message::Message::System {
3135 content: "Mid-conversation instruction.".to_string(),
3136 },
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 assert_eq!(value["system"][1]["text"], "Mid-conversation instruction.");
3159
3160 let messages = value["messages"].as_array().unwrap();
3161 assert_eq!(messages.len(), 3);
3162 assert_eq!(messages[0]["role"], "user");
3163 assert_eq!(messages[1]["role"], "assistant");
3164 assert_eq!(messages[2]["role"], "user");
3165 assert!(
3166 messages[0].to_string().contains("<file id: doc>"),
3167 "document message should follow top-level system: {messages:?}"
3168 );
3169 assert_eq!(
3170 messages
3171 .iter()
3172 .filter(|message| message.to_string().contains("<file id: doc>"))
3173 .count(),
3174 1,
3175 "document message should appear exactly once: {messages:?}"
3176 );
3177 assert!(
3178 messages
3179 .iter()
3180 .all(|message| message["role"].as_str() != Some("system"))
3181 );
3182 }
3183
3184 #[test]
3185 fn opus_4_8_preserves_system_message_after_assistant_server_tool_result() {
3186 let request = completion_request_with_history(
3187 vec![
3188 message::Message::Assistant {
3189 id: None,
3190 content: OneOrMany::many([
3191 message::AssistantContent::Text(message::Text {
3192 text: String::new(),
3193 additional_params: Some(json!({
3194 ANTHROPIC_RAW_CONTENT_KEY: {
3195 "type": "server_tool_use",
3196 "id": "srvtoolu_01",
3197 "name": "web_search",
3198 "input": {
3199 "query": "clear daytime sky color"
3200 }
3201 }
3202 })),
3203 }),
3204 message::AssistantContent::Text(message::Text {
3205 text: String::new(),
3206 additional_params: Some(json!({
3207 ANTHROPIC_RAW_CONTENT_KEY: {
3208 "type": "web_search_tool_result",
3209 "tool_use_id": "srvtoolu_01",
3210 "content": {
3211 "type": "web_search_tool_result_error",
3212 "error_code": "unavailable"
3213 }
3214 }
3215 })),
3216 }),
3217 ])
3218 .unwrap(),
3219 },
3220 message::Message::System {
3221 content: "For the rest of this conversation, answer in Spanish.".to_string(),
3222 },
3223 message::Message::assistant("Entendido."),
3224 ],
3225 None,
3226 );
3227
3228 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3229 model: CLAUDE_OPUS_4_8,
3230 request,
3231 prompt_caching: false,
3232 automatic_caching: false,
3233 automatic_caching_ttl: None,
3234 })
3235 .unwrap();
3236
3237 let value = serde_json::to_value(request).unwrap();
3238 assert!(value.get("system").is_none());
3239
3240 let messages = value["messages"].as_array().unwrap();
3241 assert_eq!(messages.len(), 3);
3242 assert_eq!(messages[0]["role"], "assistant");
3243 assert_eq!(messages[0]["content"][0]["type"], "server_tool_use");
3244 assert_eq!(messages[0]["content"][1]["type"], "web_search_tool_result");
3245 assert_eq!(messages[1]["role"], "system");
3246 assert_eq!(
3247 messages[1]["content"][0]["text"],
3248 "For the rest of this conversation, answer in Spanish."
3249 );
3250 assert_eq!(messages[2]["role"], "assistant");
3251 }
3252
3253 #[test]
3254 fn opus_4_8_preserves_system_message_after_assistant_server_tool_use() {
3255 let request = completion_request_with_history(
3256 vec![
3257 message::Message::Assistant {
3258 id: None,
3259 content: OneOrMany::one(message::AssistantContent::Text(message::Text {
3260 text: String::new(),
3261 additional_params: Some(json!({
3262 ANTHROPIC_RAW_CONTENT_KEY: {
3263 "type": "server_tool_use",
3264 "id": "srvtoolu_01",
3265 "name": "web_search",
3266 "input": {
3267 "query": "clear daytime sky color"
3268 }
3269 }
3270 })),
3271 })),
3272 },
3273 message::Message::System {
3274 content: "For the rest of this conversation, answer in Spanish.".to_string(),
3275 },
3276 message::Message::assistant("Entendido."),
3277 ],
3278 None,
3279 );
3280
3281 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3282 model: CLAUDE_OPUS_4_8,
3283 request,
3284 prompt_caching: false,
3285 automatic_caching: false,
3286 automatic_caching_ttl: None,
3287 })
3288 .unwrap();
3289
3290 let value = serde_json::to_value(request).unwrap();
3291 assert!(value.get("system").is_none());
3292
3293 let messages = value["messages"].as_array().unwrap();
3294 assert_eq!(messages.len(), 3);
3295 assert_eq!(messages[0]["role"], "assistant");
3296 assert_eq!(messages[0]["content"][0]["type"], "server_tool_use");
3297 assert_eq!(messages[1]["role"], "system");
3298 assert_eq!(
3299 messages[1]["content"][0]["text"],
3300 "For the rest of this conversation, answer in Spanish."
3301 );
3302 assert_eq!(messages[2]["role"], "assistant");
3303 }
3304
3305 #[test]
3306 fn opus_4_8_hoists_system_message_in_invalid_mid_conversation_position() {
3307 let request = completion_request_with_history(
3308 vec![
3309 message::Message::user("Review this code."),
3310 message::Message::System {
3311 content: "From now on, require explicit type annotations.".to_string(),
3312 },
3313 message::Message::user("Now review this other file."),
3314 ],
3315 None,
3316 );
3317
3318 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3319 model: CLAUDE_OPUS_4_8,
3320 request,
3321 prompt_caching: false,
3322 automatic_caching: false,
3323 automatic_caching_ttl: None,
3324 })
3325 .unwrap();
3326
3327 let value = serde_json::to_value(request).unwrap();
3328 assert_eq!(
3329 value["system"][0]["text"],
3330 "From now on, require explicit type annotations."
3331 );
3332
3333 let messages = value["messages"].as_array().unwrap();
3334 assert_eq!(messages.len(), 2);
3335 assert_eq!(messages[0]["role"], "user");
3336 assert_eq!(messages[1]["role"], "user");
3337 }
3338
3339 #[test]
3340 fn older_anthropic_models_hoist_mid_conversation_system_message() {
3341 let request = completion_request_with_history(
3342 vec![
3343 message::Message::from("Review this code."),
3344 message::Message::System {
3345 content: "From now on, require explicit type annotations.".to_string(),
3346 },
3347 ],
3348 None,
3349 );
3350
3351 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3352 model: CLAUDE_OPUS_4_7,
3353 request,
3354 prompt_caching: false,
3355 automatic_caching: false,
3356 automatic_caching_ttl: None,
3357 })
3358 .unwrap();
3359
3360 let value = serde_json::to_value(request).unwrap();
3361 assert_eq!(
3362 value["system"][0]["text"],
3363 "From now on, require explicit type annotations."
3364 );
3365
3366 let messages = value["messages"].as_array().unwrap();
3367 assert_eq!(messages.len(), 1);
3368 assert_eq!(messages[0]["role"], "user");
3369 }
3370
3371 #[test]
3372 fn test_tool_definition_cache_control_serialization() {
3373 let tool = ToolDefinition {
3374 name: "cached_tool".to_string(),
3375 description: Some("Cached tool".to_string()),
3376 input_schema: json!({"type": "object"}),
3377 cache_control: Some(CacheControl::ephemeral()),
3378 };
3379
3380 let value = serde_json::to_value(tool).unwrap();
3381 assert_eq!(value["cache_control"]["type"], "ephemeral");
3382
3383 let tool_without_cache = ToolDefinition {
3384 name: "uncached_tool".to_string(),
3385 description: Some("Uncached tool".to_string()),
3386 input_schema: json!({"type": "object"}),
3387 cache_control: None,
3388 };
3389
3390 let value = serde_json::to_value(tool_without_cache).unwrap();
3391 assert!(value.get("cache_control").is_none());
3392 }
3393
3394 #[test]
3395 fn test_apply_tool_cache_control_marks_only_final_tool() {
3396 let mut tools = vec![
3397 json!({
3398 "name": "first_tool",
3399 "description": "First tool",
3400 "input_schema": {"type": "object"}
3401 }),
3402 json!({
3403 "name": "second_tool",
3404 "description": "Second tool",
3405 "input_schema": {"type": "object"}
3406 }),
3407 ];
3408
3409 let mut remaining_cache_markers = 4;
3410 apply_tool_cache_control(
3411 &mut tools,
3412 &mut remaining_cache_markers,
3413 &CacheControl::ephemeral(),
3414 )
3415 .unwrap();
3416
3417 assert!(tools[0].get("cache_control").is_none());
3418 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3419 assert_eq!(remaining_cache_markers, 3);
3420 }
3421
3422 #[test]
3423 fn test_prompt_caching_skips_final_deferred_tool_in_request() {
3424 let request = completion_request_with_tools(
3425 Vec::new(),
3426 Some(json!({
3427 "tools": [
3428 {
3429 "name": "regular_tool",
3430 "description": "Regular tool",
3431 "input_schema": {"type": "object"}
3432 },
3433 {
3434 "name": "deferred_tool",
3435 "description": "Deferred tool",
3436 "input_schema": {"type": "object"},
3437 "defer_loading": true
3438 }
3439 ]
3440 })),
3441 );
3442
3443 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3444 model: "claude-sonnet-4-6",
3445 request,
3446 prompt_caching: true,
3447 automatic_caching: false,
3448 automatic_caching_ttl: None,
3449 })
3450 .unwrap();
3451
3452 let value = serde_json::to_value(request).unwrap();
3453 let tools = value["tools"].as_array().unwrap();
3454 assert_eq!(tools[0]["name"], "regular_tool");
3455 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3456 assert_eq!(tools[1]["name"], "deferred_tool");
3457 assert!(tools[1].get("cache_control").is_none());
3458 }
3459
3460 #[test]
3461 fn test_prompt_caching_preserves_existing_final_tool_cache_control() {
3462 let request = completion_request_with_tools(
3463 Vec::new(),
3464 Some(json!({
3465 "tools": [{
3466 "name": "cached_tool",
3467 "description": "Cached tool",
3468 "input_schema": {"type": "object"},
3469 "cache_control": {"type": "ephemeral", "ttl": "1h"}
3470 }]
3471 })),
3472 );
3473
3474 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3475 model: "claude-sonnet-4-6",
3476 request,
3477 prompt_caching: true,
3478 automatic_caching: false,
3479 automatic_caching_ttl: None,
3480 })
3481 .unwrap();
3482
3483 let value = serde_json::to_value(request).unwrap();
3484 let tools = value["tools"].as_array().unwrap();
3485 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3486 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
3487 }
3488
3489 #[test]
3490 fn test_prompt_caching_all_deferred_tools_do_not_receive_cache_control() {
3491 let request = completion_request_with_tools(
3492 Vec::new(),
3493 Some(json!({
3494 "tools": [
3495 {
3496 "name": "first_deferred_tool",
3497 "description": "First deferred tool",
3498 "input_schema": {"type": "object"},
3499 "defer_loading": true
3500 },
3501 {
3502 "name": "second_deferred_tool",
3503 "description": "Second deferred tool",
3504 "input_schema": {"type": "object"},
3505 "defer_loading": true
3506 }
3507 ]
3508 })),
3509 );
3510
3511 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3512 model: "claude-sonnet-4-6",
3513 request,
3514 prompt_caching: true,
3515 automatic_caching: false,
3516 automatic_caching_ttl: None,
3517 })
3518 .unwrap();
3519
3520 let value = serde_json::to_value(request).unwrap();
3521 let tools = value["tools"].as_array().unwrap();
3522 assert!(tools[0].get("cache_control").is_none());
3523 assert!(tools[1].get("cache_control").is_none());
3524 }
3525
3526 #[test]
3527 fn test_prompt_caching_preserves_earlier_tool_cache_control() {
3528 let request = completion_request_with_tools(
3529 Vec::new(),
3530 Some(json!({
3531 "tools": [
3532 {
3533 "name": "earlier_tool",
3534 "description": "Earlier tool",
3535 "input_schema": {"type": "object"},
3536 "cache_control": {"type": "ephemeral", "ttl": "1h"}
3537 },
3538 {
3539 "name": "later_tool",
3540 "description": "Later tool",
3541 "input_schema": {"type": "object"}
3542 }
3543 ]
3544 })),
3545 );
3546
3547 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3548 model: "claude-sonnet-4-6",
3549 request,
3550 prompt_caching: true,
3551 automatic_caching: false,
3552 automatic_caching_ttl: None,
3553 })
3554 .unwrap();
3555
3556 let value = serde_json::to_value(request).unwrap();
3557 let tools = value["tools"].as_array().unwrap();
3558 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3559 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
3560 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3561 }
3562
3563 #[test]
3564 fn test_prompt_caching_deferred_marker_does_not_suppress_loaded_tool_marker() {
3565 let request = completion_request_with_tools(
3566 Vec::new(),
3567 Some(json!({
3568 "tools": [
3569 {
3570 "name": "regular_tool",
3571 "description": "Regular tool",
3572 "input_schema": {"type": "object"}
3573 },
3574 {
3575 "name": "deferred_cached_tool",
3576 "description": "Deferred cached tool",
3577 "input_schema": {"type": "object"},
3578 "defer_loading": true,
3579 "cache_control": {"type": "ephemeral"}
3580 }
3581 ]
3582 })),
3583 );
3584
3585 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3586 model: "claude-sonnet-4-6",
3587 request,
3588 prompt_caching: true,
3589 automatic_caching: false,
3590 automatic_caching_ttl: None,
3591 })
3592 .unwrap();
3593
3594 let value = serde_json::to_value(request).unwrap();
3595 let tools = value["tools"].as_array().unwrap();
3596 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3597 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3598 }
3599
3600 #[test]
3601 fn test_prompt_caching_errors_when_tool_cache_control_ttl_order_is_invalid() {
3602 let request = completion_request_with_tools(
3603 Vec::new(),
3604 Some(json!({
3605 "tools": [
3606 {
3607 "name": "first_cached_tool",
3608 "description": "First cached tool",
3609 "input_schema": {"type": "object"},
3610 "cache_control": {"type": "ephemeral"}
3611 },
3612 {
3613 "name": "second_cached_tool",
3614 "description": "Second cached tool",
3615 "input_schema": {"type": "object"},
3616 "cache_control": {"type": "ephemeral", "ttl": "1h"}
3617 }
3618 ]
3619 })),
3620 );
3621
3622 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3623 model: "claude-sonnet-4-6",
3624 request,
3625 prompt_caching: true,
3626 automatic_caching: false,
3627 automatic_caching_ttl: None,
3628 })
3629 .unwrap_err();
3630
3631 assert!(err.to_string().contains("ttl `1h`"));
3632 }
3633
3634 #[test]
3635 fn test_prompt_caching_preserves_valid_mixed_ttl_tool_cache_controls() {
3636 let request = completion_request_with_tools(
3637 Vec::new(),
3638 Some(json!({
3639 "tools": [
3640 {
3641 "name": "first_cached_tool",
3642 "description": "First cached tool",
3643 "input_schema": {"type": "object"},
3644 "cache_control": {"type": "ephemeral", "ttl": "1h"}
3645 },
3646 {
3647 "name": "second_cached_tool",
3648 "description": "Second cached tool",
3649 "input_schema": {"type": "object"},
3650 "cache_control": {"type": "ephemeral"}
3651 }
3652 ]
3653 })),
3654 );
3655
3656 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3657 model: "claude-sonnet-4-6",
3658 request,
3659 prompt_caching: true,
3660 automatic_caching: false,
3661 automatic_caching_ttl: None,
3662 })
3663 .unwrap();
3664
3665 let value = serde_json::to_value(request).unwrap();
3666 let tools = value["tools"].as_array().unwrap();
3667 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3668 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
3669 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3670 assert!(tools[1]["cache_control"].get("ttl").is_none());
3671 }
3672
3673 #[test]
3674 fn test_prompt_caching_preserves_deferred_tool_cache_control() {
3675 let request = completion_request_with_tools(
3676 Vec::new(),
3677 Some(json!({
3678 "tools": [{
3679 "name": "deferred_cached_tool",
3680 "description": "Deferred cached tool",
3681 "input_schema": {"type": "object"},
3682 "defer_loading": true,
3683 "cache_control": {"type": "ephemeral"}
3684 }]
3685 })),
3686 );
3687
3688 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3689 model: "claude-sonnet-4-6",
3690 request,
3691 prompt_caching: true,
3692 automatic_caching: false,
3693 automatic_caching_ttl: None,
3694 })
3695 .unwrap();
3696
3697 let value = serde_json::to_value(request).unwrap();
3698 let tools = value["tools"].as_array().unwrap();
3699 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3700 }
3701
3702 #[test]
3703 fn test_prompt_caching_budget_preserves_three_tool_markers_and_skips_message() {
3704 let request = completion_request_with_tools(
3705 Vec::new(),
3706 Some(json!({
3707 "tools": [
3708 {
3709 "name": "first_cached_tool",
3710 "description": "First cached tool",
3711 "input_schema": {"type": "object"},
3712 "cache_control": {"type": "ephemeral"}
3713 },
3714 {
3715 "name": "second_cached_tool",
3716 "description": "Second cached tool",
3717 "input_schema": {"type": "object"},
3718 "cache_control": {"type": "ephemeral"}
3719 },
3720 {
3721 "name": "third_cached_tool",
3722 "description": "Third cached tool",
3723 "input_schema": {"type": "object"},
3724 "cache_control": {"type": "ephemeral"}
3725 }
3726 ]
3727 })),
3728 );
3729
3730 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3731 model: "claude-sonnet-4-6",
3732 request,
3733 prompt_caching: true,
3734 automatic_caching: false,
3735 automatic_caching_ttl: None,
3736 })
3737 .unwrap();
3738
3739 let value = serde_json::to_value(request).unwrap();
3740 let tools = value["tools"].as_array().unwrap();
3741 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3742 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
3743 assert_eq!(tools[2]["cache_control"]["type"], "ephemeral");
3744 assert!(system_has_cache_control(&value));
3745 assert!(!last_message_has_cache_control(&value));
3746 }
3747
3748 #[test]
3749 fn test_prompt_caching_errors_when_explicit_tool_markers_exceed_budget() {
3750 let request = completion_request_with_tools(
3751 Vec::new(),
3752 Some(json!({
3753 "tools": [
3754 {
3755 "name": "first_cached_tool",
3756 "description": "First cached tool",
3757 "input_schema": {"type": "object"},
3758 "cache_control": {"type": "ephemeral"}
3759 },
3760 {
3761 "name": "second_cached_tool",
3762 "description": "Second cached tool",
3763 "input_schema": {"type": "object"},
3764 "cache_control": {"type": "ephemeral"}
3765 },
3766 {
3767 "name": "third_cached_tool",
3768 "description": "Third cached tool",
3769 "input_schema": {"type": "object"},
3770 "cache_control": {"type": "ephemeral"}
3771 },
3772 {
3773 "name": "fourth_cached_tool",
3774 "description": "Fourth cached tool",
3775 "input_schema": {"type": "object"},
3776 "cache_control": {"type": "ephemeral"}
3777 },
3778 {
3779 "name": "fifth_cached_tool",
3780 "description": "Fifth cached tool",
3781 "input_schema": {"type": "object"},
3782 "cache_control": {"type": "ephemeral"}
3783 }
3784 ]
3785 })),
3786 );
3787
3788 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3789 model: "claude-sonnet-4-6",
3790 request,
3791 prompt_caching: true,
3792 automatic_caching: false,
3793 automatic_caching_ttl: None,
3794 })
3795 .unwrap_err();
3796
3797 assert!(err.to_string().contains("Too many Anthropic tool"));
3798 }
3799
3800 #[test]
3801 fn test_prompt_caching_errors_when_final_tool_marker_has_no_budget() {
3802 let request = completion_request_with_tools(
3803 Vec::new(),
3804 Some(json!({
3805 "tools": [
3806 {
3807 "name": "first_cached_tool",
3808 "description": "First cached tool",
3809 "input_schema": {"type": "object"},
3810 "cache_control": {"type": "ephemeral"}
3811 },
3812 {
3813 "name": "second_cached_tool",
3814 "description": "Second cached tool",
3815 "input_schema": {"type": "object"},
3816 "cache_control": {"type": "ephemeral"}
3817 },
3818 {
3819 "name": "third_cached_tool",
3820 "description": "Third cached tool",
3821 "input_schema": {"type": "object"},
3822 "cache_control": {"type": "ephemeral"}
3823 },
3824 {
3825 "name": "fourth_cached_tool",
3826 "description": "Fourth cached tool",
3827 "input_schema": {"type": "object"},
3828 "cache_control": {"type": "ephemeral"}
3829 },
3830 {
3831 "name": "final_uncached_tool",
3832 "description": "Final uncached tool",
3833 "input_schema": {"type": "object"}
3834 }
3835 ]
3836 })),
3837 );
3838
3839 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3840 model: "claude-sonnet-4-6",
3841 request,
3842 prompt_caching: true,
3843 automatic_caching: false,
3844 automatic_caching_ttl: None,
3845 })
3846 .unwrap_err();
3847
3848 assert!(err.to_string().contains("final non-deferred tool"));
3849 }
3850
3851 #[test]
3852 fn test_prompt_caching_replaces_null_final_tool_cache_control() {
3853 let request = completion_request_with_tools(
3854 Vec::new(),
3855 Some(json!({
3856 "tools": [{
3857 "name": "final_tool",
3858 "description": "Final tool",
3859 "input_schema": {"type": "object"},
3860 "cache_control": null
3861 }]
3862 })),
3863 );
3864
3865 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3866 model: "claude-sonnet-4-6",
3867 request,
3868 prompt_caching: true,
3869 automatic_caching: false,
3870 automatic_caching_ttl: None,
3871 })
3872 .unwrap();
3873
3874 let value = serde_json::to_value(request).unwrap();
3875 let tools = value["tools"].as_array().unwrap();
3876 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
3877 }
3878
3879 #[test]
3880 fn test_prompt_caching_ignores_null_tool_cache_control_when_budgeting() {
3881 let request = completion_request_with_tools(
3882 Vec::new(),
3883 Some(json!({
3884 "tools": [
3885 {
3886 "name": "first_null_tool",
3887 "description": "First null tool",
3888 "input_schema": {"type": "object"},
3889 "cache_control": null
3890 },
3891 {
3892 "name": "second_null_tool",
3893 "description": "Second null tool",
3894 "input_schema": {"type": "object"},
3895 "cache_control": null
3896 },
3897 {
3898 "name": "third_null_tool",
3899 "description": "Third null tool",
3900 "input_schema": {"type": "object"},
3901 "cache_control": null
3902 },
3903 {
3904 "name": "fourth_null_tool",
3905 "description": "Fourth null tool",
3906 "input_schema": {"type": "object"},
3907 "cache_control": null
3908 },
3909 {
3910 "name": "final_uncached_tool",
3911 "description": "Final uncached tool",
3912 "input_schema": {"type": "object"}
3913 }
3914 ]
3915 })),
3916 );
3917
3918 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3919 model: "claude-sonnet-4-6",
3920 request,
3921 prompt_caching: true,
3922 automatic_caching: false,
3923 automatic_caching_ttl: None,
3924 })
3925 .unwrap();
3926
3927 let value = serde_json::to_value(request).unwrap();
3928 let tools = value["tools"].as_array().unwrap();
3929 assert!(tools[0].get("cache_control").is_none());
3930 assert!(tools[1].get("cache_control").is_none());
3931 assert!(tools[2].get("cache_control").is_none());
3932 assert!(tools[3].get("cache_control").is_none());
3933 assert_eq!(tools[4]["cache_control"]["type"], "ephemeral");
3934 }
3935
3936 #[test]
3937 fn test_prompt_caching_preserves_non_null_provider_tool_cache_control_escape_hatch() {
3938 let request = completion_request_with_tools(
3939 Vec::new(),
3940 Some(json!({
3941 "tools": [{
3942 "name": "provider_tool",
3943 "description": "Provider tool",
3944 "input_schema": {"type": "object"},
3945 "cache_control": {"type": "provider_specific"}
3946 }]
3947 })),
3948 );
3949
3950 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3951 model: "claude-sonnet-4-6",
3952 request,
3953 prompt_caching: true,
3954 automatic_caching: false,
3955 automatic_caching_ttl: None,
3956 })
3957 .unwrap();
3958
3959 let value = serde_json::to_value(request).unwrap();
3960 let tools = value["tools"].as_array().unwrap();
3961 assert_eq!(tools[0]["cache_control"]["type"], "provider_specific");
3962 }
3963
3964 #[test]
3965 fn test_prompt_caching_automatic_mode_uses_reduced_marker_budget() {
3966 let request = completion_request_with_tools(
3967 Vec::new(),
3968 Some(json!({
3969 "tools": [
3970 {
3971 "name": "first_cached_tool",
3972 "description": "First cached tool",
3973 "input_schema": {"type": "object"},
3974 "cache_control": {"type": "ephemeral"}
3975 },
3976 {
3977 "name": "second_cached_tool",
3978 "description": "Second cached tool",
3979 "input_schema": {"type": "object"},
3980 "cache_control": {"type": "ephemeral"}
3981 },
3982 {
3983 "name": "third_cached_tool",
3984 "description": "Third cached tool",
3985 "input_schema": {"type": "object"},
3986 "cache_control": {"type": "ephemeral"}
3987 }
3988 ]
3989 })),
3990 );
3991
3992 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
3993 model: "claude-sonnet-4-6",
3994 request,
3995 prompt_caching: true,
3996 automatic_caching: true,
3997 automatic_caching_ttl: None,
3998 })
3999 .unwrap();
4000
4001 let value = serde_json::to_value(request).unwrap();
4002 let tools = value["tools"].as_array().unwrap();
4003 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
4004 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
4005 assert_eq!(tools[2]["cache_control"]["type"], "ephemeral");
4006 assert_eq!(value["cache_control"]["type"], "ephemeral");
4007 assert!(!system_has_cache_control(&value));
4008 assert!(!last_message_has_cache_control(&value));
4009 }
4010
4011 #[test]
4012 fn test_prompt_caching_automatic_mode_errors_when_final_tool_marker_has_no_budget() {
4013 let request = completion_request_with_tools(
4014 Vec::new(),
4015 Some(json!({
4016 "tools": [
4017 {
4018 "name": "first_cached_tool",
4019 "description": "First cached tool",
4020 "input_schema": {"type": "object"},
4021 "cache_control": {"type": "ephemeral"}
4022 },
4023 {
4024 "name": "second_cached_tool",
4025 "description": "Second cached tool",
4026 "input_schema": {"type": "object"},
4027 "cache_control": {"type": "ephemeral"}
4028 },
4029 {
4030 "name": "third_cached_tool",
4031 "description": "Third cached tool",
4032 "input_schema": {"type": "object"},
4033 "cache_control": {"type": "ephemeral"}
4034 },
4035 {
4036 "name": "final_uncached_tool",
4037 "description": "Final uncached tool",
4038 "input_schema": {"type": "object"}
4039 }
4040 ]
4041 })),
4042 );
4043
4044 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4045 model: "claude-sonnet-4-6",
4046 request,
4047 prompt_caching: true,
4048 automatic_caching: true,
4049 automatic_caching_ttl: None,
4050 })
4051 .unwrap_err();
4052
4053 assert!(err.to_string().contains("final non-deferred tool"));
4054 }
4055
4056 #[test]
4057 fn test_automatic_caching_errors_when_explicit_tool_markers_exhaust_budget() {
4058 let request = completion_request_with_tools(
4059 Vec::new(),
4060 Some(json!({
4061 "tools": [
4062 {
4063 "name": "first_cached_tool",
4064 "description": "First cached tool",
4065 "input_schema": {"type": "object"},
4066 "cache_control": {"type": "ephemeral"}
4067 },
4068 {
4069 "name": "second_cached_tool",
4070 "description": "Second cached tool",
4071 "input_schema": {"type": "object"},
4072 "cache_control": {"type": "ephemeral"}
4073 },
4074 {
4075 "name": "third_cached_tool",
4076 "description": "Third cached tool",
4077 "input_schema": {"type": "object"},
4078 "cache_control": {"type": "ephemeral"}
4079 },
4080 {
4081 "name": "fourth_cached_tool",
4082 "description": "Fourth cached tool",
4083 "input_schema": {"type": "object"},
4084 "cache_control": {"type": "ephemeral"}
4085 }
4086 ]
4087 })),
4088 );
4089
4090 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4091 model: "claude-sonnet-4-6",
4092 request,
4093 prompt_caching: false,
4094 automatic_caching: true,
4095 automatic_caching_ttl: None,
4096 })
4097 .unwrap_err();
4098
4099 assert!(err.to_string().contains("Too many Anthropic tool"));
4100 }
4101
4102 #[test]
4103 fn test_automatic_caching_1h_errors_with_explicit_five_minute_tool_marker() {
4104 let request = completion_request_with_tools(
4105 Vec::new(),
4106 Some(json!({
4107 "tools": [{
4108 "name": "cached_tool",
4109 "description": "Cached tool",
4110 "input_schema": {"type": "object"},
4111 "cache_control": {"type": "ephemeral"}
4112 }]
4113 })),
4114 );
4115
4116 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4117 model: "claude-sonnet-4-6",
4118 request,
4119 prompt_caching: false,
4120 automatic_caching: true,
4121 automatic_caching_ttl: Some(CacheTtl::OneHour),
4122 })
4123 .unwrap_err();
4124
4125 assert!(err.to_string().contains("ttl `1h`"));
4126 }
4127
4128 #[test]
4129 fn test_prompt_and_automatic_caching_1h_uses_1h_generated_markers() {
4130 let request = completion_request_with_tools(vec![generic_tool("cached_tool")], None);
4131
4132 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4133 model: "claude-sonnet-4-6",
4134 request,
4135 prompt_caching: true,
4136 automatic_caching: true,
4137 automatic_caching_ttl: Some(CacheTtl::OneHour),
4138 })
4139 .unwrap();
4140
4141 let value = serde_json::to_value(request).unwrap();
4142 let tools = value["tools"].as_array().unwrap();
4143 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
4144 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
4145 assert_eq!(
4146 value["system"]
4147 .as_array()
4148 .and_then(|blocks| blocks.last())
4149 .and_then(|block| block["cache_control"].get("ttl")),
4150 Some(&json!("1h"))
4151 );
4152 assert_eq!(value["cache_control"]["ttl"], "1h");
4153 assert!(!last_message_has_cache_control(&value));
4154 }
4155
4156 #[test]
4157 fn test_prompt_and_raw_top_level_automatic_caching_1h_uses_1h_generated_markers() {
4158 let request = completion_request_with_tools(
4159 vec![generic_tool("cached_tool")],
4160 Some(json!({
4161 "cache_control": {"type": "ephemeral", "ttl": "1h"},
4162 "metadata": {"source": "test"}
4163 })),
4164 );
4165
4166 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4167 model: "claude-sonnet-4-6",
4168 request,
4169 prompt_caching: true,
4170 automatic_caching: true,
4171 automatic_caching_ttl: None,
4172 })
4173 .unwrap();
4174
4175 let value = serde_json::to_value(request).unwrap();
4176 let tools = value["tools"].as_array().unwrap();
4177 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
4178 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
4179 assert_eq!(
4180 value["system"]
4181 .as_array()
4182 .and_then(|blocks| blocks.last())
4183 .and_then(|block| block["cache_control"].get("ttl")),
4184 Some(&json!("1h"))
4185 );
4186 assert_eq!(value["cache_control"]["ttl"], "1h");
4187 assert_eq!(value["metadata"]["source"], "test");
4188 assert!(!last_message_has_cache_control(&value));
4189 }
4190
4191 #[test]
4192 fn test_prompt_caching_uses_raw_top_level_cache_control_ttl() {
4193 let request = completion_request_with_tools(
4194 vec![generic_tool("cached_tool")],
4195 Some(json!({
4196 "cache_control": {"type": "ephemeral", "ttl": "1h"},
4197 "metadata": {"source": "raw-cache-control"}
4198 })),
4199 );
4200
4201 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4202 model: "claude-sonnet-4-6",
4203 request,
4204 prompt_caching: true,
4205 automatic_caching: false,
4206 automatic_caching_ttl: None,
4207 })
4208 .unwrap();
4209
4210 let value = serde_json::to_value(request).unwrap();
4211 let tools = value["tools"].as_array().unwrap();
4212 assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
4213 assert_eq!(tools[0]["cache_control"]["ttl"], "1h");
4214 assert_eq!(
4215 value["system"]
4216 .as_array()
4217 .and_then(|blocks| blocks.last())
4218 .and_then(|block| block["cache_control"].get("ttl")),
4219 Some(&json!("1h"))
4220 );
4221 assert_eq!(value["cache_control"]["ttl"], "1h");
4222 assert_eq!(value["metadata"]["source"], "raw-cache-control");
4223 assert!(!last_message_has_cache_control(&value));
4224 }
4225
4226 #[test]
4227 fn test_raw_top_level_automatic_caching_reduces_marker_budget() {
4228 let request = completion_request_with_tools(
4229 Vec::new(),
4230 Some(json!({
4231 "cache_control": {"type": "ephemeral"},
4232 "tools": [
4233 {
4234 "name": "first_cached_tool",
4235 "description": "First cached tool",
4236 "input_schema": {"type": "object"},
4237 "cache_control": {"type": "ephemeral"}
4238 },
4239 {
4240 "name": "second_cached_tool",
4241 "description": "Second cached tool",
4242 "input_schema": {"type": "object"},
4243 "cache_control": {"type": "ephemeral"}
4244 },
4245 {
4246 "name": "third_cached_tool",
4247 "description": "Third cached tool",
4248 "input_schema": {"type": "object"},
4249 "cache_control": {"type": "ephemeral"}
4250 },
4251 {
4252 "name": "fourth_cached_tool",
4253 "description": "Fourth cached tool",
4254 "input_schema": {"type": "object"},
4255 "cache_control": {"type": "ephemeral"}
4256 }
4257 ]
4258 })),
4259 );
4260
4261 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4262 model: "claude-sonnet-4-6",
4263 request,
4264 prompt_caching: false,
4265 automatic_caching: false,
4266 automatic_caching_ttl: None,
4267 })
4268 .unwrap_err();
4269
4270 assert!(err.to_string().contains("Too many Anthropic tool"));
4271 }
4272
4273 #[test]
4274 fn test_raw_top_level_automatic_caching_1h_errors_after_explicit_five_minute_tool_marker() {
4275 let request = completion_request_with_tools(
4276 Vec::new(),
4277 Some(json!({
4278 "cache_control": {"type": "ephemeral", "ttl": "1h"},
4279 "tools": [{
4280 "name": "cached_tool",
4281 "description": "Cached tool",
4282 "input_schema": {"type": "object"},
4283 "cache_control": {"type": "ephemeral"}
4284 }]
4285 })),
4286 );
4287
4288 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4289 model: "claude-sonnet-4-6",
4290 request,
4291 prompt_caching: false,
4292 automatic_caching: false,
4293 automatic_caching_ttl: None,
4294 })
4295 .unwrap_err();
4296
4297 assert!(err.to_string().contains("ttl `1h`"));
4298 }
4299
4300 #[test]
4301 fn test_typed_automatic_caching_ttl_errors_on_conflicting_raw_top_level_ttl() {
4302 let request = completion_request_with_tools(
4303 Vec::new(),
4304 Some(json!({
4305 "cache_control": {"type": "ephemeral"}
4306 })),
4307 );
4308
4309 let err = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4310 model: "claude-sonnet-4-6",
4311 request,
4312 prompt_caching: false,
4313 automatic_caching: true,
4314 automatic_caching_ttl: Some(CacheTtl::OneHour),
4315 })
4316 .unwrap_err();
4317
4318 assert!(
4319 err.to_string()
4320 .contains("conflicts with the typed automatic caching TTL")
4321 );
4322 }
4323
4324 #[test]
4325 fn test_prompt_caching_marks_final_tool_in_request() {
4326 let request = completion_request_with_tools(
4327 vec![generic_tool("first_tool"), generic_tool("second_tool")],
4328 None,
4329 );
4330
4331 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4332 model: "claude-sonnet-4-6",
4333 request,
4334 prompt_caching: true,
4335 automatic_caching: false,
4336 automatic_caching_ttl: None,
4337 })
4338 .unwrap();
4339
4340 let value = serde_json::to_value(request).unwrap();
4341 let tools = value["tools"].as_array().unwrap();
4342 assert_eq!(tools.len(), 2);
4343 assert!(tools[0].get("cache_control").is_none());
4344 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
4345 }
4346
4347 #[test]
4348 fn test_prompt_caching_marks_final_additional_tool_in_request() {
4349 let request = completion_request_with_tools(
4350 vec![generic_tool("rig_tool")],
4351 Some(json!({
4352 "tools": [{
4353 "name": "provider_tool",
4354 "description": "Provider tool",
4355 "input_schema": {"type": "object"}
4356 }],
4357 "metadata": {"source": "test"}
4358 })),
4359 );
4360
4361 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4362 model: "claude-sonnet-4-6",
4363 request,
4364 prompt_caching: true,
4365 automatic_caching: false,
4366 automatic_caching_ttl: None,
4367 })
4368 .unwrap();
4369
4370 let value = serde_json::to_value(request).unwrap();
4371 let tools = value["tools"].as_array().unwrap();
4372 assert_eq!(tools.len(), 2);
4373 assert!(tools[0].get("cache_control").is_none());
4374 assert_eq!(tools[1]["name"], "provider_tool");
4375 assert_eq!(tools[1]["cache_control"]["type"], "ephemeral");
4376 assert_eq!(value["metadata"]["source"], "test");
4377 }
4378
4379 #[test]
4380 fn test_prompt_caching_without_tools_omits_tools() {
4381 let request = completion_request_with_tools(Vec::new(), None);
4382
4383 let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
4384 model: "claude-sonnet-4-6",
4385 request,
4386 prompt_caching: true,
4387 automatic_caching: false,
4388 automatic_caching_ttl: None,
4389 })
4390 .unwrap();
4391
4392 let value = serde_json::to_value(request).unwrap();
4393 assert!(value.get("tools").is_none());
4394 }
4395
4396 #[test]
4397 fn test_plaintext_document_serialization() {
4398 let content = Content::Document {
4399 source: DocumentSource::Text {
4400 data: "Hello, world!".to_string(),
4401 media_type: PlainTextMediaType::Plain,
4402 },
4403 title: None,
4404 context: None,
4405 citations: None,
4406 cache_control: None,
4407 };
4408
4409 let json = serde_json::to_value(&content).unwrap();
4410 assert_eq!(json["type"], "document");
4411 assert_eq!(json["source"]["type"], "text");
4412 assert_eq!(json["source"]["media_type"], "text/plain");
4413 assert_eq!(json["source"]["data"], "Hello, world!");
4414 }
4415
4416 #[test]
4417 fn test_plaintext_document_deserialization() {
4418 let json = r#"
4419 {
4420 "type": "document",
4421 "source": {
4422 "type": "text",
4423 "media_type": "text/plain",
4424 "data": "Hello, world!"
4425 }
4426 }
4427 "#;
4428
4429 let content: Content = serde_json::from_str(json).unwrap();
4430 match content {
4431 Content::Document {
4432 source,
4433 cache_control,
4434 ..
4435 } => {
4436 assert_eq!(
4437 source,
4438 DocumentSource::Text {
4439 data: "Hello, world!".to_string(),
4440 media_type: PlainTextMediaType::Plain,
4441 }
4442 );
4443 assert_eq!(cache_control, None);
4444 }
4445 _ => panic!("Expected Document content"),
4446 }
4447 }
4448
4449 #[test]
4450 fn test_base64_pdf_document_serialization() {
4451 let content = Content::Document {
4452 source: DocumentSource::Base64 {
4453 data: "base64data".to_string(),
4454 media_type: DocumentFormat::PDF,
4455 },
4456 title: None,
4457 context: None,
4458 citations: None,
4459 cache_control: None,
4460 };
4461
4462 let json = serde_json::to_value(&content).unwrap();
4463 assert_eq!(json["type"], "document");
4464 assert_eq!(json["source"]["type"], "base64");
4465 assert_eq!(json["source"]["media_type"], "application/pdf");
4466 assert_eq!(json["source"]["data"], "base64data");
4467 }
4468
4469 #[test]
4470 fn test_base64_pdf_document_deserialization() {
4471 let json = r#"
4472 {
4473 "type": "document",
4474 "source": {
4475 "type": "base64",
4476 "media_type": "application/pdf",
4477 "data": "base64data"
4478 }
4479 }
4480 "#;
4481
4482 let content: Content = serde_json::from_str(json).unwrap();
4483 match content {
4484 Content::Document { source, .. } => {
4485 assert_eq!(
4486 source,
4487 DocumentSource::Base64 {
4488 data: "base64data".to_string(),
4489 media_type: DocumentFormat::PDF,
4490 }
4491 );
4492 }
4493 _ => panic!("Expected Document content"),
4494 }
4495 }
4496
4497 #[test]
4498 fn test_file_id_document_serialization() {
4499 let content = Content::Document {
4500 source: DocumentSource::File {
4501 file_id: "file_abc".to_string(),
4502 },
4503 title: None,
4504 context: None,
4505 citations: None,
4506 cache_control: None,
4507 };
4508
4509 let json = serde_json::to_value(&content).unwrap();
4510 assert_eq!(json["type"], "document");
4511 assert_eq!(json["source"]["type"], "file");
4512 assert_eq!(json["source"]["file_id"], "file_abc");
4513 }
4514
4515 #[test]
4516 fn test_file_id_document_deserialization() {
4517 let json = r#"
4518 {
4519 "type": "document",
4520 "source": {
4521 "type": "file",
4522 "file_id": "file_abc"
4523 }
4524 }
4525 "#;
4526
4527 let content: Content = serde_json::from_str(json).unwrap();
4528 match content {
4529 Content::Document { source, .. } => {
4530 assert_eq!(
4531 source,
4532 DocumentSource::File {
4533 file_id: "file_abc".to_string(),
4534 }
4535 );
4536 }
4537 _ => panic!("Expected Document content"),
4538 }
4539 }
4540
4541 #[test]
4542 fn test_file_id_rig_to_anthropic_conversion() {
4543 use crate::completion::message as msg;
4544
4545 let rig_message = msg::Message::User {
4546 content: OneOrMany::one(msg::UserContent::Document(msg::Document {
4547 data: DocumentSourceKind::FileId("file_abc".to_string()),
4548 media_type: None,
4549 additional_params: None,
4550 })),
4551 };
4552
4553 let anthropic_message: Message = rig_message.try_into().unwrap();
4554 assert_eq!(anthropic_message.role, Role::User);
4555
4556 let mut iter = anthropic_message.content.into_iter();
4557 match iter.next().unwrap() {
4558 Content::Document { source, .. } => {
4559 assert_eq!(
4560 source,
4561 DocumentSource::File {
4562 file_id: "file_abc".to_string(),
4563 }
4564 );
4565 }
4566 other => panic!("Expected Document content, got: {other:?}"),
4567 }
4568 }
4569
4570 #[test]
4571 fn test_file_id_anthropic_to_rig_conversion() {
4572 use crate::completion::message as msg;
4573
4574 let anthropic_message = Message {
4575 role: Role::User,
4576 content: OneOrMany::one(Content::Document {
4577 source: DocumentSource::File {
4578 file_id: "file_abc".to_string(),
4579 },
4580 title: None,
4581 context: None,
4582 citations: None,
4583 cache_control: None,
4584 }),
4585 };
4586
4587 let rig_message: msg::Message = anthropic_message.try_into().unwrap();
4588 match rig_message {
4589 msg::Message::User { content } => {
4590 let mut iter = content.into_iter();
4591 match iter.next().unwrap() {
4592 msg::UserContent::Document(msg::Document {
4593 data, media_type, ..
4594 }) => {
4595 assert_eq!(data, DocumentSourceKind::FileId("file_abc".to_string()));
4596 assert_eq!(media_type, None);
4597 }
4598 other => panic!("Expected Document content, got: {other:?}"),
4599 }
4600 }
4601 _ => panic!("Expected User message"),
4602 }
4603 }
4604
4605 #[test]
4606 fn test_plaintext_rig_to_anthropic_conversion() {
4607 use crate::completion::message as msg;
4608
4609 let rig_message = msg::Message::User {
4610 content: OneOrMany::one(msg::UserContent::document(
4611 "Some plain text content".to_string(),
4612 Some(msg::DocumentMediaType::TXT),
4613 )),
4614 };
4615
4616 let anthropic_message: Message = rig_message.try_into().unwrap();
4617 assert_eq!(anthropic_message.role, Role::User);
4618
4619 let mut iter = anthropic_message.content.into_iter();
4620 match iter.next().unwrap() {
4621 Content::Document { source, .. } => {
4622 assert_eq!(
4623 source,
4624 DocumentSource::Text {
4625 data: "Some plain text content".to_string(),
4626 media_type: PlainTextMediaType::Plain,
4627 }
4628 );
4629 }
4630 other => panic!("Expected Document content, got: {other:?}"),
4631 }
4632 }
4633
4634 #[test]
4635 fn test_plaintext_anthropic_to_rig_conversion() {
4636 use crate::completion::message as msg;
4637
4638 let anthropic_message = Message {
4639 role: Role::User,
4640 content: OneOrMany::one(Content::Document {
4641 source: DocumentSource::Text {
4642 data: "Some plain text content".to_string(),
4643 media_type: PlainTextMediaType::Plain,
4644 },
4645 title: None,
4646 context: None,
4647 citations: None,
4648 cache_control: None,
4649 }),
4650 };
4651
4652 let rig_message: msg::Message = anthropic_message.try_into().unwrap();
4653 match rig_message {
4654 msg::Message::User { content } => {
4655 let mut iter = content.into_iter();
4656 match iter.next().unwrap() {
4657 msg::UserContent::Document(msg::Document {
4658 data, media_type, ..
4659 }) => {
4660 assert_eq!(
4661 data,
4662 DocumentSourceKind::String("Some plain text content".into())
4663 );
4664 assert_eq!(media_type, Some(msg::DocumentMediaType::TXT));
4665 }
4666 other => panic!("Expected Document content, got: {other:?}"),
4667 }
4668 }
4669 _ => panic!("Expected User message"),
4670 }
4671 }
4672
4673 #[test]
4674 fn test_plaintext_roundtrip_rig_to_anthropic_and_back() {
4675 use crate::completion::message as msg;
4676
4677 let original = msg::Message::User {
4678 content: OneOrMany::one(msg::UserContent::document(
4679 "Round trip text".to_string(),
4680 Some(msg::DocumentMediaType::TXT),
4681 )),
4682 };
4683
4684 let anthropic: Message = original.clone().try_into().unwrap();
4685 let back: msg::Message = anthropic.try_into().unwrap();
4686
4687 match (&original, &back) {
4688 (
4689 msg::Message::User {
4690 content: orig_content,
4691 },
4692 msg::Message::User {
4693 content: back_content,
4694 },
4695 ) => match (orig_content.first(), back_content.first()) {
4696 (
4697 msg::UserContent::Document(msg::Document {
4698 media_type: orig_mt,
4699 ..
4700 }),
4701 msg::UserContent::Document(msg::Document {
4702 media_type: back_mt,
4703 ..
4704 }),
4705 ) => {
4706 assert_eq!(orig_mt, back_mt);
4707 }
4708 _ => panic!("Expected Document content in both"),
4709 },
4710 _ => panic!("Expected User messages"),
4711 }
4712 }
4713
4714 #[test]
4715 fn test_unsupported_document_type_returns_error() {
4716 use crate::completion::message as msg;
4717
4718 let rig_message = msg::Message::User {
4719 content: OneOrMany::one(msg::UserContent::Document(msg::Document {
4720 data: DocumentSourceKind::String("data".into()),
4721 media_type: Some(msg::DocumentMediaType::HTML),
4722 additional_params: None,
4723 })),
4724 };
4725
4726 let result: Result<Message, _> = rig_message.try_into();
4727 assert!(result.is_err());
4728 let err = result.unwrap_err().to_string();
4729 assert!(
4730 err.contains("Anthropic only supports PDF and plain text documents"),
4731 "Unexpected error: {err}"
4732 );
4733 }
4734
4735 #[test]
4736 fn test_plaintext_document_url_source_returns_error() {
4737 use crate::completion::message as msg;
4738
4739 let rig_message = msg::Message::User {
4740 content: OneOrMany::one(msg::UserContent::Document(msg::Document {
4741 data: DocumentSourceKind::Url("https://example.com/doc.txt".into()),
4742 media_type: Some(msg::DocumentMediaType::TXT),
4743 additional_params: None,
4744 })),
4745 };
4746
4747 let result: Result<Message, _> = rig_message.try_into();
4748 assert!(result.is_err());
4749 let err = result.unwrap_err().to_string();
4750 assert!(
4751 err.contains("Only string or base64 data is supported for plain text documents"),
4752 "Unexpected error: {err}"
4753 );
4754 }
4755
4756 #[test]
4757 fn test_plaintext_document_with_cache_control() {
4758 let content = Content::Document {
4759 source: DocumentSource::Text {
4760 data: "cached text".to_string(),
4761 media_type: PlainTextMediaType::Plain,
4762 },
4763 title: None,
4764 context: None,
4765 citations: None,
4766 cache_control: Some(CacheControl::ephemeral()),
4767 };
4768
4769 let json = serde_json::to_value(&content).unwrap();
4770 assert_eq!(json["source"]["type"], "text");
4771 assert_eq!(json["source"]["media_type"], "text/plain");
4772 assert_eq!(json["cache_control"]["type"], "ephemeral");
4773 }
4774
4775 #[test]
4776 fn test_message_with_plaintext_document_deserialization() {
4777 let json = r#"
4778 {
4779 "role": "user",
4780 "content": [
4781 {
4782 "type": "document",
4783 "source": {
4784 "type": "text",
4785 "media_type": "text/plain",
4786 "data": "Hello from a text file"
4787 }
4788 },
4789 {
4790 "type": "text",
4791 "text": "Summarize this document."
4792 }
4793 ]
4794 }
4795 "#;
4796
4797 let message: Message = serde_json::from_str(json).unwrap();
4798 assert_eq!(message.role, Role::User);
4799 assert_eq!(message.content.len(), 2);
4800
4801 let mut iter = message.content.into_iter();
4802
4803 match iter.next().unwrap() {
4804 Content::Document { source, .. } => {
4805 assert_eq!(
4806 source,
4807 DocumentSource::Text {
4808 data: "Hello from a text file".to_string(),
4809 media_type: PlainTextMediaType::Plain,
4810 }
4811 );
4812 }
4813 _ => panic!("Expected Document content"),
4814 }
4815
4816 match iter.next().unwrap() {
4817 Content::Text { text, .. } => {
4818 assert_eq!(text, "Summarize this document.");
4819 }
4820 _ => panic!("Expected Text content"),
4821 }
4822 }
4823
4824 #[test]
4825 fn test_assistant_reasoning_multiblock_to_anthropic_content() {
4826 let reasoning = message::Reasoning {
4827 id: None,
4828 content: vec![
4829 message::ReasoningContent::Text {
4830 text: "step one".to_string(),
4831 signature: Some("sig-1".to_string()),
4832 },
4833 message::ReasoningContent::Summary("summary".to_string()),
4834 message::ReasoningContent::Text {
4835 text: "step two".to_string(),
4836 signature: Some("sig-2".to_string()),
4837 },
4838 message::ReasoningContent::Redacted {
4839 data: "redacted block".to_string(),
4840 },
4841 ],
4842 };
4843
4844 let msg = message::Message::Assistant {
4845 id: None,
4846 content: OneOrMany::one(message::AssistantContent::Reasoning(reasoning)),
4847 };
4848 let converted: Message = msg.try_into().expect("convert assistant message");
4849 let converted_content = converted.content.iter().cloned().collect::<Vec<_>>();
4850
4851 assert_eq!(converted.role, Role::Assistant);
4852 assert_eq!(converted_content.len(), 4);
4853 assert!(matches!(
4854 converted_content.first(),
4855 Some(Content::Thinking { thinking, signature: Some(signature) })
4856 if thinking == "step one" && signature == "sig-1"
4857 ));
4858 assert!(matches!(
4859 converted_content.get(1),
4860 Some(Content::Thinking { thinking, signature: None }) if thinking == "summary"
4861 ));
4862 assert!(matches!(
4863 converted_content.get(2),
4864 Some(Content::Thinking { thinking, signature: Some(signature) })
4865 if thinking == "step two" && signature == "sig-2"
4866 ));
4867 assert!(matches!(
4868 converted_content.get(3),
4869 Some(Content::RedactedThinking { data }) if data == "redacted block"
4870 ));
4871 }
4872
4873 #[test]
4874 fn test_redacted_thinking_content_to_assistant_reasoning() {
4875 let content = Content::RedactedThinking {
4876 data: "opaque-redacted".to_string(),
4877 };
4878 let converted: message::AssistantContent =
4879 content.try_into().expect("convert redacted thinking");
4880
4881 assert!(matches!(
4882 converted,
4883 message::AssistantContent::Reasoning(message::Reasoning { content, .. })
4884 if matches!(
4885 content.first(),
4886 Some(message::ReasoningContent::Redacted { data }) if data == "opaque-redacted"
4887 )
4888 ));
4889 }
4890
4891 #[test]
4892 fn test_assistant_encrypted_reasoning_maps_to_redacted_thinking() {
4893 let reasoning = message::Reasoning {
4894 id: None,
4895 content: vec![message::ReasoningContent::Encrypted(
4896 "ciphertext".to_string(),
4897 )],
4898 };
4899 let msg = message::Message::Assistant {
4900 id: None,
4901 content: OneOrMany::one(message::AssistantContent::Reasoning(reasoning)),
4902 };
4903
4904 let converted: Message = msg.try_into().expect("convert assistant message");
4905 let converted_content = converted.content.iter().cloned().collect::<Vec<_>>();
4906
4907 assert_eq!(converted_content.len(), 1);
4908 assert!(matches!(
4909 converted_content.first(),
4910 Some(Content::RedactedThinking { data }) if data == "ciphertext"
4911 ));
4912 }
4913
4914 #[test]
4915 fn empty_end_turn_response_normalizes_to_empty_text_choice() {
4916 let response = CompletionResponse {
4917 content: vec![],
4918 id: "msg_123".to_string(),
4919 model: CLAUDE_SONNET_4_6.to_string(),
4920 role: "assistant".to_string(),
4921 stop_reason: Some("end_turn".to_string()),
4922 stop_sequence: None,
4923 usage: Usage {
4924 input_tokens: 7,
4925 cache_read_input_tokens: None,
4926 cache_creation_input_tokens: None,
4927 output_tokens: 2,
4928 },
4929 };
4930
4931 let parsed: completion::CompletionResponse<CompletionResponse> = response
4932 .try_into()
4933 .expect("empty end_turn should not error");
4934
4935 assert_eq!(parsed.choice.len(), 1);
4936 assert!(matches!(
4937 parsed.choice.first(),
4938 completion::AssistantContent::Text(text) if text.text.is_empty()
4939 ));
4940 }
4941
4942 #[test]
4943 fn empty_non_end_turn_response_still_errors() {
4944 let response = CompletionResponse {
4945 content: vec![],
4946 id: "msg_123".to_string(),
4947 model: CLAUDE_SONNET_4_6.to_string(),
4948 role: "assistant".to_string(),
4949 stop_reason: Some("tool_use".to_string()),
4950 stop_sequence: None,
4951 usage: Usage {
4952 input_tokens: 7,
4953 cache_read_input_tokens: None,
4954 cache_creation_input_tokens: None,
4955 output_tokens: 2,
4956 },
4957 };
4958
4959 let err = completion::CompletionResponse::<CompletionResponse>::try_from(response)
4960 .expect_err("empty non-end_turn should remain an error");
4961
4962 assert!(matches!(
4963 err,
4964 CompletionError::ResponseError(message) if message == EMPTY_RESPONSE_ERROR
4965 ));
4966 }
4967
4968 #[test]
4969 fn test_tool_result_content_in_message_roundtrip() {
4970 let message_json = r#"{
4971 "role": "user",
4972 "content": [
4973 {
4974 "type": "tool_result",
4975 "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
4976 "content": [
4977 {
4978 "type": "text",
4979 "text": "Here is the screenshot:"
4980 },
4981 {
4982 "type": "image",
4983 "source": {
4984 "type": "base64",
4985 "media_type": "image/png",
4986 "data": "iVBORw0KGgo..."
4987 }
4988 }
4989 ]
4990 }
4991 ]
4992 }"#;
4993
4994 let message: Message = serde_json::from_str(message_json).unwrap();
4995 let serialized = serde_json::to_value(&message).unwrap();
4996
4997 let tool_result = &serialized["content"][0];
4998 assert_eq!(tool_result["type"], "tool_result");
4999
5000 let image_content = &tool_result["content"][1];
5001 assert_eq!(image_content["type"], "image");
5002 assert_eq!(image_content["source"]["type"], "base64");
5003 assert_eq!(image_content["source"]["media_type"], "image/png");
5004 assert_eq!(image_content["source"]["data"], "iVBORw0KGgo...");
5005 }
5006
5007 #[test]
5012 fn document_serializes_citations_and_metadata() {
5013 let doc = Content::Document {
5014 source: DocumentSource::Text {
5015 data: "hello".into(),
5016 media_type: PlainTextMediaType::Plain,
5017 },
5018 title: Some("My Doc".into()),
5019 context: None,
5020 citations: Some(CitationsConfig { enabled: true }),
5021 cache_control: None,
5022 };
5023 let value = serde_json::to_value(&doc).unwrap();
5024 assert_eq!(value["citations"]["enabled"], true);
5025 assert_eq!(value["title"], "My Doc");
5026 assert!(
5027 value.get("context").is_none(),
5028 "context should be skipped when None"
5029 );
5030 }
5031
5032 #[test]
5033 fn text_serializes_without_citations_when_empty() {
5034 let content = Content::Text {
5035 text: "hello".into(),
5036 citations: Vec::new(),
5037 cache_control: None,
5038 };
5039 let value = serde_json::to_value(&content).unwrap();
5040 assert!(
5041 value.get("citations").is_none(),
5042 "empty citations vec must be skipped"
5043 );
5044 }
5045
5046 #[test]
5047 fn text_deserializes_char_location_citation() {
5048 let value = json!({
5049 "type": "text",
5050 "text": "the grass is green",
5051 "citations": [{
5052 "type": "char_location",
5053 "cited_text": "The grass is green.",
5054 "document_index": 0,
5055 "document_title": "Example",
5056 "start_char_index": 0,
5057 "end_char_index": 20
5058 }]
5059 });
5060 let parsed: Content = serde_json::from_value(value).unwrap();
5061 let Content::Text { citations, .. } = parsed else {
5062 panic!("expected Content::Text");
5063 };
5064 assert_eq!(citations.len(), 1);
5065 let Citation::CharLocation {
5066 start_char_index,
5067 end_char_index,
5068 ..
5069 } = &citations[0]
5070 else {
5071 panic!("expected CharLocation");
5072 };
5073 assert_eq!(*start_char_index, 0);
5074 assert_eq!(*end_char_index, 20);
5075 }
5076
5077 #[test]
5078 fn text_deserializes_search_result_location_citation() {
5079 let value = json!({
5080 "type": "text",
5081 "text": "API keys are required.",
5082 "citations": [{
5083 "type": "search_result_location",
5084 "cited_text": "All API requests must include an API key.",
5085 "source": "https://docs.example.com/api-reference",
5086 "title": "API Reference",
5087 "search_result_index": 0,
5088 "start_block_index": 0,
5089 "end_block_index": 1
5090 }]
5091 });
5092
5093 let parsed: Content = serde_json::from_value(value).unwrap();
5094 let Content::Text { citations, .. } = parsed else {
5095 panic!("expected Content::Text");
5096 };
5097
5098 assert!(matches!(
5099 &citations[0],
5100 Citation::SearchResultLocation {
5101 source,
5102 title: Some(title),
5103 search_result_index: 0,
5104 start_block_index: 0,
5105 end_block_index: 1,
5106 ..
5107 } if source == "https://docs.example.com/api-reference" && title == "API Reference"
5108 ));
5109 }
5110
5111 #[test]
5112 fn text_deserializes_web_search_result_location_citation() {
5113 let value = json!({
5114 "type": "text",
5115 "text": "Claude Shannon worked at Bell Labs.",
5116 "citations": [{
5117 "type": "web_search_result_location",
5118 "cited_text": "Claude Shannon was a mathematician.",
5119 "url": "https://example.com/shannon",
5120 "title": "Claude Shannon",
5121 "encrypted_index": "encrypted-reference"
5122 }]
5123 });
5124
5125 let parsed: Content = serde_json::from_value(value).unwrap();
5126 let Content::Text { citations, .. } = parsed else {
5127 panic!("expected Content::Text");
5128 };
5129
5130 assert!(matches!(
5131 &citations[0],
5132 Citation::WebSearchResultLocation {
5133 url,
5134 title,
5135 encrypted_index,
5136 ..
5137 } if url == "https://example.com/shannon"
5138 && title.as_deref() == Some("Claude Shannon")
5139 && encrypted_index == "encrypted-reference"
5140 ));
5141 }
5142
5143 #[test]
5144 fn text_deserializes_web_search_result_location_citation_with_null_title() {
5145 let value = json!({
5146 "type": "text",
5147 "text": "Claude Shannon worked at Bell Labs.",
5148 "citations": [{
5149 "type": "web_search_result_location",
5150 "cited_text": "Claude Shannon was a mathematician.",
5151 "url": "https://example.com/shannon",
5152 "title": null,
5153 "encrypted_index": "encrypted-reference"
5154 }]
5155 });
5156
5157 let parsed: Content = serde_json::from_value(value).unwrap();
5158 let Content::Text { citations, .. } = parsed else {
5159 panic!("expected Content::Text");
5160 };
5161
5162 let Citation::WebSearchResultLocation { title, .. } = &citations[0] else {
5163 panic!("expected WebSearchResultLocation");
5164 };
5165 assert_eq!(title, &None);
5166
5167 let serialized = serde_json::to_value(&citations[0]).unwrap();
5168 assert!(serialized.get("title").is_some());
5169 assert!(serialized["title"].is_null());
5170 }
5171
5172 #[test]
5173 fn web_search_response_preserves_raw_blocks_and_citations() {
5174 let value = json!({
5175 "id": "msg_web_search",
5176 "model": CLAUDE_SONNET_4_6,
5177 "role": "assistant",
5178 "stop_reason": "end_turn",
5179 "stop_sequence": null,
5180 "usage": {
5181 "input_tokens": 10,
5182 "output_tokens": 20
5183 },
5184 "content": [
5185 {
5186 "type": "server_tool_use",
5187 "id": "srvtoolu_01",
5188 "name": "web_search",
5189 "input": {
5190 "query": "claude shannon birth date"
5191 }
5192 },
5193 {
5194 "type": "web_search_tool_result",
5195 "tool_use_id": "srvtoolu_01",
5196 "content": [
5197 {
5198 "type": "web_search_result",
5199 "url": "https://example.com/shannon",
5200 "title": "Claude Shannon",
5201 "encrypted_content": "encrypted-content",
5202 "page_age": "April 30, 2025"
5203 }
5204 ]
5205 },
5206 {
5207 "type": "text",
5208 "text": "Claude Shannon was born on April 30, 1916.",
5209 "citations": [{
5210 "type": "web_search_result_location",
5211 "cited_text": "Claude Shannon was born on April 30, 1916.",
5212 "url": "https://example.com/shannon",
5213 "title": "Claude Shannon",
5214 "encrypted_index": "encrypted-index"
5215 }]
5216 }
5217 ]
5218 });
5219
5220 let response: CompletionResponse = serde_json::from_value(value).unwrap();
5221 let converted: completion::CompletionResponse<CompletionResponse> =
5222 response.try_into().unwrap();
5223 assert_eq!(converted.choice.len(), 3);
5224 assert_eq!(
5225 converted.raw_response.get_text_response().as_deref(),
5226 Some("Claude Shannon was born on April 30, 1916.")
5227 );
5228
5229 let items = converted.choice.iter().collect::<Vec<_>>();
5230 let message::AssistantContent::Text(server_tool_use) = items[0] else {
5231 panic!("expected raw server_tool_use metadata");
5232 };
5233 assert_eq!(server_tool_use.text, "");
5234 assert_eq!(
5235 server_tool_use.additional_params.as_ref().unwrap()[ANTHROPIC_RAW_CONTENT_KEY]["type"],
5236 "server_tool_use"
5237 );
5238
5239 let message::AssistantContent::Text(web_search_result) = items[1] else {
5240 panic!("expected raw web_search_tool_result metadata");
5241 };
5242 assert_eq!(
5243 web_search_result.additional_params.as_ref().unwrap()[ANTHROPIC_RAW_CONTENT_KEY]["content"]
5244 [0]["encrypted_content"],
5245 "encrypted-content"
5246 );
5247
5248 let message::AssistantContent::Text(answer) = items[2] else {
5249 panic!("expected text answer");
5250 };
5251 let citations = anthropic_citations(answer).unwrap();
5252 assert!(matches!(
5253 citations.first(),
5254 Some(Citation::WebSearchResultLocation {
5255 encrypted_index,
5256 ..
5257 }) if encrypted_index == "encrypted-index"
5258 ));
5259
5260 let round_trip: Message = message::Message::Assistant {
5261 id: converted.message_id.clone(),
5262 content: converted.choice,
5263 }
5264 .try_into()
5265 .unwrap();
5266
5267 let round_trip_items = round_trip.content.iter().collect::<Vec<_>>();
5268 assert!(matches!(
5269 round_trip_items.first(),
5270 Some(Content::ServerToolUse { id, name, input })
5271 if id == "srvtoolu_01"
5272 && name == "web_search"
5273 && input["query"] == "claude shannon birth date"
5274 ));
5275 assert!(matches!(
5276 round_trip_items.get(1),
5277 Some(Content::WebSearchToolResult {
5278 tool_use_id,
5279 content
5280 }) if tool_use_id == "srvtoolu_01"
5281 && content[0]["encrypted_content"] == "encrypted-content"
5282 ));
5283 }
5284
5285 #[test]
5286 fn web_search_tool_result_error_object_is_preserved_raw() {
5287 let value = json!({
5288 "id": "msg_web_search_error",
5289 "model": CLAUDE_SONNET_4_6,
5290 "role": "assistant",
5291 "stop_reason": "end_turn",
5292 "stop_sequence": null,
5293 "usage": {
5294 "input_tokens": 10,
5295 "output_tokens": 2
5296 },
5297 "content": [{
5298 "type": "web_search_tool_result",
5299 "tool_use_id": "srvtoolu_01",
5300 "content": {
5301 "type": "web_search_tool_result_error",
5302 "error_code": "max_uses_exceeded"
5303 }
5304 }]
5305 });
5306
5307 let response: CompletionResponse = serde_json::from_value(value).unwrap();
5308 let converted: completion::CompletionResponse<CompletionResponse> =
5309 response.try_into().unwrap();
5310 let message::AssistantContent::Text(web_search_result) = converted.choice.first() else {
5311 panic!("expected raw web_search_tool_result metadata");
5312 };
5313
5314 let raw_content =
5315 &web_search_result.additional_params.as_ref().unwrap()[ANTHROPIC_RAW_CONTENT_KEY];
5316 assert_eq!(raw_content["type"], "web_search_tool_result");
5317 assert_eq!(raw_content["content"]["error_code"], "max_uses_exceeded");
5318 assert_eq!(
5319 raw_content["content"]["type"],
5320 "web_search_tool_result_error"
5321 );
5322
5323 let round_trip: Message = message::Message::Assistant {
5324 id: converted.message_id,
5325 content: converted.choice,
5326 }
5327 .try_into()
5328 .unwrap();
5329
5330 assert!(matches!(
5331 round_trip.content.first(),
5332 Content::WebSearchToolResult {
5333 tool_use_id,
5334 content
5335 } if tool_use_id == "srvtoolu_01"
5336 && content["error_code"] == "max_uses_exceeded"
5337 ));
5338 }
5339
5340 #[test]
5341 fn text_deserializes_unknown_citation_without_failing() {
5342 let value = json!({
5343 "type": "text",
5344 "text": "future citation",
5345 "citations": [{
5346 "type": "future_location",
5347 "cited_text": "future text",
5348 "new_field": "kept"
5349 }]
5350 });
5351
5352 let parsed: Content = serde_json::from_value(value).unwrap();
5353 let Content::Text { citations, .. } = parsed else {
5354 panic!("expected Content::Text");
5355 };
5356
5357 assert!(matches!(
5358 &citations[0],
5359 Citation::Unknown(raw)
5360 if raw["type"] == "future_location" && raw["new_field"] == "kept"
5361 ));
5362 }
5363
5364 #[test]
5365 fn page_location_citation_roundtrips() {
5366 let citation = Citation::PageLocation {
5367 cited_text: "Water is essential for life.".into(),
5368 document_index: 1,
5369 document_title: Some("PDF Doc".into()),
5370 start_page_number: 5,
5371 end_page_number: 6,
5372 };
5373 let value = serde_json::to_value(&citation).unwrap();
5374 assert_eq!(value["type"], "page_location");
5375 assert_eq!(value["start_page_number"], 5);
5376 let back: Citation = serde_json::from_value(value).unwrap();
5377 assert_eq!(back, citation);
5378 }
5379
5380 #[test]
5381 fn content_block_location_citation_roundtrips() {
5382 let citation = Citation::ContentBlockLocation {
5383 cited_text: "These are important findings.".into(),
5384 document_index: 2,
5385 document_title: None,
5386 start_block_index: 0,
5387 end_block_index: 1,
5388 };
5389 let value = serde_json::to_value(&citation).unwrap();
5390 assert_eq!(value["type"], "content_block_location");
5391 assert!(value.get("document_title").is_none());
5392 let back: Citation = serde_json::from_value(value).unwrap();
5393 assert_eq!(back, citation);
5394 }
5395
5396 #[test]
5397 fn anthropic_citations_extracts_from_additional_params() {
5398 let text = message::Text {
5399 text: "the grass is green".into(),
5400 additional_params: Some(json!({
5401 "citations": [{
5402 "type": "char_location",
5403 "cited_text": "The grass is green.",
5404 "document_index": 0,
5405 "start_char_index": 0,
5406 "end_char_index": 20
5407 }]
5408 })),
5409 };
5410 let citations = anthropic_citations(&text).unwrap();
5411 assert_eq!(citations.len(), 1);
5412 }
5413
5414 #[test]
5415 fn anthropic_citations_returns_empty_when_absent() {
5416 let text = message::Text::new("hello".to_string());
5417 assert!(anthropic_citations(&text).unwrap().is_empty());
5418 }
5419
5420 #[test]
5421 fn content_text_with_citations_survives_assistant_conversion() {
5422 let content = Content::Text {
5423 text: "the grass is green".into(),
5424 citations: vec![Citation::CharLocation {
5425 cited_text: "The grass is green.".into(),
5426 document_index: 0,
5427 document_title: None,
5428 start_char_index: 0,
5429 end_char_index: 20,
5430 }],
5431 cache_control: None,
5432 };
5433 let assistant: message::AssistantContent = content.try_into().unwrap();
5434 let message::AssistantContent::Text(text) = assistant else {
5435 panic!("expected text variant");
5436 };
5437 let recovered = anthropic_citations(&text).unwrap();
5438 assert_eq!(recovered.len(), 1);
5439 }
5440
5441 #[test]
5442 fn provider_text_response_concatenates_text_blocks_without_inserted_newlines() {
5443 let response = CompletionResponse {
5444 content: vec![
5445 Content::Text {
5446 text: "According to the document, ".into(),
5447 citations: Vec::new(),
5448 cache_control: None,
5449 },
5450 Content::Text {
5451 text: "the grass is green".into(),
5452 citations: Vec::new(),
5453 cache_control: None,
5454 },
5455 Content::Text {
5456 text: " and the sky is blue.".into(),
5457 citations: Vec::new(),
5458 cache_control: None,
5459 },
5460 ],
5461 id: "msg_1".into(),
5462 model: "claude-test".into(),
5463 role: "assistant".into(),
5464 stop_reason: Some("end_turn".into()),
5465 stop_sequence: None,
5466 usage: Usage {
5467 input_tokens: 1,
5468 cache_read_input_tokens: None,
5469 cache_creation_input_tokens: None,
5470 output_tokens: 1,
5471 },
5472 };
5473
5474 assert_eq!(
5475 response.get_text_response().as_deref(),
5476 Some("According to the document, the grass is green and the sky is blue.")
5477 );
5478 }
5479
5480 #[test]
5481 fn assistant_text_citations_survive_anthropic_request_conversion() {
5482 let assistant = message::Message::Assistant {
5483 id: None,
5484 content: OneOrMany::one(message::AssistantContent::Text(message::Text {
5485 text: "the grass is green".into(),
5486 additional_params: Some(json!({
5487 "citations": [{
5488 "type": "char_location",
5489 "cited_text": "The grass is green.",
5490 "document_index": 0,
5491 "start_char_index": 0,
5492 "end_char_index": 20
5493 }]
5494 })),
5495 })),
5496 };
5497
5498 let converted: Message = assistant.try_into().unwrap();
5499 let Content::Text {
5500 citations, text, ..
5501 } = converted.content.first()
5502 else {
5503 panic!("expected assistant text content");
5504 };
5505
5506 assert_eq!(text, "the grass is green");
5507 assert_eq!(
5508 citations,
5509 vec![Citation::CharLocation {
5510 cited_text: "The grass is green.".into(),
5511 document_index: 0,
5512 document_title: None,
5513 start_char_index: 0,
5514 end_char_index: 20,
5515 }]
5516 );
5517 }
5518
5519 #[test]
5520 fn assistant_text_invalid_known_citations_are_rejected_for_anthropic_request_conversion() {
5521 let text = message::AssistantContent::Text(message::Text {
5522 text: "bad citation".into(),
5523 additional_params: Some(json!({
5524 "citations": [{
5525 "type": "char_location",
5526 "cited_text": "bad"
5527 }]
5528 })),
5529 });
5530
5531 let result = Content::try_from(text);
5532
5533 assert!(
5534 result.is_err(),
5535 "invalid Anthropic citation metadata should not be silently dropped"
5536 );
5537 }
5538
5539 #[test]
5540 fn document_additional_params_forward_to_anthropic_document() {
5541 let doc = message::UserContent::Document(message::Document {
5542 data: message::DocumentSourceKind::String("Hello world.".into()),
5543 media_type: Some(message::DocumentMediaType::TXT),
5544 additional_params: Some(json!({
5545 "title": "Doc1",
5546 "context": "ctx",
5547 "citations": { "enabled": true }
5548 })),
5549 });
5550 let msg = message::Message::User {
5551 content: OneOrMany::one(doc),
5552 };
5553 let converted: Message = msg.try_into().unwrap();
5554 let block = converted.content.first();
5555 let Content::Document {
5556 title,
5557 context,
5558 citations,
5559 ..
5560 } = block
5561 else {
5562 panic!("expected Content::Document");
5563 };
5564 assert_eq!(title.as_deref(), Some("Doc1"));
5565 assert_eq!(context.as_deref(), Some("ctx"));
5566 assert_eq!(citations, Some(CitationsConfig { enabled: true }));
5567 }
5568
5569 fn assert_reverse_document_metadata(
5570 source: DocumentSource,
5571 expected_data: DocumentSourceKind,
5572 expected_media_type: Option<message::DocumentMediaType>,
5573 ) -> message::Message {
5574 let provider_message = Message {
5575 role: Role::User,
5576 content: OneOrMany::one(Content::Document {
5577 source,
5578 title: Some("Doc1".into()),
5579 context: Some("ctx".into()),
5580 citations: Some(CitationsConfig { enabled: true }),
5581 cache_control: None,
5582 }),
5583 };
5584
5585 let generic: message::Message = provider_message.try_into().unwrap();
5586 let message::Message::User { content } = &generic else {
5587 panic!("expected generic user message");
5588 };
5589 let message::UserContent::Document(document) = content.first() else {
5590 panic!("expected generic document");
5591 };
5592
5593 assert_eq!(document.data, expected_data);
5594 assert_eq!(document.media_type, expected_media_type);
5595 let additional_params = document
5596 .additional_params
5597 .as_ref()
5598 .expect("expected Anthropic document metadata");
5599 assert_eq!(additional_params["title"], "Doc1");
5600 assert_eq!(additional_params["context"], "ctx");
5601 assert_eq!(additional_params["citations"]["enabled"], true);
5602
5603 generic
5604 }
5605
5606 #[test]
5607 fn anthropic_document_metadata_survives_reverse_conversion_for_all_sources() {
5608 assert_reverse_document_metadata(
5609 DocumentSource::Text {
5610 data: "Hello world.".into(),
5611 media_type: PlainTextMediaType::Plain,
5612 },
5613 DocumentSourceKind::String("Hello world.".into()),
5614 Some(message::DocumentMediaType::TXT),
5615 );
5616 assert_reverse_document_metadata(
5617 DocumentSource::Base64 {
5618 data: "base64-pdf".into(),
5619 media_type: DocumentFormat::PDF,
5620 },
5621 DocumentSourceKind::String("base64-pdf".into()),
5622 Some(message::DocumentMediaType::PDF),
5623 );
5624 assert_reverse_document_metadata(
5625 DocumentSource::Url {
5626 url: "https://example.com/doc.pdf".into(),
5627 },
5628 DocumentSourceKind::Url("https://example.com/doc.pdf".into()),
5629 None,
5630 );
5631 assert_reverse_document_metadata(
5632 DocumentSource::File {
5633 file_id: "file_abc".into(),
5634 },
5635 DocumentSourceKind::FileId("file_abc".into()),
5636 None,
5637 );
5638 }
5639
5640 #[test]
5641 fn anthropic_document_metadata_survives_reverse_round_trip() {
5642 let provider_message = Message {
5643 role: Role::User,
5644 content: OneOrMany::one(Content::Document {
5645 source: DocumentSource::Text {
5646 data: "Hello world.".into(),
5647 media_type: PlainTextMediaType::Plain,
5648 },
5649 title: Some("Doc1".into()),
5650 context: Some("ctx".into()),
5651 citations: Some(CitationsConfig { enabled: true }),
5652 cache_control: None,
5653 }),
5654 };
5655
5656 let generic: message::Message = provider_message.try_into().unwrap();
5657 let message::Message::User { content } = &generic else {
5658 panic!("expected generic user message");
5659 };
5660 let message::UserContent::Document(document) = content.first() else {
5661 panic!("expected generic document");
5662 };
5663 let additional_params = document
5664 .additional_params
5665 .as_ref()
5666 .expect("expected Anthropic document metadata");
5667 assert_eq!(additional_params["title"], "Doc1");
5668 assert_eq!(additional_params["context"], "ctx");
5669 assert_eq!(additional_params["citations"]["enabled"], true);
5670
5671 let round_trip: Message = generic.try_into().unwrap();
5672 let Content::Document {
5673 title,
5674 context,
5675 citations,
5676 ..
5677 } = round_trip.content.first()
5678 else {
5679 panic!("expected Anthropic document");
5680 };
5681 assert_eq!(title.as_deref(), Some("Doc1"));
5682 assert_eq!(context.as_deref(), Some("ctx"));
5683 assert_eq!(citations, Some(CitationsConfig { enabled: true }));
5684 }
5685
5686 #[test]
5687 fn anthropic_document_empty_metadata_stays_none_on_reverse_conversion() {
5688 let provider_message = Message {
5689 role: Role::User,
5690 content: OneOrMany::one(Content::Document {
5691 source: DocumentSource::Text {
5692 data: "Hello world.".into(),
5693 media_type: PlainTextMediaType::Plain,
5694 },
5695 title: None,
5696 context: None,
5697 citations: None,
5698 cache_control: None,
5699 }),
5700 };
5701
5702 let generic: message::Message = provider_message.try_into().unwrap();
5703 let message::Message::User { content } = &generic else {
5704 panic!("expected generic user message");
5705 };
5706 let message::UserContent::Document(document) = content.first() else {
5707 panic!("expected generic document");
5708 };
5709
5710 assert_eq!(document.additional_params, None);
5711 }
5712}