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