1use serde::{Deserialize, Serialize, Serializer};
2use serde_json::Value;
3
4fn serialize_nullable_content<S>(value: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
5where
6 S: Serializer,
7{
8 match value {
9 Some(s) => serializer.serialize_str(s),
10 None => serializer.serialize_none(),
11 }
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "role", rename_all = "lowercase")]
16pub enum Message {
17 System {
18 content: String,
19 },
20 User {
21 content: String,
22 },
23 #[serde(rename = "assistant")]
24 Assistant {
25 #[serde(serialize_with = "serialize_nullable_content")]
26 content: Option<String>,
27 #[serde(
28 default,
29 alias = "reasoning_content",
30 skip_serializing_if = "Option::is_none"
31 )]
32 reasoning_text: Option<String>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 reasoning_details: Option<Value>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 tool_calls: Option<Vec<ToolCall>>,
37 },
38 #[serde(rename = "tool")]
39 Tool {
40 tool_call_id: String,
41 content: String,
42 },
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ToolCall {
47 pub id: String,
48 #[serde(rename = "type")]
49 pub call_type: String,
50 pub function: FunctionCall,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct FunctionCall {
55 pub name: String,
56 pub arguments: String,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ToolDefinition {
61 #[serde(rename = "type")]
62 pub def_type: String,
63 pub function: FunctionDef,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct FunctionDef {
68 pub name: String,
69 pub description: String,
70 pub parameters: Value,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
74pub struct Usage {
75 pub prompt_tokens: Option<u32>,
76 pub completion_tokens: Option<u32>,
77 pub total_tokens: Option<u32>,
78 pub reasoning_tokens: Option<u32>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub cached_tokens: Option<u32>,
84}
85
86impl Usage {
87 pub fn accumulate(&mut self, other: &Usage) {
89 fn add_opt(a: &mut Option<u32>, b: Option<u32>) {
90 match (a.as_mut(), b) {
91 (Some(existing), Some(val)) => *existing = existing.saturating_add(val),
92 (None, Some(val)) => *a = Some(val),
93 _ => {}
94 }
95 }
96 add_opt(&mut self.prompt_tokens, other.prompt_tokens);
97 add_opt(&mut self.completion_tokens, other.completion_tokens);
98 add_opt(&mut self.total_tokens, other.total_tokens);
99 add_opt(&mut self.reasoning_tokens, other.reasoning_tokens);
100 add_opt(&mut self.cached_tokens, other.cached_tokens);
101 }
102
103 pub fn goal_token_delta(&self) -> i64 {
112 let prompt = self.prompt_tokens.unwrap_or(0) as i64;
113 let cached = self.cached_tokens.unwrap_or(0) as i64;
114 let completion = self.completion_tokens.unwrap_or(0) as i64;
115 (prompt - cached).max(0).saturating_add(completion)
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_assistant_content_null() {
125 let msg = Message::Assistant {
126 content: None,
127 reasoning_text: Some("thinking".to_string()),
128 reasoning_details: Some(serde_json::json!([{"type": "reasoning"}])),
129 tool_calls: Some(vec![ToolCall {
130 id: "call_123".to_string(),
131 call_type: "function".to_string(),
132 function: FunctionCall {
133 name: "read".to_string(),
134 arguments: r#"{"path": "src/main.rs"}"#.to_string(),
135 },
136 }]),
137 };
138 let json = serde_json::to_string(&msg).unwrap();
139 assert!(
140 json.contains("\"content\":null"),
141 "Expected \"content\":null in JSON but got: {}",
142 json
143 );
144 assert!(
145 json.contains("tool_calls"),
146 "Expected tool_calls in JSON: {}",
147 json
148 );
149 assert!(
150 json.contains("\"reasoning_text\":\"thinking\""),
151 "Expected reasoning_text in JSON: {}",
152 json
153 );
154 assert!(
155 json.contains("\"reasoning_details\":[{\"type\":\"reasoning\"}]"),
156 "Expected reasoning_details in JSON: {}",
157 json
158 );
159 }
160
161 #[test]
162 fn test_assistant_reasoning_content_alias_deserializes() {
163 let json = r#"{
164 "role":"assistant",
165 "content":"hello",
166 "reasoning_content":"thinking",
167 "tool_calls":[]
168 }"#;
169 let parsed: Message = serde_json::from_str(json).unwrap();
170 match parsed {
171 Message::Assistant {
172 content,
173 reasoning_text,
174 reasoning_details,
175 tool_calls,
176 } => {
177 assert_eq!(content.as_deref(), Some("hello"));
178 assert_eq!(reasoning_text.as_deref(), Some("thinking"));
179 assert_eq!(reasoning_details, None);
180 assert_eq!(
181 tool_calls.as_ref().map(std::vec::Vec::len),
182 Some(0),
183 "expected empty tool call list"
184 );
185 }
186 other => panic!("expected assistant message, got {:?}", other),
187 }
188 }
189
190 #[test]
191 fn test_message_role_serialization() {
192 let system = Message::System {
193 content: "hello".to_string(),
194 };
195 let json = serde_json::to_string(&system).unwrap();
196 assert!(json.contains("\"role\":\"system\""), "Got: {}", json);
197
198 let user = Message::User {
199 content: "hi".to_string(),
200 };
201 let json = serde_json::to_string(&user).unwrap();
202 assert!(json.contains("\"role\":\"user\""), "Got: {}", json);
203
204 let tool = Message::Tool {
205 tool_call_id: "call_abc".to_string(),
206 content: "result".to_string(),
207 };
208 let json = serde_json::to_string(&tool).unwrap();
209 assert!(json.contains("\"role\":\"tool\""), "Got: {}", json);
210 assert!(json.contains("tool_call_id"), "Got: {}", json);
211 }
212
213 #[test]
214 fn goal_token_delta_without_cached_tokens() {
215 let usage = Usage {
216 prompt_tokens: Some(100),
217 completion_tokens: Some(50),
218 total_tokens: Some(150),
219 reasoning_tokens: None,
220 cached_tokens: None,
221 };
222 assert_eq!(usage.goal_token_delta(), 150);
224 }
225
226 #[test]
227 fn goal_token_delta_with_cached_tokens() {
228 let usage = Usage {
229 prompt_tokens: Some(1000),
230 completion_tokens: Some(200),
231 total_tokens: Some(1200),
232 reasoning_tokens: None,
233 cached_tokens: Some(800),
234 };
235 assert_eq!(usage.goal_token_delta(), 400);
237 }
238
239 #[test]
240 fn goal_token_delta_cached_exceeds_prompt_clamps_to_zero() {
241 let usage = Usage {
242 prompt_tokens: Some(100),
243 completion_tokens: Some(50),
244 total_tokens: Some(150),
245 reasoning_tokens: None,
246 cached_tokens: Some(200),
247 };
248 assert_eq!(usage.goal_token_delta(), 50);
250 }
251
252 #[test]
253 fn goal_token_delta_all_none() {
254 let usage = Usage::default();
255 assert_eq!(usage.goal_token_delta(), 0);
256 }
257
258 #[test]
259 fn accumulate_includes_cached_tokens() {
260 let mut total = Usage {
261 prompt_tokens: Some(100),
262 completion_tokens: Some(50),
263 total_tokens: Some(150),
264 reasoning_tokens: None,
265 cached_tokens: Some(80),
266 };
267 let other = Usage {
268 prompt_tokens: Some(200),
269 completion_tokens: Some(60),
270 total_tokens: Some(260),
271 reasoning_tokens: None,
272 cached_tokens: Some(150),
273 };
274 total.accumulate(&other);
275 assert_eq!(total.prompt_tokens, Some(300));
276 assert_eq!(total.completion_tokens, Some(110));
277 assert_eq!(total.total_tokens, Some(410));
278 assert_eq!(total.cached_tokens, Some(230));
279 }
280
281 #[test]
282 fn accumulate_cached_tokens_none_plus_some() {
283 let mut total = Usage::default();
284 let other = Usage {
285 cached_tokens: Some(100),
286 ..Default::default()
287 };
288 total.accumulate(&other);
289 assert_eq!(total.cached_tokens, Some(100));
290 }
291}