Skip to main content

rig_core/providers/anthropic/
completion.rs

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