Skip to main content

rig_core/providers/anthropic/
completion.rs

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