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