Skip to main content

codineer_api/
types.rs

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/// Base64-encoded image payload matching the Anthropic Messages API format.
84#[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}