@@ -5,8 +5,12 @@ use mcp_types::CallToolResult;
5
5
use serde:: Deserialize ;
6
6
use serde:: Serialize ;
7
7
use serde:: ser:: Serializer ;
8
+ use time:: OffsetDateTime ;
9
+ use time:: format_description:: FormatItem ;
10
+ use time:: macros:: format_description;
8
11
9
12
use crate :: protocol:: InputItem ;
13
+ use crate :: protocol:: TokenUsage ;
10
14
11
15
#[ derive( Debug , Clone , Serialize , Deserialize ) ]
12
16
#[ serde( tag = "type" , rename_all = "snake_case" ) ]
@@ -39,6 +43,10 @@ pub enum ResponseItem {
39
43
Message {
40
44
role : String ,
41
45
content : Vec < ContentItem > ,
46
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
47
+ token_usage : Option < TokenUsage > ,
48
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
49
+ timestamp : Option < String > ,
42
50
} ,
43
51
Reasoning {
44
52
id : String ,
@@ -78,7 +86,12 @@ pub enum ResponseItem {
78
86
impl From < ResponseInputItem > for ResponseItem {
79
87
fn from ( item : ResponseInputItem ) -> Self {
80
88
match item {
81
- ResponseInputItem :: Message { role, content } => Self :: Message { role, content } ,
89
+ ResponseInputItem :: Message { role, content } => Self :: Message {
90
+ role,
91
+ content,
92
+ token_usage : None ,
93
+ timestamp : Some ( generate_timestamp ( ) ) ,
94
+ } ,
82
95
ResponseInputItem :: FunctionCallOutput { call_id, output } => {
83
96
Self :: FunctionCallOutput { call_id, output }
84
97
}
@@ -222,6 +235,16 @@ impl std::ops::Deref for FunctionCallOutputPayload {
222
235
}
223
236
}
224
237
238
+ /// Generate a timestamp string in the same format as session timestamps.
239
+ /// Format: "YYYY-MM-DDTHH:MM:SS.sssZ" (ISO 8601 with millisecond precision in UTC)
240
+ pub fn generate_timestamp ( ) -> String {
241
+ let timestamp_format: & [ FormatItem ] =
242
+ format_description ! ( "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z" ) ;
243
+ OffsetDateTime :: now_utc ( )
244
+ . format ( timestamp_format)
245
+ . unwrap_or_else ( |_| "1970-01-01T00:00:00.000Z" . to_string ( ) )
246
+ }
247
+
225
248
#[ cfg( test) ]
226
249
mod tests {
227
250
#![ allow( clippy:: unwrap_used) ]
@@ -260,6 +283,120 @@ mod tests {
260
283
assert_eq ! ( v. get( "output" ) . unwrap( ) . as_str( ) . unwrap( ) , "bad" ) ;
261
284
}
262
285
286
+ #[ test]
287
+ fn message_with_token_usage_and_timestamp ( ) {
288
+ let usage = TokenUsage {
289
+ input_tokens : 100 ,
290
+ output_tokens : 50 ,
291
+ total_tokens : 150 ,
292
+ cached_input_tokens : Some ( 25 ) ,
293
+ reasoning_output_tokens : None ,
294
+ } ;
295
+
296
+ let timestamp = "2025-07-15T10:30:45.123Z" . to_string ( ) ;
297
+
298
+ let message = ResponseItem :: Message {
299
+ role : "assistant" . to_string ( ) ,
300
+ content : vec ! [ ContentItem :: OutputText {
301
+ text: "Hello" . to_string( ) ,
302
+ } ] ,
303
+ token_usage : Some ( usage. clone ( ) ) ,
304
+ timestamp : Some ( timestamp. clone ( ) ) ,
305
+ } ;
306
+
307
+ // Test serialization
308
+ let json = serde_json:: to_string ( & message) . unwrap ( ) ;
309
+ let parsed: serde_json:: Value = serde_json:: from_str ( & json) . unwrap ( ) ;
310
+
311
+ assert_eq ! ( parsed[ "type" ] , "message" ) ;
312
+ assert_eq ! ( parsed[ "role" ] , "assistant" ) ;
313
+ assert_eq ! ( parsed[ "content" ] [ 0 ] [ "text" ] , "Hello" ) ;
314
+ assert_eq ! ( parsed[ "token_usage" ] [ "input_tokens" ] , 100 ) ;
315
+ assert_eq ! ( parsed[ "token_usage" ] [ "output_tokens" ] , 50 ) ;
316
+ assert_eq ! ( parsed[ "token_usage" ] [ "total_tokens" ] , 150 ) ;
317
+ assert_eq ! ( parsed[ "timestamp" ] , timestamp) ;
318
+
319
+ // Test deserialization
320
+ let deserialized: ResponseItem = serde_json:: from_str ( & json) . unwrap ( ) ;
321
+ if let ResponseItem :: Message {
322
+ role,
323
+ content,
324
+ token_usage : Some ( token_usage) ,
325
+ timestamp : Some ( ts) ,
326
+ ..
327
+ } = deserialized
328
+ {
329
+ assert_eq ! ( role, "assistant" ) ;
330
+ assert_eq ! ( content. len( ) , 1 ) ;
331
+ assert_eq ! ( token_usage. input_tokens, 100 ) ;
332
+ assert_eq ! ( token_usage. output_tokens, 50 ) ;
333
+ assert_eq ! ( ts, timestamp) ;
334
+ } else {
335
+ panic ! ( "Expected Message with token_usage and timestamp" ) ;
336
+ }
337
+ }
338
+
339
+ #[ test]
340
+ fn message_without_optional_fields ( ) {
341
+ let message = ResponseItem :: Message {
342
+ role : "user" . to_string ( ) ,
343
+ content : vec ! [ ContentItem :: InputText {
344
+ text: "Hi" . to_string( ) ,
345
+ } ] ,
346
+ token_usage : None ,
347
+ timestamp : None ,
348
+ } ;
349
+
350
+ // Test serialization - optional fields should be omitted
351
+ let json = serde_json:: to_string ( & message) . unwrap ( ) ;
352
+ let parsed: serde_json:: Value = serde_json:: from_str ( & json) . unwrap ( ) ;
353
+
354
+ assert_eq ! ( parsed[ "type" ] , "message" ) ;
355
+ assert_eq ! ( parsed[ "role" ] , "user" ) ;
356
+ assert ! ( parsed. get( "token_usage" ) . is_none( ) ) ;
357
+ assert ! ( parsed. get( "timestamp" ) . is_none( ) ) ;
358
+
359
+ // Test deserialization - should work with missing fields
360
+ let deserialized: ResponseItem = serde_json:: from_str ( & json) . unwrap ( ) ;
361
+ if let ResponseItem :: Message {
362
+ role,
363
+ token_usage,
364
+ timestamp,
365
+ ..
366
+ } = deserialized
367
+ {
368
+ assert_eq ! ( role, "user" ) ;
369
+ assert ! ( token_usage. is_none( ) ) ;
370
+ assert ! ( timestamp. is_none( ) ) ;
371
+ } else {
372
+ panic ! ( "Expected Message without optional fields" ) ;
373
+ }
374
+ }
375
+
376
+ #[ test]
377
+ fn generate_timestamp_format ( ) {
378
+ let timestamp = generate_timestamp ( ) ;
379
+
380
+ // Should be valid ISO 8601 format: YYYY-MM-DDTHH:MM:SS.sssZ
381
+ let parts: Vec < & str > = timestamp. split ( 'T' ) . collect ( ) ;
382
+ assert_eq ! ( parts. len( ) , 2 ) ;
383
+
384
+ let date_part = parts[ 0 ] ;
385
+ let time_part = parts[ 1 ] ;
386
+
387
+ // Date should be YYYY-MM-DD format
388
+ assert_eq ! ( date_part. len( ) , 10 ) ;
389
+ assert ! ( date_part. contains( '-' ) ) ;
390
+
391
+ // Time should end with Z and have milliseconds
392
+ assert ! ( time_part. ends_with( 'Z' ) ) ;
393
+ assert ! ( time_part. contains( '.' ) ) ;
394
+
395
+ // Should be able to parse as a valid timestamp
396
+ assert ! ( timestamp. len( ) >= 20 ) ; // Minimum ISO format length
397
+ assert ! ( timestamp. len( ) <= 30 ) ; // Maximum reasonable length
398
+ }
399
+
263
400
#[ test]
264
401
fn deserialize_shell_tool_call_params ( ) {
265
402
let json = r#"{
0 commit comments