Skip to main content

rig_core/providers/anthropic/
completion.rs

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