1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5pub struct MessageRequest {
6 pub model: String,
7 pub max_tokens: u32,
8 pub messages: Vec<InputMessage>,
9 #[serde(skip_serializing_if = "Option::is_none")]
10 pub system: Option<String>,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 pub tools: Option<Vec<ToolDefinition>>,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub tool_choice: Option<ToolChoice>,
15 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
16 pub stream: bool,
17}
18
19impl MessageRequest {
20 #[must_use]
21 pub fn with_streaming(mut self) -> Self {
22 self.stream = true;
23 self
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct InputMessage {
29 pub role: String,
30 pub content: Vec<InputContentBlock>,
31}
32
33impl InputMessage {
34 #[must_use]
35 pub fn user_text(text: impl Into<String>) -> Self {
36 Self {
37 role: "user".to_string(),
38 content: vec![InputContentBlock::Text { text: text.into() }],
39 }
40 }
41
42 #[must_use]
43 pub fn user_tool_result(
44 tool_use_id: impl Into<String>,
45 content: impl Into<String>,
46 is_error: bool,
47 ) -> Self {
48 Self {
49 role: "user".to_string(),
50 content: vec![InputContentBlock::ToolResult {
51 tool_use_id: tool_use_id.into(),
52 content: vec![ToolResultContentBlock::Text {
53 text: content.into(),
54 }],
55 is_error,
56 }],
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62#[serde(tag = "type", rename_all = "snake_case")]
63pub enum InputContentBlock {
64 Text {
65 text: String,
66 },
67 ToolUse {
68 id: String,
69 name: String,
70 input: Value,
71 },
72 ToolResult {
73 tool_use_id: String,
74 content: Vec<ToolResultContentBlock>,
75 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
76 is_error: bool,
77 },
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81#[serde(tag = "type", rename_all = "snake_case")]
82pub enum ToolResultContentBlock {
83 Text { text: String },
84 Json { value: Value },
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct ToolDefinition {
89 pub name: String,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub description: Option<String>,
92 pub input_schema: Value,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(tag = "type", rename_all = "snake_case")]
97pub enum ToolChoice {
98 Auto,
99 Any,
100 Tool { name: String },
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub struct MessageResponse {
105 pub id: String,
106 #[serde(rename = "type")]
107 pub kind: String,
108 pub role: String,
109 pub content: Vec<OutputContentBlock>,
110 pub model: String,
111 #[serde(default)]
112 pub stop_reason: Option<String>,
113 #[serde(default)]
114 pub stop_sequence: Option<String>,
115 pub usage: Usage,
116 #[serde(default)]
117 pub request_id: Option<String>,
118}
119
120impl MessageResponse {
121 #[must_use]
122 pub fn total_tokens(&self) -> u32 {
123 self.usage.total_tokens()
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128#[serde(tag = "type", rename_all = "snake_case")]
129pub enum OutputContentBlock {
130 Text {
131 text: String,
132 },
133 ToolUse {
134 id: String,
135 name: String,
136 input: Value,
137 },
138 Thinking {
139 #[serde(default)]
140 thinking: String,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 signature: Option<String>,
143 },
144 RedactedThinking {
145 data: Value,
146 },
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct Usage {
151 pub input_tokens: u32,
152 #[serde(default)]
153 pub cache_creation_input_tokens: u32,
154 #[serde(default)]
155 pub cache_read_input_tokens: u32,
156 pub output_tokens: u32,
157}
158
159impl Usage {
160 #[must_use]
161 pub const fn total_tokens(&self) -> u32 {
162 self.input_tokens
163 + self.output_tokens
164 + self.cache_creation_input_tokens
165 + self.cache_read_input_tokens
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
170pub struct MessageStartEvent {
171 pub message: MessageResponse,
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct MessageDeltaEvent {
176 pub delta: MessageDelta,
177 pub usage: Usage,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct MessageDelta {
182 #[serde(default)]
183 pub stop_reason: Option<String>,
184 #[serde(default)]
185 pub stop_sequence: Option<String>,
186}
187
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct ContentBlockStartEvent {
190 pub index: u32,
191 pub content_block: OutputContentBlock,
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
195pub struct ContentBlockDeltaEvent {
196 pub index: u32,
197 pub delta: ContentBlockDelta,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(tag = "type", rename_all = "snake_case")]
202pub enum ContentBlockDelta {
203 TextDelta { text: String },
204 InputJsonDelta { partial_json: String },
205 ThinkingDelta { thinking: String },
206 SignatureDelta { signature: String },
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210pub struct ContentBlockStopEvent {
211 pub index: u32,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
215pub struct MessageStopEvent {}
216
217#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
218#[serde(tag = "type", rename_all = "snake_case")]
219pub enum StreamEvent {
220 MessageStart(MessageStartEvent),
221 MessageDelta(MessageDeltaEvent),
222 ContentBlockStart(ContentBlockStartEvent),
223 ContentBlockDelta(ContentBlockDeltaEvent),
224 ContentBlockStop(ContentBlockStopEvent),
225 MessageStop(MessageStopEvent),
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use serde_json::json;
232
233 #[test]
234 fn message_request_serializes_with_streaming_and_tools() {
235 let request = MessageRequest {
236 model: "claude-opus-4-6".to_string(),
237 max_tokens: 4096,
238 messages: vec![InputMessage::user_text("hello")],
239 system: Some("you are helpful".to_string()),
240 tools: Some(vec![ToolDefinition {
241 name: "read_file".to_string(),
242 description: Some("Read a file".to_string()),
243 input_schema: json!({"type": "object", "properties": {"path": {"type": "string"}}}),
244 }]),
245 tool_choice: Some(ToolChoice::Auto),
246 stream: false,
247 };
248 let json = serde_json::to_string(&request).expect("serialize");
249 let deserialized: MessageRequest = serde_json::from_str(&json).expect("deserialize");
250 assert_eq!(deserialized, request);
251 assert!(!json.contains("\"stream\""));
252
253 let streaming = request.with_streaming();
254 let json = serde_json::to_string(&streaming).expect("serialize streaming");
255 assert!(json.contains("\"stream\":true"));
256 }
257
258 #[test]
259 fn input_content_block_round_trips_all_variants() {
260 let text = InputContentBlock::Text {
261 text: "hello".to_string(),
262 };
263 let tool_use = InputContentBlock::ToolUse {
264 id: "t1".to_string(),
265 name: "bash".to_string(),
266 input: json!({"command": "ls"}),
267 };
268 let tool_result = InputContentBlock::ToolResult {
269 tool_use_id: "t1".to_string(),
270 content: vec![ToolResultContentBlock::Text {
271 text: "output".to_string(),
272 }],
273 is_error: false,
274 };
275 for block in [text, tool_use, tool_result] {
276 let json = serde_json::to_value(&block).expect("serialize");
277 let deserialized: InputContentBlock =
278 serde_json::from_value(json).expect("deserialize");
279 assert_eq!(deserialized, block);
280 }
281 }
282
283 #[test]
284 fn message_response_deserializes_with_defaults() {
285 let json = json!({
286 "id": "msg-1",
287 "type": "message",
288 "model": "claude-opus-4-6",
289 "role": "assistant",
290 "content": [{"type": "text", "text": "hi"}],
291 "usage": {"input_tokens": 10, "output_tokens": 5}
292 });
293 let response: MessageResponse = serde_json::from_value(json).expect("deserialize");
294 assert_eq!(response.id, "msg-1");
295 assert_eq!(response.role, "assistant");
296 assert_eq!(response.usage.total_tokens(), 15);
297 }
298
299 #[test]
300 fn output_content_block_round_trips_including_thinking() {
301 let blocks = vec![
302 OutputContentBlock::Text {
303 text: "hello".to_string(),
304 },
305 OutputContentBlock::ToolUse {
306 id: "t1".to_string(),
307 name: "bash".to_string(),
308 input: json!({"command": "ls"}),
309 },
310 OutputContentBlock::Thinking {
311 thinking: "hmm".to_string(),
312 signature: Some("sig".to_string()),
313 },
314 ];
315 for block in blocks {
316 let json = serde_json::to_value(&block).expect("serialize");
317 let deserialized: OutputContentBlock =
318 serde_json::from_value(json).expect("deserialize");
319 assert_eq!(deserialized, block);
320 }
321 }
322
323 #[test]
324 fn tool_choice_variants_serialize_with_type_tag() {
325 let auto_json = serde_json::to_value(ToolChoice::Auto).expect("auto");
326 assert_eq!(auto_json, json!({"type": "auto"}));
327 let any_json = serde_json::to_value(ToolChoice::Any).expect("any");
328 assert_eq!(any_json, json!({"type": "any"}));
329 let tool_json = serde_json::to_value(ToolChoice::Tool {
330 name: "bash".to_string(),
331 })
332 .expect("tool");
333 assert_eq!(tool_json, json!({"type": "tool", "name": "bash"}));
334 }
335
336 #[test]
337 fn stream_event_deserializes_all_variants() {
338 let msg_start = json!({
339 "type": "message_start",
340 "message": {
341 "id": "msg-1", "type": "message", "model": "m",
342 "role": "assistant", "content": [],
343 "usage": {"input_tokens": 0, "output_tokens": 0}
344 }
345 });
346 let parsed: StreamEvent = serde_json::from_value(msg_start).expect("message_start");
347 assert!(matches!(parsed, StreamEvent::MessageStart(_)));
348
349 let delta = json!({
350 "type": "content_block_delta",
351 "index": 0,
352 "delta": {"type": "text_delta", "text": "hi"}
353 });
354 let parsed: StreamEvent = serde_json::from_value(delta).expect("content_block_delta");
355 assert!(matches!(parsed, StreamEvent::ContentBlockDelta(_)));
356 }
357
358 #[test]
359 fn usage_computes_total_tokens() {
360 let usage = Usage {
361 input_tokens: 100,
362 output_tokens: 50,
363 cache_read_input_tokens: 20,
364 cache_creation_input_tokens: 10,
365 };
366 assert_eq!(usage.total_tokens(), 180);
367 }
368
369 #[test]
370 fn content_block_delta_all_variants_round_trip() {
371 let deltas = vec![
372 ContentBlockDelta::TextDelta {
373 text: "hi".to_string(),
374 },
375 ContentBlockDelta::InputJsonDelta {
376 partial_json: "{\"a\"".to_string(),
377 },
378 ContentBlockDelta::ThinkingDelta {
379 thinking: "hmm".to_string(),
380 },
381 ContentBlockDelta::SignatureDelta {
382 signature: "sig".to_string(),
383 },
384 ];
385 for delta in deltas {
386 let json = serde_json::to_value(&delta).expect("serialize");
387 let deserialized: ContentBlockDelta =
388 serde_json::from_value(json).expect("deserialize");
389 assert_eq!(deserialized, delta);
390 }
391 }
392}