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