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