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