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) temperature: Option<f32>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub(crate) tools: Option<Vec<AnthropicTool>>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub(crate) tool_choice: Option<Value>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub(crate) thinking: Option<ThinkingConfig>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub(crate) reasoning: Option<Value>, #[serde(
43 default,
44 deserialize_with = "deserialize_boxed_output_config_opt",
45 skip_serializing_if = "Option::is_none"
46 )]
47 pub(crate) output_config: Option<Box<AnthropicOutputConfig>>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub(crate) context_management: Option<Value>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub(crate) fallbacks: Option<AnthropicFallbacksParam>,
52 #[serde(skip_serializing_if = "Option::is_none")]
55 pub(crate) fallback_credit_token: Option<String>,
56 pub(crate) stream: bool,
57}
58
59#[derive(Debug, Serialize, Deserialize, Clone)]
63#[serde(untagged)]
64pub enum AnthropicFallbacksParam {
65 Mode(AnthropicFallbacksKeyword),
66 Models(Vec<AnthropicFallbackParam>),
67}
68
69#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
70#[serde(rename_all = "lowercase")]
71pub enum AnthropicFallbacksKeyword {
72 Default,
73}
74
75impl AnthropicFallbacksParam {
76 pub(crate) fn models(&self) -> &[AnthropicFallbackParam] {
78 match self {
79 Self::Mode(_) => &[],
80 Self::Models(models) => models,
81 }
82 }
83}
84
85#[derive(Debug, Serialize, Deserialize, Clone)]
86pub struct AnthropicFallbackParam {
87 pub(crate) model: String,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub(crate) max_tokens: Option<u32>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub(crate) thinking: Option<ThinkingConfig>,
92}
93
94#[derive(Debug, Serialize, Deserialize, Clone)]
95#[serde(tag = "type", rename_all = "lowercase")]
96pub enum ThinkingConfig {
97 Enabled {
98 budget_tokens: u32,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 display: Option<ThinkingDisplay>,
101 },
102 Adaptive {
103 #[serde(skip_serializing_if = "Option::is_none")]
104 display: Option<ThinkingDisplay>,
105 },
106 Disabled,
107 #[serde(rename = "between_tools")]
113 BetweenTools,
114 #[serde(other)]
116 Unknown,
117}
118
119#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
121#[serde(rename_all = "lowercase")]
122pub enum ThinkingDisplay {
123 Summarized,
125 Omitted,
129 Updates,
134 #[serde(other)]
136 Unknown,
137}
138
139#[derive(Debug, Serialize, Deserialize)]
140pub struct AnthropicMessage {
141 pub(crate) role: String,
142 pub(crate) content: Vec<AnthropicContentBlock>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub(crate) clear_at: Option<MessageClearAt>,
146}
147
148#[derive(Debug, Serialize, Deserialize, Clone)]
149#[serde(tag = "type")]
150pub enum AnthropicContentBlock {
151 #[serde(rename = "text")]
152 Text {
153 text: String,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 citations: Option<Vec<TextCitation>>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 cache_control: Option<CacheControl>,
158 },
159 #[serde(rename = "image")]
160 Image {
161 source: ImageSource,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 cache_control: Option<CacheControl>,
164 },
165 #[serde(rename = "tool_use")]
166 ToolUse(Box<AnthropicToolUseBlock>),
167 #[serde(rename = "tool_result")]
168 ToolResult(Box<AnthropicToolResultBlock>),
169 #[serde(rename = "thinking")]
170 Thinking {
171 thinking: String,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 signature: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 cache_control: Option<CacheControl>,
176 },
177 #[serde(rename = "redacted_thinking")]
178 RedactedThinking {
179 data: String,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 cache_control: Option<CacheControl>,
182 },
183 #[serde(rename = "compaction")]
184 Compaction {
185 content: Option<String>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 signature: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 cache_control: Option<CacheControl>,
193 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
196 extra: Map<String, Value>,
197 },
198 #[serde(rename = "server_tool_use")]
200 ServerToolUse { id: String, name: String, input: Value },
201 #[serde(rename = "tool_search_tool_result")]
203 ToolSearchToolResult {
204 tool_use_id: String,
205 content: ToolSearchResultContent,
206 },
207 #[serde(rename = "container_upload")]
209 ContainerUpload { file_id: String },
210 #[serde(rename = "code_execution_tool_result")]
212 CodeExecutionToolResult { tool_use_id: String, content: Value },
213 #[serde(rename = "bash_code_execution_tool_result")]
215 BashCodeExecutionToolResult { tool_use_id: String, content: Value },
216 #[serde(rename = "text_editor_code_execution_tool_result")]
218 TextEditorCodeExecutionToolResult { tool_use_id: String, content: Value },
219 #[serde(rename = "web_search_tool_result")]
221 WebSearchToolResult { tool_use_id: String, content: Value },
222 #[serde(rename = "advisor_tool_result")]
226 AdvisorToolResult { tool_use_id: String, content: Value },
227 #[serde(rename = "fallback")]
229 Fallback {
230 from: AnthropicFallbackModel,
231 to: AnthropicFallbackModel,
232 },
233 #[serde(other)]
235 Unknown,
236}
237
238#[derive(Debug, Serialize, Deserialize, Clone)]
240pub struct AnthropicToolUseBlock {
241 pub(crate) id: String,
242 pub(crate) name: String,
243 pub(crate) input: Value,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub(crate) cache_control: Option<CacheControl>,
246}
247
248#[derive(Debug, Serialize, Deserialize, Clone)]
250pub struct AnthropicToolResultBlock {
251 pub(crate) tool_use_id: String,
252 pub(crate) content: Value, #[serde(skip_serializing_if = "Option::is_none")]
254 pub(crate) is_error: Option<bool>,
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub(crate) cache_control: Option<CacheControl>,
257}
258
259#[derive(Debug, Serialize, Deserialize, Clone)]
261#[serde(tag = "type")]
262pub enum ToolSearchResultContent {
263 #[serde(rename = "tool_search_tool_search_result")]
264 SearchResult { tool_references: Vec<ToolReference> },
265 #[serde(rename = "tool_search_tool_result_error")]
266 Error { error_code: String },
267 #[serde(other)]
269 Unknown,
270}
271
272#[derive(Debug, Serialize, Deserialize, Clone)]
274pub struct ToolReference {
275 #[serde(rename = "type")]
276 ref_type: Option<String>, tool_name: String,
278}
279
280#[derive(Debug, Serialize, Deserialize, Clone)]
281#[serde(tag = "type")]
282pub enum TextCitation {
283 #[serde(rename = "char_location")]
284 CharLocation {
285 cited_text: String,
286 document_index: usize,
287 document_title: Option<String>,
288 start_char_index: usize,
289 end_char_index: usize,
290 },
291 #[serde(rename = "page_location")]
292 PageLocation {
293 cited_text: String,
294 document_index: usize,
295 document_title: Option<String>,
296 start_page_number: usize,
297 end_page_number: usize,
298 },
299 #[serde(rename = "content_block_location")]
300 ContentBlockLocation {
301 cited_text: String,
302 document_index: usize,
303 document_title: Option<String>,
304 start_block_index: usize,
305 end_block_index: usize,
306 },
307 #[serde(rename = "web_search_result_location")]
308 WebSearchResultLocation {
309 url: Option<String>,
310 title: Option<String>,
311 encrypted_index: Option<String>,
312 cited_text: Option<String>,
313 },
314 #[serde(other)]
316 Unknown,
317}
318
319#[derive(Debug, Serialize, Deserialize, Clone)]
320pub struct ImageSource {
321 #[serde(rename = "type")]
322 pub(crate) source_type: String, pub(crate) media_type: String,
324 pub(crate) data: String,
325}
326
327#[derive(Debug, Serialize, Deserialize, Clone)]
328pub struct CacheControl {
329 #[serde(rename = "type")]
330 pub(crate) control_type: Cow<'static, str>, #[serde(skip_serializing_if = "Option::is_none")]
332 pub(crate) ttl: Option<Cow<'static, str>>, }
334
335#[derive(Debug, Serialize, Deserialize, Clone)]
338#[serde(untagged)]
339pub enum AnthropicTool {
340 ToolSearch(AnthropicToolSearchTool),
342 CodeExecution(AnthropicCodeExecutionTool),
344 Memory(AnthropicMemoryTool),
346 WebSearch(AnthropicWebSearchTool),
348 Advisor(AnthropicAdvisorTool),
350 Function(AnthropicFunctionTool),
352}
353
354#[derive(Debug, Serialize, Deserialize, Clone)]
356pub struct AnthropicFunctionTool {
357 pub(crate) name: String,
358 pub(crate) description: String,
359 pub(crate) input_schema: Value,
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub(crate) input_examples: Option<Vec<Value>>,
362 #[serde(skip_serializing_if = "Option::is_none")]
363 pub(crate) strict: Option<bool>,
364 #[serde(skip_serializing_if = "Option::is_none")]
365 pub(crate) allowed_callers: Option<Vec<String>>,
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub(crate) cache_control: Option<CacheControl>,
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub(crate) defer_loading: Option<bool>,
371}
372
373#[derive(Debug, Serialize, Deserialize, Clone)]
375pub struct AnthropicCodeExecutionTool {
376 #[serde(rename = "type")]
378 pub(crate) tool_type: String,
379 pub(crate) name: String,
381}
382
383#[derive(Debug, Serialize, Deserialize, Clone)]
385pub struct AnthropicMemoryTool {
386 #[serde(rename = "type")]
388 pub(crate) tool_type: String,
389 pub(crate) name: String,
391}
392
393#[derive(Debug, Serialize, Deserialize, Clone)]
395pub struct AnthropicToolSearchTool {
396 #[serde(rename = "type")]
398 pub(crate) tool_type: String,
399 pub(crate) name: String,
401}
402
403#[derive(Debug, Serialize, Deserialize, Clone)]
405pub struct AnthropicWebSearchTool {
406 #[serde(rename = "type")]
408 pub(crate) tool_type: String,
409 pub(crate) name: String,
411 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
413 pub(crate) options: Map<String, Value>,
414}
415
416#[derive(Debug, Serialize, Deserialize, Clone)]
422pub struct AnthropicAdvisorTool {
423 #[serde(rename = "type")]
425 pub(crate) tool_type: String, pub(crate) name: String, pub(crate) model: String,
430 #[serde(skip_serializing_if = "Option::is_none")]
432 pub(crate) max_uses: Option<u32>,
433 #[serde(skip_serializing_if = "Option::is_none")]
435 pub(crate) max_tokens: Option<u32>,
436 #[serde(skip_serializing_if = "Option::is_none")]
438 pub(crate) caching: Option<AnthropicAdvisorCaching>,
439}
440
441#[derive(Debug, Serialize, Deserialize, Clone)]
443pub struct AnthropicAdvisorCaching {
444 #[serde(rename = "type")]
446 pub(crate) cache_type: String, pub(crate) ttl: String,
449}
450
451#[derive(Debug, Deserialize)]
452#[serde(tag = "type")]
453pub enum AnthropicStreamEvent {
454 #[serde(rename = "message_start")]
455 MessageStart { message: AnthropicMessageResponse },
456 #[serde(rename = "content_block_start")]
457 ContentBlockStart {
458 index: usize,
459 content_block: AnthropicContentBlock,
460 },
461 #[serde(rename = "ping")]
462 Ping,
463 #[serde(rename = "content_block_delta")]
464 ContentBlockDelta { index: usize, delta: AnthropicStreamDelta },
465 #[serde(rename = "content_block_stop")]
466 ContentBlockStop { index: usize },
467 #[serde(rename = "message_delta")]
468 MessageDelta {
469 delta: AnthropicMessageDelta,
470 usage: Option<AnthropicUsage>,
471 },
472 #[serde(rename = "message_stop")]
473 MessageStop,
474 #[serde(rename = "error")]
475 Error { error: AnthropicErrorBody },
476 #[serde(other)]
478 Unknown,
479}
480
481#[derive(Debug, Deserialize)]
482#[serde(tag = "type")]
483pub enum AnthropicStreamDelta {
484 #[serde(rename = "text_delta")]
485 TextDelta { text: String },
486 #[serde(rename = "input_json_delta")]
487 InputJsonDelta { partial_json: String },
488 #[serde(rename = "thinking_delta")]
489 ThinkingDelta { thinking: String },
490 #[serde(rename = "signature_delta")]
491 SignatureDelta { signature: String },
492 #[serde(rename = "compaction_delta")]
493 CompactionDelta {
494 content: Option<String>,
495 #[serde(flatten, default)]
497 extra: Map<String, Value>,
498 },
499 #[serde(other)]
501 Unknown,
502}
503
504#[derive(Debug, Deserialize)]
505pub struct AnthropicMessageDelta {
506 pub(crate) stop_reason: Option<String>,
507 stop_sequence: Option<String>,
508 #[serde(default)]
511 pub(crate) stop_details: Option<Value>,
512}
513
514#[derive(Debug, Deserialize, Clone)]
515pub struct AnthropicStopDetails {
516 #[serde(rename = "type")]
517 stop_details_type: Option<String>,
518 category: Option<String>,
519 explanation: Option<String>,
520 fallback_credit_token: Option<String>,
523 fallback_has_prefill_claim: Option<bool>,
526}
527
528#[derive(Debug, Deserialize)]
529pub struct AnthropicMessageResponse {
530 id: String,
531 role: String,
532 content: Vec<AnthropicContentBlock>,
533 model: String,
534 stop_reason: Option<String>,
535 stop_sequence: Option<String>,
536 stop_details: Option<AnthropicStopDetails>,
537 pub(crate) usage: AnthropicUsage,
538}
539
540#[derive(Debug, Serialize, Deserialize)]
541pub struct AnthropicUsage {
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}