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, skip_serializing_if = "Option::is_none")]
543 pub(crate) speed: Option<String>,
544 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
545 pub(crate) input_tokens: u32,
546 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
547 pub(crate) output_tokens: u32,
548 pub(crate) cache_creation_input_tokens: Option<u32>,
549 pub(crate) cache_read_input_tokens: Option<u32>,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
553 iterations: Option<Vec<AnthropicUsageIteration>>,
554}
555
556#[derive(Debug, Serialize, Deserialize)]
557#[serde(tag = "type", rename_all = "snake_case")]
558pub enum AnthropicUsageIteration {
559 Message {
560 model: Option<String>,
561 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
562 input_tokens: u32,
563 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
564 output_tokens: u32,
565 cache_creation_input_tokens: Option<u32>,
566 cache_read_input_tokens: Option<u32>,
567 },
568 FallbackMessage {
569 model: Option<String>,
570 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
571 input_tokens: u32,
572 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
573 output_tokens: u32,
574 cache_creation_input_tokens: Option<u32>,
575 cache_read_input_tokens: Option<u32>,
576 },
577 AdvisorMessage {
578 model: Option<String>,
579 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
580 input_tokens: u32,
581 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
582 output_tokens: u32,
583 cache_creation_input_tokens: Option<u32>,
584 cache_read_input_tokens: Option<u32>,
585 },
586 Compaction {
587 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
588 input_tokens: u32,
589 #[serde(default, deserialize_with = "deserialize_nullable_u32")]
590 output_tokens: u32,
591 cache_creation_input_tokens: Option<u32>,
592 cache_read_input_tokens: Option<u32>,
593 },
594 #[serde(other)]
596 Unknown,
597}
598
599fn deserialize_nullable_u32<'de, D>(deserializer: D) -> Result<u32, D::Error>
600where
601 D: Deserializer<'de>,
602{
603 Option::<u32>::deserialize(deserializer).map(Option::unwrap_or_default)
604}
605
606#[derive(Debug, Deserialize)]
607pub struct AnthropicErrorBody {
608 #[serde(rename = "type")]
609 error_type: String,
610 pub(crate) message: String,
611}
612
613#[derive(Debug, Serialize, Deserialize, Clone)]
615pub struct AnthropicOutputConfig {
616 #[serde(skip_serializing_if = "Option::is_none")]
617 pub(crate) effort: Option<String>,
618 #[serde(skip_serializing_if = "Option::is_none")]
619 pub(crate) task_budget: Option<AnthropicTaskBudget>,
620 #[serde(skip_serializing_if = "Option::is_none")]
621 pub(crate) format: Option<AnthropicOutputFormat>,
622}
623
624impl AnthropicOutputConfig {
625 fn is_empty(&self) -> bool {
626 self.effort.is_none() && self.task_budget.is_none() && self.format.is_none()
627 }
628
629 fn into_boxed_if_non_empty(self) -> Option<Box<Self>> {
630 (!self.is_empty()).then_some(Box::new(self))
631 }
632}
633
634#[derive(Debug, Serialize, Deserialize, Clone)]
635pub struct AnthropicTaskBudget {
636 #[serde(rename = "type")]
637 pub(crate) budget_type: String,
638 pub(crate) total: u32,
639}
640
641#[derive(Debug, Serialize, Deserialize, Clone)]
643#[serde(tag = "type", rename_all = "snake_case")]
644pub enum AnthropicOutputFormat {
645 JsonSchema {
646 schema: Value,
647 },
648 #[serde(other)]
650 Unknown,
651}
652
653#[derive(Debug, Serialize, Deserialize)]
656pub struct CountTokensRequest {
657 model: String,
658 #[serde(skip_serializing_if = "Option::is_none")]
659 system: Option<Value>,
660 messages: Vec<AnthropicMessage>,
661 #[serde(skip_serializing_if = "Option::is_none")]
662 tools: Option<Vec<AnthropicTool>>,
663 #[serde(skip_serializing_if = "Option::is_none")]
664 thinking: Option<ThinkingConfig>,
665}
666
667fn deserialize_boxed_output_config_opt<'de, D>(deserializer: D) -> Result<Option<Box<AnthropicOutputConfig>>, D::Error>
668where
669 D: Deserializer<'de>,
670{
671 Option::<AnthropicOutputConfig>::deserialize(deserializer)
672 .map(|value| value.and_then(AnthropicOutputConfig::into_boxed_if_non_empty))
673}
674
675#[derive(Debug, Deserialize)]
677pub struct CountTokensResponse {
678 input_tokens: u32,
679}
680
681#[cfg(test)]
682mod tests {
683 use super::{AnthropicContentBlock, AnthropicOutputConfig, AnthropicRequest, AnthropicStreamEvent, TextCitation};
684
685 #[test]
686 fn stream_content_block_start_thinking_allows_missing_signature() {
687 let payload = r#"{
688 "type": "content_block_start",
689 "index": 0,
690 "content_block": {
691 "type": "thinking",
692 "thinking": "Drafting plan"
693 }
694 }"#;
695
696 let event: AnthropicStreamEvent = serde_json::from_str(payload).expect("should deserialize thinking block");
697 match event {
698 AnthropicStreamEvent::ContentBlockStart {
699 content_block: AnthropicContentBlock::Thinking { thinking, signature, .. },
700 ..
701 } => {
702 assert_eq!(thinking, "Drafting plan");
703 assert!(signature.is_none());
704 }
705 other => panic!("expected thinking content_block_start, got {other:?}"),
706 }
707 }
708
709 #[test]
710 fn stream_content_block_start_accepts_web_search_tool_result() {
711 let payload = r#"{
712 "type": "content_block_start",
713 "index": 1,
714 "content_block": {
715 "type": "web_search_tool_result",
716 "tool_use_id": "srvtoolu_123",
717 "content": [{
718 "type": "web_search_result",
719 "title": "Rust Releases",
720 "url": "https://blog.rust-lang.org"
721 }]
722 }
723 }"#;
724
725 let event: AnthropicStreamEvent =
726 serde_json::from_str(payload).expect("should deserialize web search tool result");
727 match event {
728 AnthropicStreamEvent::ContentBlockStart {
729 content_block: AnthropicContentBlock::WebSearchToolResult { tool_use_id, .. },
730 ..
731 } => assert_eq!(tool_use_id, "srvtoolu_123"),
732 other => panic!("expected web_search_tool_result content_block_start, got {other:?}"),
733 }
734 }
735
736 #[test]
737 fn stream_content_block_start_accepts_bash_code_execution_result() {
738 let payload = r#"{
739 "type": "content_block_start",
740 "index": 1,
741 "content_block": {
742 "type": "bash_code_execution_tool_result",
743 "tool_use_id": "srvtoolu_456",
744 "content": {
745 "type": "bash_code_execution_result",
746 "stdout": "Python 3.11.12",
747 "stderr": "",
748 "return_code": 0
749 }
750 }
751 }"#;
752
753 let event: AnthropicStreamEvent =
754 serde_json::from_str(payload).expect("should deserialize bash code execution result");
755 match event {
756 AnthropicStreamEvent::ContentBlockStart {
757 content_block: AnthropicContentBlock::BashCodeExecutionToolResult { tool_use_id, .. },
758 ..
759 } => assert_eq!(tool_use_id, "srvtoolu_456"),
760 other => panic!("expected bash_code_execution_tool_result content_block_start, got {other:?}"),
761 }
762 }
763
764 #[test]
765 fn stream_content_block_start_accepts_text_editor_code_execution_result() {
766 let payload = r#"{
767 "type": "content_block_start",
768 "index": 2,
769 "content_block": {
770 "type": "text_editor_code_execution_tool_result",
771 "tool_use_id": "srvtoolu_789",
772 "content": {
773 "type": "text_editor_code_execution_result",
774 "is_file_update": false
775 }
776 }
777 }"#;
778
779 let event: AnthropicStreamEvent =
780 serde_json::from_str(payload).expect("should deserialize text editor code execution result");
781 match event {
782 AnthropicStreamEvent::ContentBlockStart {
783 content_block: AnthropicContentBlock::TextEditorCodeExecutionToolResult { tool_use_id, .. },
784 ..
785 } => assert_eq!(tool_use_id, "srvtoolu_789"),
786 other => panic!("expected text_editor_code_execution_tool_result content_block_start, got {other:?}"),
787 }
788 }
789
790 #[test]
791 fn text_block_accepts_web_search_result_citation() {
792 let payload = r#"{
793 "type": "text",
794 "text": "Rust 1.82 shipped",
795 "citations": [{
796 "type": "web_search_result_location",
797 "url": "https://blog.rust-lang.org",
798 "title": "Rust Blog",
799 "encrypted_index": "enc_123",
800 "cited_text": "Rust 1.82 shipped"
801 }]
802 }"#;
803
804 let block: AnthropicContentBlock = serde_json::from_str(payload).expect("should deserialize cited text block");
805 match block {
806 AnthropicContentBlock::Text { citations: Some(citations), .. } => {
807 assert!(matches!(
808 &citations[0],
809 TextCitation::WebSearchResultLocation {
810 url: Some(url),
811 title: Some(title),
812 encrypted_index: Some(index),
813 cited_text: Some(cited_text),
814 } if url == "https://blog.rust-lang.org"
815 && title == "Rust Blog"
816 && index == "enc_123"
817 && cited_text == "Rust 1.82 shipped"
818 ));
819 }
820 other => panic!("expected text block with citations, got {other:?}"),
821 }
822 }
823
824 #[test]
825 fn empty_output_config_deserializes_to_none() {
826 let request: AnthropicRequest = serde_json::from_str(
827 r#"{
828 "model": "claude-sonnet",
829 "messages": [],
830 "max_tokens": 128,
831 "output_config": {},
832 "stream": false
833 }"#,
834 )
835 .expect("should deserialize request");
836
837 assert!(request.output_config.is_none());
838 }
839
840 #[test]
841 fn boxed_output_config_is_smaller_than_inline_option() {
842 use std::mem::size_of;
843
844 assert!(size_of::<Option<Box<AnthropicOutputConfig>>>() < size_of::<Option<AnthropicOutputConfig>>());
845 }
846}