1use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10use validator::Validate;
11
12use crate::{common::GenerationRequest, validated::Normalizable};
13
14#[serde_with::skip_serializing_none]
22#[derive(Debug, Clone, Serialize, Deserialize, Validate, schemars::JsonSchema)]
23#[validate(schema(function = "validate_message_request"))]
24pub struct CreateMessageRequest {
25 #[validate(length(min = 1, message = "model field is required and cannot be empty"))]
27 pub model: String,
28
29 #[validate(length(min = 1, message = "messages array is required and cannot be empty"))]
31 pub messages: Vec<InputMessage>,
32
33 #[validate(range(min = 1, message = "max_tokens must be greater than 0"))]
35 pub max_tokens: u32,
36
37 pub metadata: Option<Metadata>,
39
40 pub service_tier: Option<ServiceTier>,
42
43 pub stop_sequences: Option<Vec<String>>,
45
46 pub stream: Option<bool>,
48
49 pub system: Option<SystemContent>,
51
52 pub temperature: Option<f64>,
54
55 pub thinking: Option<ThinkingConfig>,
57
58 pub tool_choice: Option<ToolChoice>,
60
61 pub tools: Option<Vec<Tool>>,
63
64 pub top_k: Option<u32>,
66
67 pub top_p: Option<f64>,
69
70 pub container: Option<ContainerConfig>,
73
74 pub mcp_servers: Option<Vec<McpServerConfig>>,
76
77 pub rid: Option<String>,
79
80 #[serde(flatten)]
83 pub other: Map<String, Value>,
84}
85
86impl Normalizable for CreateMessageRequest {
87 }
89
90impl CreateMessageRequest {
91 pub fn is_stream(&self) -> bool {
93 self.stream.unwrap_or(false)
94 }
95
96 pub fn get_model(&self) -> &str {
98 &self.model
99 }
100
101 pub fn has_mcp_toolset(&self) -> bool {
103 self.tools
104 .as_ref()
105 .is_some_and(|tools| tools.iter().any(|t| matches!(t, Tool::McpToolset(_))))
106 }
107
108 pub fn mcp_server_configs(&self) -> Option<&[McpServerConfig]> {
110 self.mcp_servers
111 .as_deref()
112 .filter(|servers| !servers.is_empty())
113 }
114}
115
116impl GenerationRequest for CreateMessageRequest {
117 fn rid(&self) -> Option<&str> {
118 self.rid.as_deref()
119 }
120
121 fn is_stream(&self) -> bool {
122 self.stream.unwrap_or(false)
123 }
124
125 fn get_model(&self) -> Option<&str> {
126 Some(&self.model)
127 }
128
129 fn extract_text_for_routing(&self) -> String {
130 let mut buffer = String::new();
131 let mut has_content = false;
132
133 let push = |s: &str, has_content: &mut bool, buffer: &mut String| {
134 if s.is_empty() {
135 return;
136 }
137 if *has_content {
138 buffer.push(' ');
139 }
140 buffer.push_str(s);
141 *has_content = true;
142 };
143
144 if let Some(system) = &self.system {
145 match system {
146 SystemContent::String(s) => push(s, &mut has_content, &mut buffer),
147 SystemContent::Blocks(blocks) => {
148 for block in blocks {
149 let SystemContentBlock::Text(text_block) = block;
150 push(&text_block.text, &mut has_content, &mut buffer);
151 }
152 }
153 }
154 }
155
156 for msg in &self.messages {
157 match &msg.content {
158 InputContent::String(s) => push(s, &mut has_content, &mut buffer),
159 InputContent::Blocks(blocks) => {
160 for block in blocks {
161 if let InputContentBlock::Text(text_block) = block {
162 push(&text_block.text, &mut has_content, &mut buffer);
163 }
164 }
165 }
166 }
167 }
168
169 buffer
170 }
171}
172
173impl Tool {
174 fn matches_tool_choice_name(&self, name: &str) -> bool {
175 match self {
176 Self::Custom(tool) => tool.name == name,
177 Self::ToolSearch(tool) => tool.name == name,
178 Self::Bash(tool) => tool.name == name,
179 Self::TextEditor(tool) => tool.name == name,
180 Self::WebSearch(tool) => tool.name == name,
181 Self::McpToolset(toolset) => {
182 let default_enabled = toolset
183 .default_config
184 .as_ref()
185 .and_then(|config| config.enabled)
186 .unwrap_or(true);
187
188 toolset
189 .configs
190 .as_ref()
191 .and_then(|configs| configs.get(name))
192 .and_then(|config| config.enabled)
193 .unwrap_or(default_enabled)
194 }
195 }
196 }
197}
198fn validate_message_request(req: &CreateMessageRequest) -> Result<(), validator::ValidationError> {
200 if req.has_mcp_toolset() && req.mcp_server_configs().is_none() {
201 let mut e = validator::ValidationError::new("mcp_servers_required");
202 e.message = Some("mcp_servers is required when mcp_toolset tools are present".into());
203 return Err(e);
204 }
205
206 let Some(tool_choice) = &req.tool_choice else {
207 return Ok(());
208 };
209
210 let has_tools = req.tools.as_ref().is_some_and(|tools| !tools.is_empty());
211 let requires_tools = !matches!(tool_choice, ToolChoice::None);
212
213 if requires_tools && !has_tools {
214 let mut e = validator::ValidationError::new("tool_choice_requires_tools");
215 e.message = Some(
216 "Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified."
217 .into(),
218 );
219 return Err(e);
220 }
221
222 if let ToolChoice::Tool { name, .. } = tool_choice {
223 let tool_exists = req
224 .tools
225 .as_ref()
226 .is_some_and(|tools| tools.iter().any(|tool| tool.matches_tool_choice_name(name)));
227
228 if !tool_exists {
229 let mut e = validator::ValidationError::new("tool_choice_tool_not_found");
230 e.message = Some(
231 format!("Invalid value for 'tool_choice': tool '{name}' not found in 'tools'.")
232 .into(),
233 );
234 return Err(e);
235 }
236 }
237
238 Ok(())
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
243pub struct Metadata {
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub user_id: Option<String>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
251#[serde(rename_all = "snake_case")]
252pub enum ServiceTier {
253 Auto,
254 StandardOnly,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
259#[serde(untagged)]
260pub enum SystemContent {
261 String(String),
262 Blocks(Vec<SystemContentBlock>),
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
268#[serde(tag = "type", rename_all = "snake_case")]
269pub enum SystemContentBlock {
270 Text(TextBlock),
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
275pub struct InputMessage {
276 pub role: Role,
278
279 pub content: InputContent,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
285#[serde(rename_all = "lowercase")]
286pub enum Role {
287 User,
288 Assistant,
289 System,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
304#[serde(untagged)]
305pub enum InputContent {
306 String(String),
307 Blocks(Vec<InputContentBlock>),
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
316#[serde(tag = "type", rename_all = "snake_case")]
317pub enum InputContentBlock {
318 Text(TextBlock),
320 Image(ImageBlock),
322 Document(DocumentBlock),
324 ToolUse(ToolUseBlock),
326 ToolResult(ToolResultBlock),
328 Thinking(ThinkingBlock),
330 RedactedThinking(RedactedThinkingBlock),
332 ServerToolUse(ServerToolUseBlock),
334 SearchResult(SearchResultBlock),
336 WebSearchToolResult(WebSearchToolResultBlock),
338 ToolSearchToolResult(ToolSearchToolResultBlock),
340 ToolReference(ToolReferenceBlock),
342}
343
344#[serde_with::skip_serializing_none]
346#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
347pub struct TextBlock {
348 pub text: String,
350
351 pub cache_control: Option<CacheControl>,
353
354 pub citations: Option<Vec<Citation>>,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
360pub struct ImageBlock {
361 pub source: ImageSource,
363
364 #[serde(skip_serializing_if = "Option::is_none")]
366 pub cache_control: Option<CacheControl>,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
371#[serde(tag = "type", rename_all = "snake_case")]
372pub enum ImageSource {
373 Base64 { media_type: String, data: String },
374 Url { url: String },
375}
376
377#[serde_with::skip_serializing_none]
379#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
380pub struct DocumentBlock {
381 pub source: DocumentSource,
383
384 pub cache_control: Option<CacheControl>,
386
387 pub title: Option<String>,
389
390 pub context: Option<String>,
392
393 pub citations: Option<CitationsConfig>,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
399#[serde(tag = "type", rename_all = "snake_case")]
400pub enum DocumentSource {
401 Base64 { media_type: String, data: String },
402 Text { data: String },
403 Url { url: String },
404 Content { content: Vec<InputContentBlock> },
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
409pub struct ToolUseBlock {
410 pub id: String,
412
413 pub name: String,
415
416 pub input: Value,
418
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub cache_control: Option<CacheControl>,
422}
423
424#[serde_with::skip_serializing_none]
426#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
427pub struct ToolResultBlock {
428 pub tool_use_id: String,
430
431 pub content: Option<ToolResultContent>,
433
434 pub is_error: Option<bool>,
436
437 pub cache_control: Option<CacheControl>,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
443#[serde(untagged)]
444pub enum ToolResultContent {
445 String(String),
446 Blocks(Vec<ToolResultContentBlock>),
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
451#[serde(tag = "type", rename_all = "snake_case")]
452pub enum ToolResultContentBlock {
453 Text(TextBlock),
454 Image(ImageBlock),
455 Document(DocumentBlock),
456 SearchResult(SearchResultBlock),
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
461pub struct ThinkingBlock {
462 pub thinking: String,
464
465 pub signature: String,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
471pub struct RedactedThinkingBlock {
472 pub data: String,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
478pub struct ServerToolUseBlock {
479 pub id: String,
481
482 pub name: String,
484
485 pub input: Value,
487
488 #[serde(skip_serializing_if = "Option::is_none")]
490 pub cache_control: Option<CacheControl>,
491}
492
493#[serde_with::skip_serializing_none]
495#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
496pub struct SearchResultBlock {
497 pub source: String,
499
500 pub title: String,
502
503 pub content: Vec<TextBlock>,
505
506 pub cache_control: Option<CacheControl>,
508
509 pub citations: Option<CitationsConfig>,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
515pub struct WebSearchToolResultBlock {
516 pub tool_use_id: String,
518
519 pub content: WebSearchToolResultContent,
521
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub cache_control: Option<CacheControl>,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
529#[serde(untagged)]
530pub enum WebSearchToolResultContent {
531 Results(Vec<WebSearchResultBlock>),
532 Error(WebSearchToolResultError),
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
537pub struct WebSearchResultBlock {
538 pub title: String,
540
541 pub url: String,
543
544 pub encrypted_content: String,
546
547 #[serde(skip_serializing_if = "Option::is_none")]
549 pub page_age: Option<String>,
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
554pub struct WebSearchToolResultError {
555 #[serde(rename = "type")]
556 pub error_type: String,
557 pub error_code: WebSearchToolResultErrorCode,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
562#[serde(rename_all = "snake_case")]
563pub enum WebSearchToolResultErrorCode {
564 InvalidToolInput,
565 Unavailable,
566 MaxUsesExceeded,
567 TooManyRequests,
568 QueryTooLong,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
573#[serde(tag = "type", rename_all = "snake_case")]
574pub enum CacheControl {
575 Ephemeral,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
580pub struct CitationsConfig {
581 #[serde(skip_serializing_if = "Option::is_none")]
582 pub enabled: Option<bool>,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
587#[serde(tag = "type", rename_all = "snake_case")]
588#[expect(
589 clippy::enum_variant_names,
590 reason = "variant names match the OpenAI API citation type discriminators (char_location, page_location, etc.)"
591)]
592pub enum Citation {
593 CharLocation(CharLocationCitation),
594 PageLocation(PageLocationCitation),
595 ContentBlockLocation(ContentBlockLocationCitation),
596 WebSearchResultLocation(WebSearchResultLocationCitation),
597 SearchResultLocation(SearchResultLocationCitation),
598}
599
600#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
602pub struct CharLocationCitation {
603 pub cited_text: String,
604 pub document_index: u32,
605 pub document_title: Option<String>,
606 pub start_char_index: u32,
607 pub end_char_index: u32,
608 #[serde(skip_serializing_if = "Option::is_none")]
609 pub file_id: Option<String>,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
614pub struct PageLocationCitation {
615 pub cited_text: String,
616 pub document_index: u32,
617 pub document_title: Option<String>,
618 pub start_page_number: u32,
619 pub end_page_number: u32,
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
624pub struct ContentBlockLocationCitation {
625 pub cited_text: String,
626 pub document_index: u32,
627 pub document_title: Option<String>,
628 pub start_block_index: u32,
629 pub end_block_index: u32,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
634pub struct WebSearchResultLocationCitation {
635 pub cited_text: String,
636 pub url: String,
637 pub title: Option<String>,
638 pub encrypted_index: String,
639}
640
641#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
643pub struct SearchResultLocationCitation {
644 pub cited_text: String,
645 pub search_result_index: u32,
646 pub source: String,
647 pub title: Option<String>,
648 pub start_block_index: u32,
649 pub end_block_index: u32,
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
658#[serde(untagged)]
659#[expect(
660 clippy::enum_variant_names,
661 reason = "ToolSearch matches Anthropic API naming"
662)]
663#[schemars(rename = "MessagesTool")]
664pub enum Tool {
665 McpToolset(McpToolset),
667 Custom(CustomTool),
672 ToolSearch(ToolSearchTool),
674 Bash(BashTool),
676 TextEditor(TextEditorTool),
678 WebSearch(WebSearchTool),
680}
681
682#[serde_with::skip_serializing_none]
684#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
685pub struct CustomTool {
686 pub name: String,
688
689 #[serde(rename = "type")]
691 pub tool_type: Option<String>,
692
693 pub description: Option<String>,
695
696 pub input_schema: InputSchema,
698
699 pub defer_loading: Option<bool>,
701
702 pub cache_control: Option<CacheControl>,
704}
705
706#[serde_with::skip_serializing_none]
708#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
709pub struct InputSchema {
710 #[serde(rename = "type")]
711 pub schema_type: String,
712
713 pub properties: Option<HashMap<String, Value>>,
714
715 pub required: Option<Vec<String>>,
716
717 #[serde(flatten)]
719 pub additional: HashMap<String, Value>,
720}
721
722#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
724pub struct BashTool {
725 #[serde(rename = "type")]
726 pub tool_type: String, pub name: String, #[serde(skip_serializing_if = "Option::is_none")]
731 pub cache_control: Option<CacheControl>,
732}
733
734#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
736pub struct TextEditorTool {
737 #[serde(rename = "type")]
738 pub tool_type: String, pub name: String, #[serde(skip_serializing_if = "Option::is_none")]
743 pub cache_control: Option<CacheControl>,
744}
745
746#[serde_with::skip_serializing_none]
748#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
749pub struct WebSearchTool {
750 #[serde(rename = "type")]
751 pub tool_type: String, pub name: String, pub allowed_domains: Option<Vec<String>>,
756
757 pub blocked_domains: Option<Vec<String>>,
758
759 pub max_uses: Option<u32>,
760
761 pub user_location: Option<UserLocation>,
762
763 pub cache_control: Option<CacheControl>,
764}
765
766#[serde_with::skip_serializing_none]
768#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
769pub struct UserLocation {
770 #[serde(rename = "type")]
771 pub location_type: String, pub city: Option<String>,
774
775 pub region: Option<String>,
776
777 pub country: Option<String>,
778
779 pub timezone: Option<String>,
780}
781
782#[serde_with::skip_serializing_none]
788#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
789#[serde(tag = "type", rename_all = "snake_case")]
790#[schemars(rename = "MessagesToolChoice")]
791pub enum ToolChoice {
792 Auto {
794 disable_parallel_tool_use: Option<bool>,
795 },
796 Any {
798 disable_parallel_tool_use: Option<bool>,
799 },
800 Tool {
802 name: String,
803 disable_parallel_tool_use: Option<bool>,
804 },
805 None,
807}
808
809#[serde_with::skip_serializing_none]
815#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
816#[serde(tag = "type", rename_all = "snake_case")]
817pub enum ThinkingConfig {
818 Enabled {
820 budget_tokens: u32,
822 display: Option<ThinkingDisplay>,
824 },
825 Disabled,
827 Adaptive {
829 display: Option<ThinkingDisplay>,
832 },
833}
834
835#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
837#[serde(rename_all = "snake_case")]
838pub enum ThinkingDisplay {
839 Summarized,
841 Omitted,
843}
844
845#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
851pub struct Message {
852 pub id: String,
854
855 #[serde(rename = "type")]
857 pub message_type: String,
858
859 pub role: String,
861
862 pub content: Vec<ContentBlock>,
864
865 pub model: String,
867
868 pub stop_reason: Option<StopReason>,
870
871 pub stop_sequence: Option<String>,
873
874 pub usage: Usage,
876}
877
878#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
880#[serde(tag = "type", rename_all = "snake_case")]
881pub enum ContentBlock {
882 Text {
884 text: String,
885 #[serde(skip_serializing_if = "Option::is_none")]
886 citations: Option<Vec<Citation>>,
887 },
888 ToolUse {
890 id: String,
891 name: String,
892 input: Value,
893 },
894 Thinking { thinking: String, signature: String },
896 RedactedThinking { data: String },
898 ServerToolUse {
900 id: String,
901 name: String,
902 input: Value,
903 },
904 WebSearchToolResult {
906 tool_use_id: String,
907 content: WebSearchToolResultContent,
908 },
909 ToolSearchToolResult {
911 tool_use_id: String,
912 content: ToolSearchResultContent,
913 },
914 ToolReference {
916 tool_name: String,
917 #[serde(skip_serializing_if = "Option::is_none")]
918 description: Option<String>,
919 },
920 McpToolUse {
922 id: String,
923 name: String,
924 server_name: String,
925 input: Value,
926 },
927 McpToolResult {
929 tool_use_id: String,
930 content: Option<ToolResultContent>,
931 is_error: Option<bool>,
932 },
933}
934
935#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
937#[serde(rename_all = "snake_case")]
938pub enum StopReason {
939 EndTurn,
941 MaxTokens,
943 StopSequence,
945 ToolUse,
947 PauseTurn,
949 Refusal,
951}
952
953#[serde_with::skip_serializing_none]
955#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
956#[schemars(rename = "MessagesUsage")]
957pub struct Usage {
958 pub input_tokens: u32,
960
961 pub output_tokens: u32,
963
964 pub cache_creation_input_tokens: Option<u32>,
966
967 pub cache_read_input_tokens: Option<u32>,
969
970 pub cache_creation: Option<CacheCreation>,
972
973 pub server_tool_use: Option<ServerToolUsage>,
975
976 pub service_tier: Option<String>,
978}
979
980#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
982pub struct CacheCreation {
983 #[serde(flatten)]
984 pub tokens_by_ttl: HashMap<String, u32>,
985}
986
987#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
989pub struct ServerToolUsage {
990 pub web_search_requests: u32,
991}
992
993#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
999#[serde(tag = "type", rename_all = "snake_case")]
1000pub enum MessageStreamEvent {
1001 MessageStart { message: Message },
1003 MessageDelta {
1005 delta: MessageDelta,
1006 usage: MessageDeltaUsage,
1007 },
1008 MessageStop,
1010 ContentBlockStart {
1012 index: u32,
1013 content_block: ContentBlock,
1014 },
1015 ContentBlockDelta {
1017 index: u32,
1018 delta: ContentBlockDelta,
1019 },
1020 ContentBlockStop { index: u32 },
1022 Ping,
1024 Error { error: ErrorResponse },
1026}
1027
1028#[serde_with::skip_serializing_none]
1030#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1031pub struct MessageDelta {
1032 pub stop_reason: Option<StopReason>,
1033
1034 pub stop_sequence: Option<String>,
1035}
1036
1037#[serde_with::skip_serializing_none]
1039#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1040pub struct MessageDeltaUsage {
1041 pub output_tokens: u32,
1042
1043 pub input_tokens: Option<u32>,
1044
1045 pub cache_creation_input_tokens: Option<u32>,
1046
1047 pub cache_read_input_tokens: Option<u32>,
1048
1049 pub server_tool_use: Option<ServerToolUsage>,
1050}
1051
1052#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1054#[serde(tag = "type", rename_all = "snake_case")]
1055#[expect(
1056 clippy::enum_variant_names,
1057 reason = "variant names match the OpenAI/Anthropic streaming delta type discriminators (text_delta, input_json_delta, etc.)"
1058)]
1059pub enum ContentBlockDelta {
1060 TextDelta { text: String },
1062 InputJsonDelta { partial_json: String },
1064 ThinkingDelta { thinking: String },
1066 SignatureDelta { signature: String },
1068 CitationsDelta { citation: Citation },
1070}
1071
1072#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1078#[schemars(rename = "MessagesErrorResponse")]
1079pub struct ErrorResponse {
1080 #[serde(rename = "type")]
1081 pub error_type: String,
1082
1083 pub message: String,
1084}
1085
1086#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1088#[serde(tag = "type", rename_all = "snake_case")]
1089#[expect(
1090 clippy::enum_variant_names,
1091 reason = "variant names match the OpenAI API error type discriminators (invalid_request_error, authentication_error, etc.)"
1092)]
1093pub enum ApiError {
1094 InvalidRequestError { message: String },
1095 AuthenticationError { message: String },
1096 BillingError { message: String },
1097 PermissionError { message: String },
1098 NotFoundError { message: String },
1099 RateLimitError { message: String },
1100 TimeoutError { message: String },
1101 ApiError { message: String },
1102 OverloadedError { message: String },
1103}
1104
1105#[serde_with::skip_serializing_none]
1111#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1112pub struct CountMessageTokensRequest {
1113 pub model: String,
1115
1116 pub messages: Vec<InputMessage>,
1118
1119 pub system: Option<SystemContent>,
1121
1122 pub thinking: Option<ThinkingConfig>,
1124
1125 pub tool_choice: Option<ToolChoice>,
1127
1128 pub tools: Option<Vec<Tool>>,
1130}
1131
1132#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1134pub struct CountMessageTokensResponse {
1135 pub input_tokens: u32,
1136}
1137
1138#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1144pub struct ModelInfo {
1145 #[serde(rename = "type")]
1147 pub model_type: String,
1148
1149 pub id: String,
1151
1152 pub display_name: String,
1154
1155 pub created_at: String,
1157}
1158
1159#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1161pub struct ListModelsResponse {
1162 pub data: Vec<ModelInfo>,
1163 pub has_more: bool,
1164 pub first_id: Option<String>,
1165 pub last_id: Option<String>,
1166}
1167
1168#[serde_with::skip_serializing_none]
1174#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1175pub struct ContainerConfig {
1176 pub id: Option<String>,
1178}
1179
1180#[serde_with::skip_serializing_none]
1182#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1183pub struct McpServerConfig {
1184 #[serde(rename = "type", default = "McpServerConfig::default_type")]
1186 pub server_type: String,
1187
1188 pub name: String,
1190
1191 pub url: String,
1193
1194 pub authorization_token: Option<String>,
1196
1197 pub tool_configuration: Option<McpToolConfiguration>,
1199}
1200
1201impl McpServerConfig {
1202 fn default_type() -> String {
1203 "url".to_string()
1204 }
1205}
1206
1207#[serde_with::skip_serializing_none]
1209#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1210pub struct McpToolConfiguration {
1211 pub enabled: Option<bool>,
1213
1214 pub allowed_tools: Option<Vec<String>>,
1216}
1217
1218#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1224pub struct McpToolUseBlock {
1225 pub id: String,
1227
1228 pub name: String,
1230
1231 pub server_name: String,
1233
1234 pub input: Value,
1236
1237 #[serde(skip_serializing_if = "Option::is_none")]
1239 pub cache_control: Option<CacheControl>,
1240}
1241
1242#[serde_with::skip_serializing_none]
1244#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1245pub struct McpToolResultBlock {
1246 pub tool_use_id: String,
1248
1249 pub content: Option<ToolResultContent>,
1251
1252 pub is_error: Option<bool>,
1254
1255 pub cache_control: Option<CacheControl>,
1257}
1258
1259#[serde_with::skip_serializing_none]
1261#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1262pub struct McpToolset {
1263 #[serde(rename = "type")]
1264 pub toolset_type: String, pub mcp_server_name: String,
1268
1269 pub default_config: Option<McpToolDefaultConfig>,
1271
1272 pub configs: Option<HashMap<String, McpToolConfig>>,
1274
1275 pub cache_control: Option<CacheControl>,
1277}
1278
1279#[serde_with::skip_serializing_none]
1281#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1282pub struct McpToolDefaultConfig {
1283 pub enabled: Option<bool>,
1285
1286 pub defer_loading: Option<bool>,
1288}
1289
1290#[serde_with::skip_serializing_none]
1292#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1293pub struct McpToolConfig {
1294 pub enabled: Option<bool>,
1296
1297 pub defer_loading: Option<bool>,
1299}
1300
1301#[serde_with::skip_serializing_none]
1307#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1308pub struct CodeExecutionTool {
1309 #[serde(rename = "type")]
1310 pub tool_type: String, pub name: String, pub allowed_callers: Option<Vec<String>>,
1316
1317 pub defer_loading: Option<bool>,
1319
1320 pub strict: Option<bool>,
1322
1323 pub cache_control: Option<CacheControl>,
1325}
1326
1327#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1329pub struct CodeExecutionResultBlock {
1330 pub stdout: String,
1332
1333 pub stderr: String,
1335
1336 pub return_code: i32,
1338
1339 pub content: Vec<CodeExecutionOutputBlock>,
1341}
1342
1343#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1345pub struct CodeExecutionOutputBlock {
1346 #[serde(rename = "type")]
1347 pub block_type: String, pub file_id: String,
1351}
1352
1353#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1355pub struct CodeExecutionToolResultBlock {
1356 pub tool_use_id: String,
1358
1359 pub content: CodeExecutionToolResultContent,
1361
1362 #[serde(skip_serializing_if = "Option::is_none")]
1364 pub cache_control: Option<CacheControl>,
1365}
1366
1367#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1369#[serde(untagged)]
1370pub enum CodeExecutionToolResultContent {
1371 Success(CodeExecutionResultBlock),
1372 Error(CodeExecutionToolResultError),
1373}
1374
1375#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1377pub struct CodeExecutionToolResultError {
1378 #[serde(rename = "type")]
1379 pub error_type: String, pub error_code: CodeExecutionToolResultErrorCode,
1382}
1383
1384#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1386#[serde(rename_all = "snake_case")]
1387pub enum CodeExecutionToolResultErrorCode {
1388 Unavailable,
1389 CodeExecutionExceededTimeout,
1390 ContainerExpired,
1391 InvalidToolInput,
1392}
1393
1394#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1396pub struct BashCodeExecutionResultBlock {
1397 pub stdout: String,
1399
1400 pub stderr: String,
1402
1403 pub return_code: i32,
1405
1406 pub content: Vec<BashCodeExecutionOutputBlock>,
1408}
1409
1410#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1412pub struct BashCodeExecutionOutputBlock {
1413 #[serde(rename = "type")]
1414 pub block_type: String, pub file_id: String,
1418}
1419
1420#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1422pub struct BashCodeExecutionToolResultBlock {
1423 pub tool_use_id: String,
1425
1426 pub content: BashCodeExecutionToolResultContent,
1428
1429 #[serde(skip_serializing_if = "Option::is_none")]
1431 pub cache_control: Option<CacheControl>,
1432}
1433
1434#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1436#[serde(untagged)]
1437pub enum BashCodeExecutionToolResultContent {
1438 Success(BashCodeExecutionResultBlock),
1439 Error(BashCodeExecutionToolResultError),
1440}
1441
1442#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1444pub struct BashCodeExecutionToolResultError {
1445 #[serde(rename = "type")]
1446 pub error_type: String, pub error_code: BashCodeExecutionToolResultErrorCode,
1449}
1450
1451#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1453#[serde(rename_all = "snake_case")]
1454pub enum BashCodeExecutionToolResultErrorCode {
1455 Unavailable,
1456 CodeExecutionExceededTimeout,
1457 ContainerExpired,
1458 InvalidToolInput,
1459}
1460
1461#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1463pub struct TextEditorCodeExecutionToolResultBlock {
1464 pub tool_use_id: String,
1466
1467 pub content: TextEditorCodeExecutionToolResultContent,
1469
1470 #[serde(skip_serializing_if = "Option::is_none")]
1472 pub cache_control: Option<CacheControl>,
1473}
1474
1475#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1477#[serde(untagged)]
1478pub enum TextEditorCodeExecutionToolResultContent {
1479 CreateResult(TextEditorCodeExecutionCreateResultBlock),
1480 StrReplaceResult(TextEditorCodeExecutionStrReplaceResultBlock),
1481 ViewResult(TextEditorCodeExecutionViewResultBlock),
1482 Error(TextEditorCodeExecutionToolResultError),
1483}
1484
1485#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1487pub struct TextEditorCodeExecutionCreateResultBlock {
1488 #[serde(rename = "type")]
1489 pub block_type: String, }
1491
1492#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1494pub struct TextEditorCodeExecutionStrReplaceResultBlock {
1495 #[serde(rename = "type")]
1496 pub block_type: String, #[serde(skip_serializing_if = "Option::is_none")]
1500 pub snippet: Option<String>,
1501}
1502
1503#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1505pub struct TextEditorCodeExecutionViewResultBlock {
1506 #[serde(rename = "type")]
1507 pub block_type: String, pub content: String,
1511}
1512
1513#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1515pub struct TextEditorCodeExecutionToolResultError {
1516 #[serde(rename = "type")]
1517 pub error_type: String,
1518
1519 pub error_code: TextEditorCodeExecutionToolResultErrorCode,
1520}
1521
1522#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1524#[serde(rename_all = "snake_case")]
1525pub enum TextEditorCodeExecutionToolResultErrorCode {
1526 Unavailable,
1527 InvalidToolInput,
1528 FileNotFound,
1529 ContainerExpired,
1530}
1531
1532#[serde_with::skip_serializing_none]
1538#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1539pub struct WebFetchTool {
1540 #[serde(rename = "type")]
1541 pub tool_type: String, pub name: String, pub allowed_callers: Option<Vec<String>>,
1547
1548 pub max_uses: Option<u32>,
1550
1551 pub cache_control: Option<CacheControl>,
1553}
1554
1555#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1557pub struct WebFetchResultBlock {
1558 #[serde(rename = "type")]
1559 pub block_type: String, pub url: String,
1563
1564 pub content: DocumentBlock,
1566
1567 #[serde(skip_serializing_if = "Option::is_none")]
1569 pub retrieved_at: Option<String>,
1570}
1571
1572#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1574pub struct WebFetchToolResultBlock {
1575 pub tool_use_id: String,
1577
1578 pub content: WebFetchToolResultContent,
1580
1581 #[serde(skip_serializing_if = "Option::is_none")]
1583 pub cache_control: Option<CacheControl>,
1584}
1585
1586#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1588#[serde(untagged)]
1589pub enum WebFetchToolResultContent {
1590 Success(WebFetchResultBlock),
1591 Error(WebFetchToolResultError),
1592}
1593
1594#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1596pub struct WebFetchToolResultError {
1597 #[serde(rename = "type")]
1598 pub error_type: String, pub error_code: WebFetchToolResultErrorCode,
1601}
1602
1603#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1605#[serde(rename_all = "snake_case")]
1606pub enum WebFetchToolResultErrorCode {
1607 InvalidToolInput,
1608 Unavailable,
1609 MaxUsesExceeded,
1610 TooManyRequests,
1611 UrlNotAllowed,
1612 FetchFailed,
1613 ContentTooLarge,
1614}
1615
1616#[serde_with::skip_serializing_none]
1622#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1623pub struct ToolSearchTool {
1624 #[serde(rename = "type")]
1625 pub tool_type: String, pub name: String,
1628
1629 pub allowed_callers: Option<Vec<String>>,
1631
1632 pub cache_control: Option<CacheControl>,
1634}
1635
1636#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1638pub struct ToolReferenceBlock {
1639 #[serde(rename = "type")]
1640 pub block_type: String, pub tool_name: String,
1644
1645 #[serde(skip_serializing_if = "Option::is_none")]
1647 pub description: Option<String>,
1648}
1649
1650#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1652pub struct ToolSearchResultContent {
1653 #[serde(rename = "type")]
1654 pub block_type: String, pub tool_references: Vec<ToolReferenceBlock>,
1658}
1659
1660#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1662pub struct ToolSearchToolResultBlock {
1663 pub tool_use_id: String,
1665
1666 pub content: ToolSearchResultContent,
1668
1669 #[serde(skip_serializing_if = "Option::is_none")]
1671 pub cache_control: Option<CacheControl>,
1672}
1673
1674#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1680pub struct ContainerUploadBlock {
1681 #[serde(rename = "type")]
1682 pub block_type: String, pub file_id: String,
1686
1687 pub file_name: String,
1689
1690 #[serde(skip_serializing_if = "Option::is_none")]
1692 pub file_path: Option<String>,
1693}
1694
1695#[serde_with::skip_serializing_none]
1701#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1702pub struct MemoryTool {
1703 #[serde(rename = "type")]
1704 pub tool_type: String, pub name: String, pub allowed_callers: Option<Vec<String>>,
1710
1711 pub defer_loading: Option<bool>,
1713
1714 pub strict: Option<bool>,
1716
1717 pub input_examples: Option<Vec<Value>>,
1719
1720 pub cache_control: Option<CacheControl>,
1722}
1723
1724#[serde_with::skip_serializing_none]
1730#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1731pub struct ComputerUseTool {
1732 #[serde(rename = "type")]
1733 pub tool_type: String, pub name: String, pub display_width_px: u32,
1739
1740 pub display_height_px: u32,
1742
1743 pub display_number: Option<u32>,
1745
1746 pub allowed_callers: Option<Vec<String>>,
1748
1749 pub cache_control: Option<CacheControl>,
1751}
1752
1753#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1759#[serde(tag = "type", rename_all = "snake_case")]
1760pub enum BetaInputContentBlock {
1761 Text(TextBlock),
1763 Image(ImageBlock),
1764 Document(DocumentBlock),
1765 ToolUse(ToolUseBlock),
1766 ToolResult(ToolResultBlock),
1767 Thinking(ThinkingBlock),
1768 RedactedThinking(RedactedThinkingBlock),
1769 ServerToolUse(ServerToolUseBlock),
1770 SearchResult(SearchResultBlock),
1771 WebSearchToolResult(WebSearchToolResultBlock),
1772
1773 McpToolUse(McpToolUseBlock),
1775 McpToolResult(McpToolResultBlock),
1776
1777 CodeExecutionToolResult(CodeExecutionToolResultBlock),
1779 BashCodeExecutionToolResult(BashCodeExecutionToolResultBlock),
1780 TextEditorCodeExecutionToolResult(TextEditorCodeExecutionToolResultBlock),
1781
1782 WebFetchToolResult(WebFetchToolResultBlock),
1784
1785 ToolSearchToolResult(ToolSearchToolResultBlock),
1787 ToolReference(ToolReferenceBlock),
1788
1789 ContainerUpload(ContainerUploadBlock),
1791}
1792
1793#[serde_with::skip_serializing_none]
1795#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1796#[serde(tag = "type", rename_all = "snake_case")]
1797pub enum BetaContentBlock {
1798 Text {
1800 text: String,
1801 citations: Option<Vec<Citation>>,
1802 },
1803 ToolUse {
1804 id: String,
1805 name: String,
1806 input: Value,
1807 },
1808 Thinking {
1809 thinking: String,
1810 signature: String,
1811 },
1812 RedactedThinking {
1813 data: String,
1814 },
1815 ServerToolUse {
1816 id: String,
1817 name: String,
1818 input: Value,
1819 },
1820 WebSearchToolResult {
1821 tool_use_id: String,
1822 content: WebSearchToolResultContent,
1823 },
1824
1825 McpToolUse {
1827 id: String,
1828 name: String,
1829 server_name: String,
1830 input: Value,
1831 },
1832 McpToolResult {
1833 tool_use_id: String,
1834 content: Option<ToolResultContent>,
1835 is_error: Option<bool>,
1836 },
1837
1838 CodeExecutionToolResult {
1840 tool_use_id: String,
1841 content: CodeExecutionToolResultContent,
1842 },
1843 BashCodeExecutionToolResult {
1844 tool_use_id: String,
1845 content: BashCodeExecutionToolResultContent,
1846 },
1847 TextEditorCodeExecutionToolResult {
1848 tool_use_id: String,
1849 content: TextEditorCodeExecutionToolResultContent,
1850 },
1851
1852 WebFetchToolResult {
1854 tool_use_id: String,
1855 content: WebFetchToolResultContent,
1856 },
1857
1858 ToolSearchToolResult {
1860 tool_use_id: String,
1861 content: ToolSearchResultContent,
1862 },
1863 ToolReference {
1864 tool_name: String,
1865 description: Option<String>,
1866 },
1867
1868 ContainerUpload {
1870 file_id: String,
1871 file_name: String,
1872 file_path: Option<String>,
1873 },
1874}
1875
1876#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1878#[serde(untagged)]
1879pub enum BetaTool {
1880 Custom(CustomTool),
1882 Bash(BashTool),
1883 TextEditor(TextEditorTool),
1884 WebSearch(WebSearchTool),
1885
1886 CodeExecution(CodeExecutionTool),
1888 McpToolset(McpToolset),
1889 WebFetch(WebFetchTool),
1890 ToolSearch(ToolSearchTool),
1891 Memory(MemoryTool),
1892 ComputerUse(ComputerUseTool),
1893}
1894
1895#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1897#[serde(rename_all = "snake_case")]
1898pub enum BetaServerToolName {
1899 WebSearch,
1900 WebFetch,
1901 CodeExecution,
1902 BashCodeExecution,
1903 TextEditorCodeExecution,
1904 ToolSearchToolRegex,
1905 ToolSearchToolBm25,
1906}
1907
1908#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1910#[serde(tag = "type", rename_all = "snake_case")]
1911pub enum ServerToolCaller {
1912 Direct,
1914 #[serde(rename = "code_execution_20250825")]
1916 CodeExecution20250825,
1917}
1918
1919#[cfg(test)]
1920mod tests {
1921 use serde_json::{self, json};
1922
1923 use super::*;
1924
1925 #[test]
1926 fn test_system_blocks_preserve_type_field() {
1927 let input = json!({
1928 "model": "test",
1929 "messages": [{"role": "user", "content": "hi"}],
1930 "max_tokens": 100,
1931 "system": [
1932 {"type": "text", "text": "system prompt", "cache_control": {"type": "ephemeral"}}
1933 ]
1934 });
1935
1936 let req: CreateMessageRequest = serde_json::from_value(input).expect("should deserialize");
1937 let reserialized = serde_json::to_value(&req).expect("should serialize");
1938
1939 let system_blocks = reserialized.get("system").unwrap().as_array().unwrap();
1940 let first_block = &system_blocks[0];
1941 assert_eq!(
1942 first_block.get("type").and_then(|v| v.as_str()),
1943 Some("text"),
1944 "system block must retain 'type' field after round-trip: got {first_block:?}",
1945 );
1946 }
1947
1948 #[test]
1949 fn test_message_content_blocks_preserve_type_field() {
1950 let input = json!({
1951 "model": "test",
1952 "messages": [{
1953 "role": "user",
1954 "content": [
1955 {"type": "text", "text": "hello", "cache_control": {"type": "ephemeral"}}
1956 ]
1957 }],
1958 "max_tokens": 100
1959 });
1960
1961 let req: CreateMessageRequest = serde_json::from_value(input).expect("should deserialize");
1962 let reserialized = serde_json::to_value(&req).expect("should serialize");
1963
1964 let msg = &reserialized["messages"][0];
1965 let content_blocks = msg["content"].as_array().unwrap();
1966 let first_block = &content_blocks[0];
1967 assert_eq!(
1968 first_block.get("type").and_then(|v| v.as_str()),
1969 Some("text"),
1970 "content block must retain 'type' field: got {first_block:?}",
1971 );
1972 }
1973
1974 #[test]
1975 fn test_unknown_fields_preserved_via_flatten() {
1976 let input = json!({
1977 "model": "test-model",
1978 "messages": [{"role": "user", "content": "hello"}],
1979 "max_tokens": 100,
1980 "thinking": {"type": "adaptive"},
1981 "context_management": {"edits": [{"type": "clear_thinking", "keep": "all"}]},
1982 "output_config": {"effort": "high"},
1983 "stream": true
1984 });
1985
1986 let req: CreateMessageRequest =
1987 serde_json::from_value(input.clone()).expect("should deserialize");
1988 assert!(matches!(
1989 req.thinking,
1990 Some(ThinkingConfig::Adaptive { .. })
1991 ));
1992
1993 let reserialized = serde_json::to_value(&req).expect("should serialize");
1994 assert_eq!(
1995 reserialized.get("context_management"),
1996 input.get("context_management"),
1997 "context_management must survive round-trip"
1998 );
1999 assert_eq!(
2000 reserialized.get("output_config"),
2001 input.get("output_config"),
2002 "output_config must survive round-trip"
2003 );
2004 }
2005
2006 fn base_request() -> CreateMessageRequest {
2007 CreateMessageRequest {
2008 model: "claude-test".to_string(),
2009 messages: vec![InputMessage {
2010 role: Role::User,
2011 content: InputContent::String("hello".to_string()),
2012 }],
2013 max_tokens: 16,
2014 metadata: None,
2015 service_tier: None,
2016 stop_sequences: None,
2017 stream: None,
2018 system: None,
2019 temperature: None,
2020 thinking: None,
2021 tool_choice: None,
2022 tools: None,
2023 top_k: None,
2024 top_p: None,
2025 container: None,
2026 mcp_servers: None,
2027 rid: None,
2028 other: Map::new(),
2029 }
2030 }
2031
2032 fn custom_tool(name: &str) -> Tool {
2033 Tool::Custom(CustomTool {
2034 name: name.to_string(),
2035 tool_type: None,
2036 description: Some("test tool".to_string()),
2037 input_schema: InputSchema {
2038 schema_type: "object".to_string(),
2039 properties: None,
2040 required: None,
2041 additional: HashMap::new(),
2042 },
2043 defer_loading: None,
2044 cache_control: None,
2045 })
2046 }
2047
2048 fn mcp_toolset(configs: Option<HashMap<String, McpToolConfig>>) -> Tool {
2049 Tool::McpToolset(McpToolset {
2050 toolset_type: "mcp_toolset".to_string(),
2051 mcp_server_name: "brave".to_string(),
2052 default_config: None,
2053 configs,
2054 cache_control: None,
2055 })
2056 }
2057
2058 fn mcp_server_config() -> McpServerConfig {
2059 McpServerConfig {
2060 server_type: "url".to_string(),
2061 name: "brave".to_string(),
2062 url: "https://example.com/mcp".to_string(),
2063 authorization_token: None,
2064 tool_configuration: None,
2065 }
2066 }
2067 #[test]
2068 fn test_tool_mcp_toolset_defer_loading_deserialization() {
2069 let json = r#"{
2070 "type": "mcp_toolset",
2071 "mcp_server_name": "brave",
2072 "default_config": {"defer_loading": true}
2073 }"#;
2074
2075 let tool: Tool = serde_json::from_str(json).expect("Failed to deserialize McpToolset Tool");
2076 match tool {
2077 Tool::McpToolset(ts) => {
2078 assert_eq!(ts.mcp_server_name, "brave");
2079 let default_config = ts.default_config.expect("default_config should be Some");
2080 assert_eq!(default_config.defer_loading, Some(true));
2081 }
2082 other => panic!(
2083 "Expected McpToolset, got {:?}",
2084 std::mem::discriminant(&other)
2085 ),
2086 }
2087 }
2088
2089 #[test]
2090 fn test_tool_search_tool_deserialization() {
2091 let json = r#"{
2092 "type": "tool_search_tool_regex_20251119",
2093 "name": "tool_search_tool_regex"
2094 }"#;
2095
2096 let tool: Tool = serde_json::from_str(json).expect("Failed to deserialize ToolSearch Tool");
2097 match tool {
2098 Tool::ToolSearch(ts) => {
2099 assert_eq!(ts.name, "tool_search_tool_regex");
2100 assert_eq!(ts.tool_type, "tool_search_tool_regex_20251119");
2101 }
2102 other => panic!(
2103 "Expected ToolSearch, got {:?}",
2104 std::mem::discriminant(&other)
2105 ),
2106 }
2107 }
2108
2109 #[test]
2110 fn test_content_block_tool_search_tool_result_deserialization() {
2111 let json = r#"{
2112 "type": "tool_search_tool_result",
2113 "tool_use_id": "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2",
2114 "content": {
2115 "type": "tool_search_tool_search_result",
2116 "tool_references": [
2117 {"type": "tool_reference", "tool_name": "get_weather"}
2118 ]
2119 }
2120 }"#;
2121
2122 let block: ContentBlock = serde_json::from_str(json)
2123 .expect("Failed to deserialize tool_search_tool_result ContentBlock");
2124 match block {
2125 ContentBlock::ToolSearchToolResult {
2126 tool_use_id,
2127 content,
2128 } => {
2129 assert_eq!(tool_use_id, "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2");
2130 assert_eq!(content.tool_references.len(), 1);
2131 assert_eq!(content.tool_references[0].tool_name, "get_weather");
2132 }
2133 _ => panic!("Expected ToolSearchToolResult variant"),
2134 }
2135 }
2136
2137 #[test]
2138 fn test_content_block_server_tool_use_deserialization() {
2139 let json = r#"{
2140 "type": "server_tool_use",
2141 "id": "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2",
2142 "name": "tool_search_tool_regex",
2143 "input": {"query": "weather"}
2144 }"#;
2145
2146 let block: ContentBlock =
2147 serde_json::from_str(json).expect("Failed to deserialize server_tool_use ContentBlock");
2148 match block {
2149 ContentBlock::ServerToolUse { id, name, input: _ } => {
2150 assert_eq!(id, "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2");
2151 assert_eq!(name, "tool_search_tool_regex");
2152 }
2153 _ => panic!("Expected ServerToolUse variant"),
2154 }
2155 }
2156
2157 #[test]
2158 fn test_content_block_tool_reference_deserialization() {
2159 let json = r#"{
2160 "type": "tool_reference",
2161 "tool_name": "get_weather",
2162 "description": "Get the weather for a location"
2163 }"#;
2164
2165 let block: ContentBlock =
2166 serde_json::from_str(json).expect("Failed to deserialize tool_reference ContentBlock");
2167 match block {
2168 ContentBlock::ToolReference {
2169 tool_name,
2170 description,
2171 } => {
2172 assert_eq!(tool_name, "get_weather");
2173 assert_eq!(description.unwrap(), "Get the weather for a location");
2174 }
2175 _ => panic!("Expected ToolReference variant"),
2176 }
2177 }
2178
2179 #[test]
2180 fn test_tool_choice_auto_requires_tools() {
2181 let mut request = base_request();
2182 request.tool_choice = Some(ToolChoice::Auto {
2183 disable_parallel_tool_use: None,
2184 });
2185
2186 assert!(request.validate().is_err());
2187 }
2188
2189 #[test]
2190 fn test_tool_choice_any_requires_tools() {
2191 let mut request = base_request();
2192 request.tool_choice = Some(ToolChoice::Any {
2193 disable_parallel_tool_use: None,
2194 });
2195
2196 assert!(request.validate().is_err());
2197 }
2198
2199 #[test]
2200 fn test_tool_choice_auto_with_tools_is_valid() {
2201 let mut request = base_request();
2202 request.tool_choice = Some(ToolChoice::Auto {
2203 disable_parallel_tool_use: None,
2204 });
2205 request.tools = Some(vec![custom_tool("get_weather")]);
2206
2207 assert!(request.validate().is_ok());
2208 }
2209
2210 #[test]
2211 fn test_tool_choice_any_with_tools_is_valid() {
2212 let mut request = base_request();
2213 request.tool_choice = Some(ToolChoice::Any {
2214 disable_parallel_tool_use: None,
2215 });
2216 request.tools = Some(vec![custom_tool("get_weather")]);
2217
2218 assert!(request.validate().is_ok());
2219 }
2220
2221 #[test]
2222 fn test_tool_choice_specific_tool_requires_tools() {
2223 let mut request = base_request();
2224 request.tool_choice = Some(ToolChoice::Tool {
2225 name: "get_weather".to_string(),
2226 disable_parallel_tool_use: None,
2227 });
2228
2229 assert!(request.validate().is_err());
2230 }
2231
2232 #[test]
2233 fn test_tool_choice_specific_tool_must_exist() {
2234 let mut request = base_request();
2235 request.tool_choice = Some(ToolChoice::Tool {
2236 name: "get_weather".to_string(),
2237 disable_parallel_tool_use: None,
2238 });
2239 request.tools = Some(vec![custom_tool("search_web")]);
2240
2241 assert!(request.validate().is_err());
2242 }
2243
2244 #[test]
2245 fn test_tool_choice_none_without_tools_is_valid() {
2246 let mut request = base_request();
2247 request.tool_choice = Some(ToolChoice::None);
2248
2249 assert!(request.validate().is_ok());
2250 }
2251
2252 #[test]
2253 fn test_tool_choice_specific_tool_is_valid_when_declared() {
2254 let mut request = base_request();
2255 request.tool_choice = Some(ToolChoice::Tool {
2256 name: "get_weather".to_string(),
2257 disable_parallel_tool_use: None,
2258 });
2259 request.tools = Some(vec![custom_tool("get_weather")]);
2260
2261 assert!(request.validate().is_ok());
2262 }
2263
2264 #[test]
2265 fn test_tool_choice_specific_tool_is_valid_with_mcp_toolset() {
2266 let mut request = base_request();
2267 request.tool_choice = Some(ToolChoice::Tool {
2268 name: "get_weather".to_string(),
2269 disable_parallel_tool_use: None,
2270 });
2271 request.tools = Some(vec![mcp_toolset(None)]);
2272 request.mcp_servers = Some(vec![mcp_server_config()]);
2273
2274 assert!(request.validate().is_ok());
2275 }
2276
2277 #[test]
2278 fn test_tool_choice_specific_tool_uses_mcp_toolset_default_when_override_missing() {
2279 let mut request = base_request();
2280 request.tool_choice = Some(ToolChoice::Tool {
2281 name: "get_weather".to_string(),
2282 disable_parallel_tool_use: None,
2283 });
2284 request.tools = Some(vec![mcp_toolset(Some(HashMap::from([(
2285 "search_web".to_string(),
2286 McpToolConfig {
2287 enabled: Some(false),
2288 defer_loading: None,
2289 },
2290 )])))]);
2291 request.mcp_servers = Some(vec![mcp_server_config()]);
2292
2293 assert!(request.validate().is_ok());
2294 }
2295
2296 #[test]
2297 fn test_tool_choice_specific_tool_must_be_enabled_in_mcp_toolset_configs() {
2298 let mut request = base_request();
2299 request.tool_choice = Some(ToolChoice::Tool {
2300 name: "get_weather".to_string(),
2301 disable_parallel_tool_use: None,
2302 });
2303 request.tools = Some(vec![mcp_toolset(Some(HashMap::from([(
2304 "get_weather".to_string(),
2305 McpToolConfig {
2306 enabled: Some(false),
2307 defer_loading: None,
2308 },
2309 )])))]);
2310 request.mcp_servers = Some(vec![mcp_server_config()]);
2311
2312 assert!(request.validate().is_err());
2313 }
2314
2315 #[test]
2316 fn test_thinking_config_adaptive_minimal() {
2317 let cfg: ThinkingConfig = serde_json::from_str(r#"{"type":"adaptive"}"#).unwrap();
2318 match cfg {
2319 ThinkingConfig::Adaptive { display } => assert_eq!(display, None),
2320 other => panic!("expected Adaptive, got {other:?}"),
2321 }
2322 }
2323
2324 #[test]
2325 fn test_thinking_config_adaptive_with_display() {
2326 let cfg: ThinkingConfig =
2327 serde_json::from_str(r#"{"type":"adaptive","display":"omitted"}"#).unwrap();
2328 match cfg {
2329 ThinkingConfig::Adaptive { display } => {
2330 assert_eq!(display, Some(ThinkingDisplay::Omitted));
2331 }
2332 other => panic!("expected Adaptive, got {other:?}"),
2333 }
2334
2335 let cfg: ThinkingConfig =
2336 serde_json::from_str(r#"{"type":"adaptive","display":"summarized"}"#).unwrap();
2337 match cfg {
2338 ThinkingConfig::Adaptive { display } => {
2339 assert_eq!(display, Some(ThinkingDisplay::Summarized));
2340 }
2341 other => panic!("expected Adaptive, got {other:?}"),
2342 }
2343 }
2344
2345 #[test]
2346 fn test_thinking_config_adaptive_round_trip_omits_null_display() {
2347 let cfg = ThinkingConfig::Adaptive { display: None };
2348 let json = serde_json::to_string(&cfg).unwrap();
2349 assert_eq!(json, r#"{"type":"adaptive"}"#);
2350 }
2351
2352 #[test]
2353 fn test_thinking_config_existing_variants_still_work() {
2354 let cfg: ThinkingConfig =
2355 serde_json::from_str(r#"{"type":"enabled","budget_tokens":1024}"#).unwrap();
2356 assert!(matches!(
2357 cfg,
2358 ThinkingConfig::Enabled {
2359 budget_tokens: 1024,
2360 display: None
2361 }
2362 ));
2363
2364 let cfg: ThinkingConfig = serde_json::from_str(r#"{"type":"disabled"}"#).unwrap();
2365 assert!(matches!(cfg, ThinkingConfig::Disabled));
2366 }
2367
2368 #[test]
2369 fn test_thinking_config_enabled_with_display() {
2370 let cfg: ThinkingConfig = serde_json::from_str(
2371 r#"{"type":"enabled","budget_tokens":2048,"display":"summarized"}"#,
2372 )
2373 .unwrap();
2374 match cfg {
2375 ThinkingConfig::Enabled {
2376 budget_tokens,
2377 display,
2378 } => {
2379 assert_eq!(budget_tokens, 2048);
2380 assert_eq!(display, Some(ThinkingDisplay::Summarized));
2381 }
2382 other => panic!("expected Enabled, got {other:?}"),
2383 }
2384 }
2385
2386 #[test]
2387 fn test_thinking_config_enabled_round_trip_omits_null_display() {
2388 let cfg = ThinkingConfig::Enabled {
2389 budget_tokens: 1024,
2390 display: None,
2391 };
2392 let json = serde_json::to_string(&cfg).unwrap();
2393 assert_eq!(json, r#"{"type":"enabled","budget_tokens":1024}"#);
2394 }
2395
2396 #[test]
2397 fn test_full_message_with_tool_search_flow_deserialization() {
2398 let json = r#"{
2400 "id": "msg_01TEST",
2401 "type": "message",
2402 "role": "assistant",
2403 "model": "claude-sonnet-4-5-20250929",
2404 "content": [
2405 {
2406 "type": "server_tool_use",
2407 "id": "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2",
2408 "name": "tool_search_tool_regex",
2409 "input": {"query": "weather"}
2410 },
2411 {
2412 "type": "tool_search_tool_result",
2413 "tool_use_id": "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2",
2414 "content": {
2415 "type": "tool_search_tool_search_result",
2416 "tool_references": [
2417 {"type": "tool_reference", "tool_name": "get_weather"}
2418 ]
2419 }
2420 },
2421 {
2422 "type": "tool_use",
2423 "id": "toolu_01ABC",
2424 "name": "get_weather",
2425 "input": {"location": "San Francisco"}
2426 }
2427 ],
2428 "stop_reason": "tool_use",
2429 "stop_sequence": null,
2430 "usage": {
2431 "input_tokens": 100,
2432 "output_tokens": 50
2433 }
2434 }"#;
2435
2436 let msg: Message = serde_json::from_str(json)
2437 .expect("Failed to deserialize Message with tool search flow");
2438 assert_eq!(msg.content.len(), 3);
2439 assert!(matches!(msg.content[0], ContentBlock::ServerToolUse { .. }));
2440 assert!(matches!(
2441 msg.content[1],
2442 ContentBlock::ToolSearchToolResult { .. }
2443 ));
2444 assert!(matches!(msg.content[2], ContentBlock::ToolUse { .. }));
2445 }
2446
2447 #[test]
2448 fn test_system_role_in_messages_is_accepted_and_preserved() {
2449 let body = json!({
2453 "model": "m",
2454 "max_tokens": 16,
2455 "system": "main prompt",
2456 "messages": [
2457 {"role": "user", "content": "hi"},
2458 {"role": "system", "content": "mid-conversation system"}
2459 ]
2460 });
2461 let req: CreateMessageRequest = serde_json::from_value(body).unwrap();
2462 assert_eq!(req.messages.len(), 2);
2463 assert_eq!(req.messages[0].role, Role::User);
2464 assert_eq!(req.messages[1].role, Role::System); }
2466}