Skip to main content

rig_core/providers/anthropic/
completion.rs

1//! Anthropic completion api implementation
2
3use crate::completion::CompletionRequest;
4use crate::providers::anthropic::streaming::StreamingCompletionResponse;
5use crate::{
6    OneOrMany,
7    client::Provider,
8    completion::{self, CompletionError, GetTokenUsage},
9    http_client::HttpClientExt,
10    message::{self, DocumentMediaType, DocumentSourceKind, MessageError, MimeType, Reasoning},
11    one_or_many::string_or_one_or_many,
12    telemetry::{ProviderResponseExt, SpanCombinator},
13    wasm_compat::*,
14};
15use bytes::Bytes;
16use serde::{Deserialize, Serialize};
17use std::{convert::Infallible, str::FromStr};
18use tracing::{Instrument, Level, enabled, info_span};
19
20// ================================================================
21// Anthropic Completion API
22// ================================================================
23
24/// `claude-opus-4-6` completion model
25pub const CLAUDE_OPUS_4_6: &str = "claude-opus-4-6";
26/// `claude-opus-4-7` completion model
27pub const CLAUDE_OPUS_4_7: &str = "claude-opus-4-7";
28/// `claude-sonnet-4-6` completion model
29pub const CLAUDE_SONNET_4_6: &str = "claude-sonnet-4-6";
30/// `claude-haiku-4-5` completion model
31pub const CLAUDE_HAIKU_4_5: &str = "claude-haiku-4-5";
32
33pub const ANTHROPIC_VERSION_2023_01_01: &str = "2023-01-01";
34pub const ANTHROPIC_VERSION_2023_06_01: &str = "2023-06-01";
35pub const ANTHROPIC_VERSION_LATEST: &str = ANTHROPIC_VERSION_2023_06_01;
36const EMPTY_RESPONSE_ERROR: &str = "Response contained no message or tool call (empty)";
37
38pub trait AnthropicCompatibleProvider: Provider {
39    const PROVIDER_NAME: &'static str;
40
41    fn default_max_tokens(model: &str) -> Option<u64> {
42        let _ = model;
43        None
44    }
45}
46
47impl AnthropicCompatibleProvider for super::client::AnthropicExt {
48    const PROVIDER_NAME: &'static str = "anthropic";
49
50    fn default_max_tokens(model: &str) -> Option<u64> {
51        default_max_tokens_for_model(model)
52    }
53}
54
55#[derive(Debug, Deserialize, Serialize)]
56pub struct CompletionResponse {
57    pub content: Vec<Content>,
58    pub id: String,
59    pub model: String,
60    pub role: String,
61    pub stop_reason: Option<String>,
62    pub stop_sequence: Option<String>,
63    pub usage: Usage,
64}
65
66impl ProviderResponseExt for CompletionResponse {
67    type OutputMessage = Content;
68    type Usage = Usage;
69
70    fn get_response_id(&self) -> Option<String> {
71        Some(self.id.to_owned())
72    }
73
74    fn get_response_model_name(&self) -> Option<String> {
75        Some(self.model.to_owned())
76    }
77
78    fn get_output_messages(&self) -> Vec<Self::OutputMessage> {
79        self.content.clone()
80    }
81
82    fn get_text_response(&self) -> Option<String> {
83        let res = self
84            .content
85            .iter()
86            .filter_map(|x| {
87                if let Content::Text { text, .. } = x {
88                    Some(text.to_owned())
89                } else {
90                    None
91                }
92            })
93            .collect::<Vec<String>>()
94            .join("\n");
95
96        if res.is_empty() { None } else { Some(res) }
97    }
98
99    fn get_usage(&self) -> Option<Self::Usage> {
100        Some(self.usage.clone())
101    }
102}
103
104#[derive(Clone, Debug, Deserialize, Serialize)]
105pub struct Usage {
106    pub input_tokens: u64,
107    pub cache_read_input_tokens: Option<u64>,
108    pub cache_creation_input_tokens: Option<u64>,
109    pub output_tokens: u64,
110}
111
112impl std::fmt::Display for Usage {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(
115            f,
116            "Input tokens: {}\nCache read input tokens: {}\nCache creation input tokens: {}\nOutput tokens: {}",
117            self.input_tokens,
118            match self.cache_read_input_tokens {
119                Some(token) => token.to_string(),
120                None => "n/a".to_string(),
121            },
122            match self.cache_creation_input_tokens {
123                Some(token) => token.to_string(),
124                None => "n/a".to_string(),
125            },
126            self.output_tokens
127        )
128    }
129}
130
131impl GetTokenUsage for Usage {
132    fn token_usage(&self) -> Option<crate::completion::Usage> {
133        let mut usage = crate::completion::Usage::new();
134
135        usage.input_tokens = self.input_tokens;
136        usage.output_tokens = self.output_tokens;
137        usage.cached_input_tokens = self.cache_read_input_tokens.unwrap_or_default();
138        usage.cache_creation_input_tokens = self.cache_creation_input_tokens.unwrap_or_default();
139        usage.total_tokens = self.input_tokens
140            + self.cache_read_input_tokens.unwrap_or_default()
141            + self.cache_creation_input_tokens.unwrap_or_default()
142            + self.output_tokens;
143
144        Some(usage)
145    }
146}
147
148#[derive(Debug, Deserialize, Serialize)]
149pub struct ToolDefinition {
150    pub name: String,
151    pub description: Option<String>,
152    pub input_schema: serde_json::Value,
153}
154
155/// TTL for a cache control breakpoint.
156///
157/// The Anthropic API supports two TTL values:
158/// - `"5m"` — 5 minutes (default when `ttl` is omitted)
159/// - `"1h"` — 1 hour (requires the `extended-cache-ttl-2025-04-11` beta header)
160#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
161pub enum CacheTtl {
162    /// 5-minute TTL (default).
163    #[default]
164    #[serde(rename = "5m")]
165    FiveMinutes,
166    /// 1-hour TTL. Requires the `extended-cache-ttl-2025-04-11` beta header.
167    #[serde(rename = "1h")]
168    OneHour,
169}
170
171/// Cache control directive for Anthropic prompt caching.
172///
173/// Serialises to `{"type":"ephemeral"}` (default TTL) or
174/// `{"type":"ephemeral","ttl":"1h"}` (extended TTL).
175#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
176#[serde(tag = "type", rename_all = "snake_case")]
177pub enum CacheControl {
178    Ephemeral {
179        /// Optional TTL. Defaults to `"5m"` when omitted.
180        #[serde(skip_serializing_if = "Option::is_none")]
181        ttl: Option<CacheTtl>,
182    },
183}
184
185impl CacheControl {
186    /// Create a cache control with the default 5-minute TTL.
187    pub fn ephemeral() -> Self {
188        Self::Ephemeral { ttl: None }
189    }
190
191    /// Create a cache control with a 1-hour TTL.
192    pub fn ephemeral_1h() -> Self {
193        Self::Ephemeral {
194            ttl: Some(CacheTtl::OneHour),
195        }
196    }
197}
198
199/// System message content block with optional cache control
200#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
201#[serde(tag = "type", rename_all = "snake_case")]
202pub enum SystemContent {
203    Text {
204        text: String,
205        #[serde(skip_serializing_if = "Option::is_none")]
206        cache_control: Option<CacheControl>,
207    },
208}
209
210impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
211    type Error = CompletionError;
212
213    fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
214        let content = response
215            .content
216            .iter()
217            .map(|content| content.clone().try_into())
218            .collect::<Result<Vec<_>, _>>()?;
219
220        let choice = if content.is_empty() {
221            // Anthropic documents empty `end_turn` responses after tool-result round trips.
222            // The generic completion response still requires at least one assistant item, so
223            // normalize that terminal no-op into the same empty-text sentinel used by streaming.
224            if response.stop_reason.as_deref() == Some("end_turn") {
225                OneOrMany::one(completion::AssistantContent::text(""))
226            } else {
227                return Err(CompletionError::ResponseError(
228                    EMPTY_RESPONSE_ERROR.to_owned(),
229                ));
230            }
231        } else {
232            OneOrMany::many(content)
233                .map_err(|_| CompletionError::ResponseError(EMPTY_RESPONSE_ERROR.to_owned()))?
234        };
235
236        let usage = completion::Usage {
237            input_tokens: response.usage.input_tokens,
238            output_tokens: response.usage.output_tokens,
239            total_tokens: response.usage.input_tokens
240                + response.usage.cache_read_input_tokens.unwrap_or(0)
241                + response.usage.cache_creation_input_tokens.unwrap_or(0)
242                + response.usage.output_tokens,
243            cached_input_tokens: response.usage.cache_read_input_tokens.unwrap_or(0),
244            cache_creation_input_tokens: response.usage.cache_creation_input_tokens.unwrap_or(0),
245            reasoning_tokens: 0,
246        };
247
248        Ok(completion::CompletionResponse {
249            choice,
250            usage,
251            raw_response: response,
252            message_id: None,
253        })
254    }
255}
256
257#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
258pub struct Message {
259    pub role: Role,
260    #[serde(deserialize_with = "string_or_one_or_many")]
261    pub content: OneOrMany<Content>,
262}
263
264#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
265#[serde(rename_all = "lowercase")]
266pub enum Role {
267    User,
268    Assistant,
269}
270
271#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
272#[serde(tag = "type", rename_all = "snake_case")]
273pub enum Content {
274    Text {
275        text: String,
276        #[serde(skip_serializing_if = "Option::is_none")]
277        cache_control: Option<CacheControl>,
278    },
279    Image {
280        source: ImageSource,
281        #[serde(skip_serializing_if = "Option::is_none")]
282        cache_control: Option<CacheControl>,
283    },
284    ToolUse {
285        id: String,
286        name: String,
287        input: serde_json::Value,
288    },
289    ToolResult {
290        tool_use_id: String,
291        #[serde(deserialize_with = "string_or_one_or_many")]
292        content: OneOrMany<ToolResultContent>,
293        #[serde(skip_serializing_if = "Option::is_none")]
294        is_error: Option<bool>,
295        #[serde(skip_serializing_if = "Option::is_none")]
296        cache_control: Option<CacheControl>,
297    },
298    Document {
299        source: DocumentSource,
300        #[serde(skip_serializing_if = "Option::is_none")]
301        cache_control: Option<CacheControl>,
302    },
303    Thinking {
304        thinking: String,
305        #[serde(skip_serializing_if = "Option::is_none")]
306        signature: Option<String>,
307    },
308    RedactedThinking {
309        data: String,
310    },
311}
312
313impl FromStr for Content {
314    type Err = Infallible;
315
316    fn from_str(s: &str) -> Result<Self, Self::Err> {
317        Ok(Content::Text {
318            text: s.to_owned(),
319            cache_control: None,
320        })
321    }
322}
323
324#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
325#[serde(tag = "type", rename_all = "snake_case")]
326pub enum ToolResultContent {
327    Text { text: String },
328    Image(ImageSource),
329}
330
331impl FromStr for ToolResultContent {
332    type Err = Infallible;
333
334    fn from_str(s: &str) -> Result<Self, Self::Err> {
335        Ok(ToolResultContent::Text { text: s.to_owned() })
336    }
337}
338
339/// The source of an image content block.
340///
341/// Anthropic supports two source types for images:
342/// - `Base64`: Base64-encoded image data with media type
343/// - `Url`: URL reference to an image
344///
345/// See: <https://docs.anthropic.com/en/api/messages>
346#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
347#[serde(tag = "type", rename_all = "snake_case")]
348pub enum ImageSource {
349    #[serde(rename = "base64")]
350    Base64 {
351        data: String,
352        media_type: ImageFormat,
353    },
354    #[serde(rename = "url")]
355    Url { url: String },
356}
357
358/// The source of a document content block.
359///
360/// Anthropic supports multiple source types for documents:
361/// - `Base64`: Base64-encoded document data (used for PDFs)
362/// - `Text`: Plain text document data
363/// - `Url`: URL reference to a document
364/// - `File`: Provider-side uploaded file reference from the Files API
365#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
366#[serde(tag = "type", rename_all = "snake_case")]
367pub enum DocumentSource {
368    Base64 {
369        data: String,
370        media_type: DocumentFormat,
371    },
372    Text {
373        data: String,
374        media_type: PlainTextMediaType,
375    },
376    Url {
377        url: String,
378    },
379    File {
380        file_id: String,
381    },
382}
383
384#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
385#[serde(rename_all = "lowercase")]
386pub enum ImageFormat {
387    #[serde(rename = "image/jpeg")]
388    JPEG,
389    #[serde(rename = "image/png")]
390    PNG,
391    #[serde(rename = "image/gif")]
392    GIF,
393    #[serde(rename = "image/webp")]
394    WEBP,
395}
396
397/// The media type for base64-encoded documents.
398///
399/// Used with the `DocumentSource::Base64` variant. Currently only PDF is supported
400/// for base64-encoded document sources.
401///
402/// See: <https://docs.anthropic.com/en/docs/build-with-claude/pdf-support>
403#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
404#[serde(rename_all = "lowercase")]
405pub enum DocumentFormat {
406    #[serde(rename = "application/pdf")]
407    PDF,
408}
409
410/// The media type for plain text document sources.
411///
412/// Used with the `DocumentSource::Text` variant.
413///
414/// See: <https://docs.anthropic.com/en/api/messages>
415#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
416pub enum PlainTextMediaType {
417    #[serde(rename = "text/plain")]
418    Plain,
419}
420
421#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
422#[serde(rename_all = "lowercase")]
423pub enum SourceType {
424    BASE64,
425    URL,
426    TEXT,
427}
428
429impl From<String> for Content {
430    fn from(text: String) -> Self {
431        Content::Text {
432            text,
433            cache_control: None,
434        }
435    }
436}
437
438impl From<String> for ToolResultContent {
439    fn from(text: String) -> Self {
440        ToolResultContent::Text { text }
441    }
442}
443
444impl TryFrom<message::ContentFormat> for SourceType {
445    type Error = MessageError;
446
447    fn try_from(format: message::ContentFormat) -> Result<Self, Self::Error> {
448        match format {
449            message::ContentFormat::Base64 => Ok(SourceType::BASE64),
450            message::ContentFormat::Url => Ok(SourceType::URL),
451            message::ContentFormat::String => Ok(SourceType::TEXT),
452        }
453    }
454}
455
456impl From<SourceType> for message::ContentFormat {
457    fn from(source_type: SourceType) -> Self {
458        match source_type {
459            SourceType::BASE64 => message::ContentFormat::Base64,
460            SourceType::URL => message::ContentFormat::Url,
461            SourceType::TEXT => message::ContentFormat::String,
462        }
463    }
464}
465
466impl TryFrom<message::ImageMediaType> for ImageFormat {
467    type Error = MessageError;
468
469    fn try_from(media_type: message::ImageMediaType) -> Result<Self, Self::Error> {
470        Ok(match media_type {
471            message::ImageMediaType::JPEG => ImageFormat::JPEG,
472            message::ImageMediaType::PNG => ImageFormat::PNG,
473            message::ImageMediaType::GIF => ImageFormat::GIF,
474            message::ImageMediaType::WEBP => ImageFormat::WEBP,
475            _ => {
476                return Err(MessageError::ConversionError(
477                    format!("Unsupported image media type: {media_type:?}").to_owned(),
478                ));
479            }
480        })
481    }
482}
483
484impl From<ImageFormat> for message::ImageMediaType {
485    fn from(format: ImageFormat) -> Self {
486        match format {
487            ImageFormat::JPEG => message::ImageMediaType::JPEG,
488            ImageFormat::PNG => message::ImageMediaType::PNG,
489            ImageFormat::GIF => message::ImageMediaType::GIF,
490            ImageFormat::WEBP => message::ImageMediaType::WEBP,
491        }
492    }
493}
494
495impl TryFrom<DocumentMediaType> for DocumentFormat {
496    type Error = MessageError;
497    fn try_from(value: DocumentMediaType) -> Result<Self, Self::Error> {
498        match value {
499            DocumentMediaType::PDF => Ok(DocumentFormat::PDF),
500            other => Err(MessageError::ConversionError(format!(
501                "DocumentFormat only supports PDF for base64 sources, got: {}",
502                other.to_mime_type()
503            ))),
504        }
505    }
506}
507
508impl TryFrom<message::AssistantContent> for Content {
509    type Error = MessageError;
510    fn try_from(text: message::AssistantContent) -> Result<Self, Self::Error> {
511        match text {
512            message::AssistantContent::Text(message::Text { text }) => Ok(Content::Text {
513                text,
514                cache_control: None,
515            }),
516            message::AssistantContent::Image(_) => Err(MessageError::ConversionError(
517                "Anthropic currently doesn't support images.".to_string(),
518            )),
519            message::AssistantContent::ToolCall(message::ToolCall { id, function, .. }) => {
520                Ok(Content::ToolUse {
521                    id,
522                    name: function.name,
523                    input: function.arguments,
524                })
525            }
526            message::AssistantContent::Reasoning(reasoning) => Ok(Content::Thinking {
527                thinking: reasoning.display_text(),
528                signature: reasoning.first_signature().map(str::to_owned),
529            }),
530        }
531    }
532}
533
534fn anthropic_content_from_assistant_content(
535    content: message::AssistantContent,
536) -> Result<Vec<Content>, MessageError> {
537    match content {
538        message::AssistantContent::Text(message::Text { text }) => Ok(vec![Content::Text {
539            text,
540            cache_control: None,
541        }]),
542        message::AssistantContent::Image(_) => Err(MessageError::ConversionError(
543            "Anthropic currently doesn't support images.".to_string(),
544        )),
545        message::AssistantContent::ToolCall(message::ToolCall { id, function, .. }) => {
546            Ok(vec![Content::ToolUse {
547                id,
548                name: function.name,
549                input: function.arguments,
550            }])
551        }
552        message::AssistantContent::Reasoning(reasoning) => {
553            let mut converted = Vec::new();
554            for block in reasoning.content {
555                match block {
556                    message::ReasoningContent::Text { text, signature } => {
557                        converted.push(Content::Thinking {
558                            thinking: text,
559                            signature,
560                        });
561                    }
562                    message::ReasoningContent::Summary(summary) => {
563                        converted.push(Content::Thinking {
564                            thinking: summary,
565                            signature: None,
566                        });
567                    }
568                    message::ReasoningContent::Redacted { data }
569                    | message::ReasoningContent::Encrypted(data) => {
570                        converted.push(Content::RedactedThinking { data });
571                    }
572                }
573            }
574
575            if converted.is_empty() {
576                return Err(MessageError::ConversionError(
577                    "Cannot convert empty reasoning content to Anthropic format".to_string(),
578                ));
579            }
580
581            Ok(converted)
582        }
583    }
584}
585
586impl TryFrom<message::Message> for Message {
587    type Error = MessageError;
588
589    fn try_from(message: message::Message) -> Result<Self, Self::Error> {
590        Ok(match message {
591            message::Message::User { content } => Message {
592                role: Role::User,
593                content: content.try_map(|content| match content {
594                    message::UserContent::Text(message::Text { text }) => Ok(Content::Text {
595                        text,
596                        cache_control: None,
597                    }),
598                    message::UserContent::ToolResult(message::ToolResult {
599                        id, content, ..
600                    }) => Ok(Content::ToolResult {
601                        tool_use_id: id,
602                        content: content.try_map(|content| match content {
603                            message::ToolResultContent::Text(message::Text { text }) => {
604                                Ok(ToolResultContent::Text { text })
605                            }
606                            message::ToolResultContent::Image(image) => {
607                                let DocumentSourceKind::Base64(data) = image.data else {
608                                    return Err(MessageError::ConversionError(
609                                        "Only base64 strings can be used with the Anthropic API"
610                                            .to_string(),
611                                    ));
612                                };
613                                let media_type =
614                                    image.media_type.ok_or(MessageError::ConversionError(
615                                        "Image media type is required".to_owned(),
616                                    ))?;
617                                Ok(ToolResultContent::Image(ImageSource::Base64 {
618                                    data,
619                                    media_type: media_type.try_into()?,
620                                }))
621                            }
622                        })?,
623                        is_error: None,
624                        cache_control: None,
625                    }),
626                    message::UserContent::Image(message::Image {
627                        data, media_type, ..
628                    }) => {
629                        let source = match data {
630                            DocumentSourceKind::Base64(data) => {
631                                let media_type =
632                                    media_type.ok_or(MessageError::ConversionError(
633                                        "Image media type is required for Claude API".to_string(),
634                                    ))?;
635                                ImageSource::Base64 {
636                                    data,
637                                    media_type: ImageFormat::try_from(media_type)?,
638                                }
639                            }
640                            DocumentSourceKind::Url(url) => ImageSource::Url { url },
641                            DocumentSourceKind::Unknown => {
642                                return Err(MessageError::ConversionError(
643                                    "Image content has no body".into(),
644                                ));
645                            }
646                            doc => {
647                                return Err(MessageError::ConversionError(format!(
648                                    "Unsupported document type: {doc:?}"
649                                )));
650                            }
651                        };
652
653                        Ok(Content::Image {
654                            source,
655                            cache_control: None,
656                        })
657                    }
658                    message::UserContent::Document(message::Document {
659                        data, media_type, ..
660                    }) => {
661                        if let DocumentSourceKind::FileId(file_id) = data {
662                            return Ok(Content::Document {
663                                source: DocumentSource::File { file_id },
664                                cache_control: None,
665                            });
666                        }
667
668                        let media_type = media_type.ok_or(MessageError::ConversionError(
669                            "Document media type is required".to_string(),
670                        ))?;
671
672                        let source = match media_type {
673                            DocumentMediaType::PDF => {
674                                let data = match data {
675                                    DocumentSourceKind::Base64(data)
676                                    | DocumentSourceKind::String(data) => data,
677                                    _ => {
678                                        return Err(MessageError::ConversionError(
679                                            "Only base64 encoded data is supported for PDF documents".into(),
680                                        ));
681                                    }
682                                };
683                                DocumentSource::Base64 {
684                                    data,
685                                    media_type: DocumentFormat::PDF,
686                                }
687                            }
688                            DocumentMediaType::TXT => {
689                                let data = match data {
690                                    DocumentSourceKind::String(data)
691                                    | DocumentSourceKind::Base64(data) => data,
692                                    _ => {
693                                        return Err(MessageError::ConversionError(
694                                            "Only string or base64 data is supported for plain text documents".into(),
695                                        ));
696                                    }
697                                };
698                                DocumentSource::Text {
699                                    data,
700                                    media_type: PlainTextMediaType::Plain,
701                                }
702                            }
703                            other => {
704                                return Err(MessageError::ConversionError(format!(
705                                    "Anthropic only supports PDF and plain text documents, got: {}",
706                                    other.to_mime_type()
707                                )));
708                            }
709                        };
710
711                        Ok(Content::Document {
712                            source,
713                            cache_control: None,
714                        })
715                    }
716                    message::UserContent::Audio { .. } => Err(MessageError::ConversionError(
717                        "Audio is not supported in Anthropic".to_owned(),
718                    )),
719                    message::UserContent::Video { .. } => Err(MessageError::ConversionError(
720                        "Video is not supported in Anthropic".to_owned(),
721                    )),
722                })?,
723            },
724
725            message::Message::System { content } => Message {
726                role: Role::User,
727                content: OneOrMany::one(Content::Text {
728                    text: content,
729                    cache_control: None,
730                }),
731            },
732
733            message::Message::Assistant { content, .. } => {
734                let converted_content = content.into_iter().try_fold(
735                    Vec::new(),
736                    |mut accumulated, assistant_content| {
737                        accumulated
738                            .extend(anthropic_content_from_assistant_content(assistant_content)?);
739                        Ok::<Vec<Content>, MessageError>(accumulated)
740                    },
741                )?;
742
743                Message {
744                    content: OneOrMany::many(converted_content).map_err(|_| {
745                        MessageError::ConversionError(
746                            "Assistant message did not contain Anthropic-compatible content"
747                                .to_owned(),
748                        )
749                    })?,
750                    role: Role::Assistant,
751                }
752            }
753        })
754    }
755}
756
757impl TryFrom<Content> for message::AssistantContent {
758    type Error = MessageError;
759
760    fn try_from(content: Content) -> Result<Self, Self::Error> {
761        Ok(match content {
762            Content::Text { text, .. } => message::AssistantContent::text(text),
763            Content::ToolUse { id, name, input } => {
764                message::AssistantContent::tool_call(id, name, input)
765            }
766            Content::Thinking {
767                thinking,
768                signature,
769            } => message::AssistantContent::Reasoning(Reasoning::new_with_signature(
770                &thinking, signature,
771            )),
772            Content::RedactedThinking { data } => {
773                message::AssistantContent::Reasoning(Reasoning::redacted(data))
774            }
775            _ => {
776                return Err(MessageError::ConversionError(
777                    "Content did not contain a message, tool call, or reasoning".to_owned(),
778                ));
779            }
780        })
781    }
782}
783
784impl From<ToolResultContent> for message::ToolResultContent {
785    fn from(content: ToolResultContent) -> Self {
786        match content {
787            ToolResultContent::Text { text } => message::ToolResultContent::text(text),
788            ToolResultContent::Image(source) => match source {
789                ImageSource::Base64 { data, media_type } => {
790                    message::ToolResultContent::image_base64(data, Some(media_type.into()), None)
791                }
792                ImageSource::Url { url } => message::ToolResultContent::image_url(url, None, None),
793            },
794        }
795    }
796}
797
798impl TryFrom<Message> for message::Message {
799    type Error = MessageError;
800
801    fn try_from(message: Message) -> Result<Self, Self::Error> {
802        Ok(match message.role {
803            Role::User => message::Message::User {
804                content: message.content.try_map(|content| {
805                    Ok(match content {
806                        Content::Text { text, .. } => message::UserContent::text(text),
807                        Content::ToolResult {
808                            tool_use_id,
809                            content,
810                            ..
811                        } => message::UserContent::tool_result(
812                            tool_use_id,
813                            content.map(|content| content.into()),
814                        ),
815                        Content::Image { source, .. } => match source {
816                            ImageSource::Base64 { data, media_type } => {
817                                message::UserContent::Image(message::Image {
818                                    data: DocumentSourceKind::Base64(data),
819                                    media_type: Some(media_type.into()),
820                                    detail: None,
821                                    additional_params: None,
822                                })
823                            }
824                            ImageSource::Url { url } => {
825                                message::UserContent::Image(message::Image {
826                                    data: DocumentSourceKind::Url(url),
827                                    media_type: None,
828                                    detail: None,
829                                    additional_params: None,
830                                })
831                            }
832                        },
833                        Content::Document { source, .. } => match source {
834                            DocumentSource::Base64 { data, media_type } => {
835                                let rig_media_type = match media_type {
836                                    DocumentFormat::PDF => message::DocumentMediaType::PDF,
837                                };
838                                message::UserContent::document(data, Some(rig_media_type))
839                            }
840                            DocumentSource::Text { data, .. } => message::UserContent::document(
841                                data,
842                                Some(message::DocumentMediaType::TXT),
843                            ),
844                            DocumentSource::Url { url } => {
845                                message::UserContent::document_url(url, None)
846                            }
847                            DocumentSource::File { file_id } => {
848                                message::UserContent::Document(message::Document {
849                                    data: DocumentSourceKind::FileId(file_id),
850                                    media_type: None,
851                                    additional_params: None,
852                                })
853                            }
854                        },
855                        _ => {
856                            return Err(MessageError::ConversionError(
857                                "Unsupported content type for User role".to_owned(),
858                            ));
859                        }
860                    })
861                })?,
862            },
863            Role::Assistant => message::Message::Assistant {
864                id: None,
865                content: message.content.try_map(|content| content.try_into())?,
866            },
867        })
868    }
869}
870
871#[doc(hidden)]
872#[derive(Clone)]
873pub struct GenericCompletionModel<Ext = super::client::AnthropicExt, T = reqwest::Client> {
874    pub(crate) client: crate::client::Client<Ext, T>,
875    pub model: String,
876    pub default_max_tokens: Option<u64>,
877    /// Enable automatic prompt caching (adds cache_control breakpoints to system prompt and messages)
878    pub prompt_caching: bool,
879    /// Enable Anthropic's automatic prompt caching (adds a top-level `cache_control` field to the
880    /// request). The API automatically places the breakpoint on the last cacheable block and moves
881    /// it forward as the conversation grows. No beta header is required.
882    pub automatic_caching: bool,
883    /// TTL for automatic caching. `None` uses the API default (5 minutes).
884    /// Set to `Some(CacheTtl::OneHour)` for a 1-hour TTL (requires the
885    /// `extended-cache-ttl-2025-04-11` beta header).
886    pub automatic_caching_ttl: Option<CacheTtl>,
887}
888
889/// Anthropic completion model.
890///
891/// This preserves the historical public generic shape where the first generic
892/// parameter is the HTTP client type.
893pub type CompletionModel<T = reqwest::Client> =
894    GenericCompletionModel<super::client::AnthropicExt, T>;
895
896impl<Ext, T> GenericCompletionModel<Ext, T>
897where
898    T: HttpClientExt,
899    Ext: AnthropicCompatibleProvider + Clone + 'static,
900{
901    pub fn new(client: crate::client::Client<Ext, T>, model: impl Into<String>) -> Self {
902        let model = model.into();
903        let default_max_tokens = Ext::default_max_tokens(&model);
904
905        Self {
906            client,
907            model,
908            default_max_tokens,
909            prompt_caching: false,
910            automatic_caching: false,
911            automatic_caching_ttl: None,
912        }
913    }
914
915    pub fn with_model(client: crate::client::Client<Ext, T>, model: &str) -> Self {
916        Self {
917            client,
918            model: model.to_string(),
919            default_max_tokens: Ext::default_max_tokens(model)
920                .or_else(|| Some(default_max_tokens_with_fallback(model))),
921            prompt_caching: false,
922            automatic_caching: false,
923            automatic_caching_ttl: None,
924        }
925    }
926
927    /// Enable automatic prompt caching.
928    ///
929    /// When enabled, cache_control breakpoints are automatically added to:
930    /// - The system prompt (marked with ephemeral cache)
931    /// - The last content block of the last message (marked with ephemeral cache)
932    ///
933    /// This allows Anthropic to cache the conversation history for cost savings.
934    pub fn with_prompt_caching(mut self) -> Self {
935        self.prompt_caching = true;
936        self
937    }
938
939    /// Enable Anthropic's automatic prompt caching.
940    ///
941    /// When enabled, a top-level `cache_control: { "type": "ephemeral" }` field is added to every
942    /// request. Anthropic's API automatically applies the cache breakpoint to the last cacheable
943    /// block and moves it forward as the conversation grows — no beta header and no manual
944    /// breakpoint management are required.
945    ///
946    /// This is the recommended approach for multi-turn conversations. Use [`with_prompt_caching`]
947    /// instead when you need fine-grained, per-block control over what is cached.
948    ///
949    /// To use a one-hour TTL instead of the default five minutes, pass `ttl: "1h"` via
950    /// `additional_params` or combine with an explicit block-level breakpoint that carries the
951    /// extended TTL.
952    ///
953    /// ```ignore
954    /// let model = client.completion_model(anthropic::completion::CLAUDE_SONNET_4_6)
955    ///     .with_automatic_caching();
956    /// ```
957    ///
958    /// ## Minimum cacheable prompt length
959    ///
960    /// The combined prompt (tools + system + messages up to the automatically chosen breakpoint)
961    /// must meet the model-specific minimum or caching is silently skipped by the API:
962    ///
963    /// | Model | Minimum tokens |
964    /// |-------|---------------|
965    /// | `claude-opus-4-7`, `claude-opus-4-6`, `claude-opus-4-5` | 4 096 |
966    /// | `claude-sonnet-4-6` | 2 048 |
967    /// | `claude-sonnet-4-5`, `claude-opus-4-1`, `claude-opus-4`, `claude-sonnet-4` | 1 024 |
968    /// | `claude-haiku-4-5` | 4 096 |
969    ///
970    /// [`with_prompt_caching`]: CompletionModel::with_prompt_caching
971    pub fn with_automatic_caching(mut self) -> Self {
972        self.automatic_caching = true;
973        self
974    }
975
976    /// Enable Anthropic's automatic prompt caching with a 1-hour TTL.
977    ///
978    /// Identical to [`with_automatic_caching`] but sets `ttl: "1h"` on the
979    /// top-level `cache_control` field. Requires the
980    /// `extended-cache-ttl-2025-04-11` beta header to be sent with the client:
981    ///
982    /// ```ignore
983    /// let client = anthropic::Client::builder()
984    ///     .api_key(std::env::var("ANTHROPIC_API_KEY").unwrap())
985    ///     .anthropic_beta("extended-cache-ttl-2025-04-11")
986    ///     .build()?;
987    /// let model = client.completion_model(anthropic::completion::CLAUDE_SONNET_4_6)
988    ///     .with_automatic_caching_1h();
989    /// ```
990    ///
991    /// [`with_automatic_caching`]: CompletionModel::with_automatic_caching
992    pub fn with_automatic_caching_1h(mut self) -> Self {
993        self.automatic_caching = true;
994        self.automatic_caching_ttl = Some(CacheTtl::OneHour);
995        self
996    }
997}
998
999/// Anthropic requires a `max_tokens` parameter to be set, which is dependent on the model. If not
1000/// set or if set too high, the request will fail. The following values are based on Anthropic's
1001/// published synchronous Messages API output limits for current models.
1002fn default_max_tokens_for_model(model: &str) -> Option<u64> {
1003    if model.starts_with("claude-opus-4-7") || model.starts_with("claude-opus-4-6") {
1004        Some(128_000)
1005    } else if model.starts_with("claude-opus-4")
1006        || model.starts_with("claude-sonnet-4")
1007        || model.starts_with("claude-haiku-4-5")
1008    {
1009        Some(64_000)
1010    } else {
1011        None
1012    }
1013}
1014
1015fn default_max_tokens_with_fallback(model: &str) -> u64 {
1016    default_max_tokens_for_model(model).unwrap_or(2_048)
1017}
1018
1019#[derive(Debug, Deserialize, Serialize)]
1020pub struct Metadata {
1021    user_id: Option<String>,
1022}
1023
1024#[derive(Default, Debug, Serialize, Deserialize)]
1025#[serde(tag = "type", rename_all = "snake_case")]
1026pub enum ToolChoice {
1027    #[default]
1028    Auto,
1029    Any,
1030    None,
1031    Tool {
1032        name: String,
1033    },
1034}
1035impl TryFrom<message::ToolChoice> for ToolChoice {
1036    type Error = CompletionError;
1037
1038    fn try_from(value: message::ToolChoice) -> Result<Self, Self::Error> {
1039        let res = match value {
1040            message::ToolChoice::Auto => Self::Auto,
1041            message::ToolChoice::None => Self::None,
1042            message::ToolChoice::Required => Self::Any,
1043            message::ToolChoice::Specific { function_names } => {
1044                if function_names.len() != 1 {
1045                    return Err(CompletionError::ProviderError(
1046                        "Only one tool may be specified to be used by Claude".into(),
1047                    ));
1048                }
1049
1050                let Some(name) = function_names.into_iter().next() else {
1051                    return Err(CompletionError::ProviderError(
1052                        "Only one tool may be specified to be used by Claude".into(),
1053                    ));
1054                };
1055
1056                Self::Tool { name }
1057            }
1058        };
1059
1060        Ok(res)
1061    }
1062}
1063
1064/// Recursively ensures all object schemas respect Anthropic structured output restrictions:
1065/// - `additionalProperties` must be explicitly set to `false` on every object
1066/// - All properties must be listed in `required`
1067///
1068/// Source: <https://docs.anthropic.com/en/docs/build-with-claude/structured-outputs#json-schema-limitations>
1069fn sanitize_schema(schema: &mut serde_json::Value) {
1070    use serde_json::Value;
1071
1072    if let Value::Object(obj) = schema {
1073        let is_object_schema = obj.get("type") == Some(&Value::String("object".to_string()))
1074            || obj.contains_key("properties");
1075
1076        if is_object_schema && !obj.contains_key("additionalProperties") {
1077            obj.insert("additionalProperties".to_string(), Value::Bool(false));
1078        }
1079
1080        if let Some(Value::Object(properties)) = obj.get("properties") {
1081            let prop_keys = properties.keys().cloned().map(Value::String).collect();
1082            obj.insert("required".to_string(), Value::Array(prop_keys));
1083        }
1084
1085        // Anthropic does not support numerical constraints on integer/number types.
1086        let is_numeric_schema = obj.get("type") == Some(&Value::String("integer".to_string()))
1087            || obj.get("type") == Some(&Value::String("number".to_string()));
1088
1089        if is_numeric_schema {
1090            for key in [
1091                "minimum",
1092                "maximum",
1093                "exclusiveMinimum",
1094                "exclusiveMaximum",
1095                "multipleOf",
1096            ] {
1097                obj.remove(key);
1098            }
1099        }
1100
1101        if let Some(defs) = obj.get_mut("$defs")
1102            && let Value::Object(defs_obj) = defs
1103        {
1104            for (_, def_schema) in defs_obj.iter_mut() {
1105                sanitize_schema(def_schema);
1106            }
1107        }
1108
1109        if let Some(properties) = obj.get_mut("properties")
1110            && let Value::Object(props) = properties
1111        {
1112            for (_, prop_value) in props.iter_mut() {
1113                sanitize_schema(prop_value);
1114            }
1115        }
1116
1117        if let Some(items) = obj.get_mut("items") {
1118            sanitize_schema(items);
1119        }
1120
1121        // Anthropic doesn't support oneOf, convert to anyOf
1122        if let Some(one_of) = obj.remove("oneOf") {
1123            match obj.get_mut("anyOf") {
1124                Some(Value::Array(existing)) => {
1125                    if let Value::Array(mut incoming) = one_of {
1126                        existing.append(&mut incoming);
1127                    }
1128                }
1129                _ => {
1130                    obj.insert("anyOf".to_string(), one_of);
1131                }
1132            }
1133        }
1134
1135        for key in ["anyOf", "allOf"] {
1136            if let Some(variants) = obj.get_mut(key)
1137                && let Value::Array(variants_array) = variants
1138            {
1139                for variant in variants_array.iter_mut() {
1140                    sanitize_schema(variant);
1141                }
1142            }
1143        }
1144    }
1145}
1146
1147/// Output format specifier for Anthropic's structured output.
1148/// Source: <https://docs.anthropic.com/en/api/messages>
1149#[derive(Debug, Deserialize, Serialize)]
1150#[serde(tag = "type", rename_all = "snake_case")]
1151enum OutputFormat {
1152    /// Constrains the model's response to conform to the provided JSON schema.
1153    JsonSchema { schema: serde_json::Value },
1154}
1155
1156/// Configuration for the model's output format.
1157#[derive(Debug, Deserialize, Serialize)]
1158struct OutputConfig {
1159    format: OutputFormat,
1160}
1161
1162#[derive(Debug, Deserialize, Serialize)]
1163struct AnthropicCompletionRequest {
1164    model: String,
1165    messages: Vec<Message>,
1166    max_tokens: u64,
1167    /// System prompt as array of content blocks to support cache_control
1168    #[serde(skip_serializing_if = "Vec::is_empty")]
1169    system: Vec<SystemContent>,
1170    #[serde(skip_serializing_if = "Option::is_none")]
1171    temperature: Option<f64>,
1172    #[serde(skip_serializing_if = "Option::is_none")]
1173    tool_choice: Option<ToolChoice>,
1174    #[serde(skip_serializing_if = "Vec::is_empty")]
1175    tools: Vec<serde_json::Value>,
1176    #[serde(skip_serializing_if = "Option::is_none")]
1177    output_config: Option<OutputConfig>,
1178    #[serde(flatten, skip_serializing_if = "Option::is_none")]
1179    additional_params: Option<serde_json::Value>,
1180    /// Top-level cache_control for Anthropic's automatic caching mode. When set, the API
1181    /// automatically places the cache breakpoint on the last cacheable block and advances it as
1182    /// the conversation grows. No beta header is required.
1183    #[serde(skip_serializing_if = "Option::is_none")]
1184    cache_control: Option<CacheControl>,
1185}
1186
1187/// Helper to set cache_control on a Content block
1188fn set_content_cache_control(content: &mut Content, value: Option<CacheControl>) {
1189    match content {
1190        Content::Text { cache_control, .. } => *cache_control = value,
1191        Content::Image { cache_control, .. } => *cache_control = value,
1192        Content::ToolResult { cache_control, .. } => *cache_control = value,
1193        Content::Document { cache_control, .. } => *cache_control = value,
1194        _ => {}
1195    }
1196}
1197
1198/// Apply cache control breakpoints to system prompt and messages.
1199/// Strategy: cache the system prompt, and mark the last content block of the last message
1200/// for caching. This allows the conversation history to be cached while new messages
1201/// are added.
1202pub fn apply_cache_control(system: &mut [SystemContent], messages: &mut [Message]) {
1203    // Add cache_control to the system prompt (if non-empty)
1204    if let Some(SystemContent::Text { cache_control, .. }) = system.last_mut() {
1205        *cache_control = Some(CacheControl::ephemeral());
1206    }
1207
1208    // Clear any existing cache_control from all message content blocks
1209    for msg in messages.iter_mut() {
1210        for content in msg.content.iter_mut() {
1211            set_content_cache_control(content, None);
1212        }
1213    }
1214
1215    // Add cache_control to the last content block of the last message
1216    if let Some(last_msg) = messages.last_mut() {
1217        set_content_cache_control(last_msg.content.last_mut(), Some(CacheControl::ephemeral()));
1218    }
1219}
1220
1221pub(super) fn split_system_messages_from_history(
1222    history: Vec<message::Message>,
1223) -> (Vec<SystemContent>, Vec<message::Message>) {
1224    let mut system = Vec::new();
1225    let mut remaining = Vec::new();
1226
1227    for message in history {
1228        match message {
1229            message::Message::System { content } => {
1230                if !content.is_empty() {
1231                    system.push(SystemContent::Text {
1232                        text: content,
1233                        cache_control: None,
1234                    });
1235                }
1236            }
1237            other => remaining.push(other),
1238        }
1239    }
1240
1241    (system, remaining)
1242}
1243
1244/// Parameters for building an AnthropicCompletionRequest
1245pub struct AnthropicRequestParams<'a> {
1246    pub model: &'a str,
1247    pub request: CompletionRequest,
1248    pub prompt_caching: bool,
1249    /// Add a top-level `cache_control` field for Anthropic's automatic caching mode.
1250    pub automatic_caching: bool,
1251    /// TTL for the top-level cache_control. `None` omits the `ttl` field (API default is 5 min).
1252    pub automatic_caching_ttl: Option<CacheTtl>,
1253}
1254
1255impl TryFrom<AnthropicRequestParams<'_>> for AnthropicCompletionRequest {
1256    type Error = CompletionError;
1257
1258    fn try_from(params: AnthropicRequestParams<'_>) -> Result<Self, Self::Error> {
1259        let AnthropicRequestParams {
1260            model,
1261            request: mut req,
1262            prompt_caching,
1263            automatic_caching,
1264            automatic_caching_ttl,
1265        } = params;
1266
1267        // Check if max_tokens is set, required for Anthropic
1268        let Some(max_tokens) = req.max_tokens else {
1269            return Err(CompletionError::RequestError(
1270                "`max_tokens` must be set for Anthropic".into(),
1271            ));
1272        };
1273
1274        let mut full_history = vec![];
1275        if let Some(docs) = req.normalized_documents() {
1276            full_history.push(docs);
1277        }
1278        full_history.extend(req.chat_history);
1279        let (history_system, full_history) = split_system_messages_from_history(full_history);
1280
1281        let mut messages = full_history
1282            .into_iter()
1283            .map(Message::try_from)
1284            .collect::<Result<Vec<Message>, _>>()?;
1285
1286        let mut additional_params_payload = req
1287            .additional_params
1288            .take()
1289            .unwrap_or(serde_json::Value::Null);
1290        let mut additional_tools =
1291            extract_tools_from_additional_params(&mut additional_params_payload)?;
1292
1293        let mut tools = req
1294            .tools
1295            .into_iter()
1296            .map(|tool| ToolDefinition {
1297                name: tool.name,
1298                description: Some(tool.description),
1299                input_schema: tool.parameters,
1300            })
1301            .map(serde_json::to_value)
1302            .collect::<Result<Vec<_>, _>>()?;
1303        tools.append(&mut additional_tools);
1304
1305        // Convert system prompt to array format for cache_control support
1306        let mut system = if let Some(preamble) = req.preamble {
1307            if preamble.is_empty() {
1308                vec![]
1309            } else {
1310                vec![SystemContent::Text {
1311                    text: preamble,
1312                    cache_control: None,
1313                }]
1314            }
1315        } else {
1316            vec![]
1317        };
1318        system.extend(history_system);
1319
1320        // Apply cache control breakpoints only if prompt_caching is enabled
1321        if prompt_caching {
1322            apply_cache_control(&mut system, &mut messages);
1323        }
1324
1325        let output_config = if let Some(schema) = req.output_schema {
1326            let mut schema_value = schema.to_value();
1327            sanitize_schema(&mut schema_value);
1328            Some(OutputConfig {
1329                format: OutputFormat::JsonSchema {
1330                    schema: schema_value,
1331                },
1332            })
1333        } else {
1334            None
1335        };
1336
1337        Ok(Self {
1338            model: model.to_string(),
1339            messages,
1340            max_tokens,
1341            system,
1342            temperature: req.temperature,
1343            tool_choice: req.tool_choice.and_then(|x| ToolChoice::try_from(x).ok()),
1344            tools,
1345            output_config,
1346            // Automatic caching: one top-level field; the API moves the breakpoint automatically.
1347            cache_control: if automatic_caching {
1348                Some(CacheControl::Ephemeral {
1349                    ttl: automatic_caching_ttl,
1350                })
1351            } else {
1352                None
1353            },
1354            additional_params: if additional_params_payload.is_null() {
1355                None
1356            } else {
1357                Some(additional_params_payload)
1358            },
1359        })
1360    }
1361}
1362
1363fn extract_tools_from_additional_params(
1364    additional_params: &mut serde_json::Value,
1365) -> Result<Vec<serde_json::Value>, CompletionError> {
1366    if let Some(map) = additional_params.as_object_mut()
1367        && let Some(raw_tools) = map.remove("tools")
1368    {
1369        return serde_json::from_value::<Vec<serde_json::Value>>(raw_tools).map_err(|err| {
1370            CompletionError::RequestError(
1371                format!("Invalid Anthropic `additional_params.tools` payload: {err}").into(),
1372            )
1373        });
1374    }
1375
1376    Ok(Vec::new())
1377}
1378
1379impl<Ext, T> completion::CompletionModel for GenericCompletionModel<Ext, T>
1380where
1381    T: HttpClientExt + Clone + Default + WasmCompatSend + WasmCompatSync + 'static,
1382    Ext: AnthropicCompatibleProvider + Clone + WasmCompatSend + WasmCompatSync + 'static,
1383{
1384    type Response = CompletionResponse;
1385    type StreamingResponse = StreamingCompletionResponse;
1386    type Client = crate::client::Client<Ext, T>;
1387
1388    fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1389        Self::new(client.clone(), model.into())
1390    }
1391
1392    async fn completion(
1393        &self,
1394        mut completion_request: completion::CompletionRequest,
1395    ) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
1396        let request_model = completion_request
1397            .model
1398            .clone()
1399            .unwrap_or_else(|| self.model.clone());
1400        let span = if tracing::Span::current().is_disabled() {
1401            info_span!(
1402                target: "rig::completions",
1403                "chat",
1404                gen_ai.operation.name = "chat",
1405                gen_ai.provider.name = Ext::PROVIDER_NAME,
1406                gen_ai.request.model = &request_model,
1407                gen_ai.system_instructions = &completion_request.preamble,
1408                gen_ai.response.id = tracing::field::Empty,
1409                gen_ai.response.model = tracing::field::Empty,
1410                gen_ai.usage.output_tokens = tracing::field::Empty,
1411                gen_ai.usage.input_tokens = tracing::field::Empty,
1412                gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
1413                gen_ai.usage.cache_creation.input_tokens = tracing::field::Empty,
1414            )
1415        } else {
1416            tracing::Span::current()
1417        };
1418
1419        // Check if max_tokens is set, required for Anthropic
1420        if completion_request.max_tokens.is_none() {
1421            if let Some(tokens) = self.default_max_tokens {
1422                completion_request.max_tokens = Some(tokens);
1423            } else {
1424                return Err(CompletionError::RequestError(
1425                    "`max_tokens` must be set for Anthropic".into(),
1426                ));
1427            }
1428        }
1429
1430        let request = AnthropicCompletionRequest::try_from(AnthropicRequestParams {
1431            model: &request_model,
1432            request: completion_request,
1433            prompt_caching: self.prompt_caching,
1434            automatic_caching: self.automatic_caching,
1435            automatic_caching_ttl: self.automatic_caching_ttl.clone(),
1436        })?;
1437
1438        if enabled!(Level::TRACE) {
1439            tracing::trace!(
1440                target: "rig::completions",
1441                "Anthropic completion request: {}",
1442                serde_json::to_string_pretty(&request)?
1443            );
1444        }
1445
1446        async move {
1447            let request: Vec<u8> = serde_json::to_vec(&request)?;
1448
1449            let req = self
1450                .client
1451                .post("/v1/messages")?
1452                .body(request)
1453                .map_err(|e| CompletionError::HttpError(e.into()))?;
1454
1455            let response = self
1456                .client
1457                .send::<_, Bytes>(req)
1458                .await
1459                .map_err(CompletionError::HttpError)?;
1460
1461            if response.status().is_success() {
1462                match serde_json::from_slice::<ApiResponse<CompletionResponse>>(
1463                    response
1464                        .into_body()
1465                        .await
1466                        .map_err(CompletionError::HttpError)?
1467                        .to_vec()
1468                        .as_slice(),
1469                )? {
1470                    ApiResponse::Message(completion) => {
1471                        let span = tracing::Span::current();
1472                        span.record_response_metadata(&completion);
1473                        span.record_token_usage(&completion.usage);
1474                        if enabled!(Level::TRACE) {
1475                            tracing::trace!(
1476                                target: "rig::completions",
1477                                "Anthropic completion response: {}",
1478                                serde_json::to_string_pretty(&completion)?
1479                            );
1480                        }
1481                        completion.try_into()
1482                    }
1483                    ApiResponse::Error(ApiErrorResponse { message }) => {
1484                        Err(CompletionError::ResponseError(message))
1485                    }
1486                }
1487            } else {
1488                let text: String = String::from_utf8_lossy(
1489                    &response
1490                        .into_body()
1491                        .await
1492                        .map_err(CompletionError::HttpError)?,
1493                )
1494                .into();
1495                Err(CompletionError::ProviderError(text))
1496            }
1497        }
1498        .instrument(span)
1499        .await
1500    }
1501
1502    async fn stream(
1503        &self,
1504        request: CompletionRequest,
1505    ) -> Result<
1506        crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1507        CompletionError,
1508    > {
1509        GenericCompletionModel::stream(self, request).await
1510    }
1511}
1512
1513#[derive(Debug, Deserialize)]
1514struct ApiErrorResponse {
1515    message: String,
1516}
1517
1518#[derive(Debug, Deserialize)]
1519#[serde(tag = "type", rename_all = "snake_case")]
1520enum ApiResponse<T> {
1521    Message(T),
1522    Error(ApiErrorResponse),
1523}
1524
1525#[cfg(test)]
1526mod tests {
1527    use super::*;
1528    use serde_json::json;
1529    use serde_path_to_error::deserialize;
1530
1531    #[test]
1532    fn current_model_default_max_tokens_match_anthropic_limits() {
1533        assert_eq!(default_max_tokens_for_model(CLAUDE_OPUS_4_7), Some(128_000));
1534        assert_eq!(default_max_tokens_for_model(CLAUDE_OPUS_4_6), Some(128_000));
1535        assert_eq!(
1536            default_max_tokens_for_model(CLAUDE_SONNET_4_6),
1537            Some(64_000)
1538        );
1539        assert_eq!(default_max_tokens_for_model(CLAUDE_HAIKU_4_5), Some(64_000));
1540    }
1541
1542    #[test]
1543    fn unknown_model_uses_conservative_default_max_tokens_fallback() {
1544        assert_eq!(default_max_tokens_for_model("claude-unknown"), None);
1545        assert_eq!(default_max_tokens_with_fallback("claude-unknown"), 2_048);
1546    }
1547
1548    #[test]
1549    fn test_deserialize_message() {
1550        let assistant_message_json = r#"
1551        {
1552            "role": "assistant",
1553            "content": "\n\nHello there, how may I assist you today?"
1554        }
1555        "#;
1556
1557        let assistant_message_json2 = r#"
1558        {
1559            "role": "assistant",
1560            "content": [
1561                {
1562                    "type": "text",
1563                    "text": "\n\nHello there, how may I assist you today?"
1564                },
1565                {
1566                    "type": "tool_use",
1567                    "id": "toolu_01A09q90qw90lq917835lq9",
1568                    "name": "get_weather",
1569                    "input": {"location": "San Francisco, CA"}
1570                }
1571            ]
1572        }
1573        "#;
1574
1575        let user_message_json = r#"
1576        {
1577            "role": "user",
1578            "content": [
1579                {
1580                    "type": "image",
1581                    "source": {
1582                        "type": "base64",
1583                        "media_type": "image/jpeg",
1584                        "data": "/9j/4AAQSkZJRg..."
1585                    }
1586                },
1587                {
1588                    "type": "text",
1589                    "text": "What is in this image?"
1590                },
1591                {
1592                    "type": "tool_result",
1593                    "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
1594                    "content": "15 degrees"
1595                }
1596            ]
1597        }
1598        "#;
1599
1600        let assistant_message: Message = {
1601            let jd = &mut serde_json::Deserializer::from_str(assistant_message_json);
1602            deserialize(jd).unwrap_or_else(|err| {
1603                panic!("Deserialization error at {}: {}", err.path(), err);
1604            })
1605        };
1606
1607        let assistant_message2: Message = {
1608            let jd = &mut serde_json::Deserializer::from_str(assistant_message_json2);
1609            deserialize(jd).unwrap_or_else(|err| {
1610                panic!("Deserialization error at {}: {}", err.path(), err);
1611            })
1612        };
1613
1614        let user_message: Message = {
1615            let jd = &mut serde_json::Deserializer::from_str(user_message_json);
1616            deserialize(jd).unwrap_or_else(|err| {
1617                panic!("Deserialization error at {}: {}", err.path(), err);
1618            })
1619        };
1620
1621        let Message { role, content } = assistant_message;
1622        assert_eq!(role, Role::Assistant);
1623        assert_eq!(
1624            content.first(),
1625            Content::Text {
1626                text: "\n\nHello there, how may I assist you today?".to_owned(),
1627                cache_control: None,
1628            }
1629        );
1630
1631        let Message { role, content } = assistant_message2;
1632        {
1633            assert_eq!(role, Role::Assistant);
1634            assert_eq!(content.len(), 2);
1635
1636            let mut iter = content.into_iter();
1637
1638            match iter.next().unwrap() {
1639                Content::Text { text, .. } => {
1640                    assert_eq!(text, "\n\nHello there, how may I assist you today?");
1641                }
1642                _ => panic!("Expected text content"),
1643            }
1644
1645            match iter.next().unwrap() {
1646                Content::ToolUse { id, name, input } => {
1647                    assert_eq!(id, "toolu_01A09q90qw90lq917835lq9");
1648                    assert_eq!(name, "get_weather");
1649                    assert_eq!(input, json!({"location": "San Francisco, CA"}));
1650                }
1651                _ => panic!("Expected tool use content"),
1652            }
1653
1654            assert_eq!(iter.next(), None);
1655        }
1656
1657        let Message { role, content } = user_message;
1658        {
1659            assert_eq!(role, Role::User);
1660            assert_eq!(content.len(), 3);
1661
1662            let mut iter = content.into_iter();
1663
1664            match iter.next().unwrap() {
1665                Content::Image { source, .. } => {
1666                    assert_eq!(
1667                        source,
1668                        ImageSource::Base64 {
1669                            data: "/9j/4AAQSkZJRg...".to_owned(),
1670                            media_type: ImageFormat::JPEG,
1671                        }
1672                    );
1673                }
1674                _ => panic!("Expected image content"),
1675            }
1676
1677            match iter.next().unwrap() {
1678                Content::Text { text, .. } => {
1679                    assert_eq!(text, "What is in this image?");
1680                }
1681                _ => panic!("Expected text content"),
1682            }
1683
1684            match iter.next().unwrap() {
1685                Content::ToolResult {
1686                    tool_use_id,
1687                    content,
1688                    is_error,
1689                    ..
1690                } => {
1691                    assert_eq!(tool_use_id, "toolu_01A09q90qw90lq917835lq9");
1692                    assert_eq!(
1693                        content.first(),
1694                        ToolResultContent::Text {
1695                            text: "15 degrees".to_owned()
1696                        }
1697                    );
1698                    assert_eq!(is_error, None);
1699                }
1700                _ => panic!("Expected tool result content"),
1701            }
1702
1703            assert_eq!(iter.next(), None);
1704        }
1705    }
1706
1707    #[test]
1708    fn test_message_to_message_conversion() {
1709        let user_message: Message = serde_json::from_str(
1710            r#"
1711        {
1712            "role": "user",
1713            "content": [
1714                {
1715                    "type": "image",
1716                    "source": {
1717                        "type": "base64",
1718                        "media_type": "image/jpeg",
1719                        "data": "/9j/4AAQSkZJRg..."
1720                    }
1721                },
1722                {
1723                    "type": "text",
1724                    "text": "What is in this image?"
1725                },
1726                {
1727                    "type": "document",
1728                    "source": {
1729                        "type": "base64",
1730                        "data": "base64_encoded_pdf_data",
1731                        "media_type": "application/pdf"
1732                    }
1733                }
1734            ]
1735        }
1736        "#,
1737        )
1738        .unwrap();
1739
1740        let assistant_message = Message {
1741            role: Role::Assistant,
1742            content: OneOrMany::one(Content::ToolUse {
1743                id: "toolu_01A09q90qw90lq917835lq9".to_string(),
1744                name: "get_weather".to_string(),
1745                input: json!({"location": "San Francisco, CA"}),
1746            }),
1747        };
1748
1749        let tool_message = Message {
1750            role: Role::User,
1751            content: OneOrMany::one(Content::ToolResult {
1752                tool_use_id: "toolu_01A09q90qw90lq917835lq9".to_string(),
1753                content: OneOrMany::one(ToolResultContent::Text {
1754                    text: "15 degrees".to_string(),
1755                }),
1756                is_error: None,
1757                cache_control: None,
1758            }),
1759        };
1760
1761        let converted_user_message: message::Message = user_message.clone().try_into().unwrap();
1762        let converted_assistant_message: message::Message =
1763            assistant_message.clone().try_into().unwrap();
1764        let converted_tool_message: message::Message = tool_message.clone().try_into().unwrap();
1765
1766        match converted_user_message.clone() {
1767            message::Message::User { content } => {
1768                assert_eq!(content.len(), 3);
1769
1770                let mut iter = content.into_iter();
1771
1772                match iter.next().unwrap() {
1773                    message::UserContent::Image(message::Image {
1774                        data, media_type, ..
1775                    }) => {
1776                        assert_eq!(data, DocumentSourceKind::base64("/9j/4AAQSkZJRg..."));
1777                        assert_eq!(media_type, Some(message::ImageMediaType::JPEG));
1778                    }
1779                    _ => panic!("Expected image content"),
1780                }
1781
1782                match iter.next().unwrap() {
1783                    message::UserContent::Text(message::Text { text }) => {
1784                        assert_eq!(text, "What is in this image?");
1785                    }
1786                    _ => panic!("Expected text content"),
1787                }
1788
1789                match iter.next().unwrap() {
1790                    message::UserContent::Document(message::Document {
1791                        data, media_type, ..
1792                    }) => {
1793                        assert_eq!(
1794                            data,
1795                            DocumentSourceKind::String("base64_encoded_pdf_data".into())
1796                        );
1797                        assert_eq!(media_type, Some(message::DocumentMediaType::PDF));
1798                    }
1799                    _ => panic!("Expected document content"),
1800                }
1801
1802                assert_eq!(iter.next(), None);
1803            }
1804            _ => panic!("Expected user message"),
1805        }
1806
1807        match converted_tool_message.clone() {
1808            message::Message::User { content } => {
1809                let message::ToolResult { id, content, .. } = match content.first() {
1810                    message::UserContent::ToolResult(tool_result) => tool_result,
1811                    _ => panic!("Expected tool result content"),
1812                };
1813                assert_eq!(id, "toolu_01A09q90qw90lq917835lq9");
1814                match content.first() {
1815                    message::ToolResultContent::Text(message::Text { text }) => {
1816                        assert_eq!(text, "15 degrees");
1817                    }
1818                    _ => panic!("Expected text content"),
1819                }
1820            }
1821            _ => panic!("Expected tool result content"),
1822        }
1823
1824        match converted_assistant_message.clone() {
1825            message::Message::Assistant { content, .. } => {
1826                assert_eq!(content.len(), 1);
1827
1828                match content.first() {
1829                    message::AssistantContent::ToolCall(message::ToolCall {
1830                        id, function, ..
1831                    }) => {
1832                        assert_eq!(id, "toolu_01A09q90qw90lq917835lq9");
1833                        assert_eq!(function.name, "get_weather");
1834                        assert_eq!(function.arguments, json!({"location": "San Francisco, CA"}));
1835                    }
1836                    _ => panic!("Expected tool call content"),
1837                }
1838            }
1839            _ => panic!("Expected assistant message"),
1840        }
1841
1842        let original_user_message: Message = converted_user_message.try_into().unwrap();
1843        let original_assistant_message: Message = converted_assistant_message.try_into().unwrap();
1844        let original_tool_message: Message = converted_tool_message.try_into().unwrap();
1845
1846        assert_eq!(user_message, original_user_message);
1847        assert_eq!(assistant_message, original_assistant_message);
1848        assert_eq!(tool_message, original_tool_message);
1849    }
1850
1851    #[test]
1852    fn test_content_format_conversion() {
1853        use crate::completion::message::ContentFormat;
1854
1855        let source_type: SourceType = ContentFormat::Url.try_into().unwrap();
1856        assert_eq!(source_type, SourceType::URL);
1857
1858        let content_format: ContentFormat = SourceType::URL.into();
1859        assert_eq!(content_format, ContentFormat::Url);
1860
1861        let source_type: SourceType = ContentFormat::Base64.try_into().unwrap();
1862        assert_eq!(source_type, SourceType::BASE64);
1863
1864        let content_format: ContentFormat = SourceType::BASE64.into();
1865        assert_eq!(content_format, ContentFormat::Base64);
1866
1867        let source_type: SourceType = ContentFormat::String.try_into().unwrap();
1868        assert_eq!(source_type, SourceType::TEXT);
1869
1870        let content_format: ContentFormat = SourceType::TEXT.into();
1871        assert_eq!(content_format, ContentFormat::String);
1872    }
1873
1874    #[test]
1875    fn test_cache_control_serialization() {
1876        // Test SystemContent with cache_control
1877        let system = SystemContent::Text {
1878            text: "You are a helpful assistant.".to_string(),
1879            cache_control: Some(CacheControl::ephemeral()),
1880        };
1881        let json = serde_json::to_string(&system).unwrap();
1882        assert!(json.contains(r#""cache_control":{"type":"ephemeral"}"#));
1883        assert!(json.contains(r#""type":"text""#));
1884
1885        // Test SystemContent without cache_control (should not have cache_control field)
1886        let system_no_cache = SystemContent::Text {
1887            text: "Hello".to_string(),
1888            cache_control: None,
1889        };
1890        let json_no_cache = serde_json::to_string(&system_no_cache).unwrap();
1891        assert!(!json_no_cache.contains("cache_control"));
1892
1893        // Test Content::Text with cache_control
1894        let content = Content::Text {
1895            text: "Test message".to_string(),
1896            cache_control: Some(CacheControl::ephemeral()),
1897        };
1898        let json_content = serde_json::to_string(&content).unwrap();
1899        assert!(json_content.contains(r#""cache_control":{"type":"ephemeral"}"#));
1900
1901        // Test apply_cache_control function
1902        let mut system_vec = vec![SystemContent::Text {
1903            text: "System prompt".to_string(),
1904            cache_control: None,
1905        }];
1906        let mut messages = vec![
1907            Message {
1908                role: Role::User,
1909                content: OneOrMany::one(Content::Text {
1910                    text: "First message".to_string(),
1911                    cache_control: None,
1912                }),
1913            },
1914            Message {
1915                role: Role::Assistant,
1916                content: OneOrMany::one(Content::Text {
1917                    text: "Response".to_string(),
1918                    cache_control: None,
1919                }),
1920            },
1921        ];
1922
1923        apply_cache_control(&mut system_vec, &mut messages);
1924
1925        // System should have cache_control
1926        match &system_vec[0] {
1927            SystemContent::Text { cache_control, .. } => {
1928                assert!(cache_control.is_some());
1929            }
1930        }
1931
1932        // Only the last content block of last message should have cache_control
1933        // First message should NOT have cache_control
1934        for content in messages[0].content.iter() {
1935            if let Content::Text { cache_control, .. } = content {
1936                assert!(cache_control.is_none());
1937            }
1938        }
1939
1940        // Last message SHOULD have cache_control
1941        for content in messages[1].content.iter() {
1942            if let Content::Text { cache_control, .. } = content {
1943                assert!(cache_control.is_some());
1944            }
1945        }
1946    }
1947
1948    #[test]
1949    fn test_plaintext_document_serialization() {
1950        let content = Content::Document {
1951            source: DocumentSource::Text {
1952                data: "Hello, world!".to_string(),
1953                media_type: PlainTextMediaType::Plain,
1954            },
1955            cache_control: None,
1956        };
1957
1958        let json = serde_json::to_value(&content).unwrap();
1959        assert_eq!(json["type"], "document");
1960        assert_eq!(json["source"]["type"], "text");
1961        assert_eq!(json["source"]["media_type"], "text/plain");
1962        assert_eq!(json["source"]["data"], "Hello, world!");
1963    }
1964
1965    #[test]
1966    fn test_plaintext_document_deserialization() {
1967        let json = r#"
1968        {
1969            "type": "document",
1970            "source": {
1971                "type": "text",
1972                "media_type": "text/plain",
1973                "data": "Hello, world!"
1974            }
1975        }
1976        "#;
1977
1978        let content: Content = serde_json::from_str(json).unwrap();
1979        match content {
1980            Content::Document {
1981                source,
1982                cache_control,
1983            } => {
1984                assert_eq!(
1985                    source,
1986                    DocumentSource::Text {
1987                        data: "Hello, world!".to_string(),
1988                        media_type: PlainTextMediaType::Plain,
1989                    }
1990                );
1991                assert_eq!(cache_control, None);
1992            }
1993            _ => panic!("Expected Document content"),
1994        }
1995    }
1996
1997    #[test]
1998    fn test_base64_pdf_document_serialization() {
1999        let content = Content::Document {
2000            source: DocumentSource::Base64 {
2001                data: "base64data".to_string(),
2002                media_type: DocumentFormat::PDF,
2003            },
2004            cache_control: None,
2005        };
2006
2007        let json = serde_json::to_value(&content).unwrap();
2008        assert_eq!(json["type"], "document");
2009        assert_eq!(json["source"]["type"], "base64");
2010        assert_eq!(json["source"]["media_type"], "application/pdf");
2011        assert_eq!(json["source"]["data"], "base64data");
2012    }
2013
2014    #[test]
2015    fn test_base64_pdf_document_deserialization() {
2016        let json = r#"
2017        {
2018            "type": "document",
2019            "source": {
2020                "type": "base64",
2021                "media_type": "application/pdf",
2022                "data": "base64data"
2023            }
2024        }
2025        "#;
2026
2027        let content: Content = serde_json::from_str(json).unwrap();
2028        match content {
2029            Content::Document { source, .. } => {
2030                assert_eq!(
2031                    source,
2032                    DocumentSource::Base64 {
2033                        data: "base64data".to_string(),
2034                        media_type: DocumentFormat::PDF,
2035                    }
2036                );
2037            }
2038            _ => panic!("Expected Document content"),
2039        }
2040    }
2041
2042    #[test]
2043    fn test_file_id_document_serialization() {
2044        let content = Content::Document {
2045            source: DocumentSource::File {
2046                file_id: "file_abc".to_string(),
2047            },
2048            cache_control: None,
2049        };
2050
2051        let json = serde_json::to_value(&content).unwrap();
2052        assert_eq!(json["type"], "document");
2053        assert_eq!(json["source"]["type"], "file");
2054        assert_eq!(json["source"]["file_id"], "file_abc");
2055    }
2056
2057    #[test]
2058    fn test_file_id_document_deserialization() {
2059        let json = r#"
2060        {
2061            "type": "document",
2062            "source": {
2063                "type": "file",
2064                "file_id": "file_abc"
2065            }
2066        }
2067        "#;
2068
2069        let content: Content = serde_json::from_str(json).unwrap();
2070        match content {
2071            Content::Document { source, .. } => {
2072                assert_eq!(
2073                    source,
2074                    DocumentSource::File {
2075                        file_id: "file_abc".to_string(),
2076                    }
2077                );
2078            }
2079            _ => panic!("Expected Document content"),
2080        }
2081    }
2082
2083    #[test]
2084    fn test_file_id_rig_to_anthropic_conversion() {
2085        use crate::completion::message as msg;
2086
2087        let rig_message = msg::Message::User {
2088            content: OneOrMany::one(msg::UserContent::Document(msg::Document {
2089                data: DocumentSourceKind::FileId("file_abc".to_string()),
2090                media_type: None,
2091                additional_params: None,
2092            })),
2093        };
2094
2095        let anthropic_message: Message = rig_message.try_into().unwrap();
2096        assert_eq!(anthropic_message.role, Role::User);
2097
2098        let mut iter = anthropic_message.content.into_iter();
2099        match iter.next().unwrap() {
2100            Content::Document { source, .. } => {
2101                assert_eq!(
2102                    source,
2103                    DocumentSource::File {
2104                        file_id: "file_abc".to_string(),
2105                    }
2106                );
2107            }
2108            other => panic!("Expected Document content, got: {other:?}"),
2109        }
2110    }
2111
2112    #[test]
2113    fn test_file_id_anthropic_to_rig_conversion() {
2114        use crate::completion::message as msg;
2115
2116        let anthropic_message = Message {
2117            role: Role::User,
2118            content: OneOrMany::one(Content::Document {
2119                source: DocumentSource::File {
2120                    file_id: "file_abc".to_string(),
2121                },
2122                cache_control: None,
2123            }),
2124        };
2125
2126        let rig_message: msg::Message = anthropic_message.try_into().unwrap();
2127        match rig_message {
2128            msg::Message::User { content } => {
2129                let mut iter = content.into_iter();
2130                match iter.next().unwrap() {
2131                    msg::UserContent::Document(msg::Document {
2132                        data, media_type, ..
2133                    }) => {
2134                        assert_eq!(data, DocumentSourceKind::FileId("file_abc".to_string()));
2135                        assert_eq!(media_type, None);
2136                    }
2137                    other => panic!("Expected Document content, got: {other:?}"),
2138                }
2139            }
2140            _ => panic!("Expected User message"),
2141        }
2142    }
2143
2144    #[test]
2145    fn test_plaintext_rig_to_anthropic_conversion() {
2146        use crate::completion::message as msg;
2147
2148        let rig_message = msg::Message::User {
2149            content: OneOrMany::one(msg::UserContent::document(
2150                "Some plain text content".to_string(),
2151                Some(msg::DocumentMediaType::TXT),
2152            )),
2153        };
2154
2155        let anthropic_message: Message = rig_message.try_into().unwrap();
2156        assert_eq!(anthropic_message.role, Role::User);
2157
2158        let mut iter = anthropic_message.content.into_iter();
2159        match iter.next().unwrap() {
2160            Content::Document { source, .. } => {
2161                assert_eq!(
2162                    source,
2163                    DocumentSource::Text {
2164                        data: "Some plain text content".to_string(),
2165                        media_type: PlainTextMediaType::Plain,
2166                    }
2167                );
2168            }
2169            other => panic!("Expected Document content, got: {other:?}"),
2170        }
2171    }
2172
2173    #[test]
2174    fn test_plaintext_anthropic_to_rig_conversion() {
2175        use crate::completion::message as msg;
2176
2177        let anthropic_message = Message {
2178            role: Role::User,
2179            content: OneOrMany::one(Content::Document {
2180                source: DocumentSource::Text {
2181                    data: "Some plain text content".to_string(),
2182                    media_type: PlainTextMediaType::Plain,
2183                },
2184                cache_control: None,
2185            }),
2186        };
2187
2188        let rig_message: msg::Message = anthropic_message.try_into().unwrap();
2189        match rig_message {
2190            msg::Message::User { content } => {
2191                let mut iter = content.into_iter();
2192                match iter.next().unwrap() {
2193                    msg::UserContent::Document(msg::Document {
2194                        data, media_type, ..
2195                    }) => {
2196                        assert_eq!(
2197                            data,
2198                            DocumentSourceKind::String("Some plain text content".into())
2199                        );
2200                        assert_eq!(media_type, Some(msg::DocumentMediaType::TXT));
2201                    }
2202                    other => panic!("Expected Document content, got: {other:?}"),
2203                }
2204            }
2205            _ => panic!("Expected User message"),
2206        }
2207    }
2208
2209    #[test]
2210    fn test_plaintext_roundtrip_rig_to_anthropic_and_back() {
2211        use crate::completion::message as msg;
2212
2213        let original = msg::Message::User {
2214            content: OneOrMany::one(msg::UserContent::document(
2215                "Round trip text".to_string(),
2216                Some(msg::DocumentMediaType::TXT),
2217            )),
2218        };
2219
2220        let anthropic: Message = original.clone().try_into().unwrap();
2221        let back: msg::Message = anthropic.try_into().unwrap();
2222
2223        match (&original, &back) {
2224            (
2225                msg::Message::User {
2226                    content: orig_content,
2227                },
2228                msg::Message::User {
2229                    content: back_content,
2230                },
2231            ) => match (orig_content.first(), back_content.first()) {
2232                (
2233                    msg::UserContent::Document(msg::Document {
2234                        media_type: orig_mt,
2235                        ..
2236                    }),
2237                    msg::UserContent::Document(msg::Document {
2238                        media_type: back_mt,
2239                        ..
2240                    }),
2241                ) => {
2242                    assert_eq!(orig_mt, back_mt);
2243                }
2244                _ => panic!("Expected Document content in both"),
2245            },
2246            _ => panic!("Expected User messages"),
2247        }
2248    }
2249
2250    #[test]
2251    fn test_unsupported_document_type_returns_error() {
2252        use crate::completion::message as msg;
2253
2254        let rig_message = msg::Message::User {
2255            content: OneOrMany::one(msg::UserContent::Document(msg::Document {
2256                data: DocumentSourceKind::String("data".into()),
2257                media_type: Some(msg::DocumentMediaType::HTML),
2258                additional_params: None,
2259            })),
2260        };
2261
2262        let result: Result<Message, _> = rig_message.try_into();
2263        assert!(result.is_err());
2264        let err = result.unwrap_err().to_string();
2265        assert!(
2266            err.contains("Anthropic only supports PDF and plain text documents"),
2267            "Unexpected error: {err}"
2268        );
2269    }
2270
2271    #[test]
2272    fn test_plaintext_document_url_source_returns_error() {
2273        use crate::completion::message as msg;
2274
2275        let rig_message = msg::Message::User {
2276            content: OneOrMany::one(msg::UserContent::Document(msg::Document {
2277                data: DocumentSourceKind::Url("https://example.com/doc.txt".into()),
2278                media_type: Some(msg::DocumentMediaType::TXT),
2279                additional_params: None,
2280            })),
2281        };
2282
2283        let result: Result<Message, _> = rig_message.try_into();
2284        assert!(result.is_err());
2285        let err = result.unwrap_err().to_string();
2286        assert!(
2287            err.contains("Only string or base64 data is supported for plain text documents"),
2288            "Unexpected error: {err}"
2289        );
2290    }
2291
2292    #[test]
2293    fn test_plaintext_document_with_cache_control() {
2294        let content = Content::Document {
2295            source: DocumentSource::Text {
2296                data: "cached text".to_string(),
2297                media_type: PlainTextMediaType::Plain,
2298            },
2299            cache_control: Some(CacheControl::ephemeral()),
2300        };
2301
2302        let json = serde_json::to_value(&content).unwrap();
2303        assert_eq!(json["source"]["type"], "text");
2304        assert_eq!(json["source"]["media_type"], "text/plain");
2305        assert_eq!(json["cache_control"]["type"], "ephemeral");
2306    }
2307
2308    #[test]
2309    fn test_message_with_plaintext_document_deserialization() {
2310        let json = r#"
2311        {
2312            "role": "user",
2313            "content": [
2314                {
2315                    "type": "document",
2316                    "source": {
2317                        "type": "text",
2318                        "media_type": "text/plain",
2319                        "data": "Hello from a text file"
2320                    }
2321                },
2322                {
2323                    "type": "text",
2324                    "text": "Summarize this document."
2325                }
2326            ]
2327        }
2328        "#;
2329
2330        let message: Message = serde_json::from_str(json).unwrap();
2331        assert_eq!(message.role, Role::User);
2332        assert_eq!(message.content.len(), 2);
2333
2334        let mut iter = message.content.into_iter();
2335
2336        match iter.next().unwrap() {
2337            Content::Document { source, .. } => {
2338                assert_eq!(
2339                    source,
2340                    DocumentSource::Text {
2341                        data: "Hello from a text file".to_string(),
2342                        media_type: PlainTextMediaType::Plain,
2343                    }
2344                );
2345            }
2346            _ => panic!("Expected Document content"),
2347        }
2348
2349        match iter.next().unwrap() {
2350            Content::Text { text, .. } => {
2351                assert_eq!(text, "Summarize this document.");
2352            }
2353            _ => panic!("Expected Text content"),
2354        }
2355    }
2356
2357    #[test]
2358    fn test_assistant_reasoning_multiblock_to_anthropic_content() {
2359        let reasoning = message::Reasoning {
2360            id: None,
2361            content: vec![
2362                message::ReasoningContent::Text {
2363                    text: "step one".to_string(),
2364                    signature: Some("sig-1".to_string()),
2365                },
2366                message::ReasoningContent::Summary("summary".to_string()),
2367                message::ReasoningContent::Text {
2368                    text: "step two".to_string(),
2369                    signature: Some("sig-2".to_string()),
2370                },
2371                message::ReasoningContent::Redacted {
2372                    data: "redacted block".to_string(),
2373                },
2374            ],
2375        };
2376
2377        let msg = message::Message::Assistant {
2378            id: None,
2379            content: OneOrMany::one(message::AssistantContent::Reasoning(reasoning)),
2380        };
2381        let converted: Message = msg.try_into().expect("convert assistant message");
2382        let converted_content = converted.content.iter().cloned().collect::<Vec<_>>();
2383
2384        assert_eq!(converted.role, Role::Assistant);
2385        assert_eq!(converted_content.len(), 4);
2386        assert!(matches!(
2387            converted_content.first(),
2388            Some(Content::Thinking { thinking, signature: Some(signature) })
2389                if thinking == "step one" && signature == "sig-1"
2390        ));
2391        assert!(matches!(
2392            converted_content.get(1),
2393            Some(Content::Thinking { thinking, signature: None }) if thinking == "summary"
2394        ));
2395        assert!(matches!(
2396            converted_content.get(2),
2397            Some(Content::Thinking { thinking, signature: Some(signature) })
2398                if thinking == "step two" && signature == "sig-2"
2399        ));
2400        assert!(matches!(
2401            converted_content.get(3),
2402            Some(Content::RedactedThinking { data }) if data == "redacted block"
2403        ));
2404    }
2405
2406    #[test]
2407    fn test_redacted_thinking_content_to_assistant_reasoning() {
2408        let content = Content::RedactedThinking {
2409            data: "opaque-redacted".to_string(),
2410        };
2411        let converted: message::AssistantContent =
2412            content.try_into().expect("convert redacted thinking");
2413
2414        assert!(matches!(
2415            converted,
2416            message::AssistantContent::Reasoning(message::Reasoning { content, .. })
2417                if matches!(
2418                    content.first(),
2419                    Some(message::ReasoningContent::Redacted { data }) if data == "opaque-redacted"
2420                )
2421        ));
2422    }
2423
2424    #[test]
2425    fn test_assistant_encrypted_reasoning_maps_to_redacted_thinking() {
2426        let reasoning = message::Reasoning {
2427            id: None,
2428            content: vec![message::ReasoningContent::Encrypted(
2429                "ciphertext".to_string(),
2430            )],
2431        };
2432        let msg = message::Message::Assistant {
2433            id: None,
2434            content: OneOrMany::one(message::AssistantContent::Reasoning(reasoning)),
2435        };
2436
2437        let converted: Message = msg.try_into().expect("convert assistant message");
2438        let converted_content = converted.content.iter().cloned().collect::<Vec<_>>();
2439
2440        assert_eq!(converted_content.len(), 1);
2441        assert!(matches!(
2442            converted_content.first(),
2443            Some(Content::RedactedThinking { data }) if data == "ciphertext"
2444        ));
2445    }
2446
2447    #[test]
2448    fn empty_end_turn_response_normalizes_to_empty_text_choice() {
2449        let response = CompletionResponse {
2450            content: vec![],
2451            id: "msg_123".to_string(),
2452            model: CLAUDE_SONNET_4_6.to_string(),
2453            role: "assistant".to_string(),
2454            stop_reason: Some("end_turn".to_string()),
2455            stop_sequence: None,
2456            usage: Usage {
2457                input_tokens: 7,
2458                cache_read_input_tokens: None,
2459                cache_creation_input_tokens: None,
2460                output_tokens: 2,
2461            },
2462        };
2463
2464        let parsed: completion::CompletionResponse<CompletionResponse> = response
2465            .try_into()
2466            .expect("empty end_turn should not error");
2467
2468        assert_eq!(parsed.choice.len(), 1);
2469        assert!(matches!(
2470            parsed.choice.first(),
2471            completion::AssistantContent::Text(text) if text.text.is_empty()
2472        ));
2473    }
2474
2475    #[test]
2476    fn empty_non_end_turn_response_still_errors() {
2477        let response = CompletionResponse {
2478            content: vec![],
2479            id: "msg_123".to_string(),
2480            model: CLAUDE_SONNET_4_6.to_string(),
2481            role: "assistant".to_string(),
2482            stop_reason: Some("tool_use".to_string()),
2483            stop_sequence: None,
2484            usage: Usage {
2485                input_tokens: 7,
2486                cache_read_input_tokens: None,
2487                cache_creation_input_tokens: None,
2488                output_tokens: 2,
2489            },
2490        };
2491
2492        let err = completion::CompletionResponse::<CompletionResponse>::try_from(response)
2493            .expect_err("empty non-end_turn should remain an error");
2494
2495        assert!(matches!(
2496            err,
2497            CompletionError::ResponseError(message) if message == EMPTY_RESPONSE_ERROR
2498        ));
2499    }
2500}