1use serde::{Deserialize, Serialize};
2
3use super::{ContentBlock, ResponseContentBlock, Role, StopReason, Usage};
4
5#[derive(Debug, Deserialize, Default, Clone, PartialEq, Serialize)]
6pub struct Response {
7 pub id: String,
8 pub r#type: String,
9 pub role: Role,
10 pub content: Vec<ResponseContentBlock>,
11 pub model: String,
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub stop_reason: Option<StopReason>,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub stop_sequence: Option<String>,
16 pub usage: Usage,
17}
18
19#[cfg(test)]
20mod tests {
21 use crate::messages::{
22 BaseContentBlock, ContentBlock, RedactedThinkingContentBlock, RequestOnlyContentBlock,
23 Role, ToolUseContentBlock,
24 };
25
26 use super::*;
27 #[test]
28 fn serde() {
29 let tests = vec![
30 (
31 "simple",
32 r#"{
33 "content": [
34 {
35 "text": "Hi! My name is Claude.",
36 "type": "text"
37 }
38 ],
39 "id": "msg_013Zva2CMHLNnXjNJJKqJ2EF",
40 "model": "claude-3-opus-20240229",
41 "role": "assistant",
42 "stop_reason": "end_turn",
43 "stop_sequence": null,
44 "type": "message",
45 "usage": {
46 "input_tokens": 10,
47 "output_tokens": 25
48 }
49 }"#,
50 Response {
51 id: "msg_013Zva2CMHLNnXjNJJKqJ2EF".to_string(),
52 model: "claude-3-opus-20240229".to_string(),
53 content: vec![
54 ResponseContentBlock::Base(BaseContentBlock::Text {
55 text: "Hi! My name is Claude.".to_string(),
56 }),
57 ],
58 role: Role::Assistant,
59 stop_reason: Some(StopReason::EndTurn),
60 stop_sequence: None,
61 r#type: "message".to_string(),
62 usage: Usage {
63 input_tokens: Some(10),
64 output_tokens: 25,
65 },
66 },
67 ),
68 (
69 "tool_use",
70 r#"{
71 "id": "msg_01Aq9w938a90dw8q",
72 "type": "message",
73 "model": "claude-3-opus-20240229",
74 "stop_reason": "tool_use",
75 "role": "assistant",
76 "stop_sequence": null,
77 "content": [
78 {
79 "type": "text",
80 "text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
81 },
82 {
83 "type": "tool_use",
84 "id": "toolu_01A09q90qw90lq917835lq9",
85 "name": "get_weather",
86 "input": {"location": "San Francisco, CA", "unit": "celsius"}
87 }
88 ],
89 "usage": {
90 "input_tokens": 527,
91 "output_tokens": 137
92 }
93 }"#,
94 Response {
95 id: "msg_01Aq9w938a90dw8q".to_string(),
96 model: "claude-3-opus-20240229".to_string(),
97 content: vec![
98 ResponseContentBlock::Base(BaseContentBlock::Text {
99 text: "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>".to_string(),
100 }),
101 ResponseContentBlock::Base(BaseContentBlock::ToolUse(ToolUseContentBlock {
102 id: "toolu_01A09q90qw90lq917835lq9".to_string(),
103 name: "get_weather".to_string(),
104 input: serde_json::json!({
105 "location": "San Francisco, CA",
106 "unit": "celsius"
107 }),
108 })),
109 ],
110 role: Role::Assistant,
111 stop_reason: Some(StopReason::ToolUse),
112 stop_sequence: None,
113 r#type: "message".to_string(),
114 usage: Usage {
115 input_tokens: Some(527),
116 output_tokens: 137,
117 },
118 },
119 ),
120 (
121 "thinking_block",
122 r#"{
123 "id": "msg_01Bq9w938a90dw8r",
124 "type": "message",
125 "model": "claude-3-opus-20240229",
126 "stop_reason": "end_turn",
127 "role": "assistant",
128 "stop_sequence": null,
129 "content": [
130 {
131 "type": "thinking",
132 "thinking": "Let me analyze this step by step...",
133 "signature": "WaUjzkypQ2mUEVM36O2TxuC06KN8xyfbJwyem2dw3URve/op91XWHOEBLLqIOMfFG/UvLEczmEsUjavL...."
134 },
135 {
136 "type": "redacted_thinking",
137 "data": "EmwKAhgBEgy3va3pzix/LafPsn4aDFIT2Xlxh0L5L8rLVyIwxtE3rAFBa8cr3qpP..."
138 },
139 {
140 "type": "text",
141 "text": "Based on my analysis..."
142 }
143 ],
144 "usage": {
145 "input_tokens": 320,
146 "output_tokens": 150
147 }
148 }"#,
149 Response {
150 id: "msg_01Bq9w938a90dw8r".to_string(),
151 model: "claude-3-opus-20240229".to_string(),
152 content: vec![
153 ResponseContentBlock::Base(BaseContentBlock::Thinking {
154 thinking: "Let me analyze this step by step...".to_string(),
155 signature: Some("WaUjzkypQ2mUEVM36O2TxuC06KN8xyfbJwyem2dw3URve/op91XWHOEBLLqIOMfFG/UvLEczmEsUjavL....".to_string()),
156 }),
157 ResponseContentBlock::RedactedThinking(RedactedThinkingContentBlock::RedactedThinking {
158 data: "EmwKAhgBEgy3va3pzix/LafPsn4aDFIT2Xlxh0L5L8rLVyIwxtE3rAFBa8cr3qpP...".to_string(),
159 }),
160 ResponseContentBlock::Base(BaseContentBlock::Text {
161 text: "Based on my analysis...".to_string(),
162 }),
163 ],
164 role: Role::Assistant,
165 stop_reason: Some(StopReason::EndTurn),
166 stop_sequence: None,
167 r#type: "message".to_string(),
168 usage: Usage {
169 input_tokens: Some(320),
170 output_tokens: 150,
171 },
172 },
173 ),
174 ];
175 for (name, json, expected) in tests {
176 let actual: Response = serde_json::from_str(json).unwrap();
178 assert_eq!(actual, expected, "deserialize test failed: {}", name);
179 let serialized = serde_json::to_string(&expected).unwrap();
181 let actual: Response = serde_json::from_str(&serialized).unwrap();
182 assert_eq!(actual, expected, "serialize test failed: {}", name);
183 }
184 }
185}