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
379impl<'de> Deserialize<'de> for ToolResultContentBlock {
380 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
381 where
382 D: serde::Deserializer<'de>,
383 {
384 let value = serde_json::Value::deserialize(deserializer)?;
385 match value.get("type").and_then(|value| value.as_str()) {
386 Some("text") => {
387 let text = value
388 .get("text")
389 .and_then(|value| value.as_str())
390 .ok_or_else(|| serde::de::Error::missing_field("text"))?;
391 Ok(Self::Text {
392 text: text.to_string(),
393 })
394 }
395 Some("image") => {
396 let source = value
397 .get("source")
398 .cloned()
399 .ok_or_else(|| serde::de::Error::missing_field("source"))
400 .and_then(|value| {
401 serde_json::from_value(value).map_err(serde::de::Error::custom)
402 })?;
403 Ok(Self::Image { source })
404 }
405 None => match value.get("text").and_then(|value| value.as_str()) {
406 Some(text) => Ok(Self::Text {
407 text: text.to_string(),
408 }),
409 None => Ok(Self::Other(value)),
410 },
411 _ => Ok(Self::Other(value)),
412 }
413 }
414}
415
416impl<'de> Deserialize<'de> for AnthropicContentBlock {
420 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
421 where
422 D: serde::Deserializer<'de>,
423 {
424 let value = serde_json::Value::deserialize(deserializer)?;
425 let block_type = value
426 .get("type")
427 .and_then(|t| t.as_str())
428 .unwrap_or("")
429 .to_string();
430
431 match block_type.as_str() {
432 "text" => {
433 let text = value
434 .get("text")
435 .and_then(|t| t.as_str())
436 .ok_or_else(|| serde::de::Error::missing_field("text"))?
437 .to_string();
438 let citations: Option<Vec<serde_json::Value>> = value
439 .get("citations")
440 .cloned()
441 .and_then(|v| serde_json::from_value(v).ok());
442 let cache_control: Option<CacheControl> = value
443 .get("cache_control")
444 .cloned()
445 .and_then(|v| serde_json::from_value(v).ok());
446 Ok(AnthropicContentBlock::Text {
447 text,
448 citations,
449 cache_control,
450 })
451 }
452 "image" => {
453 let source: AnthropicImageSource =
454 serde_json::from_value(value.get("source").cloned().unwrap_or_default())
455 .map_err(serde::de::Error::custom)?;
456 Ok(AnthropicContentBlock::Image { source })
457 }
458 "tool_use" => {
459 let id = value
460 .get("id")
461 .and_then(|v| v.as_str())
462 .ok_or_else(|| serde::de::Error::missing_field("id"))?
463 .to_string();
464 let name = value
465 .get("name")
466 .and_then(|v| v.as_str())
467 .ok_or_else(|| serde::de::Error::missing_field("name"))?
468 .to_string();
469 let input = value.get("input").cloned().unwrap_or(serde_json::json!({}));
470 let cache_control: Option<CacheControl> = value
471 .get("cache_control")
472 .cloned()
473 .and_then(|v| serde_json::from_value(v).ok());
474 Ok(AnthropicContentBlock::ToolUse {
475 id,
476 name,
477 input,
478 cache_control,
479 })
480 }
481 "tool_result" => {
482 let tool_use_id = value
483 .get("tool_use_id")
484 .and_then(|v| v.as_str())
485 .ok_or_else(|| serde::de::Error::missing_field("tool_use_id"))?
486 .to_string();
487 let content: Option<ToolResultContent> = value
488 .get("content")
489 .cloned()
490 .and_then(|v| serde_json::from_value(v).ok());
491 let is_error = value.get("is_error").and_then(|v| v.as_bool());
492 let cache_control: Option<CacheControl> = value
493 .get("cache_control")
494 .cloned()
495 .and_then(|v| serde_json::from_value(v).ok());
496 Ok(AnthropicContentBlock::ToolResult {
497 tool_use_id,
498 content,
499 is_error,
500 cache_control,
501 })
502 }
503 "thinking" => {
504 let thinking = value
505 .get("thinking")
506 .and_then(|v| v.as_str())
507 .ok_or_else(|| serde::de::Error::missing_field("thinking"))?
508 .to_string();
509 let signature = value
510 .get("signature")
511 .and_then(|v| v.as_str())
512 .ok_or_else(|| serde::de::Error::missing_field("signature"))?
513 .to_string();
514 let cache_control: Option<CacheControl> = value
515 .get("cache_control")
516 .cloned()
517 .and_then(|v| serde_json::from_value(v).ok());
518 Ok(AnthropicContentBlock::Thinking {
519 thinking,
520 signature,
521 cache_control,
522 })
523 }
524 "redacted_thinking" => {
525 let data = value
526 .get("data")
527 .and_then(|v| v.as_str())
528 .ok_or_else(|| serde::de::Error::missing_field("data"))?
529 .to_string();
530 Ok(AnthropicContentBlock::RedactedThinking { data })
531 }
532 "server_tool_use" => {
533 let id = value
534 .get("id")
535 .and_then(|v| v.as_str())
536 .ok_or_else(|| serde::de::Error::missing_field("id"))?
537 .to_string();
538 let name = value
539 .get("name")
540 .and_then(|v| v.as_str())
541 .ok_or_else(|| serde::de::Error::missing_field("name"))?
542 .to_string();
543 let input = value.get("input").cloned().unwrap_or(serde_json::json!({}));
544 Ok(AnthropicContentBlock::ServerToolUse { id, name, input })
545 }
546 "web_search_tool_result" => {
547 let tool_use_id = value
548 .get("tool_use_id")
549 .and_then(|v| v.as_str())
550 .ok_or_else(|| serde::de::Error::missing_field("tool_use_id"))?
551 .to_string();
552 let content = value
553 .get("content")
554 .cloned()
555 .unwrap_or(serde_json::json!([]));
556 Ok(AnthropicContentBlock::WebSearchToolResult {
557 tool_use_id,
558 content,
559 })
560 }
561 other => {
562 tracing::debug!(
563 "Unrecognized Anthropic content block type '{}', preserving as Other",
564 other
565 );
566 Ok(AnthropicContentBlock::Other(value))
567 }
568 }
569 }
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct AnthropicImageSource {
575 #[serde(rename = "type")]
576 pub source_type: String,
577 pub media_type: String,
578 pub data: String,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct AnthropicTool {
590 pub name: String,
592 #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
595 pub tool_type: Option<String>,
596 #[serde(skip_serializing_if = "Option::is_none")]
597 pub description: Option<String>,
598 #[serde(default, skip_serializing_if = "Option::is_none")]
601 pub input_schema: Option<serde_json::Value>,
602 #[serde(default, skip_serializing_if = "Option::is_none")]
604 pub cache_control: Option<CacheControl>,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize)]
609#[serde(untagged)]
610pub enum AnthropicToolChoice {
611 Named(AnthropicToolChoiceNamed),
614 Simple(AnthropicToolChoiceSimple),
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct AnthropicToolChoiceSimple {
621 #[serde(rename = "type")]
622 pub choice_type: AnthropicToolChoiceMode,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
626 pub disable_parallel_tool_use: Option<bool>,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
630#[serde(rename_all = "lowercase")]
631pub enum AnthropicToolChoiceMode {
632 Auto,
633 Any,
634 None,
635 Tool,
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct AnthropicToolChoiceNamed {
641 #[serde(rename = "type")]
642 pub choice_type: AnthropicToolChoiceMode,
643 pub name: String,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub disable_parallel_tool_use: Option<bool>,
648}
649#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct AnthropicMessageResponse {
652 pub id: String,
653 #[serde(rename = "type")]
654 pub object_type: String,
655 pub role: String,
656 pub content: Vec<AnthropicResponseContentBlock>,
657 pub model: String,
658 pub stop_reason: Option<AnthropicStopReason>,
659 pub stop_sequence: Option<String>,
660 pub usage: AnthropicUsage,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize)]
669#[serde(tag = "type")]
670pub enum AnthropicResponseContentBlock {
671 #[serde(rename = "thinking")]
672 Thinking { thinking: String, signature: String },
673 #[serde(rename = "text")]
674 Text {
675 text: String,
676 #[serde(default, skip_serializing_if = "Option::is_none")]
677 citations: Option<Vec<serde_json::Value>>,
678 },
679 #[serde(rename = "tool_use")]
680 ToolUse {
681 id: String,
682 name: String,
683 input: serde_json::Value,
684 },
685 #[serde(rename = "redacted_thinking")]
686 RedactedThinking { data: String },
687 #[serde(rename = "server_tool_use")]
688 ServerToolUse {
689 id: String,
690 name: String,
691 #[serde(default)]
692 input: serde_json::Value,
693 },
694 #[serde(rename = "web_search_tool_result")]
695 WebSearchToolResult {
696 tool_use_id: String,
697 #[serde(default)]
698 content: serde_json::Value,
699 },
700 #[serde(untagged)]
704 Other(serde_json::Value),
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize, Default)]
709pub struct AnthropicUsage {
710 pub input_tokens: u32,
711 pub output_tokens: u32,
712 #[serde(default, skip_serializing_if = "Option::is_none")]
714 pub cache_creation_input_tokens: Option<u32>,
715 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub cache_read_input_tokens: Option<u32>,
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
722#[serde(rename_all = "snake_case")]
723pub enum AnthropicStopReason {
724 EndTurn,
725 MaxTokens,
726 StopSequence,
727 ToolUse,
728 PauseTurn,
731 Refusal,
733}
734#[derive(Debug, Clone, Serialize, Deserialize)]
736#[serde(tag = "type")]
737pub enum AnthropicStreamEvent {
738 #[serde(rename = "message_start")]
739 MessageStart { message: AnthropicMessageResponse },
740
741 #[serde(rename = "content_block_start")]
742 ContentBlockStart {
743 index: u32,
744 content_block: AnthropicResponseContentBlock,
745 },
746
747 #[serde(rename = "content_block_delta")]
748 ContentBlockDelta { index: u32, delta: AnthropicDelta },
749
750 #[serde(rename = "content_block_stop")]
751 ContentBlockStop { index: u32 },
752
753 #[serde(rename = "message_delta")]
754 MessageDelta {
755 delta: AnthropicMessageDeltaBody,
756 usage: AnthropicUsage,
757 },
758
759 #[serde(rename = "message_stop")]
760 MessageStop {},
761
762 #[serde(rename = "ping")]
763 Ping {},
764
765 #[serde(rename = "error")]
766 Error { error: AnthropicErrorBody },
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize)]
771#[serde(tag = "type")]
772pub enum AnthropicDelta {
773 #[serde(rename = "thinking_delta")]
774 ThinkingDelta { thinking: String },
775 #[serde(rename = "text_delta")]
776 TextDelta { text: String },
777 #[serde(rename = "input_json_delta")]
778 InputJsonDelta { partial_json: String },
779 #[serde(rename = "signature_delta")]
781 SignatureDelta { signature: String },
782 #[serde(rename = "citations_delta")]
784 CitationsDelta { citation: serde_json::Value },
785}
786
787#[derive(Debug, Clone, Serialize, Deserialize)]
789pub struct AnthropicMessageDeltaBody {
790 pub stop_reason: Option<AnthropicStopReason>,
791 #[serde(skip_serializing_if = "Option::is_none")]
792 pub stop_sequence: Option<String>,
793}
794#[derive(Debug, Clone, Serialize, Deserialize)]
796pub struct AnthropicErrorResponse {
797 #[serde(rename = "type")]
798 pub object_type: String,
799 pub error: AnthropicErrorBody,
800}
801
802#[derive(Debug, Clone, Serialize, Deserialize)]
804pub struct AnthropicErrorBody {
805 #[serde(rename = "type")]
806 pub error_type: String,
807 pub message: String,
808}
809
810impl AnthropicErrorResponse {
811 pub fn invalid_request(message: impl Into<String>) -> Self {
813 Self {
814 object_type: "error".to_string(),
815 error: AnthropicErrorBody {
816 error_type: "invalid_request_error".to_string(),
817 message: message.into(),
818 },
819 }
820 }
821
822 pub fn api_error(message: impl Into<String>) -> Self {
824 Self {
825 object_type: "error".to_string(),
826 error: AnthropicErrorBody {
827 error_type: "api_error".to_string(),
828 message: message.into(),
829 },
830 }
831 }
832
833 pub fn not_found(message: impl Into<String>) -> Self {
835 Self {
836 object_type: "error".to_string(),
837 error: AnthropicErrorBody {
838 error_type: "not_found_error".to_string(),
839 message: message.into(),
840 },
841 }
842 }
843}
844#[derive(Debug, Clone, Deserialize)]
846pub struct AnthropicCountTokensRequest {
847 pub model: String,
848 pub messages: Vec<AnthropicMessage>,
849 #[serde(
850 default,
851 skip_serializing_if = "Option::is_none",
852 deserialize_with = "deserialize_system_prompt"
853 )]
854 pub system: Option<SystemContent>,
855 #[serde(default)]
856 pub tools: Option<Vec<AnthropicTool>>,
857}
858
859#[derive(Debug, Clone, Serialize)]
861pub struct AnthropicCountTokensResponse {
862 pub input_tokens: u32,
863}
864
865impl AnthropicCountTokensRequest {
866 pub fn estimate_tokens(&self) -> u32 {
868 let mut total_len: usize = 0;
869
870 if let Some(system) = &self.system {
871 total_len += system.text.len();
872 }
873
874 for msg in &self.messages {
875 total_len += match msg.role {
877 AnthropicRole::User => 4,
878 AnthropicRole::Assistant => 9,
879 AnthropicRole::System => 6,
880 };
881 match &msg.content {
883 AnthropicMessageContent::Text { content } => total_len += content.len(),
884 AnthropicMessageContent::Blocks { content } => {
885 for block in content {
886 total_len += estimate_block_len(block);
887 }
888 }
889 }
890 }
891
892 if let Some(tools) = &self.tools {
893 for tool in tools {
894 total_len += tool.name.len();
895 if let Some(desc) = &tool.description {
896 total_len += desc.len();
897 }
898 if let Some(schema) = &tool.input_schema {
899 total_len += schema.to_string().len();
900 }
901 }
902 }
903
904 let tokens = total_len / 3;
905 if tokens == 0 && total_len > 0 {
906 1
907 } else {
908 tokens as u32
909 }
910 }
911}
912
913fn estimate_block_len(block: &AnthropicContentBlock) -> usize {
914 match block {
915 AnthropicContentBlock::Text { text, .. } => text.len(),
916 AnthropicContentBlock::ToolUse { name, input, .. } => name.len() + input.to_string().len(),
917 AnthropicContentBlock::ToolResult { content, .. } => content
918 .as_ref()
919 .map(|c| match c {
920 ToolResultContent::Text(s) => s.len(),
921 ToolResultContent::Blocks(blocks) => blocks
922 .iter()
923 .map(|b| match b {
924 ToolResultContentBlock::Text { text } => text.len(),
925 ToolResultContentBlock::Image { .. } => 256,
926 ToolResultContentBlock::Other(v) => v.to_string().len(),
927 })
928 .sum(),
929 })
930 .unwrap_or(0),
931 AnthropicContentBlock::Thinking { thinking, .. } => thinking.len(),
932 AnthropicContentBlock::RedactedThinking { data, .. } => data.len(),
933 AnthropicContentBlock::ServerToolUse { name, input, .. } => {
934 name.len() + input.to_string().len()
935 }
936 AnthropicContentBlock::WebSearchToolResult { content, .. } => content.to_string().len(),
937 AnthropicContentBlock::Image { .. } => 256, AnthropicContentBlock::Other(v) => v.to_string().len(),
939 }
940}
941
942#[cfg(test)]
943mod tests {
944 use super::*;
945
946 #[test]
947 fn messages_request_keeps_nvext_opaque() {
948 let request: AnthropicCreateMessageRequest = serde_json::from_value(serde_json::json!({
949 "model": "test-model",
950 "max_tokens": 16,
951 "messages": [{"role": "user", "content": "hi"}],
952 "nvext": {
953 "unknown_future_extension": {"nested": true},
954 "agent_context": {"trajectory_id": 7}
955 }
956 }))
957 .unwrap();
958
959 let nvext = request.nvext.expect("opaque nvext value");
960 assert_eq!(nvext["unknown_future_extension"]["nested"], true);
961 assert_eq!(nvext["agent_context"]["trajectory_id"], 7);
962 }
963
964 #[test]
965 fn tool_result_blocks_preserve_image_and_reject_document() {
966 let input = serde_json::json!([
967 {"type": "text", "text": "Screenshot captured"},
968 {
969 "type": "image",
970 "source": {
971 "type": "base64",
972 "media_type": "image/png",
973 "data": "aGVsbG8="
974 }
975 },
976 {
977 "type": "document",
978 "source": {
979 "type": "base64",
980 "media_type": "application/pdf",
981 "data": "aGVsbG8="
982 }
983 }
984 ]);
985 let content: ToolResultContent = serde_json::from_value(input.clone()).unwrap();
986
987 let ToolResultContent::Blocks(blocks) = &content else {
988 panic!("expected content blocks");
989 };
990 assert!(matches!(blocks[1], ToolResultContentBlock::Image { .. }));
991 assert!(matches!(blocks[2], ToolResultContentBlock::Other(_)));
992 assert_eq!(serde_json::to_value(content).unwrap(), input);
993
994 let legacy: ToolResultContentBlock =
995 serde_json::from_value(serde_json::json!({"text": "legacy"})).unwrap();
996 assert!(matches!(legacy, ToolResultContentBlock::Text { .. }));
997 }
998}