1use 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
20pub const CLAUDE_OPUS_4_6: &str = "claude-opus-4-6";
26pub const CLAUDE_OPUS_4_7: &str = "claude-opus-4-7";
28pub const CLAUDE_SONNET_4_6: &str = "claude-sonnet-4-6";
30pub 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#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
161pub enum CacheTtl {
162 #[default]
164 #[serde(rename = "5m")]
165 FiveMinutes,
166 #[serde(rename = "1h")]
168 OneHour,
169}
170
171#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
176#[serde(tag = "type", rename_all = "snake_case")]
177pub enum CacheControl {
178 Ephemeral {
179 #[serde(skip_serializing_if = "Option::is_none")]
181 ttl: Option<CacheTtl>,
182 },
183}
184
185impl CacheControl {
186 pub fn ephemeral() -> Self {
188 Self::Ephemeral { ttl: None }
189 }
190
191 pub fn ephemeral_1h() -> Self {
193 Self::Ephemeral {
194 ttl: Some(CacheTtl::OneHour),
195 }
196 }
197}
198
199#[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 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#[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#[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#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
404#[serde(rename_all = "lowercase")]
405pub enum DocumentFormat {
406 #[serde(rename = "application/pdf")]
407 PDF,
408}
409
410#[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 pub prompt_caching: bool,
879 pub automatic_caching: bool,
883 pub automatic_caching_ttl: Option<CacheTtl>,
887}
888
889pub 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 pub fn with_prompt_caching(mut self) -> Self {
935 self.prompt_caching = true;
936 self
937 }
938
939 pub fn with_automatic_caching(mut self) -> Self {
972 self.automatic_caching = true;
973 self
974 }
975
976 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
999fn 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
1064fn 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 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 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#[derive(Debug, Deserialize, Serialize)]
1150#[serde(tag = "type", rename_all = "snake_case")]
1151enum OutputFormat {
1152 JsonSchema { schema: serde_json::Value },
1154}
1155
1156#[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 #[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 #[serde(skip_serializing_if = "Option::is_none")]
1184 cache_control: Option<CacheControl>,
1185}
1186
1187fn 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
1198pub fn apply_cache_control(system: &mut [SystemContent], messages: &mut [Message]) {
1203 if let Some(SystemContent::Text { cache_control, .. }) = system.last_mut() {
1205 *cache_control = Some(CacheControl::ephemeral());
1206 }
1207
1208 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 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
1244pub struct AnthropicRequestParams<'a> {
1246 pub model: &'a str,
1247 pub request: CompletionRequest,
1248 pub prompt_caching: bool,
1249 pub automatic_caching: bool,
1251 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 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 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 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 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 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 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 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 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 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 match &system_vec[0] {
1927 SystemContent::Text { cache_control, .. } => {
1928 assert!(cache_control.is_some());
1929 }
1930 }
1931
1932 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 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}