Skip to main content

rig_core/providers/anthropic/
completion.rs

1//! Anthropic Messages payloads, conversion, citations, and prompt-cache configuration.
2//!
3//! ```
4//! use rig_core::providers::anthropic::completion::CacheControl;
5//!
6//! let cache = CacheControl::ephemeral_1h();
7//! ```
8
9use crate::completion::CompletionRequest;
10use crate::error::EncodeError;
11use crate::json_utils::string_or_vec;
12use crate::{
13    completion,
14    message::{self, DocumentMediaType, DocumentSourceKind, MessageError, MimeType},
15};
16use serde::{Deserialize, Serialize};
17use std::{convert::Infallible, str::FromStr};
18
19/// Claude Fable 5.1, API ID `claude-fable-5-1`: 128K default `max_tokens`,
20/// mid-conversation system messages kept in `messages`. It rejects a forced
21/// tool choice, so the extractor uses native structured output instead of
22/// forcing its `submit` tool.
23pub const CLAUDE_FABLE_5_1: &str = "claude-fable-5-1";
24/// Claude Opus 5.5, API ID `claude-opus-5-5`: 128K default `max_tokens`,
25/// mid-conversation system messages kept in `messages`. It rejects a forced
26/// tool choice, so the extractor uses native structured output instead of
27/// forcing its `submit` tool.
28pub const CLAUDE_OPUS_5_5: &str = "claude-opus-5-5";
29/// Claude Sonnet 5.5, API ID `claude-sonnet-5-5`: 128K default `max_tokens`,
30/// mid-conversation system messages kept in `messages` (unlike Claude
31/// Sonnet 5). It rejects a forced tool choice, so the extractor uses native
32/// structured output instead of forcing its `submit` tool.
33pub const CLAUDE_SONNET_5_5: &str = "claude-sonnet-5-5";
34/// `claude-fable-5` completion model
35pub const CLAUDE_FABLE_5: &str = "claude-fable-5";
36/// `claude-opus-5` completion model
37pub const CLAUDE_OPUS_5: &str = "claude-opus-5";
38/// `claude-sonnet-5` completion model
39pub const CLAUDE_SONNET_5: &str = "claude-sonnet-5";
40/// `claude-opus-4-6` completion model
41pub const CLAUDE_OPUS_4_6: &str = "claude-opus-4-6";
42/// `claude-opus-4-7` completion model
43pub const CLAUDE_OPUS_4_7: &str = "claude-opus-4-7";
44/// `claude-opus-4-8` completion model
45pub const CLAUDE_OPUS_4_8: &str = "claude-opus-4-8";
46/// `claude-sonnet-4-6` completion model
47pub const CLAUDE_SONNET_4_6: &str = "claude-sonnet-4-6";
48/// `claude-haiku-4-5` completion model
49pub const CLAUDE_HAIKU_4_5: &str = "claude-haiku-4-5";
50
51pub const ANTHROPIC_VERSION_2023_01_01: &str = "2023-01-01";
52pub const ANTHROPIC_VERSION_2023_06_01: &str = "2023-06-01";
53pub const ANTHROPIC_VERSION_LATEST: &str = ANTHROPIC_VERSION_2023_06_01;
54pub(crate) const ANTHROPIC_RAW_CONTENT_KEY: &str = "anthropic_content";
55
56#[derive(Debug, Deserialize, Serialize)]
57pub struct CompletionResponse {
58    pub content: Vec<Content>,
59    pub id: String,
60    pub model: String,
61    pub role: String,
62    pub stop_reason: Option<String>,
63    pub stop_sequence: Option<String>,
64    pub usage: Usage,
65}
66
67/// Normalize a Messages `stop_reason`, preserving unrecognized values verbatim.
68pub(crate) fn map_finish_reason(stop_reason: &str) -> completion::FinishReason {
69    match stop_reason {
70        // `stop_sequence` is a natural termination too: the model completed its
71        // turn by emitting one of the caller's stop sequences.
72        "end_turn" | "stop_sequence" => completion::FinishReason::Stop,
73        "max_tokens" => completion::FinishReason::Length,
74        "tool_use" => completion::FinishReason::ToolCalls,
75        // Anthropic's classifier-driven refusal; the closest normalized reason
76        // is content filtering.
77        "refusal" => completion::FinishReason::ContentFilter,
78        other => completion::FinishReason::Other(other.to_owned()),
79    }
80}
81
82/// Anthropic's `usage`, as sent: `input_tokens` excludes the cache reads and
83/// writes counted beside it, which rig's [`Usage`](crate::completion::Usage)
84/// counts in its input.
85#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
86pub struct Usage {
87    /// Input tokens neither read from nor written to a cache.
88    pub input_tokens: u64,
89    pub cache_read_input_tokens: Option<u64>,
90    pub cache_creation_input_tokens: Option<u64>,
91    /// Per-TTL breakdown of `cache_creation_input_tokens`. Absent when the
92    /// provider does not report it; the aggregate above is always authoritative.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub cache_creation: Option<CacheCreation>,
95    pub output_tokens: u64,
96    /// Breakdown of `output_tokens`. Absent when the provider does not report
97    /// it (a turn with extended thinking disabled).
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub output_tokens_details: Option<OutputTokensDetails>,
100}
101
102/// Breakdown of `usage.output_tokens`, including thinking tokens already counted
103/// in that total. Deserialization ignores unrecognized fields.
104#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
105pub struct OutputTokensDetails {
106    /// Output tokens spent on extended thinking this turn.
107    #[serde(default)]
108    pub thinking_tokens: u64,
109}
110
111/// Cache-write tokens by TTL (`usage.cache_creation`).
112/// Deserialization ignores unrecognized fields.
113#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq)]
114pub struct CacheCreation {
115    /// Tokens written to the 5-minute cache on this turn.
116    #[serde(default)]
117    pub ephemeral_5m_input_tokens: u64,
118    /// Tokens written to the 1-hour cache on this turn.
119    #[serde(default)]
120    pub ephemeral_1h_input_tokens: u64,
121}
122
123impl std::fmt::Display for Usage {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        write!(
126            f,
127            "Uncached input tokens: {}\nCache read input tokens: {}\nCache creation input tokens: {}\nOutput tokens: {}",
128            self.input_tokens,
129            self.cache_read_input_tokens
130                .map_or_else(|| "n/a".to_string(), |token| token.to_string()),
131            self.cache_creation_input_tokens
132                .map_or_else(|| "n/a".to_string(), |token| token.to_string()),
133            self.output_tokens
134        )
135    }
136}
137
138/// Rig's input is Anthropic's `input_tokens` plus its cache reads and writes,
139/// its output `output_tokens` (thinking included), and its total their sum;
140/// without an uncached input count, input and the total stay absent.
141pub(super) fn anthropic_usage_totals(
142    input_tokens: Option<u64>,
143    output_tokens: u64,
144    cache_read: Option<u64>,
145    cache_creation: Option<u64>,
146    output_tokens_details: Option<OutputTokensDetails>,
147) -> crate::completion::Usage {
148    let input_tokens = input_tokens
149        .map(|uncached| uncached + cache_read.unwrap_or(0) + cache_creation.unwrap_or(0));
150    crate::completion::Usage {
151        input_tokens,
152        output_tokens: Some(output_tokens),
153        cached_input_tokens: cache_read,
154        cache_creation_input_tokens: cache_creation,
155        reasoning_tokens: output_tokens_details.map(|details| details.thinking_tokens),
156        total_tokens: input_tokens.map(|input| input + output_tokens),
157        tool_use_prompt_tokens: None,
158    }
159}
160
161impl From<&Usage> for crate::completion::Usage {
162    fn from(value: &Usage) -> crate::completion::Usage {
163        anthropic_usage_totals(
164            Some(value.input_tokens),
165            value.output_tokens,
166            value.cache_read_input_tokens,
167            value.cache_creation_input_tokens,
168            value.output_tokens_details,
169        )
170    }
171}
172
173impl From<Usage> for crate::completion::Usage {
174    fn from(value: Usage) -> crate::completion::Usage {
175        (&value).into()
176    }
177}
178
179#[derive(Debug, Deserialize, Serialize)]
180pub struct ToolDefinition {
181    pub name: String,
182    pub description: Option<String>,
183    pub input_schema: serde_json::Value,
184    /// Whether Anthropic must constrain tool arguments to `input_schema`.
185    #[serde(default, skip_serializing_if = "crate::json_utils::is_false")]
186    pub strict: bool,
187    /// Cache breakpoint marker. Set on the last tool in the array to cache
188    /// the tools layer independently of the system prompt. Anthropic accepts
189    /// up to 4 `cache_control` markers per request.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub cache_control: Option<CacheControl>,
192}
193
194/// Cache-breakpoint lifetime: five minutes by default, or one hour.
195#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
196pub enum CacheTtl {
197    /// 5-minute TTL (default).
198    #[default]
199    #[serde(rename = "5m")]
200    FiveMinutes,
201    /// 1-hour TTL.
202    #[serde(rename = "1h")]
203    OneHour,
204}
205
206/// Cache control directive for Anthropic prompt caching.
207///
208/// Serialises to `{"type":"ephemeral"}` (default TTL) or
209/// `{"type":"ephemeral","ttl":"1h"}` (extended TTL).
210#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
211#[serde(tag = "type", rename_all = "snake_case")]
212pub enum CacheControl {
213    Ephemeral {
214        /// Optional TTL. Defaults to `"5m"` when omitted.
215        #[serde(skip_serializing_if = "Option::is_none")]
216        ttl: Option<CacheTtl>,
217    },
218}
219
220impl CacheControl {
221    /// Create a cache control with the default 5-minute TTL.
222    pub fn ephemeral() -> Self {
223        Self::Ephemeral { ttl: None }
224    }
225
226    /// Create a cache control with a 1-hour TTL.
227    pub fn ephemeral_1h() -> Self {
228        Self::Ephemeral {
229            ttl: Some(CacheTtl::OneHour),
230        }
231    }
232}
233
234/// System message content block with optional cache control
235#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
236#[serde(tag = "type", rename_all = "snake_case")]
237pub enum SystemContent {
238    Text {
239        text: String,
240        #[serde(skip_serializing_if = "Option::is_none")]
241        cache_control: Option<CacheControl>,
242    },
243}
244
245#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
246pub struct Message {
247    pub role: Role,
248    #[serde(deserialize_with = "string_or_vec")]
249    pub content: Vec<Content>,
250}
251
252#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
253#[serde(rename_all = "lowercase")]
254pub enum Role {
255    User,
256    Assistant,
257    System,
258}
259
260#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
261#[serde(tag = "type", rename_all = "snake_case")]
262pub enum Content {
263    Text {
264        text: String,
265        /// Citations returned by Claude pointing back into the source documents.
266        /// Empty (and skipped during serialization) on request-side blocks.
267        #[serde(
268            default,
269            deserialize_with = "null_as_empty_vec",
270            skip_serializing_if = "Vec::is_empty"
271        )]
272        citations: Vec<Citation>,
273        #[serde(skip_serializing_if = "Option::is_none")]
274        cache_control: Option<CacheControl>,
275    },
276    Image {
277        source: ImageSource,
278        #[serde(skip_serializing_if = "Option::is_none")]
279        cache_control: Option<CacheControl>,
280    },
281    ToolUse {
282        id: String,
283        name: String,
284        input: serde_json::Value,
285    },
286    ServerToolUse {
287        id: String,
288        name: String,
289        #[serde(default)]
290        input: serde_json::Value,
291    },
292    WebSearchToolResult {
293        tool_use_id: String,
294        content: serde_json::Value,
295    },
296    /// The result of an Anthropic-hosted code execution tool call.
297    CodeExecutionToolResult {
298        tool_use_id: String,
299        content: serde_json::Value,
300    },
301    ToolResult {
302        tool_use_id: String,
303        #[serde(deserialize_with = "string_or_vec")]
304        content: Vec<ToolResultContent>,
305        #[serde(skip_serializing_if = "Option::is_none")]
306        is_error: Option<bool>,
307        #[serde(skip_serializing_if = "Option::is_none")]
308        cache_control: Option<CacheControl>,
309    },
310    Document {
311        source: DocumentSource,
312        /// Optional document title, passed to the model but not citable.
313        #[serde(default, skip_serializing_if = "Option::is_none")]
314        title: Option<String>,
315        /// Optional document context (e.g. metadata), passed to the model but
316        /// not citable. Useful for storing additional information about the
317        /// document that should not appear in citation `cited_text`.
318        #[serde(default, skip_serializing_if = "Option::is_none")]
319        context: Option<String>,
320        /// Configuration for enabling citations on this document. When `enabled`
321        /// is true, Claude returns citation metadata on response text blocks
322        /// pointing back into this document's content.
323        #[serde(default, skip_serializing_if = "Option::is_none")]
324        citations: Option<CitationsConfig>,
325        #[serde(skip_serializing_if = "Option::is_none")]
326        cache_control: Option<CacheControl>,
327    },
328    Thinking {
329        thinking: String,
330        #[serde(skip_serializing_if = "Option::is_none")]
331        signature: Option<String>,
332    },
333    RedactedThinking {
334        data: String,
335    },
336}
337
338impl FromStr for Content {
339    type Err = Infallible;
340
341    fn from_str(s: &str) -> Result<Self, Self::Err> {
342        Ok(Content::from(s.to_owned()))
343    }
344}
345
346/// Enable [citation metadata](https://docs.anthropic.com/en/docs/build-with-claude/citations)
347/// on response text referencing this document.
348/// Enable citations on all or none of a request's documents; mixed settings fail.
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350pub struct CitationsConfig {
351    /// Whether citation tracking is enabled for this document.
352    pub enabled: bool,
353}
354
355/// A citation pointing to source text using a source-specific locator.
356/// Known tags require valid payloads. Unknown tags retain their raw JSON.
357/// See the [wire format](https://docs.anthropic.com/en/docs/build-with-claude/citations).
358#[derive(Debug, Clone, PartialEq, Eq)]
359pub enum Citation {
360    /// A citation locating a character span in a plain text document.
361    CharLocation(CharLocationCitation),
362    /// A citation locating a page range in a PDF document.
363    PageLocation(PageLocationCitation),
364    /// A citation locating a block range in a custom-content document.
365    ContentBlockLocation(ContentBlockLocationCitation),
366    /// A citation locating a block range in a user-provided search result.
367    SearchResultLocation(SearchResultLocationCitation),
368    /// A citation emitted by Anthropic's server-side web search tool.
369    WebSearchResultLocation(WebSearchResultLocationCitation),
370    /// A forward-compatible raw citation payload for citation types this crate
371    /// does not yet model.
372    Unknown(serde_json::Value),
373}
374
375/// Payload of a [`Citation::CharLocation`].
376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
377pub struct CharLocationCitation {
378    /// The exact text being cited. Not counted toward output tokens.
379    pub cited_text: String,
380    /// 0-indexed position of the source document in the request's document list.
381    pub document_index: usize,
382    /// Optional title of the source document, echoed back from the request.
383    #[serde(default, skip_serializing_if = "Option::is_none")]
384    pub document_title: Option<String>,
385    /// 0-indexed character offset where the cited span begins.
386    pub start_char_index: usize,
387    /// Character offset where the cited span ends (exclusive).
388    pub end_char_index: usize,
389}
390
391/// Payload of a [`Citation::PageLocation`].
392#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
393pub struct PageLocationCitation {
394    /// The exact text being cited. Not counted toward output tokens.
395    pub cited_text: String,
396    /// 0-indexed position of the source document in the request's document list.
397    pub document_index: usize,
398    /// Optional title of the source document, echoed back from the request.
399    #[serde(default, skip_serializing_if = "Option::is_none")]
400    pub document_title: Option<String>,
401    /// 1-indexed page number where the cited span begins.
402    pub start_page_number: u32,
403    /// 1-indexed page number where the cited span ends (exclusive).
404    pub end_page_number: u32,
405}
406
407/// Payload of a [`Citation::ContentBlockLocation`].
408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409pub struct ContentBlockLocationCitation {
410    /// The exact text being cited. Not counted toward output tokens.
411    pub cited_text: String,
412    /// 0-indexed position of the source document in the request's document list.
413    pub document_index: usize,
414    /// Optional title of the source document, echoed back from the request.
415    #[serde(default, skip_serializing_if = "Option::is_none")]
416    pub document_title: Option<String>,
417    /// 0-indexed content block index where the cited span begins.
418    pub start_block_index: usize,
419    /// Content block index where the cited span ends (exclusive).
420    pub end_block_index: usize,
421}
422
423/// Payload of a [`Citation::SearchResultLocation`].
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
425pub struct SearchResultLocationCitation {
426    /// The exact text being cited. Not counted toward output tokens.
427    pub cited_text: String,
428    /// Source URL or identifier from the original search result.
429    pub source: String,
430    /// Title from the original search result.
431    #[serde(default, skip_serializing_if = "Option::is_none")]
432    pub title: Option<String>,
433    /// 0-indexed position of the cited search result across all search
434    /// result blocks in the request.
435    pub search_result_index: usize,
436    /// 0-indexed content block index where the cited span begins.
437    pub start_block_index: usize,
438    /// Content block index where the cited span ends (exclusive).
439    pub end_block_index: usize,
440}
441
442/// Payload of a [`Citation::WebSearchResultLocation`].
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
444pub struct WebSearchResultLocationCitation {
445    /// The exact text being cited. Not counted toward output tokens.
446    pub cited_text: String,
447    /// URL of the cited source.
448    pub url: String,
449    /// Source title, serialized as `null` when absent.
450    pub title: Option<String>,
451    /// Encrypted reference that must be preserved for multi-turn
452    /// conversations.
453    pub encrypted_index: String,
454}
455
456impl Serialize for Citation {
457    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
458    where
459        S: serde::Serializer,
460    {
461        /// Serialize the per-variant DTO and insert the wire `type` tag.
462        fn tagged<S, T>(serializer: S, tag: &str, fields: &T) -> Result<S::Ok, S::Error>
463        where
464            S: serde::Serializer,
465            T: Serialize,
466        {
467            let mut value = serde_json::to_value(fields).map_err(serde::ser::Error::custom)?;
468            if let serde_json::Value::Object(obj) = &mut value {
469                obj.insert("type".into(), serde_json::json!(tag));
470            }
471            value.serialize(serializer)
472        }
473
474        match self {
475            Citation::CharLocation(fields) => tagged(serializer, "char_location", fields),
476            Citation::PageLocation(fields) => tagged(serializer, "page_location", fields),
477            Citation::ContentBlockLocation(fields) => {
478                tagged(serializer, "content_block_location", fields)
479            }
480            Citation::SearchResultLocation(fields) => {
481                tagged(serializer, "search_result_location", fields)
482            }
483            Citation::WebSearchResultLocation(fields) => {
484                tagged(serializer, "web_search_result_location", fields)
485            }
486            Citation::Unknown(raw) => raw.serialize(serializer),
487        }
488    }
489}
490
491impl<'de> Deserialize<'de> for Citation {
492    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
493    where
494        D: serde::Deserializer<'de>,
495    {
496        /// Decode the payload of an already tag-matched citation. A modeled tag
497        /// carrying a defective payload is an error, never a silent
498        /// [`Citation::Unknown`].
499        fn payload<T, E>(value: serde_json::Value) -> Result<T, E>
500        where
501            T: serde::de::DeserializeOwned,
502            E: serde::de::Error,
503        {
504            serde_json::from_value(value).map_err(E::custom)
505        }
506
507        // Explicit dispatch prevents malformed known citations from falling back to Unknown.
508        let value = serde_json::Value::deserialize(deserializer)?;
509        let Some(citation_type) = value.get("type").and_then(serde_json::Value::as_str) else {
510            return Ok(Citation::Unknown(value));
511        };
512
513        match citation_type {
514            "char_location" => Ok(Citation::CharLocation(payload(value)?)),
515            "page_location" => Ok(Citation::PageLocation(payload(value)?)),
516            "content_block_location" => Ok(Citation::ContentBlockLocation(payload(value)?)),
517            "search_result_location" => Ok(Citation::SearchResultLocation(payload(value)?)),
518            "web_search_result_location" => Ok(Citation::WebSearchResultLocation(payload(value)?)),
519            _ => Ok(Citation::Unknown(value)),
520        }
521    }
522}
523
524/// Deserialize a vector, treating explicit JSON `null` as empty.
525/// Anthropic can send null citations on text-block start events.
526fn null_as_empty_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
527where
528    D: serde::Deserializer<'de>,
529    T: serde::Deserialize<'de>,
530{
531    Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
532}
533
534/// Extract Anthropic-specific document fields (`title`, `context`, `citations`)
535/// from the generic [`message::Document::additional_params`] JSON blob.
536///
537/// Return absent fields when parameters are missing. Ignore non-string title
538/// and context values; reject a present, invalid [`CitationsConfig`].
539fn extract_anthropic_doc_params(
540    additional_params: Option<message::AdditionalParams>,
541) -> Result<(Option<String>, Option<String>, Option<CitationsConfig>), MessageError> {
542    let Some(value) = additional_params else {
543        return Ok((None, None, None));
544    };
545    let title = value
546        .get("title")
547        .and_then(|v| v.as_str())
548        .map(String::from);
549    let context = value
550        .get("context")
551        .and_then(|v| v.as_str())
552        .map(String::from);
553    let citations = value
554        .get("citations")
555        .cloned()
556        .map(serde_json::from_value::<CitationsConfig>)
557        .transpose()
558        .map_err(|e| {
559            MessageError::ConversionError(format!(
560                "Document `additional_params.citations` is not a valid CitationsConfig: {e}",
561            ))
562        })?;
563    Ok((title, context, citations))
564}
565
566/// Extract Anthropic citations attached to a generic [`message::Text`] block.
567///
568/// Citations are returned by Claude on assistant text blocks when the request
569/// enabled them via [`CitationsConfig`]. Internally they are stored as JSON in
570/// [`message::Text::additional_params`] so they survive conversion through the
571/// generic [`message::AssistantContent`] surface.
572///
573/// Returns `Ok(vec![])` when no citations are attached. Unknown citation types
574/// are preserved as [`Citation::Unknown`]. Returns an error if the `citations`
575/// field is malformed or if a known citation type has an invalid shape.
576///
577/// ```no_run
578/// use rig_core::completion::message::{self, AssistantContent};
579/// use rig_core::providers::anthropic::completion::anthropic_citations;
580///
581/// fn print_citations(content: &AssistantContent) {
582///     if let AssistantContent::Text(text) = content
583///         && let Ok(citations) = anthropic_citations(text)
584///         && !citations.is_empty()
585///     {
586///         println!("{citations:?}");
587///     }
588/// }
589/// # let _ = message::Text::new("");
590/// ```
591pub fn anthropic_citations(text: &message::Text) -> Result<Vec<Citation>, serde_json::Error> {
592    match text
593        .additional_params
594        .as_ref()
595        .and_then(|v| v.get("citations"))
596    {
597        Some(c) => <Vec<Citation> as serde::Deserialize>::deserialize(c),
598        None => Ok(Vec::new()),
599    }
600}
601
602fn extract_anthropic_text_citations(text: &message::Text) -> Result<Vec<Citation>, MessageError> {
603    anthropic_citations(text).map_err(|err| {
604        MessageError::ConversionError(format!(
605            "Text `additional_params.citations` is not valid Anthropic citations: {err}"
606        ))
607    })
608}
609
610fn anthropic_text_content_from_message_text(text: message::Text) -> Result<Content, MessageError> {
611    if let Some(raw_content) = extract_anthropic_raw_content(&text)? {
612        if !text.text.is_empty() {
613            return Err(MessageError::ConversionError(format!(
614                "Text `{ANTHROPIC_RAW_CONTENT_KEY}` metadata cannot be combined with non-empty text"
615            )));
616        }
617
618        return Ok(raw_content);
619    }
620
621    let citations = extract_anthropic_text_citations(&text)?;
622    Ok(Content::Text {
623        text: text.text,
624        citations,
625        cache_control: None,
626    })
627}
628
629fn extract_anthropic_raw_content(text: &message::Text) -> Result<Option<Content>, MessageError> {
630    let Some(raw_content) = text
631        .additional_params
632        .as_ref()
633        .and_then(|value| value.get(ANTHROPIC_RAW_CONTENT_KEY))
634    else {
635        return Ok(None);
636    };
637
638    let content = <Content as serde::Deserialize>::deserialize(raw_content).map_err(|err| {
639        MessageError::ConversionError(format!(
640            "Text `{ANTHROPIC_RAW_CONTENT_KEY}` metadata is not valid Anthropic content: {err}"
641        ))
642    })?;
643
644    match content {
645        Content::ServerToolUse { .. }
646        | Content::WebSearchToolResult { .. }
647        | Content::CodeExecutionToolResult { .. } => Ok(Some(content)),
648        _ => Err(MessageError::ConversionError(format!(
649            "Text `{ANTHROPIC_RAW_CONTENT_KEY}` metadata only supports Anthropic server_tool_use, web_search_tool_result, and code_execution_tool_result blocks"
650        ))),
651    }
652}
653
654#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
655#[serde(tag = "type", rename_all = "snake_case")]
656pub enum ToolResultContent {
657    Text { text: String },
658    Image { source: ImageSource },
659}
660
661impl FromStr for ToolResultContent {
662    type Err = Infallible;
663
664    fn from_str(s: &str) -> Result<Self, Self::Err> {
665        Ok(ToolResultContent::Text { text: s.to_owned() })
666    }
667}
668
669/// The source of an image content block.
670///
671/// Anthropic supports two source types for images:
672/// - `Base64`: Base64-encoded image data with media type
673/// - `Url`: URL reference to an image
674///
675/// See: <https://docs.anthropic.com/en/api/messages>
676#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
677#[serde(tag = "type", rename_all = "snake_case")]
678pub enum ImageSource {
679    #[serde(rename = "base64")]
680    Base64 {
681        data: String,
682        media_type: ImageFormat,
683    },
684    #[serde(rename = "url")]
685    Url { url: String },
686}
687
688/// The source of a document content block.
689///
690/// Anthropic supports multiple source types for documents:
691/// - `Base64`: Base64-encoded document data (used for PDFs)
692/// - `Text`: Plain text document data
693/// - `Url`: URL reference to a document
694/// - `File`: Provider-side uploaded file reference from the Files API
695#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
696#[serde(tag = "type", rename_all = "snake_case")]
697pub enum DocumentSource {
698    Base64 {
699        data: String,
700        media_type: DocumentFormat,
701    },
702    Text {
703        data: String,
704        media_type: PlainTextMediaType,
705    },
706    Url {
707        url: String,
708    },
709    File {
710        file_id: String,
711    },
712}
713
714#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
715#[serde(rename_all = "lowercase")]
716pub enum ImageFormat {
717    #[serde(rename = "image/jpeg")]
718    JPEG,
719    #[serde(rename = "image/png")]
720    PNG,
721    #[serde(rename = "image/gif")]
722    GIF,
723    #[serde(rename = "image/webp")]
724    WEBP,
725}
726
727/// The media type for base64-encoded documents.
728///
729/// Used with the `DocumentSource::Base64` variant. Currently only PDF is supported
730/// for base64-encoded document sources.
731///
732/// See: <https://docs.anthropic.com/en/docs/build-with-claude/pdf-support>
733#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
734#[serde(rename_all = "lowercase")]
735pub enum DocumentFormat {
736    #[serde(rename = "application/pdf")]
737    PDF,
738}
739
740/// The media type for plain text document sources.
741///
742/// Used with the `DocumentSource::Text` variant.
743///
744/// See: <https://docs.anthropic.com/en/api/messages>
745#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
746pub enum PlainTextMediaType {
747    #[serde(rename = "text/plain")]
748    Plain,
749}
750
751impl From<String> for Content {
752    fn from(text: String) -> Self {
753        Content::Text {
754            text,
755            citations: Vec::new(),
756            cache_control: None,
757        }
758    }
759}
760
761impl From<String> for ToolResultContent {
762    fn from(text: String) -> Self {
763        ToolResultContent::Text { text }
764    }
765}
766
767impl TryFrom<message::ImageMediaType> for ImageFormat {
768    type Error = MessageError;
769
770    fn try_from(media_type: message::ImageMediaType) -> Result<Self, Self::Error> {
771        Ok(match media_type {
772            message::ImageMediaType::JPEG => ImageFormat::JPEG,
773            message::ImageMediaType::PNG => ImageFormat::PNG,
774            message::ImageMediaType::GIF => ImageFormat::GIF,
775            message::ImageMediaType::WEBP => ImageFormat::WEBP,
776            _ => {
777                return Err(MessageError::ConversionError(format!(
778                    "Unsupported image media type: {media_type:?}"
779                )));
780            }
781        })
782    }
783}
784
785/// Preserve object arguments or decode an object from a JSON string.
786/// Replace other values with `{}` because Messages requires object tool input.
787fn coerce_tool_input(input: serde_json::Value) -> serde_json::Value {
788    match input {
789        v @ serde_json::Value::Object(_) => v,
790        serde_json::Value::String(s) => match serde_json::from_str::<serde_json::Value>(&s) {
791            Ok(serde_json::Value::Object(m)) => serde_json::Value::Object(m),
792            _ => serde_json::json!({}),
793        },
794        _ => serde_json::json!({}),
795    }
796}
797
798fn anthropic_content_from_assistant_content(
799    content: message::AssistantContent,
800    issuers: &[message::Issuer],
801) -> Result<Vec<Content>, MessageError> {
802    match content {
803        message::AssistantContent::Text(text) => {
804            // Anthropic rejects empty text; only supported raw hosted-tool metadata
805            // can give an otherwise empty block replayable content.
806            if text.text.is_empty() && extract_anthropic_raw_content(&text)?.is_none() {
807                return Ok(Vec::new());
808            }
809            Ok(vec![anthropic_text_content_from_message_text(text)?])
810        }
811        message::AssistantContent::Image(_) => Err(MessageError::ConversionError(
812            "Anthropic currently doesn't support images.".to_string(),
813        )),
814        message::AssistantContent::ToolCall(tool_call) => Ok(vec![Content::ToolUse {
815            // The wire requires a non-empty id: the provider-issued one when it
816            // exists, else rig's minted handle.
817            id: tool_call.id.wire().into_owned(),
818            name: tool_call.function.name.into(),
819            input: coerce_tool_input(tool_call.function.arguments),
820        }]),
821        message::AssistantContent::Reasoning(reasoning) => {
822            // Reasoning another service issued is not replayed.
823            let Some(reasoning) = reasoning.open_for(issuers) else {
824                return Ok(Vec::new());
825            };
826            let mut converted = Vec::new();
827            for block in reasoning.content.clone() {
828                match block {
829                    message::ReasoningContent::Text { text, signature } => {
830                        converted.push(Content::Thinking {
831                            thinking: text,
832                            signature,
833                        });
834                    }
835                    message::ReasoningContent::Summary(summary) => {
836                        converted.push(Content::Thinking {
837                            thinking: summary,
838                            signature: None,
839                        });
840                    }
841                    message::ReasoningContent::Redacted { data }
842                    | message::ReasoningContent::Encrypted(data) => {
843                        converted.push(Content::RedactedThinking { data });
844                    }
845                }
846            }
847
848            if converted.is_empty() {
849                return Err(MessageError::ConversionError(
850                    "Cannot convert empty reasoning content to Anthropic format".to_string(),
851                ));
852            }
853
854            Ok(converted)
855        }
856    }
857}
858
859/// The issuer-free conversion: no reasoning opens, so none is replayed. A
860/// request's own conversion ([`AnthropicRequestParams`]) replays the
861/// reasoning its issuers open.
862impl TryFrom<message::Message> for Message {
863    type Error = MessageError;
864
865    fn try_from(message: message::Message) -> Result<Self, Self::Error> {
866        Message::from_message(message, &[])
867    }
868}
869
870impl Message {
871    /// `message` on the wire, replaying the reasoning `issuers` open.
872    fn from_message(
873        message: message::Message,
874        issuers: &[message::Issuer],
875    ) -> Result<Self, MessageError> {
876        Ok(match message {
877            message::Message::User { content } => Message {
878                role: Role::User,
879                content: content.into_iter().map(|content| match content {
880                    message::UserContent::Text(message::Text { text, .. }) => {
881                        Ok(Content::from(text))
882                    }
883                    message::UserContent::ToolResult(tool_result) => Ok(Content::ToolResult {
884                        tool_use_id: tool_result.call.wire().into_owned(),
885                        content: tool_result.content.into_iter().map(|content| match content {
886                            message::ToolResultContent::Text(message::Text { text, .. }) => {
887                                Ok(ToolResultContent::Text { text })
888                            }
889                            message::ToolResultContent::Json { value } => {
890                                Ok(ToolResultContent::Text {
891                                    text: value.to_string(),
892                                })
893                            }
894                            message::ToolResultContent::Image(image) => {
895                                let DocumentSourceKind::Base64(data) = image.data else {
896                                    return Err(MessageError::ConversionError(
897                                        "Only base64 strings can be used with the Anthropic API"
898                                            .to_string(),
899                                    ));
900                                };
901                                let media_type =
902                                    image.media_type.ok_or(MessageError::ConversionError(
903                                        "Image media type is required".to_owned(),
904                                    ))?;
905                                Ok(ToolResultContent::Image {
906                                    source: ImageSource::Base64 {
907                                        data,
908                                        media_type: media_type.try_into()?,
909                                    },
910                                })
911                            }
912                        }).collect::<Result<Vec<_>, _>>()?,
913                        is_error: None,
914                        cache_control: None,
915                    }),
916                    message::UserContent::Image(message::Image {
917                        data, media_type, ..
918                    }) => {
919                        let source = match data {
920                            DocumentSourceKind::Base64(data) => {
921                                let media_type =
922                                    media_type.ok_or(MessageError::ConversionError(
923                                        "Image media type is required for Claude API".to_string(),
924                                    ))?;
925                                ImageSource::Base64 {
926                                    data,
927                                    media_type: ImageFormat::try_from(media_type)?,
928                                }
929                            }
930                            DocumentSourceKind::Url(url) => ImageSource::Url { url },
931                            DocumentSourceKind::Unknown => {
932                                return Err(MessageError::ConversionError(
933                                    "Image content has no body".into(),
934                                ));
935                            }
936                            doc => {
937                                return Err(MessageError::ConversionError(format!(
938                                    "Unsupported document type: {doc:?}"
939                                )));
940                            }
941                        };
942
943                        Ok(Content::Image {
944                            source,
945                            cache_control: None,
946                        })
947                    }
948                    message::UserContent::Document(message::Document {
949                        data,
950                        media_type,
951                        additional_params,
952                    }) => {
953                        let (title, context, citations) =
954                            extract_anthropic_doc_params(additional_params)?;
955
956                        if let DocumentSourceKind::FileId(file_id) = data {
957                            return Ok(Content::Document {
958                                source: DocumentSource::File { file_id },
959                                title,
960                                context,
961                                citations,
962                                cache_control: None,
963                            });
964                        }
965
966                        let media_type = match media_type {
967                            Some(media_type) => media_type,
968                            // Anthropic's URL document source has no media-type field and is
969                            // defined specifically for PDFs, so the source itself is sufficient.
970                            None if matches!(&data, DocumentSourceKind::Url(_)) => {
971                                DocumentMediaType::PDF
972                            }
973                            None => {
974                                return Err(MessageError::ConversionError(
975                                    "Document media type is required".to_string(),
976                                ));
977                            }
978                        };
979
980                        let source = match media_type {
981                            DocumentMediaType::PDF => match data {
982                                DocumentSourceKind::Base64(data)
983                                | DocumentSourceKind::String(data) => DocumentSource::Base64 {
984                                    data,
985                                    media_type: DocumentFormat::PDF,
986                                },
987                                DocumentSourceKind::Url(url) => DocumentSource::Url { url },
988                                _ => {
989                                    return Err(MessageError::ConversionError(
990                                        "Only base64 encoded data or URLs are supported for PDF documents".into(),
991                                    ));
992                                }
993                            },
994                            DocumentMediaType::TXT => {
995                                let (DocumentSourceKind::String(data)
996                                | DocumentSourceKind::Base64(data)) = data
997                                else {
998                                    return Err(MessageError::ConversionError(
999                                        "Only string or base64 data is supported for plain text documents".into(),
1000                                    ));
1001                                };
1002                                DocumentSource::Text {
1003                                    data,
1004                                    media_type: PlainTextMediaType::Plain,
1005                                }
1006                            }
1007                            other => {
1008                                return Err(MessageError::ConversionError(format!(
1009                                    "Anthropic only supports PDF and plain text documents, got: {}",
1010                                    other.to_mime_type()
1011                                )));
1012                            }
1013                        };
1014
1015                        Ok(Content::Document {
1016                            source,
1017                            title,
1018                            context,
1019                            citations,
1020                            cache_control: None,
1021                        })
1022                    }
1023                    message::UserContent::Audio { .. } => Err(MessageError::ConversionError(
1024                        "Audio is not supported in Anthropic".to_owned(),
1025                    )),
1026                    message::UserContent::Video { .. } => Err(MessageError::ConversionError(
1027                        "Video is not supported in Anthropic".to_owned(),
1028                    )),
1029                }).collect::<Result<Vec<_>, _>>()?,
1030            },
1031
1032            message::Message::System { content } => Message {
1033                role: Role::System,
1034                content: vec![Content::from(content)],
1035            },
1036
1037            message::Message::Assistant { content, .. } => {
1038                let converted_content = content.into_iter().try_fold(
1039                    Vec::new(),
1040                    |mut accumulated, assistant_content| {
1041                        accumulated.extend(anthropic_content_from_assistant_content(
1042                            assistant_content,
1043                            issuers,
1044                        )?);
1045                        Ok::<Vec<Content>, MessageError>(accumulated)
1046                    },
1047                )?;
1048
1049                Message {
1050                    content: crate::message::require_non_empty(converted_content, || {
1051                        MessageError::ConversionError(
1052                            "Assistant message did not contain Anthropic-compatible content"
1053                                .to_owned(),
1054                        )
1055                    })?,
1056                    role: Role::Assistant,
1057                }
1058            }
1059        })
1060    }
1061}
1062
1063/// Whether `model` is `id` or one of its dated snapshots (`<id>-YYYYMMDD`),
1064/// and not a later model whose ID merely starts with `id`.
1065pub(super) fn is_model(model: &str, id: &str) -> bool {
1066    model
1067        .strip_prefix(id)
1068        .is_some_and(|rest| rest.is_empty() || rest.starts_with("-20"))
1069}
1070
1071/// Models whose published synchronous output limit is 128K tokens.
1072const OUTPUT_128K: [&str; 10] = [
1073    CLAUDE_FABLE_5_1,
1074    CLAUDE_FABLE_5,
1075    CLAUDE_OPUS_5_5,
1076    CLAUDE_OPUS_5,
1077    CLAUDE_SONNET_5_5,
1078    CLAUDE_SONNET_5,
1079    CLAUDE_OPUS_4_8,
1080    CLAUDE_OPUS_4_7,
1081    CLAUDE_OPUS_4_6,
1082    CLAUDE_SONNET_4_6,
1083];
1084
1085/// Models that accept `role: "system"` inside `messages`, per Anthropic's
1086/// mid-conversation system messages page. Claude Sonnet 5 does not.
1087const MID_CONVERSATION_SYSTEM: [&str; 6] = [
1088    CLAUDE_FABLE_5_1,
1089    CLAUDE_FABLE_5,
1090    CLAUDE_OPUS_5_5,
1091    CLAUDE_OPUS_5,
1092    CLAUDE_SONNET_5_5,
1093    CLAUDE_OPUS_4_8,
1094];
1095
1096/// Return the published synchronous output limit for a recognized model.
1097/// Unknown models require an explicit `max_tokens` value.
1098pub(super) fn default_max_tokens_for_model(model: &str) -> Option<u64> {
1099    if OUTPUT_128K.iter().any(|id| is_model(model, id)) {
1100        Some(128_000)
1101    } else if model.starts_with("claude-opus-4")
1102        || model.starts_with("claude-sonnet-4")
1103        || model.starts_with("claude-haiku-4-5")
1104    {
1105        Some(64_000)
1106    } else {
1107        None
1108    }
1109}
1110
1111/// Models that answer a forced `tool_choice` (`any` or `tool`) with a 400, per
1112/// their what's-new pages.
1113const REJECTS_FORCED_TOOL_CHOICE: [&str; 3] =
1114    [CLAUDE_OPUS_5_5, CLAUDE_SONNET_5_5, CLAUDE_FABLE_5_1];
1115
1116/// Whether `model` rejects a forced tool choice.
1117pub(super) fn rejects_forced_tool_choice(model: &str) -> bool {
1118    REJECTS_FORCED_TOOL_CHOICE
1119        .iter()
1120        .any(|id| is_model(model, id))
1121}
1122
1123/// Whether `model` accepts `role: "system"` inside `messages`.
1124pub(super) fn supports_mid_conversation_system_messages(model: &str) -> bool {
1125    MID_CONVERSATION_SYSTEM.iter().any(|id| is_model(model, id))
1126}
1127
1128#[derive(Default, Debug, Serialize, Deserialize)]
1129#[serde(tag = "type", rename_all = "snake_case")]
1130pub enum ToolChoice {
1131    #[default]
1132    Auto,
1133    Any,
1134    None,
1135    Tool {
1136        name: String,
1137    },
1138}
1139impl TryFrom<message::ToolChoice> for ToolChoice {
1140    type Error = EncodeError;
1141
1142    fn try_from(value: message::ToolChoice) -> Result<Self, Self::Error> {
1143        let res = match value {
1144            message::ToolChoice::Auto => Self::Auto,
1145            message::ToolChoice::None => Self::None,
1146            message::ToolChoice::Required => Self::Any,
1147            message::ToolChoice::Specific { function_names } => {
1148                if function_names.len() != 1 {
1149                    return Err(EncodeError::request(
1150                        "Only one tool may be specified to be used by Claude",
1151                    ));
1152                }
1153
1154                let Some(name) = function_names.into_iter().next() else {
1155                    return Err(EncodeError::request(
1156                        "Only one tool may be specified to be used by Claude",
1157                    ));
1158                };
1159
1160                Self::Tool { name }
1161            }
1162        };
1163
1164        Ok(res)
1165    }
1166}
1167
1168/// Require all object properties, disallow additional properties, and remove
1169/// numeric constraints for Anthropic structured output.
1170fn sanitize_schema(schema: &mut serde_json::Value) {
1171    crate::providers::internal::schema::sanitize_schema(
1172        schema,
1173        crate::providers::internal::schema::SanitizeOptions {
1174            strip_ref_siblings: false,
1175            inject_empty_properties: false,
1176            strip_numeric_constraints: true,
1177        },
1178    );
1179}
1180
1181/// Adapt a strict tool schema using Anthropic's SDK transformation policy.
1182///
1183/// Strict tools support optional parameters, so declared `required` lists are
1184/// preserved. Unsupported validation keywords are moved into descriptions as
1185/// model guidance instead of reaching the constrained-decoding compiler.
1186pub(super) fn sanitize_strict_tool_schema(schema: &mut serde_json::Value) {
1187    let mut original = std::mem::take(schema);
1188    inline_local_root_reference(&mut original);
1189    flatten_root_all_of(&mut original);
1190    // Tool-input roots require an explicit object type even when properties imply it.
1191    if let serde_json::Value::Object(source) = &mut original
1192        && !source.contains_key("type")
1193        && (source.contains_key("properties") || source.contains_key("$ref"))
1194    {
1195        source.insert(
1196            "type".to_string(),
1197            serde_json::Value::String("object".to_string()),
1198        );
1199    }
1200    *schema = transform_strict_tool_schema(original);
1201}
1202
1203/// Anthropic rejects `allOf` at the top level of a tool input even when every
1204/// branch describes an object. Merge those object branches into the root while
1205/// preserving per-property collisions as nested `allOf` constraints.
1206fn flatten_root_all_of(schema: &mut serde_json::Value) {
1207    use serde_json::{Map, Value};
1208
1209    let Value::Object(root) = schema else {
1210        return;
1211    };
1212    let Some(all_of) = root.remove("allOf") else {
1213        return;
1214    };
1215    let mut conflicting_constraints = Map::new();
1216    merge_root_all_of(root, all_of, &mut conflicting_constraints);
1217    if !conflicting_constraints.is_empty() {
1218        root.insert(
1219            "rootAllOfConstraints".to_string(),
1220            Value::Object(conflicting_constraints),
1221        );
1222    }
1223}
1224
1225/// Anthropic requires a tool input's root to have `type: object`, but rejects
1226/// `type` beside `$ref`. Resolve local root references before transformation so
1227/// both requirements can be met while retaining definitions needed by nested
1228/// references.
1229fn inline_local_root_reference(schema: &mut serde_json::Value) {
1230    use serde_json::Value;
1231
1232    let mut seen = std::collections::BTreeSet::new();
1233    loop {
1234        let Some(reference) = schema
1235            .get("$ref")
1236            .and_then(serde_json::Value::as_str)
1237            .map(str::to_string)
1238        else {
1239            return;
1240        };
1241        let Some(pointer) = reference.strip_prefix('#') else {
1242            return;
1243        };
1244        if !seen.insert(reference.clone()) {
1245            return;
1246        }
1247        let Some(Value::Object(mut referenced)) = schema.pointer(pointer).cloned() else {
1248            return;
1249        };
1250        let Some(mut root) = schema.as_object().cloned() else {
1251            return;
1252        };
1253        root.remove("$ref");
1254
1255        for keyword in ["$defs", "definitions"] {
1256            let Some(root_definitions) = root.remove(keyword) else {
1257                continue;
1258            };
1259            let definitions =
1260                merge_document_definitions(root_definitions, referenced.remove(keyword));
1261            referenced.insert(keyword.to_string(), definitions);
1262        }
1263
1264        merge_root_reference_siblings(&mut referenced, root);
1265
1266        *schema = Value::Object(referenced);
1267    }
1268}
1269
1270fn merge_document_definitions(
1271    root_definitions: serde_json::Value,
1272    local_definitions: Option<serde_json::Value>,
1273) -> serde_json::Value {
1274    use serde_json::Value;
1275
1276    match (root_definitions, local_definitions) {
1277        (Value::Object(root_definitions), Some(Value::Object(mut local_definitions))) => {
1278            // Absolute JSON pointers still resolve from the document root.
1279            // Keep those root targets authoritative when an inlined schema
1280            // happens to define the same name locally.
1281            local_definitions.extend(root_definitions);
1282            Value::Object(local_definitions)
1283        }
1284        (root_definitions, _) => root_definitions,
1285    }
1286}
1287
1288/// Merge keywords adjacent to a root `$ref` into its resolved object. JSON
1289/// Schema applies those siblings conjunctively; simply replacing the root with
1290/// the referenced object would silently discard valid constraints.
1291fn merge_root_reference_siblings(
1292    referenced: &mut serde_json::Map<String, serde_json::Value>,
1293    siblings: serde_json::Map<String, serde_json::Value>,
1294) {
1295    use serde_json::{Map, Value};
1296
1297    let mut conflicting_constraints = Map::new();
1298    for (keyword, sibling) in siblings {
1299        match keyword.as_str() {
1300            "properties" => merge_schema_properties(referenced, sibling),
1301            "required" => merge_required_properties(referenced, sibling),
1302            "allOf" => merge_root_all_of(referenced, sibling, &mut conflicting_constraints),
1303            // Root unions are unsupported; retain their constraints as model guidance.
1304            "anyOf" | "oneOf" => {
1305                conflicting_constraints.insert(keyword, sibling);
1306            }
1307            // These describe the root document rather than adding a second
1308            // validation constraint. Prefer the root-level annotation.
1309            "description" | "title" | "$schema" | "$id" | "$comment" | "default" | "examples"
1310            | "deprecated" | "readOnly" | "writeOnly" => {
1311                referenced.insert(keyword, sibling);
1312            }
1313            _ => match referenced.get(&keyword) {
1314                None => {
1315                    referenced.insert(keyword, sibling);
1316                }
1317                Some(existing) if existing == &sibling => {}
1318                Some(_) => {
1319                    conflicting_constraints.insert(keyword, sibling);
1320                }
1321            },
1322        }
1323    }
1324
1325    if !conflicting_constraints.is_empty() {
1326        // Root allOf is unsupported, so unmerged constraints remain model guidance.
1327        referenced.insert(
1328            "rootRefSiblingConstraints".to_string(),
1329            Value::Object(conflicting_constraints),
1330        );
1331    }
1332}
1333
1334fn merge_root_all_of(
1335    schema: &mut serde_json::Map<String, serde_json::Value>,
1336    sibling: serde_json::Value,
1337    conflicting_constraints: &mut serde_json::Map<String, serde_json::Value>,
1338) {
1339    use serde_json::Value;
1340
1341    let Value::Array(branches) = sibling else {
1342        conflicting_constraints.insert("allOf".to_string(), sibling);
1343        return;
1344    };
1345    let mut unsupported_branches = Vec::new();
1346    for branch in branches {
1347        match branch {
1348            Value::Object(mut branch) => {
1349                if branch.contains_key("$ref") {
1350                    for keyword in ["$defs", "definitions"] {
1351                        let Some(root_definitions) = schema.get(keyword).cloned() else {
1352                            continue;
1353                        };
1354                        let definitions =
1355                            merge_document_definitions(root_definitions, branch.remove(keyword));
1356                        branch.insert(keyword.to_string(), definitions);
1357                    }
1358                    let mut branch = Value::Object(branch);
1359                    inline_local_root_reference(&mut branch);
1360                    match branch {
1361                        Value::Object(branch) => merge_root_reference_siblings(schema, branch),
1362                        branch => unsupported_branches.push(branch),
1363                    }
1364                } else {
1365                    merge_root_reference_siblings(schema, branch);
1366                }
1367            }
1368            branch => unsupported_branches.push(branch),
1369        }
1370    }
1371    if !unsupported_branches.is_empty() {
1372        conflicting_constraints.insert("allOf".to_string(), Value::Array(unsupported_branches));
1373    }
1374}
1375
1376fn merge_schema_properties(
1377    schema: &mut serde_json::Map<String, serde_json::Value>,
1378    sibling: serde_json::Value,
1379) {
1380    use serde_json::{Map, Value};
1381
1382    let Value::Object(sibling_properties) = sibling else {
1383        schema.entry("properties".to_string()).or_insert(sibling);
1384        return;
1385    };
1386    let properties = schema
1387        .entry("properties".to_string())
1388        .or_insert_with(|| Value::Object(Map::new()));
1389    let Value::Object(properties) = properties else {
1390        return;
1391    };
1392
1393    for (name, sibling_schema) in sibling_properties {
1394        match properties.remove(&name) {
1395            None => {
1396                properties.insert(name, sibling_schema);
1397            }
1398            Some(existing) if existing == sibling_schema => {
1399                properties.insert(name, existing);
1400            }
1401            Some(existing) => {
1402                properties.insert(
1403                    name,
1404                    Value::Object(Map::from_iter([(
1405                        "allOf".to_string(),
1406                        Value::Array(vec![existing, sibling_schema]),
1407                    )])),
1408                );
1409            }
1410        }
1411    }
1412}
1413
1414fn merge_required_properties(
1415    schema: &mut serde_json::Map<String, serde_json::Value>,
1416    sibling: serde_json::Value,
1417) {
1418    use serde_json::Value;
1419
1420    let Value::Array(sibling_required) = sibling else {
1421        schema.entry("required".to_string()).or_insert(sibling);
1422        return;
1423    };
1424    let required = schema
1425        .entry("required".to_string())
1426        .or_insert_with(|| Value::Array(Vec::new()));
1427    let Value::Array(required) = required else {
1428        return;
1429    };
1430    for name in sibling_required {
1431        if !required.contains(&name) {
1432            required.push(name);
1433        }
1434    }
1435}
1436
1437fn transform_strict_tool_schema(schema: serde_json::Value) -> serde_json::Value {
1438    use serde_json::{Map, Value};
1439
1440    let Value::Object(mut source) = schema else {
1441        return schema;
1442    };
1443    let mut strict = Map::new();
1444
1445    for keyword in ["$defs", "definitions"] {
1446        if let Some(definitions) = source.remove(keyword) {
1447            match definitions {
1448                Value::Object(definitions) => {
1449                    strict.insert(
1450                        keyword.to_string(),
1451                        Value::Object(
1452                            definitions
1453                                .into_iter()
1454                                .map(|(name, schema)| (name, transform_strict_tool_schema(schema)))
1455                                .collect(),
1456                        ),
1457                    );
1458                }
1459                definitions => {
1460                    source.insert(keyword.to_string(), definitions);
1461                }
1462            }
1463        }
1464    }
1465
1466    if let Some(reference) = source.remove("$ref") {
1467        strict.insert("$ref".to_string(), reference);
1468        return Value::Object(strict);
1469    }
1470
1471    let schema_type = source.remove("type");
1472    let any_of = source.remove("anyOf");
1473    let one_of = source.remove("oneOf");
1474    let all_of = source.remove("allOf");
1475    let alternatives = match (any_of, one_of, all_of) {
1476        (Some(Value::Array(variants)), _, _) => Some(("anyOf", variants)),
1477        (_, Some(Value::Array(variants)), _) => Some(("anyOf", variants)),
1478        (_, _, Some(Value::Array(variants))) => Some(("allOf", variants)),
1479        _ => None,
1480    };
1481    if let Some((keyword, variants)) = alternatives {
1482        strict.insert(
1483            keyword.to_string(),
1484            Value::Array(
1485                variants
1486                    .into_iter()
1487                    .map(transform_strict_tool_schema)
1488                    .collect(),
1489            ),
1490        );
1491    } else if let Some(schema_type) = schema_type.clone() {
1492        strict.insert("type".to_string(), schema_type);
1493    }
1494
1495    if let Some(Value::Array(values)) = source.remove("enum") {
1496        strict.insert("enum".to_string(), Value::Array(values));
1497    }
1498    if let Some(constant) = source.remove("const") {
1499        strict.insert("const".to_string(), constant);
1500    }
1501    for keyword in ["description", "title"] {
1502        if let Some(Value::String(value)) = source.remove(keyword) {
1503            strict.insert(keyword.to_string(), Value::String(value));
1504        }
1505    }
1506
1507    let has_properties = source.contains_key("properties");
1508    let properties_imply_object = schema_type.is_none() && has_properties;
1509    if properties_imply_object {
1510        strict.insert("type".to_string(), Value::String("object".to_string()));
1511    }
1512    if schema_has_type(schema_type.as_ref(), "object") || has_properties {
1513        let properties = match source.remove("properties") {
1514            Some(Value::Object(properties)) => properties
1515                .into_iter()
1516                .map(|(name, schema)| (name, transform_strict_tool_schema(schema)))
1517                .collect(),
1518            _ => Map::new(),
1519        };
1520        strict.insert("properties".to_string(), Value::Object(properties));
1521        source.remove("additionalProperties");
1522        strict.insert("additionalProperties".to_string(), Value::Bool(false));
1523        if let Some(Value::Array(required)) = source.remove("required") {
1524            strict.insert("required".to_string(), Value::Array(required));
1525        }
1526    }
1527
1528    if schema_has_type(schema_type.as_ref(), "string")
1529        && let Some(format) = source.remove("format")
1530    {
1531        const SUPPORTED_FORMATS: &[&str] = &[
1532            "date-time",
1533            "time",
1534            "date",
1535            "duration",
1536            "email",
1537            "hostname",
1538            "uri",
1539            "ipv4",
1540            "ipv6",
1541            "uuid",
1542        ];
1543        if format
1544            .as_str()
1545            .is_some_and(|format| SUPPORTED_FORMATS.contains(&format))
1546        {
1547            strict.insert("format".to_string(), format);
1548        } else {
1549            source.insert("format".to_string(), format);
1550        }
1551    }
1552
1553    if schema_has_type(schema_type.as_ref(), "array") {
1554        if let Some(items) = source.remove("items") {
1555            strict.insert("items".to_string(), transform_strict_tool_schema(items));
1556        }
1557        if let Some(min_items) = source.remove("minItems") {
1558            if matches!(min_items.as_u64(), Some(0 | 1)) {
1559                strict.insert("minItems".to_string(), min_items);
1560            } else {
1561                source.insert("minItems".to_string(), min_items);
1562            }
1563        }
1564    }
1565
1566    if !source.is_empty() {
1567        let hints = source
1568            .into_iter()
1569            .map(|(keyword, value)| {
1570                let value = match value {
1571                    Value::String(value) => value,
1572                    value => value.to_string(),
1573                };
1574                format!("{keyword}: {value}")
1575            })
1576            .collect::<Vec<_>>()
1577            .join(", ");
1578        let suffix = format!("{{{hints}}}");
1579        match strict.get_mut("description") {
1580            Some(Value::String(description)) => {
1581                description.push_str("\n\n");
1582                description.push_str(&suffix);
1583            }
1584            _ => {
1585                strict.insert("description".to_string(), Value::String(suffix));
1586            }
1587        }
1588    }
1589
1590    Value::Object(strict)
1591}
1592
1593fn schema_has_type(schema_type: Option<&serde_json::Value>, expected: &str) -> bool {
1594    match schema_type {
1595        Some(serde_json::Value::String(schema_type)) => schema_type == expected,
1596        Some(serde_json::Value::Array(schema_types)) => schema_types
1597            .iter()
1598            .any(|schema_type| schema_type.as_str() == Some(expected)),
1599        _ => false,
1600    }
1601}
1602
1603/// Output format specifier for Anthropic's structured output.
1604/// Source: <https://docs.anthropic.com/en/api/messages>
1605#[derive(Debug, Deserialize, Serialize)]
1606#[serde(tag = "type", rename_all = "snake_case")]
1607enum OutputFormat {
1608    /// Constrains the model's response to conform to the provided JSON schema.
1609    JsonSchema { schema: serde_json::Value },
1610}
1611
1612/// Configuration for the model's output format.
1613#[derive(Debug, Deserialize, Serialize)]
1614struct OutputConfig {
1615    format: OutputFormat,
1616}
1617
1618#[derive(Debug, Deserialize, Serialize)]
1619pub(super) struct AnthropicCompletionRequest {
1620    model: String,
1621    messages: Vec<Message>,
1622    max_tokens: u64,
1623    /// System prompt as array of content blocks to support cache_control
1624    #[serde(skip_serializing_if = "Vec::is_empty")]
1625    system: Vec<SystemContent>,
1626    #[serde(skip_serializing_if = "Option::is_none")]
1627    temperature: Option<f64>,
1628    #[serde(skip_serializing_if = "Option::is_none")]
1629    tool_choice: Option<ToolChoice>,
1630    #[serde(skip_serializing_if = "Vec::is_empty")]
1631    tools: Vec<serde_json::Value>,
1632    #[serde(skip_serializing_if = "Option::is_none")]
1633    output_config: Option<OutputConfig>,
1634    #[serde(flatten, skip_serializing_if = "Option::is_none")]
1635    additional_params: Option<serde_json::Value>,
1636    /// Top-level cache_control for Anthropic's automatic caching mode. When set, the API
1637    /// automatically places the cache breakpoint on the last cacheable block and advances it as
1638    /// the conversation grows. No beta header is required.
1639    #[serde(skip_serializing_if = "Option::is_none")]
1640    cache_control: Option<CacheControl>,
1641}
1642
1643/// Helper to set cache_control on a Content block
1644fn set_content_cache_control(content: &mut Content, value: Option<CacheControl>) {
1645    match content {
1646        Content::Text { cache_control, .. } => *cache_control = value,
1647        Content::Image { cache_control, .. } => *cache_control = value,
1648        Content::ToolResult { cache_control, .. } => *cache_control = value,
1649        Content::Document { cache_control, .. } => *cache_control = value,
1650        _ => {}
1651    }
1652}
1653
1654const MAX_CACHE_CONTROL_MARKERS: usize = 4;
1655
1656fn final_cacheable_tool_idx(tools: &[serde_json::Value]) -> Option<usize> {
1657    tools.iter().rposition(|tool| {
1658        tool.as_object().is_some_and(|tool| {
1659            !matches!(
1660                tool.get("defer_loading"),
1661                Some(serde_json::Value::Bool(true))
1662            )
1663        })
1664    })
1665}
1666
1667fn tool_cache_control_count(tools: &[serde_json::Value]) -> usize {
1668    tools
1669        .iter()
1670        .filter(|tool| tool_cache_control_value(tool).is_some())
1671        .count()
1672}
1673
1674fn tool_cache_control_value(tool: &serde_json::Value) -> Option<&serde_json::Value> {
1675    tool.get("cache_control")
1676        .filter(|cache_control| !cache_control.is_null())
1677}
1678
1679fn normalize_tool_cache_control(tools: &mut [serde_json::Value]) {
1680    for tool in tools.iter_mut() {
1681        if let Some(tool) = tool.as_object_mut()
1682            && tool
1683                .get("cache_control")
1684                .is_some_and(serde_json::Value::is_null)
1685        {
1686            tool.remove("cache_control");
1687        }
1688    }
1689}
1690
1691fn build_cache_control(ttl: Option<CacheTtl>) -> CacheControl {
1692    CacheControl::Ephemeral { ttl }
1693}
1694
1695#[derive(Clone, Copy, PartialEq, Eq)]
1696enum CacheControlTtl {
1697    FiveMinutes,
1698    OneHour,
1699}
1700
1701fn cache_control_ttl(cache_control: &CacheControl) -> CacheControlTtl {
1702    match cache_control {
1703        CacheControl::Ephemeral {
1704            ttl: Some(CacheTtl::OneHour),
1705        } => CacheControlTtl::OneHour,
1706        CacheControl::Ephemeral { .. } => CacheControlTtl::FiveMinutes,
1707    }
1708}
1709
1710fn cache_control_ttl_from_json(cache_control: &serde_json::Value) -> CacheControlTtl {
1711    match cache_control.get("ttl") {
1712        Some(serde_json::Value::String(ttl)) if ttl == "1h" => CacheControlTtl::OneHour,
1713        _ => CacheControlTtl::FiveMinutes,
1714    }
1715}
1716
1717fn content_cache_control(content: &Content) -> Option<&CacheControl> {
1718    match content {
1719        Content::Text { cache_control, .. }
1720        | Content::Image { cache_control, .. }
1721        | Content::ToolResult { cache_control, .. }
1722        | Content::Document { cache_control, .. } => cache_control.as_ref(),
1723        _ => None,
1724    }
1725}
1726
1727fn validate_cache_control_ttl(
1728    ttl: CacheControlTtl,
1729    shorter_ttl_seen: &mut bool,
1730) -> Result<(), EncodeError> {
1731    match ttl {
1732        CacheControlTtl::OneHour if *shorter_ttl_seen => Err(EncodeError::request(
1733            "Anthropic cache_control markers with ttl `1h` must appear before markers with \
1734                 the default 5-minute TTL",
1735        )),
1736        CacheControlTtl::OneHour => Ok(()),
1737        CacheControlTtl::FiveMinutes => {
1738            *shorter_ttl_seen = true;
1739            Ok(())
1740        }
1741    }
1742}
1743
1744fn validate_cache_control_ttl_order(
1745    system: &[SystemContent],
1746    messages: &[Message],
1747    tools: &[serde_json::Value],
1748    top_level_cache_control: Option<&CacheControl>,
1749) -> Result<(), EncodeError> {
1750    let mut shorter_ttl_seen = false;
1751
1752    for tool in tools {
1753        if let Some(cache_control) = tool_cache_control_value(tool) {
1754            validate_cache_control_ttl(
1755                cache_control_ttl_from_json(cache_control),
1756                &mut shorter_ttl_seen,
1757            )?;
1758        }
1759    }
1760
1761    for SystemContent::Text { cache_control, .. } in system {
1762        if let Some(cache_control) = cache_control {
1763            validate_cache_control_ttl(cache_control_ttl(cache_control), &mut shorter_ttl_seen)?;
1764        }
1765    }
1766
1767    for message in messages {
1768        for content in message.content.iter() {
1769            if let Some(cache_control) = content_cache_control(content) {
1770                validate_cache_control_ttl(
1771                    cache_control_ttl(cache_control),
1772                    &mut shorter_ttl_seen,
1773                )?;
1774            }
1775        }
1776    }
1777
1778    if let Some(cache_control) = top_level_cache_control {
1779        validate_cache_control_ttl(cache_control_ttl(cache_control), &mut shorter_ttl_seen)?;
1780    }
1781
1782    Ok(())
1783}
1784
1785fn top_level_cache_control_ttl(cache_control: Option<&CacheControl>) -> Option<CacheTtl> {
1786    cache_control
1787        .map(|cache_control| match cache_control {
1788            CacheControl::Ephemeral { ttl } => ttl.clone(),
1789        })
1790        .unwrap_or_default()
1791}
1792
1793/// Apply a cache-control breakpoint to the final cacheable tool definition in the request.
1794fn apply_tool_cache_control(
1795    tools: &mut [serde_json::Value],
1796    remaining_cache_markers: &mut usize,
1797    cache_control: &CacheControl,
1798) -> Result<(), EncodeError> {
1799    let Some(idx) = final_cacheable_tool_idx(tools) else {
1800        return Ok(());
1801    };
1802
1803    let Some(tool) = tools
1804        .get_mut(idx)
1805        .and_then(serde_json::Value::as_object_mut)
1806    else {
1807        return Ok(());
1808    };
1809
1810    if tool
1811        .get("cache_control")
1812        .is_some_and(|cache_control| !cache_control.is_null())
1813    {
1814        return Ok(());
1815    }
1816
1817    if *remaining_cache_markers == 0 {
1818        return Err(EncodeError::request(
1819            "Anthropic manual prompt caching requires a cache_control marker on the final \
1820             non-deferred tool, but explicit tool markers exhaust the available cache point budget",
1821        ));
1822    }
1823
1824    tool.insert(
1825        "cache_control".to_string(),
1826        serde_json::to_value(cache_control)?,
1827    );
1828    *remaining_cache_markers -= 1;
1829
1830    Ok(())
1831}
1832
1833fn apply_system_cache_control(
1834    system: &mut [SystemContent],
1835    remaining_cache_markers: &mut usize,
1836    cache_control_value: &CacheControl,
1837) {
1838    if *remaining_cache_markers == 0 {
1839        return;
1840    }
1841
1842    if let Some(SystemContent::Text { cache_control, .. }) = system.last_mut()
1843        && cache_control.is_none()
1844    {
1845        *cache_control = Some(cache_control_value.clone());
1846        *remaining_cache_markers -= 1;
1847    }
1848}
1849
1850fn clear_message_cache_control(messages: &mut [Message]) {
1851    for msg in messages.iter_mut() {
1852        for content in msg.content.iter_mut() {
1853            set_content_cache_control(content, None);
1854        }
1855    }
1856}
1857
1858fn apply_message_cache_control(
1859    messages: &mut [Message],
1860    remaining_cache_markers: &mut usize,
1861    cache_control: &CacheControl,
1862) {
1863    clear_message_cache_control(messages);
1864
1865    if *remaining_cache_markers == 0 {
1866        return;
1867    }
1868
1869    if let Some(last_msg) = messages.last_mut()
1870        && let Some(last_content) = last_msg.content.last_mut()
1871    {
1872        set_content_cache_control(last_content, Some(cache_control.clone()));
1873        *remaining_cache_markers -= 1;
1874    }
1875}
1876
1877pub(super) fn apply_prompt_cache_control(
1878    system: &mut [SystemContent],
1879    messages: &mut [Message],
1880    tools: &mut [serde_json::Value],
1881    prompt_caching: bool,
1882    static_prefix_cache_ttl: Option<&CacheTtl>,
1883    top_level_cache_control: Option<&CacheControl>,
1884) -> Result<(), EncodeError> {
1885    normalize_tool_cache_control(tools);
1886
1887    let max_cache_markers = if top_level_cache_control.is_some() {
1888        MAX_CACHE_CONTROL_MARKERS - 1
1889    } else {
1890        MAX_CACHE_CONTROL_MARKERS
1891    };
1892    let tool_cache_markers = tool_cache_control_count(tools);
1893
1894    if tool_cache_markers > max_cache_markers {
1895        return Err(EncodeError::request(format!(
1896            "Too many Anthropic tool `cache_control` markers: {tool_cache_markers} exceeds \
1897                 the available prompt caching budget of {max_cache_markers}"
1898        )));
1899    }
1900
1901    let mut remaining_cache_markers = max_cache_markers - tool_cache_markers;
1902
1903    // Diagnose conflicting builder settings before generic TTL-order validation.
1904    let top_level_ttl = top_level_cache_control_ttl(top_level_cache_control);
1905    if static_prefix_cache_ttl == Some(&CacheTtl::FiveMinutes)
1906        && top_level_ttl == Some(CacheTtl::OneHour)
1907    {
1908        return Err(EncodeError::request(
1909            "`with_static_prefix_cache_ttl(CacheTtl::FiveMinutes)` conflicts with the 1-hour \
1910             top-level cache TTL (`with_automatic_caching_1h` or a raw top-level \
1911             `cache_control`): Anthropic requires 1h markers to precede 5-minute ones, and the \
1912             static prefix precedes the conversation tail",
1913        ));
1914    }
1915
1916    // Manual prompt caching marks the prefix and the tail; a static-prefix TTL
1917    // alone marks just the prefix (the tail stays with the automatic/top-level
1918    // breakpoint, or uncached).
1919    if prompt_caching || static_prefix_cache_ttl.is_some() {
1920        let static_cache_control =
1921            build_cache_control(static_prefix_cache_ttl.cloned().or(top_level_ttl.clone()));
1922
1923        apply_tool_cache_control(tools, &mut remaining_cache_markers, &static_cache_control)?;
1924        apply_system_cache_control(system, &mut remaining_cache_markers, &static_cache_control);
1925    }
1926
1927    if prompt_caching {
1928        if top_level_cache_control.is_some() {
1929            clear_message_cache_control(messages);
1930        } else {
1931            let tail_cache_control = build_cache_control(top_level_ttl);
1932            apply_message_cache_control(
1933                messages,
1934                &mut remaining_cache_markers,
1935                &tail_cache_control,
1936            );
1937        }
1938    }
1939
1940    validate_cache_control_ttl_order(system, messages, tools, top_level_cache_control)?;
1941
1942    Ok(())
1943}
1944
1945pub(super) fn extract_top_level_cache_control(
1946    additional_params: &mut serde_json::Value,
1947) -> Result<Option<CacheControl>, EncodeError> {
1948    if let Some(map) = additional_params.as_object_mut()
1949        && let Some(raw_cache_control) = map.remove("cache_control")
1950    {
1951        if raw_cache_control.is_null() {
1952            return Ok(None);
1953        }
1954
1955        return serde_json::from_value::<CacheControl>(raw_cache_control)
1956            .map(Some)
1957            .map_err(|err| {
1958                EncodeError::request(format!(
1959                    "Invalid Anthropic `additional_params.cache_control` payload: {err}"
1960                ))
1961            });
1962    }
1963
1964    Ok(None)
1965}
1966
1967pub(super) fn resolve_top_level_cache_control(
1968    automatic_caching: bool,
1969    automatic_caching_ttl: Option<&CacheTtl>,
1970    additional_params: &mut serde_json::Value,
1971) -> Result<Option<CacheControl>, EncodeError> {
1972    let raw_cache_control = extract_top_level_cache_control(additional_params)?;
1973    let typed_cache_control = automatic_caching.then_some(CacheControl::Ephemeral {
1974        ttl: automatic_caching_ttl.cloned(),
1975    });
1976
1977    match (typed_cache_control, raw_cache_control) {
1978        (Some(typed_cache_control), Some(raw_cache_control)) => {
1979            if automatic_caching_ttl.is_some()
1980                && cache_control_ttl(&typed_cache_control) != cache_control_ttl(&raw_cache_control)
1981            {
1982                return Err(EncodeError::request(
1983                    "Anthropic `additional_params.cache_control` conflicts with the typed \
1984                     automatic caching TTL",
1985                ));
1986            }
1987
1988            Ok(Some(raw_cache_control))
1989        }
1990        (Some(typed_cache_control), None) => Ok(Some(typed_cache_control)),
1991        (None, raw_cache_control) => Ok(raw_cache_control),
1992    }
1993}
1994
1995/// Split `history` into the top-level `system` field and `messages`.
1996///
1997/// On a model that takes mid-conversation system messages, one in a position
1998/// Anthropic rejects (after an assistant turn, or before a user turn) moves to
1999/// the next valid slot: right after the next user turn that ends the array or
2000/// precedes an assistant turn. Hoisting it into `system` instead would change
2001/// the prompt prefix, which misses the cache from the first token and, on
2002/// models that bind thinking blocks to their conversation, turns every earlier
2003/// thinking block into a 400. It is hoisted only when no such slot exists.
2004pub(super) fn split_system_messages_from_history(
2005    history: &[message::Message],
2006    preserve_mid_conversation_system_messages: bool,
2007) -> (Vec<SystemContent>, Vec<message::Message>) {
2008    let mut system = Vec::new();
2009    let mut remaining = Vec::new();
2010    let mut deferred: Vec<String> = Vec::new();
2011
2012    for (index, message) in history.iter().enumerate() {
2013        match message {
2014            message::Message::System { content } => {
2015                if content.is_empty() {
2016                    continue;
2017                }
2018                if preserve_mid_conversation_system_messages {
2019                    if is_valid_mid_conversation_system_message(history, index) {
2020                        remaining.push(message.clone());
2021                        continue;
2022                    }
2023                    if index > 0 && next_system_message_slot(history, index).is_some() {
2024                        deferred.push(content.clone());
2025                        continue;
2026                    }
2027                }
2028                system.push(SystemContent::Text {
2029                    text: content.clone(),
2030                    cache_control: None,
2031                });
2032            }
2033            other => {
2034                remaining.push(other.clone());
2035                if !deferred.is_empty() && is_system_message_slot(history, index) {
2036                    remaining.push(message::Message::System {
2037                        content: std::mem::take(&mut deferred).join("\n\n"),
2038                    });
2039                }
2040            }
2041        }
2042    }
2043
2044    (system, remaining)
2045}
2046
2047/// The first index after `index` a misplaced system message can follow.
2048fn next_system_message_slot(history: &[message::Message], index: usize) -> Option<usize> {
2049    (index + 1..history.len()).find(|&slot| is_system_message_slot(history, slot))
2050}
2051
2052/// Whether a system message may sit right after `history[index]`: it is a
2053/// user turn, and what follows is the end of the array or an assistant turn.
2054fn is_system_message_slot(history: &[message::Message], index: usize) -> bool {
2055    matches!(history.get(index), Some(message::Message::User { .. }))
2056        && history
2057            .get(index + 1)
2058            .is_none_or(|message| matches!(message, message::Message::Assistant { .. }))
2059}
2060
2061fn is_valid_mid_conversation_system_message(history: &[message::Message], index: usize) -> bool {
2062    let follows_valid_turn = index > 0
2063        && history.get(index - 1).is_some_and(|message| {
2064            matches!(message, message::Message::User { .. })
2065                || assistant_ends_in_server_tool_block(message)
2066        });
2067    let is_last_or_precedes_assistant = history
2068        .get(index + 1)
2069        .is_none_or(|message| matches!(message, message::Message::Assistant { .. }));
2070
2071    follows_valid_turn && is_last_or_precedes_assistant
2072}
2073
2074fn assistant_ends_in_server_tool_block(message: &message::Message) -> bool {
2075    let message::Message::Assistant { content, .. } = message else {
2076        return false;
2077    };
2078
2079    let Some(message::AssistantContent::Text(text)) = content.iter().last() else {
2080        return false;
2081    };
2082
2083    let Some(raw_type) = text
2084        .additional_params
2085        .as_ref()
2086        .and_then(|params| params.get(ANTHROPIC_RAW_CONTENT_KEY))
2087        .and_then(|raw_content| raw_content.get("type"))
2088        .and_then(serde_json::Value::as_str)
2089    else {
2090        return false;
2091    };
2092
2093    matches!(
2094        raw_type,
2095        "server_tool_use" | "web_search_tool_result" | "code_execution_tool_result"
2096    )
2097}
2098
2099/// Parameters for building an AnthropicCompletionRequest
2100pub struct AnthropicRequestParams<'a> {
2101    pub model: &'a str,
2102    pub request: CompletionRequest,
2103    pub prompt_caching: bool,
2104    /// Add a top-level `cache_control` field for Anthropic's automatic caching mode.
2105    pub automatic_caching: bool,
2106    /// TTL for the top-level cache_control. `None` omits the `ttl` field (API default is 5 min).
2107    pub automatic_caching_ttl: Option<CacheTtl>,
2108    /// TTL for the static prefix (tools + system). `None` inherits the top-level TTL.
2109    pub static_prefix_cache_ttl: Option<CacheTtl>,
2110    /// The issuers whose reasoning the request replays.
2111    pub issuers: &'a [message::Issuer],
2112}
2113
2114impl AnthropicCompletionRequest {
2115    /// Build the typed request, optionally transforming generated tools with `strict`.
2116    /// Reject missing token limits, invalid message conversions, and cache conflicts.
2117    pub(super) fn try_from_params(
2118        params: AnthropicRequestParams<'_>,
2119        strict: Option<fn(&mut ToolDefinition)>,
2120    ) -> Result<Self, EncodeError> {
2121        let AnthropicRequestParams {
2122            model,
2123            request: mut req,
2124            prompt_caching,
2125            automatic_caching,
2126            automatic_caching_ttl,
2127            static_prefix_cache_ttl,
2128            issuers,
2129        } = params;
2130        let chat_history = req.chat_history_with_documents();
2131
2132        let Some(max_tokens) = req.max_tokens else {
2133            return Err(EncodeError::request(
2134                "`max_tokens` must be set for Anthropic",
2135            ));
2136        };
2137
2138        let (history_system, chat_history) = split_system_messages_from_history(
2139            &chat_history,
2140            supports_mid_conversation_system_messages(model),
2141        );
2142        let mut full_history = vec![];
2143        full_history.extend(chat_history);
2144
2145        let mut messages = full_history
2146            .iter()
2147            .cloned()
2148            .map(|message| Message::from_message(message, issuers))
2149            .collect::<Result<Vec<_>, _>>()?;
2150        // Server-tool references are preserved opaque content, not local calls.
2151        // Reserve their genuine handles so arbitrary local hints cannot alias them.
2152        let server_ids = messages
2153            .iter()
2154            .flat_map(|message| &message.content)
2155            .filter_map(|part| match part {
2156                Content::ServerToolUse { id, .. } => Some(id.clone()),
2157                Content::WebSearchToolResult { tool_use_id, .. }
2158                | Content::CodeExecutionToolResult { tool_use_id, .. } => Some(tool_use_id.clone()),
2159                _ => None,
2160            });
2161        let tool_ids =
2162            crate::providers::internal::wire_ids::WireIds::with_reserved(&full_history, server_ids);
2163        for (position, message) in messages.iter_mut().enumerate() {
2164            tool_ids
2165                .apply(
2166                    position,
2167                    message.content.iter_mut().filter_map(|part| match part {
2168                        Content::ToolUse { id, .. } => Some(id),
2169                        Content::ToolResult { tool_use_id, .. } => Some(tool_use_id),
2170                        _ => None,
2171                    }),
2172                )
2173                .map_err(EncodeError::request)?;
2174        }
2175
2176        let mut additional_params_payload = req
2177            .additional_params
2178            .take()
2179            .unwrap_or(serde_json::Value::Null);
2180        let top_level_cache_control = resolve_top_level_cache_control(
2181            automatic_caching,
2182            automatic_caching_ttl.as_ref(),
2183            &mut additional_params_payload,
2184        )?;
2185        let mut tools = build_tool_definitions(req.tools, &mut additional_params_payload, strict)?;
2186
2187        let mut system = history_system;
2188
2189        apply_prompt_cache_control(
2190            &mut system,
2191            &mut messages,
2192            &mut tools,
2193            prompt_caching,
2194            static_prefix_cache_ttl.as_ref(),
2195            top_level_cache_control.as_ref(),
2196        )?;
2197
2198        let output_config = if let Some(schema) = req.output_schema {
2199            let mut schema_value = schema.to_value();
2200            sanitize_schema(&mut schema_value);
2201            Some(OutputConfig {
2202                format: OutputFormat::JsonSchema {
2203                    schema: schema_value,
2204                },
2205            })
2206        } else {
2207            None
2208        };
2209
2210        Ok(Self {
2211            model: model.to_string(),
2212            messages,
2213            max_tokens,
2214            system,
2215            temperature: req.temperature,
2216            tool_choice: req.tool_choice.map(ToolChoice::try_from).transpose()?,
2217            tools,
2218            output_config,
2219            cache_control: top_level_cache_control,
2220            additional_params: if additional_params_payload.is_null() {
2221                None
2222            } else {
2223                Some(additional_params_payload)
2224            },
2225        })
2226    }
2227}
2228
2229impl TryFrom<AnthropicRequestParams<'_>> for AnthropicCompletionRequest {
2230    type Error = EncodeError;
2231
2232    fn try_from(params: AnthropicRequestParams<'_>) -> Result<Self, Self::Error> {
2233        Self::try_from_params(params, None)
2234    }
2235}
2236
2237pub(super) fn extract_tools_from_additional_params(
2238    additional_params: &mut serde_json::Value,
2239) -> Result<Vec<serde_json::Value>, EncodeError> {
2240    if let Some(map) = additional_params.as_object_mut()
2241        && let Some(raw_tools) = map.remove("tools")
2242    {
2243        return serde_json::from_value::<Vec<serde_json::Value>>(raw_tools).map_err(|err| {
2244            EncodeError::request(format!(
2245                "Invalid Anthropic `additional_params.tools` payload: {err}"
2246            ))
2247        });
2248    }
2249
2250    Ok(Vec::new())
2251}
2252
2253pub(super) fn build_tool_definitions(
2254    tools: Vec<completion::ToolDefinition>,
2255    additional_params_payload: &mut serde_json::Value,
2256    strict: Option<fn(&mut ToolDefinition)>,
2257) -> Result<Vec<serde_json::Value>, EncodeError> {
2258    let mut additional_tools = extract_tools_from_additional_params(additional_params_payload)?;
2259
2260    let mut tools = tools
2261        .into_iter()
2262        .map(|tool| {
2263            let input_schema = tool.parameters;
2264            let mut tool = ToolDefinition {
2265                name: tool.name,
2266                description: Some(tool.description),
2267                input_schema,
2268                strict: false,
2269                cache_control: None,
2270            };
2271            if let Some(strict) = strict {
2272                strict(&mut tool);
2273            }
2274
2275            tool
2276        })
2277        .map(serde_json::to_value)
2278        .collect::<Result<Vec<_>, _>>()?;
2279    tools.append(&mut additional_tools);
2280
2281    Ok(tools)
2282}
2283
2284#[cfg(test)]
2285mod tests;