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    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}