1use std::borrow::Cow;
2
3use serde::{Deserialize, Deserializer, Serialize};
4use serde_json::{Map, Value};
5
6use crate::provider::MessageClearAt;
7
8#[derive(Debug, Serialize, Deserialize, Clone)]
9#[serde(tag = "type", rename_all = "lowercase")]
10pub enum AnthropicFallback {
11 #[serde(rename = "fallback")]
12 Fallback {
13 from: AnthropicFallbackModel,
14 to: AnthropicFallbackModel,
15 },
16}
17
18#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct AnthropicFallbackModel {
20 model: String,
21}
22
23#[derive(Debug, Serialize, Deserialize)]
24pub struct AnthropicRequest {
25 pub(crate) model: String,
26 pub(crate) messages: Vec<AnthropicMessage>,
27 pub(crate) max_tokens: u32,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub(crate) cache_control: Option<CacheControl>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub(crate) system: Option<Value>, #[serde(skip_serializing_if = "Option::is_none")]
33 pub(crate) tools: Option<Vec<AnthropicTool>>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub(crate) tool_choice: Option<Value>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub(crate) thinking: Option<ThinkingConfig>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub(crate) reasoning: Option<Value>, #[serde(
41 default,
42 deserialize_with = "deserialize_boxed_output_config_opt",
43 skip_serializing_if = "Option::is_none"
44 )]
45 pub(crate) output_config: Option<Box<AnthropicOutputConfig>>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub(crate) context_management: Option<Value>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub(crate) fallbacks: Option<AnthropicFallbacksParam>,
50 #[serde(skip_serializing_if = "Option::is_none")]
53 pub(crate) fallback_credit_token: Option<String>,
54 pub(crate) stream: bool,
55}
56
57#[derive(Debug, Serialize, Deserialize, Clone)]
61#[serde(untagged)]
62pub enum AnthropicFallbacksParam {
63 Mode(AnthropicFallbacksKeyword),
64 Models(Vec<AnthropicFallbackParam>),
65}
66
67#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
68#[serde(rename_all = "lowercase")]
69pub enum AnthropicFallbacksKeyword {
70 Default,
71}
72
73impl AnthropicFallbacksParam {
74 pub(crate) fn models(&self) -> &[AnthropicFallbackParam] {
76 match self {
77 Self::Mode(_) => &[],
78 Self::Models(models) => models,
79 }
80 }
81}
82
83#[derive(Debug, Serialize, Deserialize, Clone)]
84pub struct AnthropicFallbackParam {
85 pub(crate) model: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub(crate) max_tokens: Option<u32>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub(crate) thinking: Option<ThinkingConfig>,
90}
91
92#[derive(Debug, Serialize, Deserialize, Clone)]
93#[serde(tag = "type", rename_all = "lowercase")]
94pub enum ThinkingConfig {
95 Enabled {
96 budget_tokens: u32,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 display: Option<ThinkingDisplay>,
99 },
100 Adaptive {
101 #[serde(skip_serializing_if = "Option::is_none")]
102 display: Option<ThinkingDisplay>,
103 },
104 Disabled,
105 #[serde(rename = "between_tools")]
111 BetweenTools,
112 #[serde(other)]
114 Unknown,
115}
116
117#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
119#[serde(rename_all = "lowercase")]
120pub enum ThinkingDisplay {
121 Summarized,
123 Omitted,
127 Updates,
132 #[serde(other)]
134 Unknown,
135}
136
137#[derive(Debug, Serialize, Deserialize)]
138pub struct AnthropicMessage {
139 pub(crate) role: String,
140 pub(crate) content: Vec<AnthropicContentBlock>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub(crate) clear_at: Option<MessageClearAt>,
144}
145
146#[derive(Debug, Serialize, Deserialize, Clone)]
147#[serde(tag = "type")]
148pub enum AnthropicContentBlock {
149 #[serde(rename = "text")]
150 Text {
151 text: String,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 citations: Option<Vec<TextCitation>>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 cache_control: Option<CacheControl>,
156 },
157 #[serde(rename = "image")]
158 Image {
159 source: ImageSource,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 cache_control: Option<CacheControl>,
162 },
163 #[serde(rename = "tool_use")]
164 ToolUse(Box<AnthropicToolUseBlock>),
165 #[serde(rename = "tool_result")]
166 ToolResult(Box<AnthropicToolResultBlock>),
167 #[serde(rename = "thinking")]
168 Thinking {
169 thinking: String,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 signature: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 cache_control: Option<CacheControl>,
174 },
175 #[serde(rename = "redacted_thinking")]
176 RedactedThinking {
177 data: String,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 cache_control: Option<CacheControl>,
180 },
181 #[serde(rename = "compaction")]
182 Compaction {
183 content: Option<String>,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 signature: Option<String>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 cache_control: Option<CacheControl>,
191 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
194 extra: Map<String, Value>,
195 },
196 #[serde(rename = "server_tool_use")]
198 ServerToolUse { id: String, name: String, input: Value },
199 #[serde(rename = "tool_search_tool_result")]
201 ToolSearchToolResult {
202 tool_use_id: String,
203 content: ToolSearchResultContent,
204 },
205 #[serde(rename = "container_upload")]
207 ContainerUpload { file_id: String },
208 #[serde(rename = "code_execution_tool_result")]
210 CodeExecutionToolResult { tool_use_id: String, content: Value },
211 #[serde(rename = "bash_code_execution_tool_result")]
213 BashCodeExecutionToolResult { tool_use_id: String, content: Value },
214 #[serde(rename = "text_editor_code_execution_tool_result")]
216 TextEditorCodeExecutionToolResult { tool_use_id: String, content: Value },
217 #[serde(rename = "web_search_tool_result")]
219 WebSearchToolResult { tool_use_id: String, content: Value },
220 #[serde(rename = "advisor_tool_result")]
224 AdvisorToolResult { tool_use_id: String, content: Value },
225 #[serde(rename = "fallback")]
227 Fallback {
228 from: AnthropicFallbackModel,
229 to: AnthropicFallbackModel,
230 },
231 #[serde(other)]
233 Unknown,
234}
235
236#[derive(Debug, Serialize, Deserialize, Clone)]
238pub struct AnthropicToolUseBlock {
239 pub(crate) id: String,
240 pub(crate) name: String,
241 pub(crate) input: Value,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub(crate) cache_control: Option<CacheControl>,
244}
245
246#[derive(Debug, Serialize, Deserialize, Clone)]
248pub struct AnthropicToolResultBlock {
249 pub(crate) tool_use_id: String,
250 pub(crate) content: Value, #[serde(skip_serializing_if = "Option::is_none")]
252 pub(crate) is_error: Option<bool>,
253 #[serde(skip_serializing_if = "Option::is_none")]
254 pub(crate) cache_control: Option<CacheControl>,
255}
256
257#[derive(Debug, Serialize, Deserialize, Clone)]
259#[serde(tag = "type")]
260pub enum ToolSearchResultContent {
261 #[serde(rename = "tool_search_tool_search_result")]
262 SearchResult { tool_references: Vec<ToolReference> },
263 #[serde(rename = "tool_search_tool_result_error")]
264 Error { error_code: String },
265 #[serde(other)]
267 Unknown,
268}
269
270#[derive(Debug, Serialize, Deserialize, Clone)]
272pub struct ToolReference {
273 #[serde(rename = "type")]
274 ref_type: Option<String>, tool_name: String,
276}
277
278#[derive(Debug, Serialize, Deserialize, Clone)]
279#[serde(tag = "type")]
280pub enum TextCitation {
281 #[serde(rename = "char_location")]
282 CharLocation {
283 cited_text: String,
284 document_index: usize,
285 document_title: Option<String>,
286 start_char_index: usize,
287 end_char_index: usize,
288 },
289 #[serde(rename = "page_location")]
290 PageLocation {
291 cited_text: String,
292 document_index: usize,
293 document_title: Option<String>,
294 start_page_number: usize,
295 end_page_number: usize,
296 },
297 #[serde(rename = "content_block_location")]
298 ContentBlockLocation {
299 cited_text: String,
300 document_index: usize,
301 document_title: Option<String>,
302 start_block_index: usize,
303 end_block_index: usize,
304 },
305 #[serde(rename = "web_search_result_location")]
306 WebSearchResultLocation {
307 url: Option<String>,
308 title: Option<String>,
309 encrypted_index: Option<String>,
310 cited_text: Option<String>,
311 },
312 #[serde(other)]
314 Unknown,
315}
316
317#[derive(Debug, Serialize, Deserialize, Clone)]
318pub struct ImageSource {
319 #[serde(rename = "type")]
320 pub(crate) source_type: String, pub(crate) media_type: String,
322 pub(crate) data: String,
323}
324
325#[derive(Debug, Serialize, Deserialize, Clone)]
326pub struct CacheControl {
327 #[serde(rename = "type")]
328 pub(crate) control_type: Cow<'static, str>, #[serde(skip_serializing_if = "Option::is_none")]
330 pub(crate) ttl: Option<Cow<'static, str>>, }
332
333#[derive(Debug, Serialize, Deserialize, Clone)]
336#[serde(untagged)]
337pub enum AnthropicTool {
338 ToolSearch(AnthropicToolSearchTool),
340 CodeExecution(AnthropicCodeExecutionTool),
342 Memory(AnthropicMemoryTool),
344 WebSearch(AnthropicWebSearchTool),
346 Advisor(AnthropicAdvisorTool),
348 Function(AnthropicFunctionTool),
350}
351
352#[derive(Debug, Serialize, Deserialize, Clone)]
354pub struct AnthropicFunctionTool {
355 pub(crate) name: String,
356 pub(crate) description: String,
357 pub(crate) input_schema: Value,
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub(crate) input_examples: Option<Vec<Value>>,
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub(crate) strict: Option<bool>,
362 #[serde(skip_serializing_if = "Option::is_none")]
363 pub(crate) allowed_callers: Option<Vec<String>>,
364 #[serde(skip_serializing_if = "Option::is_none")]
365 pub(crate) cache_control: Option<CacheControl>,
366 #[serde(skip_serializing_if = "Option::is_none")]
368 pub(crate) defer_loading: Option<bool>,
369}
370
371#[derive(Debug, Serialize, Deserialize, Clone)]
373pub struct AnthropicCodeExecutionTool {
374 #[serde(rename = "type")]
376 pub(crate) tool_type: String,
377 pub(crate) name: String,
379}
380
381#[derive(Debug, Serialize, Deserialize, Clone)]
383pub struct AnthropicMemoryTool {
384 #[serde(rename = "type")]
386 pub(crate) tool_type: String,
387 pub(crate) name: String,
389}
390
391#[derive(Debug, Serialize, Deserialize, Clone)]
393pub struct AnthropicToolSearchTool {
394 #[serde(rename = "type")]
396 pub(crate) tool_type: String,
397 pub(crate) name: String,
399}
400
401#[derive(Debug, Serialize, Deserialize, Clone)]
403pub struct AnthropicWebSearchTool {
404 #[serde(rename = "type")]
406 pub(crate) tool_type: String,
407 pub(crate) name: String,
409 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
411 pub(crate) options: Map<String, Value>,
412}
413
414#[derive(Debug, Serialize, Deserialize, Clone)]
420pub struct AnthropicAdvisorTool {
421 #[serde(rename = "type")]
423 pub(crate) tool_type: String, pub(crate) name: String, pub(crate) model: String,
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub(crate) max_uses: Option<u32>,
431 #[serde(skip_serializing_if = "Option::is_none")]
433 pub(crate) max_tokens: Option<u32>,
434 #[serde(skip_serializing_if = "Option::is_none")]
436 pub(crate) caching: Option<AnthropicAdvisorCaching>,
437}
438
439#[derive(Debug, Serialize, Deserialize, Clone)]
441pub struct AnthropicAdvisorCaching {
442 #[serde(rename = "type")]
444 pub(crate) cache_type: String, pub(crate) ttl: String,
447}
448
449#[derive(Debug, Deserialize)]
450#[serde(tag = "type")]
451pub enum AnthropicStreamEvent {
452 #[serde(rename = "message_start")]
453 MessageStart { message: AnthropicMessageResponse },
454 #[serde(rename = "content_block_start")]
455 ContentBlockStart {
456 index: usize,
457 content_block: AnthropicContentBlock,
458 },
459 #[serde(rename = "ping")]
460 Ping,
461 #[serde(rename = "content_block_delta")]
462 ContentBlockDelta { index: usize, delta: AnthropicStreamDelta },
463 #[serde(rename = "content_block_stop")]
464 ContentBlockStop { index: usize },
465 #[serde(rename = "message_delta")]
466 MessageDelta {
467 delta: AnthropicMessageDelta,
468 usage: Option<AnthropicUsage>,
469 },
470 #[serde(rename = "message_stop")]
471 MessageStop,
472 #[serde(rename = "error")]
473 Error { error: AnthropicErrorBody },
474 #[serde(other)]
476 Unknown,
477}
478
479#[derive(Debug, Deserialize)]
480#[serde(tag = "type")]
481pub enum AnthropicStreamDelta {
482 #[serde(rename = "text_delta")]
483 TextDelta { text: String },
484 #[serde(rename = "input_json_delta")]
485 InputJsonDelta { partial_json: String },
486 #[serde(rename = "thinking_delta")]
487 ThinkingDelta { thinking: String },
488 #[serde(rename = "signature_delta")]
489 SignatureDelta { signature: String },
490 #[serde(rename = "compaction_delta")]
491 CompactionDelta {
492 content: Option<String>,
493 #[serde(flatten, default)]
495 extra: Map<String, Value>,
496 },
497 #[serde(other)]
499 Unknown,
500}
501
502#[derive(Debug, Deserialize)]
503pub struct AnthropicMessageDelta {
504 pub(crate) stop_reason: Option<String>,
505 stop_sequence: Option<String>,
506 #[serde(default)]
509 pub(crate) stop_details: Option<Value>,
510}
511
512#[derive(Debug, Deserialize, Clone)]
513pub struct AnthropicStopDetails {
514 #[serde(rename = "type")]
515 stop_details_type: Option<String>,
516 category: Option<String>,
517 explanation: Option<String>,
518 fallback_credit_token: Option<String>,
521 fallback_has_prefill_claim: Option<bool>,
524}
525
526#[derive(Debug, Deserialize)]
527pub struct AnthropicMessageResponse {
528 id: String,
529 role: String,
530 content: Vec<AnthropicContentBlock>,
531 model: String,
532 stop_reason: Option<String>,
533 stop_sequence: Option<String>,
534 stop_details: Option<AnthropicStopDetails>,
535 pub(crate) usage: AnthropicUsage,
536}
537
538#[derive(Debug, Serialize, Deserialize)]
539pub struct AnthropicUsage {
540 #[serde(default, skip_serializing_if = "Option::is_none")]
541 pub(crate) speed: Option<String>,
542 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
543 pub(crate) input_tokens: u32,
544 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
545 pub(crate) output_tokens: u32,
546 pub(crate) cache_creation_input_tokens: Option<u32>,
547 pub(crate) cache_read_input_tokens: Option<u32>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
551 iterations: Option<Vec<AnthropicUsageIteration>>,
552}
553
554#[derive(Debug, Serialize, Deserialize)]
555#[serde(tag = "type", rename_all = "snake_case")]
556pub enum AnthropicUsageIteration {
557 Message {
558 model: Option<String>,
559 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
560 input_tokens: u32,
561 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
562 output_tokens: u32,
563 cache_creation_input_tokens: Option<u32>,
564 cache_read_input_tokens: Option<u32>,
565 },
566 FallbackMessage {
567 model: Option<String>,
568 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
569 input_tokens: u32,
570 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
571 output_tokens: u32,
572 cache_creation_input_tokens: Option<u32>,
573 cache_read_input_tokens: Option<u32>,
574 },
575 AdvisorMessage {
576 model: Option<String>,
577 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
578 input_tokens: u32,
579 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
580 output_tokens: u32,
581 cache_creation_input_tokens: Option<u32>,
582 cache_read_input_tokens: Option<u32>,
583 },
584 Compaction {
585 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
586 input_tokens: u32,
587 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
588 output_tokens: u32,
589 cache_creation_input_tokens: Option<u32>,
590 cache_read_input_tokens: Option<u32>,
591 },
592 #[serde(other)]
594 Unknown,
595}
596
597fn deserialize_nullable_u32<'de, D>(deserializer: D) -> Result<u32, D::Error>
598where
599 D: Deserializer<'de>,
600{
601 Option::<u32>::deserialize(deserializer).map(Option::unwrap_or_default)
602}
603
604#[derive(Debug, Deserialize)]
605pub struct AnthropicErrorBody {
606 #[serde(rename = "type")]
607 error_type: String,
608 pub(crate) message: String,
609}
610
611#[derive(Debug, Serialize, Deserialize, Clone)]
613pub struct AnthropicOutputConfig {
614 #[serde(skip_serializing_if = "Option::is_none")]
615 pub(crate) effort: Option<String>,
616 #[serde(skip_serializing_if = "Option::is_none")]
617 pub(crate) task_budget: Option<AnthropicTaskBudget>,
618 #[serde(skip_serializing_if = "Option::is_none")]
619 pub(crate) format: Option<AnthropicOutputFormat>,
620}
621
622impl AnthropicOutputConfig {
623 fn is_empty(&self) -> bool {
624 self.effort.is_none() && self.task_budget.is_none() && self.format.is_none()
625 }
626
627 fn into_boxed_if_non_empty(self) -> Option<Box<Self>> {
628 (!self.is_empty()).then_some(Box::new(self))
629 }
630}
631
632#[derive(Debug, Serialize, Deserialize, Clone)]
633pub struct AnthropicTaskBudget {
634 #[serde(rename = "type")]
635 pub(crate) budget_type: String,
636 pub(crate) total: u32,
637}
638
639#[derive(Debug, Serialize, Deserialize, Clone)]
641#[serde(tag = "type", rename_all = "snake_case")]
642pub enum AnthropicOutputFormat {
643 JsonSchema {
644 schema: Value,
645 },
646 #[serde(other)]
648 Unknown,
649}
650
651#[derive(Debug, Serialize, Deserialize)]
654pub struct CountTokensRequest {
655 model: String,
656 #[serde(skip_serializing_if = "Option::is_none")]
657 system: Option<Value>,
658 messages: Vec<AnthropicMessage>,
659 #[serde(skip_serializing_if = "Option::is_none")]
660 tools: Option<Vec<AnthropicTool>>,
661 #[serde(skip_serializing_if = "Option::is_none")]
662 thinking: Option<ThinkingConfig>,
663}
664
665fn deserialize_boxed_output_config_opt<'de, D>(deserializer: D) -> Result<Option<Box<AnthropicOutputConfig>>, D::Error>
666where
667 D: Deserializer<'de>,
668{
669 Option::<AnthropicOutputConfig>::deserialize(deserializer)
670 .map(|value| value.and_then(AnthropicOutputConfig::into_boxed_if_non_empty))
671}
672
673#[derive(Debug, Deserialize)]
675pub struct CountTokensResponse {
676 input_tokens: u32,
677}
678
679#[cfg(test)]
680mod tests {
681 use super::{AnthropicContentBlock, AnthropicOutputConfig, AnthropicRequest, AnthropicStreamEvent, TextCitation};
682
683 #[test]
684 fn stream_content_block_start_thinking_allows_missing_signature() {
685 let payload = r#"{
686 "type": "content_block_start",
687 "index": 0,
688 "content_block": {
689 "type": "thinking",
690 "thinking": "Drafting plan"
691 }
692 }"#;
693
694 let event: AnthropicStreamEvent = serde_json::from_str(payload).expect("should deserialize thinking block");
695 match event {
696 AnthropicStreamEvent::ContentBlockStart {
697 content_block: AnthropicContentBlock::Thinking { thinking, signature, .. },
698 ..
699 } => {
700 assert_eq!(thinking, "Drafting plan");
701 assert!(signature.is_none());
702 }
703 other => panic!("expected thinking content_block_start, got {other:?}"),
704 }
705 }
706
707 #[test]
708 fn stream_content_block_start_accepts_web_search_tool_result() {
709 let payload = r#"{
710 "type": "content_block_start",
711 "index": 1,
712 "content_block": {
713 "type": "web_search_tool_result",
714 "tool_use_id": "srvtoolu_123",
715 "content": [{
716 "type": "web_search_result",
717 "title": "Rust Releases",
718 "url": "https://blog.rust-lang.org"
719 }]
720 }
721 }"#;
722
723 let event: AnthropicStreamEvent =
724 serde_json::from_str(payload).expect("should deserialize web search tool result");
725 match event {
726 AnthropicStreamEvent::ContentBlockStart {
727 content_block: AnthropicContentBlock::WebSearchToolResult { tool_use_id, .. },
728 ..
729 } => assert_eq!(tool_use_id, "srvtoolu_123"),
730 other => panic!("expected web_search_tool_result content_block_start, got {other:?}"),
731 }
732 }
733
734 #[test]
735 fn stream_content_block_start_accepts_bash_code_execution_result() {
736 let payload = r#"{
737 "type": "content_block_start",
738 "index": 1,
739 "content_block": {
740 "type": "bash_code_execution_tool_result",
741 "tool_use_id": "srvtoolu_456",
742 "content": {
743 "type": "bash_code_execution_result",
744 "stdout": "Python 3.11.12",
745 "stderr": "",
746 "return_code": 0
747 }
748 }
749 }"#;
750
751 let event: AnthropicStreamEvent =
752 serde_json::from_str(payload).expect("should deserialize bash code execution result");
753 match event {
754 AnthropicStreamEvent::ContentBlockStart {
755 content_block: AnthropicContentBlock::BashCodeExecutionToolResult { tool_use_id, .. },
756 ..
757 } => assert_eq!(tool_use_id, "srvtoolu_456"),
758 other => panic!("expected bash_code_execution_tool_result content_block_start, got {other:?}"),
759 }
760 }
761
762 #[test]
763 fn stream_content_block_start_accepts_text_editor_code_execution_result() {
764 let payload = r#"{
765 "type": "content_block_start",
766 "index": 2,
767 "content_block": {
768 "type": "text_editor_code_execution_tool_result",
769 "tool_use_id": "srvtoolu_789",
770 "content": {
771 "type": "text_editor_code_execution_result",
772 "is_file_update": false
773 }
774 }
775 }"#;
776
777 let event: AnthropicStreamEvent =
778 serde_json::from_str(payload).expect("should deserialize text editor code execution result");
779 match event {
780 AnthropicStreamEvent::ContentBlockStart {
781 content_block: AnthropicContentBlock::TextEditorCodeExecutionToolResult { tool_use_id, .. },
782 ..
783 } => assert_eq!(tool_use_id, "srvtoolu_789"),
784 other => panic!("expected text_editor_code_execution_tool_result content_block_start, got {other:?}"),
785 }
786 }
787
788 #[test]
789 fn text_block_accepts_web_search_result_citation() {
790 let payload = r#"{
791 "type": "text",
792 "text": "Rust 1.82 shipped",
793 "citations": [{
794 "type": "web_search_result_location",
795 "url": "https://blog.rust-lang.org",
796 "title": "Rust Blog",
797 "encrypted_index": "enc_123",
798 "cited_text": "Rust 1.82 shipped"
799 }]
800 }"#;
801
802 let block: AnthropicContentBlock = serde_json::from_str(payload).expect("should deserialize cited text block");
803 match block {
804 AnthropicContentBlock::Text { citations: Some(citations), .. } => {
805 assert!(matches!(
806 &citations[0],
807 TextCitation::WebSearchResultLocation {
808 url: Some(url),
809 title: Some(title),
810 encrypted_index: Some(index),
811 cited_text: Some(cited_text),
812 } if url == "https://blog.rust-lang.org"
813 && title == "Rust Blog"
814 && index == "enc_123"
815 && cited_text == "Rust 1.82 shipped"
816 ));
817 }
818 other => panic!("expected text block with citations, got {other:?}"),
819 }
820 }
821
822 #[test]
823 fn empty_output_config_deserializes_to_none() {
824 let request: AnthropicRequest = serde_json::from_str(
825 r#"{
826 "model": "claude-sonnet",
827 "messages": [],
828 "max_tokens": 128,
829 "output_config": {},
830 "stream": false
831 }"#,
832 )
833 .expect("should deserialize request");
834
835 assert!(request.output_config.is_none());
836 }
837
838 #[test]
839 fn boxed_output_config_is_smaller_than_inline_option() {
840 use std::mem::size_of;
841
842 assert!(size_of::<Option<Box<AnthropicOutputConfig>>>() < size_of::<Option<AnthropicOutputConfig>>());
843 }
844}