1use crate::completion::CompletionRequest;
10use crate::error::EncodeError;
11use crate::json_utils::string_or_vec;
12use crate::{
13 completion,
14 message::{self, DocumentMediaType, DocumentSourceKind, MessageError, MimeType},
15};
16use serde::{Deserialize, Serialize};
17use std::{convert::Infallible, str::FromStr};
18
19pub const CLAUDE_FABLE_5_1: &str = "claude-fable-5-1";
24pub const CLAUDE_OPUS_5_5: &str = "claude-opus-5-5";
29pub const CLAUDE_SONNET_5_5: &str = "claude-sonnet-5-5";
34pub const CLAUDE_FABLE_5: &str = "claude-fable-5";
36pub const CLAUDE_OPUS_5: &str = "claude-opus-5";
38pub const CLAUDE_SONNET_5: &str = "claude-sonnet-5";
40pub const CLAUDE_OPUS_4_6: &str = "claude-opus-4-6";
42pub const CLAUDE_OPUS_4_7: &str = "claude-opus-4-7";
44pub const CLAUDE_OPUS_4_8: &str = "claude-opus-4-8";
46pub const CLAUDE_SONNET_4_6: &str = "claude-sonnet-4-6";
48pub const CLAUDE_HAIKU_4_5: &str = "claude-haiku-4-5";
50
51pub const ANTHROPIC_VERSION_2023_01_01: &str = "2023-01-01";
52pub const ANTHROPIC_VERSION_2023_06_01: &str = "2023-06-01";
53pub const ANTHROPIC_VERSION_LATEST: &str = ANTHROPIC_VERSION_2023_06_01;
54pub(crate) const ANTHROPIC_RAW_CONTENT_KEY: &str = "anthropic_content";
55
56#[derive(Debug, Deserialize, Serialize)]
57pub struct CompletionResponse {
58 pub content: Vec<Content>,
59 pub id: String,
60 pub model: String,
61 pub role: String,
62 pub stop_reason: Option<String>,
63 pub stop_sequence: Option<String>,
64 pub usage: Usage,
65}
66
67pub(crate) fn map_finish_reason(stop_reason: &str) -> completion::FinishReason {
69 match stop_reason {
70 "end_turn" | "stop_sequence" => completion::FinishReason::Stop,
73 "max_tokens" => completion::FinishReason::Length,
74 "tool_use" => completion::FinishReason::ToolCalls,
75 "refusal" => completion::FinishReason::ContentFilter,
78 other => completion::FinishReason::Other(other.to_owned()),
79 }
80}
81
82#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
86pub struct Usage {
87 pub input_tokens: u64,
89 pub cache_read_input_tokens: Option<u64>,
90 pub cache_creation_input_tokens: Option<u64>,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub cache_creation: Option<CacheCreation>,
95 pub output_tokens: u64,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub output_tokens_details: Option<OutputTokensDetails>,
100}
101
102#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
105pub struct OutputTokensDetails {
106 #[serde(default)]
108 pub thinking_tokens: u64,
109}
110
111#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq)]
114pub struct CacheCreation {
115 #[serde(default)]
117 pub ephemeral_5m_input_tokens: u64,
118 #[serde(default)]
120 pub ephemeral_1h_input_tokens: u64,
121}
122
123impl std::fmt::Display for Usage {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 write!(
126 f,
127 "Uncached input tokens: {}\nCache read input tokens: {}\nCache creation input tokens: {}\nOutput tokens: {}",
128 self.input_tokens,
129 self.cache_read_input_tokens
130 .map_or_else(|| "n/a".to_string(), |token| token.to_string()),
131 self.cache_creation_input_tokens
132 .map_or_else(|| "n/a".to_string(), |token| token.to_string()),
133 self.output_tokens
134 )
135 }
136}
137
138pub(super) fn anthropic_usage_totals(
142 input_tokens: Option<u64>,
143 output_tokens: u64,
144 cache_read: Option<u64>,
145 cache_creation: Option<u64>,
146 output_tokens_details: Option<OutputTokensDetails>,
147) -> crate::completion::Usage {
148 let input_tokens = input_tokens
149 .map(|uncached| uncached + cache_read.unwrap_or(0) + cache_creation.unwrap_or(0));
150 crate::completion::Usage {
151 input_tokens,
152 output_tokens: Some(output_tokens),
153 cached_input_tokens: cache_read,
154 cache_creation_input_tokens: cache_creation,
155 reasoning_tokens: output_tokens_details.map(|details| details.thinking_tokens),
156 total_tokens: input_tokens.map(|input| input + output_tokens),
157 tool_use_prompt_tokens: None,
158 }
159}
160
161impl From<&Usage> for crate::completion::Usage {
162 fn from(value: &Usage) -> crate::completion::Usage {
163 anthropic_usage_totals(
164 Some(value.input_tokens),
165 value.output_tokens,
166 value.cache_read_input_tokens,
167 value.cache_creation_input_tokens,
168 value.output_tokens_details,
169 )
170 }
171}
172
173impl From<Usage> for crate::completion::Usage {
174 fn from(value: Usage) -> crate::completion::Usage {
175 (&value).into()
176 }
177}
178
179#[derive(Debug, Deserialize, Serialize)]
180pub struct ToolDefinition {
181 pub name: String,
182 pub description: Option<String>,
183 pub input_schema: serde_json::Value,
184 #[serde(default, skip_serializing_if = "crate::json_utils::is_false")]
186 pub strict: bool,
187 #[serde(skip_serializing_if = "Option::is_none")]
191 pub cache_control: Option<CacheControl>,
192}
193
194#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
196pub enum CacheTtl {
197 #[default]
199 #[serde(rename = "5m")]
200 FiveMinutes,
201 #[serde(rename = "1h")]
203 OneHour,
204}
205
206#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
211#[serde(tag = "type", rename_all = "snake_case")]
212pub enum CacheControl {
213 Ephemeral {
214 #[serde(skip_serializing_if = "Option::is_none")]
216 ttl: Option<CacheTtl>,
217 },
218}
219
220impl CacheControl {
221 pub fn ephemeral() -> Self {
223 Self::Ephemeral { ttl: None }
224 }
225
226 pub fn ephemeral_1h() -> Self {
228 Self::Ephemeral {
229 ttl: Some(CacheTtl::OneHour),
230 }
231 }
232}
233
234#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
236#[serde(tag = "type", rename_all = "snake_case")]
237pub enum SystemContent {
238 Text {
239 text: String,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 cache_control: Option<CacheControl>,
242 },
243}
244
245#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
246pub struct Message {
247 pub role: Role,
248 #[serde(deserialize_with = "string_or_vec")]
249 pub content: Vec<Content>,
250}
251
252#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
253#[serde(rename_all = "lowercase")]
254pub enum Role {
255 User,
256 Assistant,
257 System,
258}
259
260#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
261#[serde(tag = "type", rename_all = "snake_case")]
262pub enum Content {
263 Text {
264 text: String,
265 #[serde(
268 default,
269 deserialize_with = "null_as_empty_vec",
270 skip_serializing_if = "Vec::is_empty"
271 )]
272 citations: Vec<Citation>,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 cache_control: Option<CacheControl>,
275 },
276 Image {
277 source: ImageSource,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 cache_control: Option<CacheControl>,
280 },
281 ToolUse {
282 id: String,
283 name: String,
284 input: serde_json::Value,
285 },
286 ServerToolUse {
287 id: String,
288 name: String,
289 #[serde(default)]
290 input: serde_json::Value,
291 },
292 WebSearchToolResult {
293 tool_use_id: String,
294 content: serde_json::Value,
295 },
296 CodeExecutionToolResult {
298 tool_use_id: String,
299 content: serde_json::Value,
300 },
301 ToolResult {
302 tool_use_id: String,
303 #[serde(deserialize_with = "string_or_vec")]
304 content: Vec<ToolResultContent>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 is_error: Option<bool>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 cache_control: Option<CacheControl>,
309 },
310 Document {
311 source: DocumentSource,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
314 title: Option<String>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
319 context: Option<String>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
324 citations: Option<CitationsConfig>,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 cache_control: Option<CacheControl>,
327 },
328 Thinking {
329 thinking: String,
330 #[serde(skip_serializing_if = "Option::is_none")]
331 signature: Option<String>,
332 },
333 RedactedThinking {
334 data: String,
335 },
336}
337
338impl FromStr for Content {
339 type Err = Infallible;
340
341 fn from_str(s: &str) -> Result<Self, Self::Err> {
342 Ok(Content::from(s.to_owned()))
343 }
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350pub struct CitationsConfig {
351 pub enabled: bool,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq)]
359pub enum Citation {
360 CharLocation(CharLocationCitation),
362 PageLocation(PageLocationCitation),
364 ContentBlockLocation(ContentBlockLocationCitation),
366 SearchResultLocation(SearchResultLocationCitation),
368 WebSearchResultLocation(WebSearchResultLocationCitation),
370 Unknown(serde_json::Value),
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
377pub struct CharLocationCitation {
378 pub cited_text: String,
380 pub document_index: usize,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub document_title: Option<String>,
385 pub start_char_index: usize,
387 pub end_char_index: usize,
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
393pub struct PageLocationCitation {
394 pub cited_text: String,
396 pub document_index: usize,
398 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub document_title: Option<String>,
401 pub start_page_number: u32,
403 pub end_page_number: u32,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409pub struct ContentBlockLocationCitation {
410 pub cited_text: String,
412 pub document_index: usize,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub document_title: Option<String>,
417 pub start_block_index: usize,
419 pub end_block_index: usize,
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
425pub struct SearchResultLocationCitation {
426 pub cited_text: String,
428 pub source: String,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
432 pub title: Option<String>,
433 pub search_result_index: usize,
436 pub start_block_index: usize,
438 pub end_block_index: usize,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
444pub struct WebSearchResultLocationCitation {
445 pub cited_text: String,
447 pub url: String,
449 pub title: Option<String>,
451 pub encrypted_index: String,
454}
455
456impl Serialize for Citation {
457 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
458 where
459 S: serde::Serializer,
460 {
461 fn tagged<S, T>(serializer: S, tag: &str, fields: &T) -> Result<S::Ok, S::Error>
463 where
464 S: serde::Serializer,
465 T: Serialize,
466 {
467 let mut value = serde_json::to_value(fields).map_err(serde::ser::Error::custom)?;
468 if let serde_json::Value::Object(obj) = &mut value {
469 obj.insert("type".into(), serde_json::json!(tag));
470 }
471 value.serialize(serializer)
472 }
473
474 match self {
475 Citation::CharLocation(fields) => tagged(serializer, "char_location", fields),
476 Citation::PageLocation(fields) => tagged(serializer, "page_location", fields),
477 Citation::ContentBlockLocation(fields) => {
478 tagged(serializer, "content_block_location", fields)
479 }
480 Citation::SearchResultLocation(fields) => {
481 tagged(serializer, "search_result_location", fields)
482 }
483 Citation::WebSearchResultLocation(fields) => {
484 tagged(serializer, "web_search_result_location", fields)
485 }
486 Citation::Unknown(raw) => raw.serialize(serializer),
487 }
488 }
489}
490
491impl<'de> Deserialize<'de> for Citation {
492 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
493 where
494 D: serde::Deserializer<'de>,
495 {
496 fn payload<T, E>(value: serde_json::Value) -> Result<T, E>
500 where
501 T: serde::de::DeserializeOwned,
502 E: serde::de::Error,
503 {
504 serde_json::from_value(value).map_err(E::custom)
505 }
506
507 let value = serde_json::Value::deserialize(deserializer)?;
509 let Some(citation_type) = value.get("type").and_then(serde_json::Value::as_str) else {
510 return Ok(Citation::Unknown(value));
511 };
512
513 match citation_type {
514 "char_location" => Ok(Citation::CharLocation(payload(value)?)),
515 "page_location" => Ok(Citation::PageLocation(payload(value)?)),
516 "content_block_location" => Ok(Citation::ContentBlockLocation(payload(value)?)),
517 "search_result_location" => Ok(Citation::SearchResultLocation(payload(value)?)),
518 "web_search_result_location" => Ok(Citation::WebSearchResultLocation(payload(value)?)),
519 _ => Ok(Citation::Unknown(value)),
520 }
521 }
522}
523
524fn null_as_empty_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
527where
528 D: serde::Deserializer<'de>,
529 T: serde::Deserialize<'de>,
530{
531 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
532}
533
534fn extract_anthropic_doc_params(
540 additional_params: Option<message::AdditionalParams>,
541) -> Result<(Option<String>, Option<String>, Option<CitationsConfig>), MessageError> {
542 let Some(value) = additional_params else {
543 return Ok((None, None, None));
544 };
545 let title = value
546 .get("title")
547 .and_then(|v| v.as_str())
548 .map(String::from);
549 let context = value
550 .get("context")
551 .and_then(|v| v.as_str())
552 .map(String::from);
553 let citations = value
554 .get("citations")
555 .cloned()
556 .map(serde_json::from_value::<CitationsConfig>)
557 .transpose()
558 .map_err(|e| {
559 MessageError::ConversionError(format!(
560 "Document `additional_params.citations` is not a valid CitationsConfig: {e}",
561 ))
562 })?;
563 Ok((title, context, citations))
564}
565
566pub fn anthropic_citations(text: &message::Text) -> Result<Vec<Citation>, serde_json::Error> {
592 match text
593 .additional_params
594 .as_ref()
595 .and_then(|v| v.get("citations"))
596 {
597 Some(c) => <Vec<Citation> as serde::Deserialize>::deserialize(c),
598 None => Ok(Vec::new()),
599 }
600}
601
602fn extract_anthropic_text_citations(text: &message::Text) -> Result<Vec<Citation>, MessageError> {
603 anthropic_citations(text).map_err(|err| {
604 MessageError::ConversionError(format!(
605 "Text `additional_params.citations` is not valid Anthropic citations: {err}"
606 ))
607 })
608}
609
610fn anthropic_text_content_from_message_text(text: message::Text) -> Result<Content, MessageError> {
611 if let Some(raw_content) = extract_anthropic_raw_content(&text)? {
612 if !text.text.is_empty() {
613 return Err(MessageError::ConversionError(format!(
614 "Text `{ANTHROPIC_RAW_CONTENT_KEY}` metadata cannot be combined with non-empty text"
615 )));
616 }
617
618 return Ok(raw_content);
619 }
620
621 let citations = extract_anthropic_text_citations(&text)?;
622 Ok(Content::Text {
623 text: text.text,
624 citations,
625 cache_control: None,
626 })
627}
628
629fn extract_anthropic_raw_content(text: &message::Text) -> Result<Option<Content>, MessageError> {
630 let Some(raw_content) = text
631 .additional_params
632 .as_ref()
633 .and_then(|value| value.get(ANTHROPIC_RAW_CONTENT_KEY))
634 else {
635 return Ok(None);
636 };
637
638 let content = <Content as serde::Deserialize>::deserialize(raw_content).map_err(|err| {
639 MessageError::ConversionError(format!(
640 "Text `{ANTHROPIC_RAW_CONTENT_KEY}` metadata is not valid Anthropic content: {err}"
641 ))
642 })?;
643
644 match content {
645 Content::ServerToolUse { .. }
646 | Content::WebSearchToolResult { .. }
647 | Content::CodeExecutionToolResult { .. } => Ok(Some(content)),
648 _ => Err(MessageError::ConversionError(format!(
649 "Text `{ANTHROPIC_RAW_CONTENT_KEY}` metadata only supports Anthropic server_tool_use, web_search_tool_result, and code_execution_tool_result blocks"
650 ))),
651 }
652}
653
654#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
655#[serde(tag = "type", rename_all = "snake_case")]
656pub enum ToolResultContent {
657 Text { text: String },
658 Image { source: ImageSource },
659}
660
661impl FromStr for ToolResultContent {
662 type Err = Infallible;
663
664 fn from_str(s: &str) -> Result<Self, Self::Err> {
665 Ok(ToolResultContent::Text { text: s.to_owned() })
666 }
667}
668
669#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
677#[serde(tag = "type", rename_all = "snake_case")]
678pub enum ImageSource {
679 #[serde(rename = "base64")]
680 Base64 {
681 data: String,
682 media_type: ImageFormat,
683 },
684 #[serde(rename = "url")]
685 Url { url: String },
686}
687
688#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
696#[serde(tag = "type", rename_all = "snake_case")]
697pub enum DocumentSource {
698 Base64 {
699 data: String,
700 media_type: DocumentFormat,
701 },
702 Text {
703 data: String,
704 media_type: PlainTextMediaType,
705 },
706 Url {
707 url: String,
708 },
709 File {
710 file_id: String,
711 },
712}
713
714#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
715#[serde(rename_all = "lowercase")]
716pub enum ImageFormat {
717 #[serde(rename = "image/jpeg")]
718 JPEG,
719 #[serde(rename = "image/png")]
720 PNG,
721 #[serde(rename = "image/gif")]
722 GIF,
723 #[serde(rename = "image/webp")]
724 WEBP,
725}
726
727#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
734#[serde(rename_all = "lowercase")]
735pub enum DocumentFormat {
736 #[serde(rename = "application/pdf")]
737 PDF,
738}
739
740#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
746pub enum PlainTextMediaType {
747 #[serde(rename = "text/plain")]
748 Plain,
749}
750
751impl From<String> for Content {
752 fn from(text: String) -> Self {
753 Content::Text {
754 text,
755 citations: Vec::new(),
756 cache_control: None,
757 }
758 }
759}
760
761impl From<String> for ToolResultContent {
762 fn from(text: String) -> Self {
763 ToolResultContent::Text { text }
764 }
765}
766
767impl TryFrom<message::ImageMediaType> for ImageFormat {
768 type Error = MessageError;
769
770 fn try_from(media_type: message::ImageMediaType) -> Result<Self, Self::Error> {
771 Ok(match media_type {
772 message::ImageMediaType::JPEG => ImageFormat::JPEG,
773 message::ImageMediaType::PNG => ImageFormat::PNG,
774 message::ImageMediaType::GIF => ImageFormat::GIF,
775 message::ImageMediaType::WEBP => ImageFormat::WEBP,
776 _ => {
777 return Err(MessageError::ConversionError(format!(
778 "Unsupported image media type: {media_type:?}"
779 )));
780 }
781 })
782 }
783}
784
785fn coerce_tool_input(input: serde_json::Value) -> serde_json::Value {
788 match input {
789 v @ serde_json::Value::Object(_) => v,
790 serde_json::Value::String(s) => match serde_json::from_str::<serde_json::Value>(&s) {
791 Ok(serde_json::Value::Object(m)) => serde_json::Value::Object(m),
792 _ => serde_json::json!({}),
793 },
794 _ => serde_json::json!({}),
795 }
796}
797
798fn anthropic_content_from_assistant_content(
799 content: message::AssistantContent,
800 issuers: &[message::Issuer],
801) -> Result<Vec<Content>, MessageError> {
802 match content {
803 message::AssistantContent::Text(text) => {
804 if text.text.is_empty() && extract_anthropic_raw_content(&text)?.is_none() {
807 return Ok(Vec::new());
808 }
809 Ok(vec![anthropic_text_content_from_message_text(text)?])
810 }
811 message::AssistantContent::Image(_) => Err(MessageError::ConversionError(
812 "Anthropic currently doesn't support images.".to_string(),
813 )),
814 message::AssistantContent::ToolCall(tool_call) => Ok(vec![Content::ToolUse {
815 id: tool_call.id.wire().into_owned(),
818 name: tool_call.function.name.into(),
819 input: coerce_tool_input(tool_call.function.arguments),
820 }]),
821 message::AssistantContent::Reasoning(reasoning) => {
822 let Some(reasoning) = reasoning.open_for(issuers) else {
824 return Ok(Vec::new());
825 };
826 let mut converted = Vec::new();
827 for block in reasoning.content.clone() {
828 match block {
829 message::ReasoningContent::Text { text, signature } => {
830 converted.push(Content::Thinking {
831 thinking: text,
832 signature,
833 });
834 }
835 message::ReasoningContent::Summary(summary) => {
836 converted.push(Content::Thinking {
837 thinking: summary,
838 signature: None,
839 });
840 }
841 message::ReasoningContent::Redacted { data }
842 | message::ReasoningContent::Encrypted(data) => {
843 converted.push(Content::RedactedThinking { data });
844 }
845 }
846 }
847
848 if converted.is_empty() {
849 return Err(MessageError::ConversionError(
850 "Cannot convert empty reasoning content to Anthropic format".to_string(),
851 ));
852 }
853
854 Ok(converted)
855 }
856 }
857}
858
859impl TryFrom<message::Message> for Message {
863 type Error = MessageError;
864
865 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
866 Message::from_message(message, &[])
867 }
868}
869
870impl Message {
871 fn from_message(
873 message: message::Message,
874 issuers: &[message::Issuer],
875 ) -> Result<Self, MessageError> {
876 Ok(match message {
877 message::Message::User { content } => Message {
878 role: Role::User,
879 content: content.into_iter().map(|content| match content {
880 message::UserContent::Text(message::Text { text, .. }) => {
881 Ok(Content::from(text))
882 }
883 message::UserContent::ToolResult(tool_result) => Ok(Content::ToolResult {
884 tool_use_id: tool_result.call.wire().into_owned(),
885 content: tool_result.content.into_iter().map(|content| match content {
886 message::ToolResultContent::Text(message::Text { text, .. }) => {
887 Ok(ToolResultContent::Text { text })
888 }
889 message::ToolResultContent::Json { value } => {
890 Ok(ToolResultContent::Text {
891 text: value.to_string(),
892 })
893 }
894 message::ToolResultContent::Image(image) => {
895 let DocumentSourceKind::Base64(data) = image.data else {
896 return Err(MessageError::ConversionError(
897 "Only base64 strings can be used with the Anthropic API"
898 .to_string(),
899 ));
900 };
901 let media_type =
902 image.media_type.ok_or(MessageError::ConversionError(
903 "Image media type is required".to_owned(),
904 ))?;
905 Ok(ToolResultContent::Image {
906 source: ImageSource::Base64 {
907 data,
908 media_type: media_type.try_into()?,
909 },
910 })
911 }
912 }).collect::<Result<Vec<_>, _>>()?,
913 is_error: None,
914 cache_control: None,
915 }),
916 message::UserContent::Image(message::Image {
917 data, media_type, ..
918 }) => {
919 let source = match data {
920 DocumentSourceKind::Base64(data) => {
921 let media_type =
922 media_type.ok_or(MessageError::ConversionError(
923 "Image media type is required for Claude API".to_string(),
924 ))?;
925 ImageSource::Base64 {
926 data,
927 media_type: ImageFormat::try_from(media_type)?,
928 }
929 }
930 DocumentSourceKind::Url(url) => ImageSource::Url { url },
931 DocumentSourceKind::Unknown => {
932 return Err(MessageError::ConversionError(
933 "Image content has no body".into(),
934 ));
935 }
936 doc => {
937 return Err(MessageError::ConversionError(format!(
938 "Unsupported document type: {doc:?}"
939 )));
940 }
941 };
942
943 Ok(Content::Image {
944 source,
945 cache_control: None,
946 })
947 }
948 message::UserContent::Document(message::Document {
949 data,
950 media_type,
951 additional_params,
952 }) => {
953 let (title, context, citations) =
954 extract_anthropic_doc_params(additional_params)?;
955
956 if let DocumentSourceKind::FileId(file_id) = data {
957 return Ok(Content::Document {
958 source: DocumentSource::File { file_id },
959 title,
960 context,
961 citations,
962 cache_control: None,
963 });
964 }
965
966 let media_type = match media_type {
967 Some(media_type) => media_type,
968 None if matches!(&data, DocumentSourceKind::Url(_)) => {
971 DocumentMediaType::PDF
972 }
973 None => {
974 return Err(MessageError::ConversionError(
975 "Document media type is required".to_string(),
976 ));
977 }
978 };
979
980 let source = match media_type {
981 DocumentMediaType::PDF => match data {
982 DocumentSourceKind::Base64(data)
983 | DocumentSourceKind::String(data) => DocumentSource::Base64 {
984 data,
985 media_type: DocumentFormat::PDF,
986 },
987 DocumentSourceKind::Url(url) => DocumentSource::Url { url },
988 _ => {
989 return Err(MessageError::ConversionError(
990 "Only base64 encoded data or URLs are supported for PDF documents".into(),
991 ));
992 }
993 },
994 DocumentMediaType::TXT => {
995 let (DocumentSourceKind::String(data)
996 | DocumentSourceKind::Base64(data)) = data
997 else {
998 return Err(MessageError::ConversionError(
999 "Only string or base64 data is supported for plain text documents".into(),
1000 ));
1001 };
1002 DocumentSource::Text {
1003 data,
1004 media_type: PlainTextMediaType::Plain,
1005 }
1006 }
1007 other => {
1008 return Err(MessageError::ConversionError(format!(
1009 "Anthropic only supports PDF and plain text documents, got: {}",
1010 other.to_mime_type()
1011 )));
1012 }
1013 };
1014
1015 Ok(Content::Document {
1016 source,
1017 title,
1018 context,
1019 citations,
1020 cache_control: None,
1021 })
1022 }
1023 message::UserContent::Audio { .. } => Err(MessageError::ConversionError(
1024 "Audio is not supported in Anthropic".to_owned(),
1025 )),
1026 message::UserContent::Video { .. } => Err(MessageError::ConversionError(
1027 "Video is not supported in Anthropic".to_owned(),
1028 )),
1029 }).collect::<Result<Vec<_>, _>>()?,
1030 },
1031
1032 message::Message::System { content } => Message {
1033 role: Role::System,
1034 content: vec![Content::from(content)],
1035 },
1036
1037 message::Message::Assistant { content, .. } => {
1038 let converted_content = content.into_iter().try_fold(
1039 Vec::new(),
1040 |mut accumulated, assistant_content| {
1041 accumulated.extend(anthropic_content_from_assistant_content(
1042 assistant_content,
1043 issuers,
1044 )?);
1045 Ok::<Vec<Content>, MessageError>(accumulated)
1046 },
1047 )?;
1048
1049 Message {
1050 content: crate::message::require_non_empty(converted_content, || {
1051 MessageError::ConversionError(
1052 "Assistant message did not contain Anthropic-compatible content"
1053 .to_owned(),
1054 )
1055 })?,
1056 role: Role::Assistant,
1057 }
1058 }
1059 })
1060 }
1061}
1062
1063pub(super) fn is_model(model: &str, id: &str) -> bool {
1066 model
1067 .strip_prefix(id)
1068 .is_some_and(|rest| rest.is_empty() || rest.starts_with("-20"))
1069}
1070
1071const OUTPUT_128K: [&str; 10] = [
1073 CLAUDE_FABLE_5_1,
1074 CLAUDE_FABLE_5,
1075 CLAUDE_OPUS_5_5,
1076 CLAUDE_OPUS_5,
1077 CLAUDE_SONNET_5_5,
1078 CLAUDE_SONNET_5,
1079 CLAUDE_OPUS_4_8,
1080 CLAUDE_OPUS_4_7,
1081 CLAUDE_OPUS_4_6,
1082 CLAUDE_SONNET_4_6,
1083];
1084
1085const MID_CONVERSATION_SYSTEM: [&str; 6] = [
1088 CLAUDE_FABLE_5_1,
1089 CLAUDE_FABLE_5,
1090 CLAUDE_OPUS_5_5,
1091 CLAUDE_OPUS_5,
1092 CLAUDE_SONNET_5_5,
1093 CLAUDE_OPUS_4_8,
1094];
1095
1096pub(super) fn default_max_tokens_for_model(model: &str) -> Option<u64> {
1099 if OUTPUT_128K.iter().any(|id| is_model(model, id)) {
1100 Some(128_000)
1101 } else if model.starts_with("claude-opus-4")
1102 || model.starts_with("claude-sonnet-4")
1103 || model.starts_with("claude-haiku-4-5")
1104 {
1105 Some(64_000)
1106 } else {
1107 None
1108 }
1109}
1110
1111const REJECTS_FORCED_TOOL_CHOICE: [&str; 3] =
1114 [CLAUDE_OPUS_5_5, CLAUDE_SONNET_5_5, CLAUDE_FABLE_5_1];
1115
1116pub(super) fn rejects_forced_tool_choice(model: &str) -> bool {
1118 REJECTS_FORCED_TOOL_CHOICE
1119 .iter()
1120 .any(|id| is_model(model, id))
1121}
1122
1123pub(super) fn supports_mid_conversation_system_messages(model: &str) -> bool {
1125 MID_CONVERSATION_SYSTEM.iter().any(|id| is_model(model, id))
1126}
1127
1128#[derive(Default, Debug, Serialize, Deserialize)]
1129#[serde(tag = "type", rename_all = "snake_case")]
1130pub enum ToolChoice {
1131 #[default]
1132 Auto,
1133 Any,
1134 None,
1135 Tool {
1136 name: String,
1137 },
1138}
1139impl TryFrom<message::ToolChoice> for ToolChoice {
1140 type Error = EncodeError;
1141
1142 fn try_from(value: message::ToolChoice) -> Result<Self, Self::Error> {
1143 let res = match value {
1144 message::ToolChoice::Auto => Self::Auto,
1145 message::ToolChoice::None => Self::None,
1146 message::ToolChoice::Required => Self::Any,
1147 message::ToolChoice::Specific { function_names } => {
1148 if function_names.len() != 1 {
1149 return Err(EncodeError::request(
1150 "Only one tool may be specified to be used by Claude",
1151 ));
1152 }
1153
1154 let Some(name) = function_names.into_iter().next() else {
1155 return Err(EncodeError::request(
1156 "Only one tool may be specified to be used by Claude",
1157 ));
1158 };
1159
1160 Self::Tool { name }
1161 }
1162 };
1163
1164 Ok(res)
1165 }
1166}
1167
1168fn sanitize_schema(schema: &mut serde_json::Value) {
1171 crate::providers::internal::schema::sanitize_schema(
1172 schema,
1173 crate::providers::internal::schema::SanitizeOptions {
1174 strip_ref_siblings: false,
1175 inject_empty_properties: false,
1176 strip_numeric_constraints: true,
1177 },
1178 );
1179}
1180
1181pub(super) fn sanitize_strict_tool_schema(schema: &mut serde_json::Value) {
1187 let mut original = std::mem::take(schema);
1188 inline_local_root_reference(&mut original);
1189 flatten_root_all_of(&mut original);
1190 if let serde_json::Value::Object(source) = &mut original
1192 && !source.contains_key("type")
1193 && (source.contains_key("properties") || source.contains_key("$ref"))
1194 {
1195 source.insert(
1196 "type".to_string(),
1197 serde_json::Value::String("object".to_string()),
1198 );
1199 }
1200 *schema = transform_strict_tool_schema(original);
1201}
1202
1203fn flatten_root_all_of(schema: &mut serde_json::Value) {
1207 use serde_json::{Map, Value};
1208
1209 let Value::Object(root) = schema else {
1210 return;
1211 };
1212 let Some(all_of) = root.remove("allOf") else {
1213 return;
1214 };
1215 let mut conflicting_constraints = Map::new();
1216 merge_root_all_of(root, all_of, &mut conflicting_constraints);
1217 if !conflicting_constraints.is_empty() {
1218 root.insert(
1219 "rootAllOfConstraints".to_string(),
1220 Value::Object(conflicting_constraints),
1221 );
1222 }
1223}
1224
1225fn inline_local_root_reference(schema: &mut serde_json::Value) {
1230 use serde_json::Value;
1231
1232 let mut seen = std::collections::BTreeSet::new();
1233 loop {
1234 let Some(reference) = schema
1235 .get("$ref")
1236 .and_then(serde_json::Value::as_str)
1237 .map(str::to_string)
1238 else {
1239 return;
1240 };
1241 let Some(pointer) = reference.strip_prefix('#') else {
1242 return;
1243 };
1244 if !seen.insert(reference.clone()) {
1245 return;
1246 }
1247 let Some(Value::Object(mut referenced)) = schema.pointer(pointer).cloned() else {
1248 return;
1249 };
1250 let Some(mut root) = schema.as_object().cloned() else {
1251 return;
1252 };
1253 root.remove("$ref");
1254
1255 for keyword in ["$defs", "definitions"] {
1256 let Some(root_definitions) = root.remove(keyword) else {
1257 continue;
1258 };
1259 let definitions =
1260 merge_document_definitions(root_definitions, referenced.remove(keyword));
1261 referenced.insert(keyword.to_string(), definitions);
1262 }
1263
1264 merge_root_reference_siblings(&mut referenced, root);
1265
1266 *schema = Value::Object(referenced);
1267 }
1268}
1269
1270fn merge_document_definitions(
1271 root_definitions: serde_json::Value,
1272 local_definitions: Option<serde_json::Value>,
1273) -> serde_json::Value {
1274 use serde_json::Value;
1275
1276 match (root_definitions, local_definitions) {
1277 (Value::Object(root_definitions), Some(Value::Object(mut local_definitions))) => {
1278 local_definitions.extend(root_definitions);
1282 Value::Object(local_definitions)
1283 }
1284 (root_definitions, _) => root_definitions,
1285 }
1286}
1287
1288fn merge_root_reference_siblings(
1292 referenced: &mut serde_json::Map<String, serde_json::Value>,
1293 siblings: serde_json::Map<String, serde_json::Value>,
1294) {
1295 use serde_json::{Map, Value};
1296
1297 let mut conflicting_constraints = Map::new();
1298 for (keyword, sibling) in siblings {
1299 match keyword.as_str() {
1300 "properties" => merge_schema_properties(referenced, sibling),
1301 "required" => merge_required_properties(referenced, sibling),
1302 "allOf" => merge_root_all_of(referenced, sibling, &mut conflicting_constraints),
1303 "anyOf" | "oneOf" => {
1305 conflicting_constraints.insert(keyword, sibling);
1306 }
1307 "description" | "title" | "$schema" | "$id" | "$comment" | "default" | "examples"
1310 | "deprecated" | "readOnly" | "writeOnly" => {
1311 referenced.insert(keyword, sibling);
1312 }
1313 _ => match referenced.get(&keyword) {
1314 None => {
1315 referenced.insert(keyword, sibling);
1316 }
1317 Some(existing) if existing == &sibling => {}
1318 Some(_) => {
1319 conflicting_constraints.insert(keyword, sibling);
1320 }
1321 },
1322 }
1323 }
1324
1325 if !conflicting_constraints.is_empty() {
1326 referenced.insert(
1328 "rootRefSiblingConstraints".to_string(),
1329 Value::Object(conflicting_constraints),
1330 );
1331 }
1332}
1333
1334fn merge_root_all_of(
1335 schema: &mut serde_json::Map<String, serde_json::Value>,
1336 sibling: serde_json::Value,
1337 conflicting_constraints: &mut serde_json::Map<String, serde_json::Value>,
1338) {
1339 use serde_json::Value;
1340
1341 let Value::Array(branches) = sibling else {
1342 conflicting_constraints.insert("allOf".to_string(), sibling);
1343 return;
1344 };
1345 let mut unsupported_branches = Vec::new();
1346 for branch in branches {
1347 match branch {
1348 Value::Object(mut branch) => {
1349 if branch.contains_key("$ref") {
1350 for keyword in ["$defs", "definitions"] {
1351 let Some(root_definitions) = schema.get(keyword).cloned() else {
1352 continue;
1353 };
1354 let definitions =
1355 merge_document_definitions(root_definitions, branch.remove(keyword));
1356 branch.insert(keyword.to_string(), definitions);
1357 }
1358 let mut branch = Value::Object(branch);
1359 inline_local_root_reference(&mut branch);
1360 match branch {
1361 Value::Object(branch) => merge_root_reference_siblings(schema, branch),
1362 branch => unsupported_branches.push(branch),
1363 }
1364 } else {
1365 merge_root_reference_siblings(schema, branch);
1366 }
1367 }
1368 branch => unsupported_branches.push(branch),
1369 }
1370 }
1371 if !unsupported_branches.is_empty() {
1372 conflicting_constraints.insert("allOf".to_string(), Value::Array(unsupported_branches));
1373 }
1374}
1375
1376fn merge_schema_properties(
1377 schema: &mut serde_json::Map<String, serde_json::Value>,
1378 sibling: serde_json::Value,
1379) {
1380 use serde_json::{Map, Value};
1381
1382 let Value::Object(sibling_properties) = sibling else {
1383 schema.entry("properties".to_string()).or_insert(sibling);
1384 return;
1385 };
1386 let properties = schema
1387 .entry("properties".to_string())
1388 .or_insert_with(|| Value::Object(Map::new()));
1389 let Value::Object(properties) = properties else {
1390 return;
1391 };
1392
1393 for (name, sibling_schema) in sibling_properties {
1394 match properties.remove(&name) {
1395 None => {
1396 properties.insert(name, sibling_schema);
1397 }
1398 Some(existing) if existing == sibling_schema => {
1399 properties.insert(name, existing);
1400 }
1401 Some(existing) => {
1402 properties.insert(
1403 name,
1404 Value::Object(Map::from_iter([(
1405 "allOf".to_string(),
1406 Value::Array(vec![existing, sibling_schema]),
1407 )])),
1408 );
1409 }
1410 }
1411 }
1412}
1413
1414fn merge_required_properties(
1415 schema: &mut serde_json::Map<String, serde_json::Value>,
1416 sibling: serde_json::Value,
1417) {
1418 use serde_json::Value;
1419
1420 let Value::Array(sibling_required) = sibling else {
1421 schema.entry("required".to_string()).or_insert(sibling);
1422 return;
1423 };
1424 let required = schema
1425 .entry("required".to_string())
1426 .or_insert_with(|| Value::Array(Vec::new()));
1427 let Value::Array(required) = required else {
1428 return;
1429 };
1430 for name in sibling_required {
1431 if !required.contains(&name) {
1432 required.push(name);
1433 }
1434 }
1435}
1436
1437fn transform_strict_tool_schema(schema: serde_json::Value) -> serde_json::Value {
1438 use serde_json::{Map, Value};
1439
1440 let Value::Object(mut source) = schema else {
1441 return schema;
1442 };
1443 let mut strict = Map::new();
1444
1445 for keyword in ["$defs", "definitions"] {
1446 if let Some(definitions) = source.remove(keyword) {
1447 match definitions {
1448 Value::Object(definitions) => {
1449 strict.insert(
1450 keyword.to_string(),
1451 Value::Object(
1452 definitions
1453 .into_iter()
1454 .map(|(name, schema)| (name, transform_strict_tool_schema(schema)))
1455 .collect(),
1456 ),
1457 );
1458 }
1459 definitions => {
1460 source.insert(keyword.to_string(), definitions);
1461 }
1462 }
1463 }
1464 }
1465
1466 if let Some(reference) = source.remove("$ref") {
1467 strict.insert("$ref".to_string(), reference);
1468 return Value::Object(strict);
1469 }
1470
1471 let schema_type = source.remove("type");
1472 let any_of = source.remove("anyOf");
1473 let one_of = source.remove("oneOf");
1474 let all_of = source.remove("allOf");
1475 let alternatives = match (any_of, one_of, all_of) {
1476 (Some(Value::Array(variants)), _, _) => Some(("anyOf", variants)),
1477 (_, Some(Value::Array(variants)), _) => Some(("anyOf", variants)),
1478 (_, _, Some(Value::Array(variants))) => Some(("allOf", variants)),
1479 _ => None,
1480 };
1481 if let Some((keyword, variants)) = alternatives {
1482 strict.insert(
1483 keyword.to_string(),
1484 Value::Array(
1485 variants
1486 .into_iter()
1487 .map(transform_strict_tool_schema)
1488 .collect(),
1489 ),
1490 );
1491 } else if let Some(schema_type) = schema_type.clone() {
1492 strict.insert("type".to_string(), schema_type);
1493 }
1494
1495 if let Some(Value::Array(values)) = source.remove("enum") {
1496 strict.insert("enum".to_string(), Value::Array(values));
1497 }
1498 if let Some(constant) = source.remove("const") {
1499 strict.insert("const".to_string(), constant);
1500 }
1501 for keyword in ["description", "title"] {
1502 if let Some(Value::String(value)) = source.remove(keyword) {
1503 strict.insert(keyword.to_string(), Value::String(value));
1504 }
1505 }
1506
1507 let has_properties = source.contains_key("properties");
1508 let properties_imply_object = schema_type.is_none() && has_properties;
1509 if properties_imply_object {
1510 strict.insert("type".to_string(), Value::String("object".to_string()));
1511 }
1512 if schema_has_type(schema_type.as_ref(), "object") || has_properties {
1513 let properties = match source.remove("properties") {
1514 Some(Value::Object(properties)) => properties
1515 .into_iter()
1516 .map(|(name, schema)| (name, transform_strict_tool_schema(schema)))
1517 .collect(),
1518 _ => Map::new(),
1519 };
1520 strict.insert("properties".to_string(), Value::Object(properties));
1521 source.remove("additionalProperties");
1522 strict.insert("additionalProperties".to_string(), Value::Bool(false));
1523 if let Some(Value::Array(required)) = source.remove("required") {
1524 strict.insert("required".to_string(), Value::Array(required));
1525 }
1526 }
1527
1528 if schema_has_type(schema_type.as_ref(), "string")
1529 && let Some(format) = source.remove("format")
1530 {
1531 const SUPPORTED_FORMATS: &[&str] = &[
1532 "date-time",
1533 "time",
1534 "date",
1535 "duration",
1536 "email",
1537 "hostname",
1538 "uri",
1539 "ipv4",
1540 "ipv6",
1541 "uuid",
1542 ];
1543 if format
1544 .as_str()
1545 .is_some_and(|format| SUPPORTED_FORMATS.contains(&format))
1546 {
1547 strict.insert("format".to_string(), format);
1548 } else {
1549 source.insert("format".to_string(), format);
1550 }
1551 }
1552
1553 if schema_has_type(schema_type.as_ref(), "array") {
1554 if let Some(items) = source.remove("items") {
1555 strict.insert("items".to_string(), transform_strict_tool_schema(items));
1556 }
1557 if let Some(min_items) = source.remove("minItems") {
1558 if matches!(min_items.as_u64(), Some(0 | 1)) {
1559 strict.insert("minItems".to_string(), min_items);
1560 } else {
1561 source.insert("minItems".to_string(), min_items);
1562 }
1563 }
1564 }
1565
1566 if !source.is_empty() {
1567 let hints = source
1568 .into_iter()
1569 .map(|(keyword, value)| {
1570 let value = match value {
1571 Value::String(value) => value,
1572 value => value.to_string(),
1573 };
1574 format!("{keyword}: {value}")
1575 })
1576 .collect::<Vec<_>>()
1577 .join(", ");
1578 let suffix = format!("{{{hints}}}");
1579 match strict.get_mut("description") {
1580 Some(Value::String(description)) => {
1581 description.push_str("\n\n");
1582 description.push_str(&suffix);
1583 }
1584 _ => {
1585 strict.insert("description".to_string(), Value::String(suffix));
1586 }
1587 }
1588 }
1589
1590 Value::Object(strict)
1591}
1592
1593fn schema_has_type(schema_type: Option<&serde_json::Value>, expected: &str) -> bool {
1594 match schema_type {
1595 Some(serde_json::Value::String(schema_type)) => schema_type == expected,
1596 Some(serde_json::Value::Array(schema_types)) => schema_types
1597 .iter()
1598 .any(|schema_type| schema_type.as_str() == Some(expected)),
1599 _ => false,
1600 }
1601}
1602
1603#[derive(Debug, Deserialize, Serialize)]
1606#[serde(tag = "type", rename_all = "snake_case")]
1607enum OutputFormat {
1608 JsonSchema { schema: serde_json::Value },
1610}
1611
1612#[derive(Debug, Deserialize, Serialize)]
1614struct OutputConfig {
1615 format: OutputFormat,
1616}
1617
1618#[derive(Debug, Deserialize, Serialize)]
1619pub(super) struct AnthropicCompletionRequest {
1620 model: String,
1621 messages: Vec<Message>,
1622 max_tokens: u64,
1623 #[serde(skip_serializing_if = "Vec::is_empty")]
1625 system: Vec<SystemContent>,
1626 #[serde(skip_serializing_if = "Option::is_none")]
1627 temperature: Option<f64>,
1628 #[serde(skip_serializing_if = "Option::is_none")]
1629 tool_choice: Option<ToolChoice>,
1630 #[serde(skip_serializing_if = "Vec::is_empty")]
1631 tools: Vec<serde_json::Value>,
1632 #[serde(skip_serializing_if = "Option::is_none")]
1633 output_config: Option<OutputConfig>,
1634 #[serde(flatten, skip_serializing_if = "Option::is_none")]
1635 additional_params: Option<serde_json::Value>,
1636 #[serde(skip_serializing_if = "Option::is_none")]
1640 cache_control: Option<CacheControl>,
1641}
1642
1643fn set_content_cache_control(content: &mut Content, value: Option<CacheControl>) {
1645 match content {
1646 Content::Text { cache_control, .. } => *cache_control = value,
1647 Content::Image { cache_control, .. } => *cache_control = value,
1648 Content::ToolResult { cache_control, .. } => *cache_control = value,
1649 Content::Document { cache_control, .. } => *cache_control = value,
1650 _ => {}
1651 }
1652}
1653
1654const MAX_CACHE_CONTROL_MARKERS: usize = 4;
1655
1656fn final_cacheable_tool_idx(tools: &[serde_json::Value]) -> Option<usize> {
1657 tools.iter().rposition(|tool| {
1658 tool.as_object().is_some_and(|tool| {
1659 !matches!(
1660 tool.get("defer_loading"),
1661 Some(serde_json::Value::Bool(true))
1662 )
1663 })
1664 })
1665}
1666
1667fn tool_cache_control_count(tools: &[serde_json::Value]) -> usize {
1668 tools
1669 .iter()
1670 .filter(|tool| tool_cache_control_value(tool).is_some())
1671 .count()
1672}
1673
1674fn tool_cache_control_value(tool: &serde_json::Value) -> Option<&serde_json::Value> {
1675 tool.get("cache_control")
1676 .filter(|cache_control| !cache_control.is_null())
1677}
1678
1679fn normalize_tool_cache_control(tools: &mut [serde_json::Value]) {
1680 for tool in tools.iter_mut() {
1681 if let Some(tool) = tool.as_object_mut()
1682 && tool
1683 .get("cache_control")
1684 .is_some_and(serde_json::Value::is_null)
1685 {
1686 tool.remove("cache_control");
1687 }
1688 }
1689}
1690
1691fn build_cache_control(ttl: Option<CacheTtl>) -> CacheControl {
1692 CacheControl::Ephemeral { ttl }
1693}
1694
1695#[derive(Clone, Copy, PartialEq, Eq)]
1696enum CacheControlTtl {
1697 FiveMinutes,
1698 OneHour,
1699}
1700
1701fn cache_control_ttl(cache_control: &CacheControl) -> CacheControlTtl {
1702 match cache_control {
1703 CacheControl::Ephemeral {
1704 ttl: Some(CacheTtl::OneHour),
1705 } => CacheControlTtl::OneHour,
1706 CacheControl::Ephemeral { .. } => CacheControlTtl::FiveMinutes,
1707 }
1708}
1709
1710fn cache_control_ttl_from_json(cache_control: &serde_json::Value) -> CacheControlTtl {
1711 match cache_control.get("ttl") {
1712 Some(serde_json::Value::String(ttl)) if ttl == "1h" => CacheControlTtl::OneHour,
1713 _ => CacheControlTtl::FiveMinutes,
1714 }
1715}
1716
1717fn content_cache_control(content: &Content) -> Option<&CacheControl> {
1718 match content {
1719 Content::Text { cache_control, .. }
1720 | Content::Image { cache_control, .. }
1721 | Content::ToolResult { cache_control, .. }
1722 | Content::Document { cache_control, .. } => cache_control.as_ref(),
1723 _ => None,
1724 }
1725}
1726
1727fn validate_cache_control_ttl(
1728 ttl: CacheControlTtl,
1729 shorter_ttl_seen: &mut bool,
1730) -> Result<(), EncodeError> {
1731 match ttl {
1732 CacheControlTtl::OneHour if *shorter_ttl_seen => Err(EncodeError::request(
1733 "Anthropic cache_control markers with ttl `1h` must appear before markers with \
1734 the default 5-minute TTL",
1735 )),
1736 CacheControlTtl::OneHour => Ok(()),
1737 CacheControlTtl::FiveMinutes => {
1738 *shorter_ttl_seen = true;
1739 Ok(())
1740 }
1741 }
1742}
1743
1744fn validate_cache_control_ttl_order(
1745 system: &[SystemContent],
1746 messages: &[Message],
1747 tools: &[serde_json::Value],
1748 top_level_cache_control: Option<&CacheControl>,
1749) -> Result<(), EncodeError> {
1750 let mut shorter_ttl_seen = false;
1751
1752 for tool in tools {
1753 if let Some(cache_control) = tool_cache_control_value(tool) {
1754 validate_cache_control_ttl(
1755 cache_control_ttl_from_json(cache_control),
1756 &mut shorter_ttl_seen,
1757 )?;
1758 }
1759 }
1760
1761 for SystemContent::Text { cache_control, .. } in system {
1762 if let Some(cache_control) = cache_control {
1763 validate_cache_control_ttl(cache_control_ttl(cache_control), &mut shorter_ttl_seen)?;
1764 }
1765 }
1766
1767 for message in messages {
1768 for content in message.content.iter() {
1769 if let Some(cache_control) = content_cache_control(content) {
1770 validate_cache_control_ttl(
1771 cache_control_ttl(cache_control),
1772 &mut shorter_ttl_seen,
1773 )?;
1774 }
1775 }
1776 }
1777
1778 if let Some(cache_control) = top_level_cache_control {
1779 validate_cache_control_ttl(cache_control_ttl(cache_control), &mut shorter_ttl_seen)?;
1780 }
1781
1782 Ok(())
1783}
1784
1785fn top_level_cache_control_ttl(cache_control: Option<&CacheControl>) -> Option<CacheTtl> {
1786 cache_control
1787 .map(|cache_control| match cache_control {
1788 CacheControl::Ephemeral { ttl } => ttl.clone(),
1789 })
1790 .unwrap_or_default()
1791}
1792
1793fn apply_tool_cache_control(
1795 tools: &mut [serde_json::Value],
1796 remaining_cache_markers: &mut usize,
1797 cache_control: &CacheControl,
1798) -> Result<(), EncodeError> {
1799 let Some(idx) = final_cacheable_tool_idx(tools) else {
1800 return Ok(());
1801 };
1802
1803 let Some(tool) = tools
1804 .get_mut(idx)
1805 .and_then(serde_json::Value::as_object_mut)
1806 else {
1807 return Ok(());
1808 };
1809
1810 if tool
1811 .get("cache_control")
1812 .is_some_and(|cache_control| !cache_control.is_null())
1813 {
1814 return Ok(());
1815 }
1816
1817 if *remaining_cache_markers == 0 {
1818 return Err(EncodeError::request(
1819 "Anthropic manual prompt caching requires a cache_control marker on the final \
1820 non-deferred tool, but explicit tool markers exhaust the available cache point budget",
1821 ));
1822 }
1823
1824 tool.insert(
1825 "cache_control".to_string(),
1826 serde_json::to_value(cache_control)?,
1827 );
1828 *remaining_cache_markers -= 1;
1829
1830 Ok(())
1831}
1832
1833fn apply_system_cache_control(
1834 system: &mut [SystemContent],
1835 remaining_cache_markers: &mut usize,
1836 cache_control_value: &CacheControl,
1837) {
1838 if *remaining_cache_markers == 0 {
1839 return;
1840 }
1841
1842 if let Some(SystemContent::Text { cache_control, .. }) = system.last_mut()
1843 && cache_control.is_none()
1844 {
1845 *cache_control = Some(cache_control_value.clone());
1846 *remaining_cache_markers -= 1;
1847 }
1848}
1849
1850fn clear_message_cache_control(messages: &mut [Message]) {
1851 for msg in messages.iter_mut() {
1852 for content in msg.content.iter_mut() {
1853 set_content_cache_control(content, None);
1854 }
1855 }
1856}
1857
1858fn apply_message_cache_control(
1859 messages: &mut [Message],
1860 remaining_cache_markers: &mut usize,
1861 cache_control: &CacheControl,
1862) {
1863 clear_message_cache_control(messages);
1864
1865 if *remaining_cache_markers == 0 {
1866 return;
1867 }
1868
1869 if let Some(last_msg) = messages.last_mut()
1870 && let Some(last_content) = last_msg.content.last_mut()
1871 {
1872 set_content_cache_control(last_content, Some(cache_control.clone()));
1873 *remaining_cache_markers -= 1;
1874 }
1875}
1876
1877pub(super) fn apply_prompt_cache_control(
1878 system: &mut [SystemContent],
1879 messages: &mut [Message],
1880 tools: &mut [serde_json::Value],
1881 prompt_caching: bool,
1882 static_prefix_cache_ttl: Option<&CacheTtl>,
1883 top_level_cache_control: Option<&CacheControl>,
1884) -> Result<(), EncodeError> {
1885 normalize_tool_cache_control(tools);
1886
1887 let max_cache_markers = if top_level_cache_control.is_some() {
1888 MAX_CACHE_CONTROL_MARKERS - 1
1889 } else {
1890 MAX_CACHE_CONTROL_MARKERS
1891 };
1892 let tool_cache_markers = tool_cache_control_count(tools);
1893
1894 if tool_cache_markers > max_cache_markers {
1895 return Err(EncodeError::request(format!(
1896 "Too many Anthropic tool `cache_control` markers: {tool_cache_markers} exceeds \
1897 the available prompt caching budget of {max_cache_markers}"
1898 )));
1899 }
1900
1901 let mut remaining_cache_markers = max_cache_markers - tool_cache_markers;
1902
1903 let top_level_ttl = top_level_cache_control_ttl(top_level_cache_control);
1905 if static_prefix_cache_ttl == Some(&CacheTtl::FiveMinutes)
1906 && top_level_ttl == Some(CacheTtl::OneHour)
1907 {
1908 return Err(EncodeError::request(
1909 "`with_static_prefix_cache_ttl(CacheTtl::FiveMinutes)` conflicts with the 1-hour \
1910 top-level cache TTL (`with_automatic_caching_1h` or a raw top-level \
1911 `cache_control`): Anthropic requires 1h markers to precede 5-minute ones, and the \
1912 static prefix precedes the conversation tail",
1913 ));
1914 }
1915
1916 if prompt_caching || static_prefix_cache_ttl.is_some() {
1920 let static_cache_control =
1921 build_cache_control(static_prefix_cache_ttl.cloned().or(top_level_ttl.clone()));
1922
1923 apply_tool_cache_control(tools, &mut remaining_cache_markers, &static_cache_control)?;
1924 apply_system_cache_control(system, &mut remaining_cache_markers, &static_cache_control);
1925 }
1926
1927 if prompt_caching {
1928 if top_level_cache_control.is_some() {
1929 clear_message_cache_control(messages);
1930 } else {
1931 let tail_cache_control = build_cache_control(top_level_ttl);
1932 apply_message_cache_control(
1933 messages,
1934 &mut remaining_cache_markers,
1935 &tail_cache_control,
1936 );
1937 }
1938 }
1939
1940 validate_cache_control_ttl_order(system, messages, tools, top_level_cache_control)?;
1941
1942 Ok(())
1943}
1944
1945pub(super) fn extract_top_level_cache_control(
1946 additional_params: &mut serde_json::Value,
1947) -> Result<Option<CacheControl>, EncodeError> {
1948 if let Some(map) = additional_params.as_object_mut()
1949 && let Some(raw_cache_control) = map.remove("cache_control")
1950 {
1951 if raw_cache_control.is_null() {
1952 return Ok(None);
1953 }
1954
1955 return serde_json::from_value::<CacheControl>(raw_cache_control)
1956 .map(Some)
1957 .map_err(|err| {
1958 EncodeError::request(format!(
1959 "Invalid Anthropic `additional_params.cache_control` payload: {err}"
1960 ))
1961 });
1962 }
1963
1964 Ok(None)
1965}
1966
1967pub(super) fn resolve_top_level_cache_control(
1968 automatic_caching: bool,
1969 automatic_caching_ttl: Option<&CacheTtl>,
1970 additional_params: &mut serde_json::Value,
1971) -> Result<Option<CacheControl>, EncodeError> {
1972 let raw_cache_control = extract_top_level_cache_control(additional_params)?;
1973 let typed_cache_control = automatic_caching.then_some(CacheControl::Ephemeral {
1974 ttl: automatic_caching_ttl.cloned(),
1975 });
1976
1977 match (typed_cache_control, raw_cache_control) {
1978 (Some(typed_cache_control), Some(raw_cache_control)) => {
1979 if automatic_caching_ttl.is_some()
1980 && cache_control_ttl(&typed_cache_control) != cache_control_ttl(&raw_cache_control)
1981 {
1982 return Err(EncodeError::request(
1983 "Anthropic `additional_params.cache_control` conflicts with the typed \
1984 automatic caching TTL",
1985 ));
1986 }
1987
1988 Ok(Some(raw_cache_control))
1989 }
1990 (Some(typed_cache_control), None) => Ok(Some(typed_cache_control)),
1991 (None, raw_cache_control) => Ok(raw_cache_control),
1992 }
1993}
1994
1995pub(super) fn split_system_messages_from_history(
2005 history: &[message::Message],
2006 preserve_mid_conversation_system_messages: bool,
2007) -> (Vec<SystemContent>, Vec<message::Message>) {
2008 let mut system = Vec::new();
2009 let mut remaining = Vec::new();
2010 let mut deferred: Vec<String> = Vec::new();
2011
2012 for (index, message) in history.iter().enumerate() {
2013 match message {
2014 message::Message::System { content } => {
2015 if content.is_empty() {
2016 continue;
2017 }
2018 if preserve_mid_conversation_system_messages {
2019 if is_valid_mid_conversation_system_message(history, index) {
2020 remaining.push(message.clone());
2021 continue;
2022 }
2023 if index > 0 && next_system_message_slot(history, index).is_some() {
2024 deferred.push(content.clone());
2025 continue;
2026 }
2027 }
2028 system.push(SystemContent::Text {
2029 text: content.clone(),
2030 cache_control: None,
2031 });
2032 }
2033 other => {
2034 remaining.push(other.clone());
2035 if !deferred.is_empty() && is_system_message_slot(history, index) {
2036 remaining.push(message::Message::System {
2037 content: std::mem::take(&mut deferred).join("\n\n"),
2038 });
2039 }
2040 }
2041 }
2042 }
2043
2044 (system, remaining)
2045}
2046
2047fn next_system_message_slot(history: &[message::Message], index: usize) -> Option<usize> {
2049 (index + 1..history.len()).find(|&slot| is_system_message_slot(history, slot))
2050}
2051
2052fn is_system_message_slot(history: &[message::Message], index: usize) -> bool {
2055 matches!(history.get(index), Some(message::Message::User { .. }))
2056 && history
2057 .get(index + 1)
2058 .is_none_or(|message| matches!(message, message::Message::Assistant { .. }))
2059}
2060
2061fn is_valid_mid_conversation_system_message(history: &[message::Message], index: usize) -> bool {
2062 let follows_valid_turn = index > 0
2063 && history.get(index - 1).is_some_and(|message| {
2064 matches!(message, message::Message::User { .. })
2065 || assistant_ends_in_server_tool_block(message)
2066 });
2067 let is_last_or_precedes_assistant = history
2068 .get(index + 1)
2069 .is_none_or(|message| matches!(message, message::Message::Assistant { .. }));
2070
2071 follows_valid_turn && is_last_or_precedes_assistant
2072}
2073
2074fn assistant_ends_in_server_tool_block(message: &message::Message) -> bool {
2075 let message::Message::Assistant { content, .. } = message else {
2076 return false;
2077 };
2078
2079 let Some(message::AssistantContent::Text(text)) = content.iter().last() else {
2080 return false;
2081 };
2082
2083 let Some(raw_type) = text
2084 .additional_params
2085 .as_ref()
2086 .and_then(|params| params.get(ANTHROPIC_RAW_CONTENT_KEY))
2087 .and_then(|raw_content| raw_content.get("type"))
2088 .and_then(serde_json::Value::as_str)
2089 else {
2090 return false;
2091 };
2092
2093 matches!(
2094 raw_type,
2095 "server_tool_use" | "web_search_tool_result" | "code_execution_tool_result"
2096 )
2097}
2098
2099pub struct AnthropicRequestParams<'a> {
2101 pub model: &'a str,
2102 pub request: CompletionRequest,
2103 pub prompt_caching: bool,
2104 pub automatic_caching: bool,
2106 pub automatic_caching_ttl: Option<CacheTtl>,
2108 pub static_prefix_cache_ttl: Option<CacheTtl>,
2110 pub issuers: &'a [message::Issuer],
2112}
2113
2114impl AnthropicCompletionRequest {
2115 pub(super) fn try_from_params(
2118 params: AnthropicRequestParams<'_>,
2119 strict: Option<fn(&mut ToolDefinition)>,
2120 ) -> Result<Self, EncodeError> {
2121 let AnthropicRequestParams {
2122 model,
2123 request: mut req,
2124 prompt_caching,
2125 automatic_caching,
2126 automatic_caching_ttl,
2127 static_prefix_cache_ttl,
2128 issuers,
2129 } = params;
2130 let chat_history = req.chat_history_with_documents();
2131
2132 let Some(max_tokens) = req.max_tokens else {
2133 return Err(EncodeError::request(
2134 "`max_tokens` must be set for Anthropic",
2135 ));
2136 };
2137
2138 let (history_system, chat_history) = split_system_messages_from_history(
2139 &chat_history,
2140 supports_mid_conversation_system_messages(model),
2141 );
2142 let mut full_history = vec![];
2143 full_history.extend(chat_history);
2144
2145 let mut messages = full_history
2146 .iter()
2147 .cloned()
2148 .map(|message| Message::from_message(message, issuers))
2149 .collect::<Result<Vec<_>, _>>()?;
2150 let server_ids = messages
2153 .iter()
2154 .flat_map(|message| &message.content)
2155 .filter_map(|part| match part {
2156 Content::ServerToolUse { id, .. } => Some(id.clone()),
2157 Content::WebSearchToolResult { tool_use_id, .. }
2158 | Content::CodeExecutionToolResult { tool_use_id, .. } => Some(tool_use_id.clone()),
2159 _ => None,
2160 });
2161 let tool_ids =
2162 crate::providers::internal::wire_ids::WireIds::with_reserved(&full_history, server_ids);
2163 for (position, message) in messages.iter_mut().enumerate() {
2164 tool_ids
2165 .apply(
2166 position,
2167 message.content.iter_mut().filter_map(|part| match part {
2168 Content::ToolUse { id, .. } => Some(id),
2169 Content::ToolResult { tool_use_id, .. } => Some(tool_use_id),
2170 _ => None,
2171 }),
2172 )
2173 .map_err(EncodeError::request)?;
2174 }
2175
2176 let mut additional_params_payload = req
2177 .additional_params
2178 .take()
2179 .unwrap_or(serde_json::Value::Null);
2180 let top_level_cache_control = resolve_top_level_cache_control(
2181 automatic_caching,
2182 automatic_caching_ttl.as_ref(),
2183 &mut additional_params_payload,
2184 )?;
2185 let mut tools = build_tool_definitions(req.tools, &mut additional_params_payload, strict)?;
2186
2187 let mut system = history_system;
2188
2189 apply_prompt_cache_control(
2190 &mut system,
2191 &mut messages,
2192 &mut tools,
2193 prompt_caching,
2194 static_prefix_cache_ttl.as_ref(),
2195 top_level_cache_control.as_ref(),
2196 )?;
2197
2198 let output_config = if let Some(schema) = req.output_schema {
2199 let mut schema_value = schema.to_value();
2200 sanitize_schema(&mut schema_value);
2201 Some(OutputConfig {
2202 format: OutputFormat::JsonSchema {
2203 schema: schema_value,
2204 },
2205 })
2206 } else {
2207 None
2208 };
2209
2210 Ok(Self {
2211 model: model.to_string(),
2212 messages,
2213 max_tokens,
2214 system,
2215 temperature: req.temperature,
2216 tool_choice: req.tool_choice.map(ToolChoice::try_from).transpose()?,
2217 tools,
2218 output_config,
2219 cache_control: top_level_cache_control,
2220 additional_params: if additional_params_payload.is_null() {
2221 None
2222 } else {
2223 Some(additional_params_payload)
2224 },
2225 })
2226 }
2227}
2228
2229impl TryFrom<AnthropicRequestParams<'_>> for AnthropicCompletionRequest {
2230 type Error = EncodeError;
2231
2232 fn try_from(params: AnthropicRequestParams<'_>) -> Result<Self, Self::Error> {
2233 Self::try_from_params(params, None)
2234 }
2235}
2236
2237pub(super) fn extract_tools_from_additional_params(
2238 additional_params: &mut serde_json::Value,
2239) -> Result<Vec<serde_json::Value>, EncodeError> {
2240 if let Some(map) = additional_params.as_object_mut()
2241 && let Some(raw_tools) = map.remove("tools")
2242 {
2243 return serde_json::from_value::<Vec<serde_json::Value>>(raw_tools).map_err(|err| {
2244 EncodeError::request(format!(
2245 "Invalid Anthropic `additional_params.tools` payload: {err}"
2246 ))
2247 });
2248 }
2249
2250 Ok(Vec::new())
2251}
2252
2253pub(super) fn build_tool_definitions(
2254 tools: Vec<completion::ToolDefinition>,
2255 additional_params_payload: &mut serde_json::Value,
2256 strict: Option<fn(&mut ToolDefinition)>,
2257) -> Result<Vec<serde_json::Value>, EncodeError> {
2258 let mut additional_tools = extract_tools_from_additional_params(additional_params_payload)?;
2259
2260 let mut tools = tools
2261 .into_iter()
2262 .map(|tool| {
2263 let input_schema = tool.parameters;
2264 let mut tool = ToolDefinition {
2265 name: tool.name,
2266 description: Some(tool.description),
2267 input_schema,
2268 strict: false,
2269 cache_control: None,
2270 };
2271 if let Some(strict) = strict {
2272 strict(&mut tool);
2273 }
2274
2275 tool
2276 })
2277 .map(serde_json::to_value)
2278 .collect::<Result<Vec<_>, _>>()?;
2279 tools.append(&mut additional_tools);
2280
2281 Ok(tools)
2282}
2283
2284#[cfg(test)]
2285mod tests;