1use serde::{Deserialize, Serialize};
10
11#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
13pub struct CacheControl {
14 #[serde(rename = "type")]
15 pub control_type: CacheControlType,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub ttl: Option<String>,
19}
20
21#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
22#[serde(rename_all = "lowercase")]
23pub enum CacheControlType {
24 #[default]
25 Ephemeral,
26 #[serde(other)]
27 Unknown,
28}
29
30const MIN_TTL_SECONDS: u64 = 300;
31const MAX_TTL_SECONDS: u64 = 3600;
32
33impl CacheControl {
34 pub fn ttl_seconds(&self) -> u64 {
40 let raw = match self.ttl.as_deref() {
41 None => return MIN_TTL_SECONDS,
42 Some("5m") => 300,
43 Some("1h") => 3600,
44 Some(other) => match other.parse::<u64>() {
45 Ok(secs) => secs,
46 Err(_) => {
47 tracing::warn!("Unrecognized TTL '{}', defaulting to 300s", other);
48 return MIN_TTL_SECONDS;
49 }
50 },
51 };
52 raw.clamp(MIN_TTL_SECONDS, MAX_TTL_SECONDS)
53 }
54}
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct SystemContent {
58 pub text: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub cache_control: Option<CacheControl>,
63}
64
65fn deserialize_system_prompt<'de, D>(deserializer: D) -> Result<Option<SystemContent>, D::Error>
69where
70 D: serde::Deserializer<'de>,
71{
72 #[derive(Deserialize)]
73 #[serde(untagged)]
74 enum SystemPrompt {
75 Text(String),
76 Blocks(Vec<SystemBlock>),
77 }
78
79 #[derive(Deserialize)]
80 struct SystemBlock {
81 text: String,
82 #[serde(default)]
83 cache_control: Option<CacheControl>,
84 }
85
86 let maybe: Option<SystemPrompt> = Option::deserialize(deserializer)?;
87 Ok(maybe.map(|sp| match sp {
88 SystemPrompt::Text(s) => SystemContent {
89 text: s,
90 cache_control: None,
91 },
92 SystemPrompt::Blocks(blocks) => {
93 let cache_control = blocks.iter().rev().find_map(|b| b.cache_control.clone());
94 let text = blocks
95 .into_iter()
96 .map(|b| b.text)
97 .collect::<Vec<_>>()
98 .join("\n");
99 SystemContent {
100 text,
101 cache_control,
102 }
103 }
104 }))
105}
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct AnthropicCreateMessageRequest {
109 pub model: String,
111
112 pub max_tokens: u32,
114
115 pub messages: Vec<AnthropicMessage>,
117
118 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub nvext: Option<serde_json::Value>,
122
123 #[serde(
125 default,
126 skip_serializing_if = "Option::is_none",
127 deserialize_with = "deserialize_system_prompt"
128 )]
129 pub system: Option<SystemContent>,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub temperature: Option<f32>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub top_p: Option<f32>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub top_k: Option<u32>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub stop_sequences: Option<Vec<String>>,
146
147 #[serde(default)]
149 pub stream: bool,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub metadata: Option<serde_json::Value>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub tools: Option<Vec<AnthropicTool>>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub tool_choice: Option<AnthropicToolChoice>,
162
163 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub cache_control: Option<CacheControl>,
169
170 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub thinking: Option<ThinkingConfig>,
176
177 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub service_tier: Option<String>,
180
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub container: Option<String>,
184
185 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub output_config: Option<serde_json::Value>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ThinkingConfig {
200 #[serde(rename = "type")]
202 pub thinking_type: String,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub budget_tokens: Option<u32>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct AnthropicMessage {
211 pub role: AnthropicRole,
212 #[serde(flatten)]
213 pub content: AnthropicMessageContent,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
218#[serde(rename_all = "lowercase")]
219pub enum AnthropicRole {
220 User,
221 Assistant,
222 System,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(untagged)]
230pub enum AnthropicMessageContent {
231 Text { content: String },
233 Blocks { content: Vec<AnthropicContentBlock> },
235}
236
237#[derive(Debug, Clone, Serialize)]
244#[serde(tag = "type")]
245pub enum AnthropicContentBlock {
246 #[serde(rename = "text")]
250 Text {
251 text: String,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 citations: Option<Vec<serde_json::Value>>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 cache_control: Option<CacheControl>,
256 },
257 #[serde(rename = "image")]
259 Image { source: AnthropicImageSource },
260 #[serde(rename = "tool_use")]
262 ToolUse {
263 id: String,
264 name: String,
265 input: serde_json::Value,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 cache_control: Option<CacheControl>,
268 },
269 #[serde(rename = "tool_result")]
271 ToolResult {
272 tool_use_id: String,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
274 content: Option<ToolResultContent>,
275 #[serde(skip_serializing_if = "Option::is_none")]
276 is_error: Option<bool>,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 cache_control: Option<CacheControl>,
279 },
280 #[serde(rename = "thinking")]
282 Thinking {
283 thinking: String,
284 signature: String,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 cache_control: Option<CacheControl>,
287 },
288 #[serde(rename = "redacted_thinking")]
292 RedactedThinking { data: String },
293 #[serde(rename = "server_tool_use")]
297 ServerToolUse {
298 id: String,
299 name: String,
300 #[serde(default)]
301 input: serde_json::Value,
302 },
303 #[serde(rename = "web_search_tool_result")]
306 WebSearchToolResult {
307 tool_use_id: String,
308 #[serde(default)]
309 content: serde_json::Value,
310 },
311 #[serde(untagged)]
315 Other(serde_json::Value),
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
321#[serde(untagged)]
322pub enum ToolResultContent {
323 Text(String),
324 Blocks(Vec<ToolResultContentBlock>),
325}
326
327impl ToolResultContent {
328 pub fn into_text(self) -> String {
330 match self {
331 ToolResultContent::Text(s) => s,
332 ToolResultContent::Blocks(blocks) => blocks
333 .into_iter()
334 .filter_map(|b| match b {
335 ToolResultContentBlock::Text { text } => Some(text),
336 ToolResultContentBlock::Image { .. } | ToolResultContentBlock::Other(_) => None,
337 })
338 .collect::<Vec<_>>()
339 .join(""),
340 }
341 }
342}
343
344#[derive(Debug, Clone)]
346pub enum ToolResultContentBlock {
347 Text {
348 text: String,
349 },
350 Image {
352 source: AnthropicImageSource,
353 },
354 Other(serde_json::Value),
356}
357
358impl Serialize for ToolResultContentBlock {
359 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
360 where
361 S: serde::Serializer,
362 {
363 match self {
364 Self::Text { text } => serde_json::json!({
365 "type": "text",
366 "text": text,
367 })
368 .serialize(serializer),
369 Self::Image { source } => serde_json::json!({
370 "type": "image",
371 "source": source,
372 })
373 .serialize(serializer),
374 Self::Other(value) => value.serialize(serializer),
375 }
376 }
377}
378
379fn take_field(value: &mut serde_json::Value, field: &str) -> Option<serde_json::Value> {
380 value.as_object_mut()?.remove(field)
381}
382
383fn take_required_string<E: serde::de::Error>(
384 value: &mut serde_json::Value,
385 field: &'static str,
386) -> Result<String, E> {
387 match take_field(value, field) {
388 Some(serde_json::Value::String(text)) => Ok(text),
389 _ => Err(E::missing_field(field)),
390 }
391}
392
393impl<'de> Deserialize<'de> for ToolResultContentBlock {
394 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
395 where
396 D: serde::Deserializer<'de>,
397 {
398 let mut value = serde_json::Value::deserialize(deserializer)?;
399 match value.get("type").and_then(|value| value.as_str()) {
400 Some("text") => {
401 let text = take_required_string::<D::Error>(&mut value, "text")?;
402 Ok(Self::Text { text })
403 }
404 Some("image") => {
405 let source = take_field(&mut value, "source")
406 .ok_or_else(|| serde::de::Error::missing_field("source"))
407 .and_then(|value| {
408 serde_json::from_value(value).map_err(serde::de::Error::custom)
409 })?;
410 Ok(Self::Image { source })
411 }
412 None if value.get("text").is_some_and(serde_json::Value::is_string) => Ok(Self::Text {
413 text: take_required_string::<D::Error>(&mut value, "text")?,
414 }),
415 _ => Ok(Self::Other(value)),
416 }
417 }
418}
419
420impl<'de> Deserialize<'de> for AnthropicContentBlock {
424 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
425 where
426 D: serde::Deserializer<'de>,
427 {
428 let mut value = serde_json::Value::deserialize(deserializer)?;
429 let block_type = value.get("type").and_then(|t| t.as_str()).unwrap_or("");
430
431 match block_type {
432 "text" => {
433 let text = take_required_string::<D::Error>(&mut value, "text")?;
434 let citations: Option<Vec<serde_json::Value>> = take_field(&mut value, "citations")
435 .and_then(|v| serde_json::from_value(v).ok());
436 let cache_control: Option<CacheControl> = take_field(&mut value, "cache_control")
437 .and_then(|v| serde_json::from_value(v).ok());
438 Ok(AnthropicContentBlock::Text {
439 text,
440 citations,
441 cache_control,
442 })
443 }
444 "image" => {
445 let source: AnthropicImageSource =
446 serde_json::from_value(take_field(&mut value, "source").unwrap_or_default())
447 .map_err(serde::de::Error::custom)?;
448 Ok(AnthropicContentBlock::Image { source })
449 }
450 "tool_use" => {
451 let id = take_required_string::<D::Error>(&mut value, "id")?;
452 let name = take_required_string::<D::Error>(&mut value, "name")?;
453 let input =
454 take_field(&mut value, "input").unwrap_or_else(|| serde_json::json!({}));
455 let cache_control: Option<CacheControl> = take_field(&mut value, "cache_control")
456 .and_then(|v| serde_json::from_value(v).ok());
457 Ok(AnthropicContentBlock::ToolUse {
458 id,
459 name,
460 input,
461 cache_control,
462 })
463 }
464 "tool_result" => {
465 let tool_use_id = take_required_string::<D::Error>(&mut value, "tool_use_id")?;
466 let content: Option<ToolResultContent> =
467 take_field(&mut value, "content").and_then(|v| serde_json::from_value(v).ok());
468 let is_error = value.get("is_error").and_then(|v| v.as_bool());
469 let cache_control: Option<CacheControl> = take_field(&mut value, "cache_control")
470 .and_then(|v| serde_json::from_value(v).ok());
471 Ok(AnthropicContentBlock::ToolResult {
472 tool_use_id,
473 content,
474 is_error,
475 cache_control,
476 })
477 }
478 "thinking" => {
479 let thinking = take_required_string::<D::Error>(&mut value, "thinking")?;
480 let signature = take_required_string::<D::Error>(&mut value, "signature")?;
481 let cache_control: Option<CacheControl> = take_field(&mut value, "cache_control")
482 .and_then(|v| serde_json::from_value(v).ok());
483 Ok(AnthropicContentBlock::Thinking {
484 thinking,
485 signature,
486 cache_control,
487 })
488 }
489 "redacted_thinking" => {
490 let data = take_required_string::<D::Error>(&mut value, "data")?;
491 Ok(AnthropicContentBlock::RedactedThinking { data })
492 }
493 "server_tool_use" => {
494 let id = take_required_string::<D::Error>(&mut value, "id")?;
495 let name = take_required_string::<D::Error>(&mut value, "name")?;
496 let input =
497 take_field(&mut value, "input").unwrap_or_else(|| serde_json::json!({}));
498 Ok(AnthropicContentBlock::ServerToolUse { id, name, input })
499 }
500 "web_search_tool_result" => {
501 let tool_use_id = take_required_string::<D::Error>(&mut value, "tool_use_id")?;
502 let content =
503 take_field(&mut value, "content").unwrap_or_else(|| serde_json::json!([]));
504 Ok(AnthropicContentBlock::WebSearchToolResult {
505 tool_use_id,
506 content,
507 })
508 }
509 other => {
510 tracing::debug!(
511 "Unrecognized Anthropic content block type '{}', preserving as Other",
512 other
513 );
514 Ok(AnthropicContentBlock::Other(value))
515 }
516 }
517 }
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct AnthropicImageSource {
523 #[serde(rename = "type")]
524 pub source_type: String,
525 pub media_type: String,
526 pub data: String,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct AnthropicTool {
538 pub name: String,
540 #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
543 pub tool_type: Option<String>,
544 #[serde(skip_serializing_if = "Option::is_none")]
545 pub description: Option<String>,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub input_schema: Option<serde_json::Value>,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub strict: Option<bool>,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub cache_control: Option<CacheControl>,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
560#[serde(untagged)]
561pub enum AnthropicToolChoice {
562 Named(AnthropicToolChoiceNamed),
565 Simple(AnthropicToolChoiceSimple),
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct AnthropicToolChoiceSimple {
572 #[serde(rename = "type")]
573 pub choice_type: AnthropicToolChoiceMode,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
577 pub disable_parallel_tool_use: Option<bool>,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
581#[serde(rename_all = "lowercase")]
582pub enum AnthropicToolChoiceMode {
583 Auto,
584 Any,
585 None,
586 Tool,
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize)]
591pub struct AnthropicToolChoiceNamed {
592 #[serde(rename = "type")]
593 pub choice_type: AnthropicToolChoiceMode,
594 pub name: String,
595 #[serde(default, skip_serializing_if = "Option::is_none")]
598 pub disable_parallel_tool_use: Option<bool>,
599}
600#[derive(Debug, Clone, Serialize, Deserialize)]
602pub struct AnthropicMessageResponse {
603 pub id: String,
604 #[serde(rename = "type")]
605 pub object_type: String,
606 pub role: String,
607 pub content: Vec<AnthropicResponseContentBlock>,
608 pub model: String,
609 pub stop_reason: Option<AnthropicStopReason>,
610 pub stop_sequence: Option<String>,
611 pub usage: AnthropicUsage,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize)]
620#[serde(tag = "type")]
621pub enum AnthropicResponseContentBlock {
622 #[serde(rename = "thinking")]
623 Thinking { thinking: String, signature: String },
624 #[serde(rename = "text")]
625 Text {
626 text: String,
627 #[serde(default, skip_serializing_if = "Option::is_none")]
628 citations: Option<Vec<serde_json::Value>>,
629 },
630 #[serde(rename = "tool_use")]
631 ToolUse {
632 id: String,
633 name: String,
634 input: serde_json::Value,
635 },
636 #[serde(rename = "redacted_thinking")]
637 RedactedThinking { data: String },
638 #[serde(rename = "server_tool_use")]
639 ServerToolUse {
640 id: String,
641 name: String,
642 #[serde(default)]
643 input: serde_json::Value,
644 },
645 #[serde(rename = "web_search_tool_result")]
646 WebSearchToolResult {
647 tool_use_id: String,
648 #[serde(default)]
649 content: serde_json::Value,
650 },
651 #[serde(untagged)]
655 Other(serde_json::Value),
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize, Default)]
660pub struct AnthropicUsage {
661 pub input_tokens: u32,
662 pub output_tokens: u32,
663 #[serde(default, skip_serializing_if = "Option::is_none")]
665 pub cache_creation_input_tokens: Option<u32>,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
668 pub cache_read_input_tokens: Option<u32>,
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
673#[serde(rename_all = "snake_case")]
674pub enum AnthropicStopReason {
675 EndTurn,
676 MaxTokens,
677 StopSequence,
678 ToolUse,
679 PauseTurn,
682 Refusal,
684}
685#[derive(Debug, Clone, Serialize, Deserialize)]
687#[serde(tag = "type")]
688pub enum AnthropicStreamEvent {
689 #[serde(rename = "message_start")]
690 MessageStart { message: AnthropicMessageResponse },
691
692 #[serde(rename = "content_block_start")]
693 ContentBlockStart {
694 index: u32,
695 content_block: AnthropicResponseContentBlock,
696 },
697
698 #[serde(rename = "content_block_delta")]
699 ContentBlockDelta { index: u32, delta: AnthropicDelta },
700
701 #[serde(rename = "content_block_stop")]
702 ContentBlockStop { index: u32 },
703
704 #[serde(rename = "message_delta")]
705 MessageDelta {
706 delta: AnthropicMessageDeltaBody,
707 usage: AnthropicUsage,
708 },
709
710 #[serde(rename = "message_stop")]
711 MessageStop {},
712
713 #[serde(rename = "ping")]
714 Ping {},
715
716 #[serde(rename = "error")]
717 Error { error: AnthropicErrorBody },
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize)]
722#[serde(tag = "type")]
723pub enum AnthropicDelta {
724 #[serde(rename = "thinking_delta")]
725 ThinkingDelta { thinking: String },
726 #[serde(rename = "text_delta")]
727 TextDelta { text: String },
728 #[serde(rename = "input_json_delta")]
729 InputJsonDelta { partial_json: String },
730 #[serde(rename = "signature_delta")]
732 SignatureDelta { signature: String },
733 #[serde(rename = "citations_delta")]
735 CitationsDelta { citation: serde_json::Value },
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct AnthropicMessageDeltaBody {
741 pub stop_reason: Option<AnthropicStopReason>,
742 #[serde(skip_serializing_if = "Option::is_none")]
743 pub stop_sequence: Option<String>,
744}
745#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct AnthropicErrorResponse {
748 #[serde(rename = "type")]
749 pub object_type: String,
750 pub error: AnthropicErrorBody,
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize)]
755pub struct AnthropicErrorBody {
756 #[serde(rename = "type")]
757 pub error_type: String,
758 pub message: String,
759}
760
761impl AnthropicErrorResponse {
762 pub fn invalid_request(message: impl Into<String>) -> Self {
764 Self {
765 object_type: "error".to_string(),
766 error: AnthropicErrorBody {
767 error_type: "invalid_request_error".to_string(),
768 message: message.into(),
769 },
770 }
771 }
772
773 pub fn api_error(message: impl Into<String>) -> Self {
775 Self {
776 object_type: "error".to_string(),
777 error: AnthropicErrorBody {
778 error_type: "api_error".to_string(),
779 message: message.into(),
780 },
781 }
782 }
783
784 pub fn not_found(message: impl Into<String>) -> Self {
786 Self {
787 object_type: "error".to_string(),
788 error: AnthropicErrorBody {
789 error_type: "not_found_error".to_string(),
790 message: message.into(),
791 },
792 }
793 }
794}
795#[derive(Debug, Clone, Deserialize)]
797pub struct AnthropicCountTokensRequest {
798 pub model: String,
799 pub messages: Vec<AnthropicMessage>,
800 #[serde(
801 default,
802 skip_serializing_if = "Option::is_none",
803 deserialize_with = "deserialize_system_prompt"
804 )]
805 pub system: Option<SystemContent>,
806 #[serde(default)]
807 pub tools: Option<Vec<AnthropicTool>>,
808}
809
810#[derive(Debug, Clone, Serialize)]
812pub struct AnthropicCountTokensResponse {
813 pub input_tokens: u32,
814}
815
816impl AnthropicCountTokensRequest {
817 pub fn estimate_tokens(&self) -> u32 {
819 estimate_input_tokens(&self.messages, self.system.as_ref(), self.tools.as_deref())
820 }
821}
822
823impl AnthropicCreateMessageRequest {
824 pub fn estimate_tokens(&self) -> u32 {
826 estimate_input_tokens(&self.messages, self.system.as_ref(), self.tools.as_deref())
827 }
828}
829
830fn estimate_input_tokens(
831 messages: &[AnthropicMessage],
832 system: Option<&SystemContent>,
833 tools: Option<&[AnthropicTool]>,
834) -> u32 {
835 let mut total_len: usize = 0;
836
837 if let Some(system) = system {
838 total_len += system.text.len();
839 }
840
841 for msg in messages {
842 total_len += match msg.role {
844 AnthropicRole::User => 4,
845 AnthropicRole::Assistant => 9,
846 AnthropicRole::System => 6,
847 };
848 match &msg.content {
850 AnthropicMessageContent::Text { content } => total_len += content.len(),
851 AnthropicMessageContent::Blocks { content } => {
852 for block in content {
853 total_len += estimate_block_len(block);
854 }
855 }
856 }
857 }
858
859 if let Some(tools) = tools {
860 for tool in tools {
861 total_len += tool.name.len();
862 if let Some(desc) = &tool.description {
863 total_len += desc.len();
864 }
865 if let Some(schema) = &tool.input_schema {
866 total_len += schema.to_string().len();
867 }
868 }
869 }
870
871 let tokens = total_len / 3;
872 if tokens == 0 && total_len > 0 {
873 1
874 } else {
875 tokens as u32
876 }
877}
878
879fn estimate_block_len(block: &AnthropicContentBlock) -> usize {
880 match block {
881 AnthropicContentBlock::Text { text, .. } => text.len(),
882 AnthropicContentBlock::ToolUse { name, input, .. } => name.len() + input.to_string().len(),
883 AnthropicContentBlock::ToolResult { content, .. } => content
884 .as_ref()
885 .map(|c| match c {
886 ToolResultContent::Text(s) => s.len(),
887 ToolResultContent::Blocks(blocks) => blocks
888 .iter()
889 .map(|b| match b {
890 ToolResultContentBlock::Text { text } => text.len(),
891 ToolResultContentBlock::Image { .. } => 256,
892 ToolResultContentBlock::Other(v) => v.to_string().len(),
893 })
894 .sum(),
895 })
896 .unwrap_or(0),
897 AnthropicContentBlock::Thinking { thinking, .. } => thinking.len(),
898 AnthropicContentBlock::RedactedThinking { data, .. } => data.len(),
899 AnthropicContentBlock::ServerToolUse { name, input, .. } => {
900 name.len() + input.to_string().len()
901 }
902 AnthropicContentBlock::WebSearchToolResult { content, .. } => content.to_string().len(),
903 AnthropicContentBlock::Image { .. } => 256, AnthropicContentBlock::Other(v) => v.to_string().len(),
905 }
906}
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911
912 #[test]
913 fn tool_strict_round_trips() {
914 for strict in [Some(true), Some(false), None] {
915 let mut input = serde_json::json!({
916 "name": "set_status",
917 "input_schema": {"type": "object"}
918 });
919 if let Some(strict) = strict {
920 input["strict"] = strict.into();
921 }
922 let tool: AnthropicTool = serde_json::from_value(input.clone()).unwrap();
923 assert_eq!(serde_json::to_value(tool).unwrap(), input);
924 }
925 }
926
927 #[test]
928 fn messages_request_keeps_nvext_opaque() {
929 let request: AnthropicCreateMessageRequest = serde_json::from_value(serde_json::json!({
930 "model": "test-model",
931 "max_tokens": 16,
932 "messages": [{"role": "user", "content": "hi"}],
933 "nvext": {
934 "unknown_future_extension": {"nested": true},
935 "agent_context": {"trajectory_id": 7}
936 }
937 }))
938 .unwrap();
939
940 let nvext = request.nvext.expect("opaque nvext value");
941 assert_eq!(nvext["unknown_future_extension"]["nested"], true);
942 assert_eq!(nvext["agent_context"]["trajectory_id"], 7);
943 }
944
945 #[test]
946 fn tool_result_blocks_preserve_image_and_reject_document() {
947 let input = serde_json::json!([
948 {"type": "text", "text": "Screenshot captured"},
949 {
950 "type": "image",
951 "source": {
952 "type": "base64",
953 "media_type": "image/png",
954 "data": "aGVsbG8="
955 }
956 },
957 {
958 "type": "document",
959 "source": {
960 "type": "base64",
961 "media_type": "application/pdf",
962 "data": "aGVsbG8="
963 }
964 },
965 {"type": "", "text": "not legacy text"}
966 ]);
967 let content: ToolResultContent = serde_json::from_value(input.clone()).unwrap();
968
969 let ToolResultContent::Blocks(blocks) = &content else {
970 panic!("expected content blocks");
971 };
972 assert!(matches!(blocks[1], ToolResultContentBlock::Image { .. }));
973 assert!(matches!(blocks[2], ToolResultContentBlock::Other(_)));
974 assert!(matches!(blocks[3], ToolResultContentBlock::Other(_)));
975 assert_eq!(serde_json::to_value(content).unwrap(), input);
976
977 for input in [
978 serde_json::json!({"text": "legacy"}),
979 serde_json::json!({"type": null, "text": "legacy"}),
980 serde_json::json!({"type": false, "text": "legacy"}),
981 ] {
982 let legacy: ToolResultContentBlock = serde_json::from_value(input).unwrap();
983 assert!(matches!(legacy, ToolResultContentBlock::Text { .. }));
984 }
985 }
986}