1use std::collections::HashMap;
7
8use chrono::{DateTime, NaiveDate, Utc};
9use serde::{Deserialize, Serialize};
10
11pub use bamboo_domain::TokenUsage;
15
16#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "snake_case")]
19pub enum RoundStatus {
20 Running,
22 Success,
24 Error,
26 Cancelled,
28}
29
30impl RoundStatus {
31 pub fn as_str(self) -> &'static str {
32 match self {
33 Self::Running => "running",
34 Self::Success => "success",
35 Self::Error => "error",
36 Self::Cancelled => "cancelled",
37 }
38 }
39
40 pub fn from_db(value: &str) -> Option<Self> {
41 match value {
42 "running" => Some(Self::Running),
43 "success" => Some(Self::Success),
44 "error" => Some(Self::Error),
45 "cancelled" => Some(Self::Cancelled),
46 _ => None,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "snake_case")]
54pub enum SessionStatus {
55 Running,
57 Completed,
59 Error,
61 Cancelled,
63}
64
65impl SessionStatus {
66 pub fn as_str(self) -> &'static str {
67 match self {
68 Self::Running => "running",
69 Self::Completed => "completed",
70 Self::Error => "error",
71 Self::Cancelled => "cancelled",
72 }
73 }
74
75 pub fn from_db(value: &str) -> Option<Self> {
76 match value {
77 "running" => Some(Self::Running),
78 "completed" => Some(Self::Completed),
79 "error" => Some(Self::Error),
80 "cancelled" => Some(Self::Cancelled),
81 _ => None,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
88pub struct ToolCallMetrics {
89 pub tool_call_id: String,
91 pub tool_name: String,
93 pub started_at: DateTime<Utc>,
95 pub completed_at: Option<DateTime<Utc>>,
97 pub success: Option<bool>,
99 pub error: Option<String>,
101 pub duration_ms: Option<u64>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107pub struct RoundMetrics {
108 pub round_id: String,
110 pub session_id: String,
112 pub model: String,
114 pub started_at: DateTime<Utc>,
116 pub completed_at: Option<DateTime<Utc>>,
118 pub token_usage: TokenUsage,
120 pub tool_calls: Vec<ToolCallMetrics>,
122 pub status: RoundStatus,
124 pub error: Option<String>,
126 pub duration_ms: Option<u64>,
128 #[serde(default)]
130 pub prompt_cached_tool_outputs: u32,
131 #[serde(default)]
133 pub compression_count: u32,
134 #[serde(default)]
136 pub tokens_saved: u32,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
141pub struct SessionMetrics {
142 pub session_id: String,
144 pub model: String,
146 pub started_at: DateTime<Utc>,
148 pub completed_at: Option<DateTime<Utc>>,
150 pub total_rounds: u32,
152 pub total_token_usage: TokenUsage,
154 pub tool_call_count: u32,
156 pub tool_breakdown: HashMap<String, u32>,
158 pub status: SessionStatus,
160 pub message_count: u32,
162 pub duration_ms: Option<u64>,
164 #[serde(default)]
166 pub prompt_cached_tool_outputs: u64,
167 #[serde(default)]
169 pub total_compression_events: u64,
170 #[serde(default)]
172 pub total_tokens_saved: u64,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
177pub struct SessionDetail {
178 pub session: SessionMetrics,
180 pub rounds: Vec<RoundMetrics>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
186pub struct DailyMetrics {
187 pub date: NaiveDate,
189 pub total_sessions: u32,
191 pub total_rounds: u32,
193 pub total_token_usage: TokenUsage,
195 pub total_tool_calls: u32,
197 pub model_breakdown: HashMap<String, TokenUsage>,
199 pub tool_breakdown: HashMap<String, u32>,
201 #[serde(default)]
203 pub prompt_cached_tool_outputs: u64,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
208pub struct MetricsSummary {
209 pub total_sessions: u64,
211 pub total_tokens: TokenUsage,
213 pub total_tool_calls: u64,
215 pub active_sessions: u64,
217 #[serde(default)]
219 pub prompt_cached_tool_outputs: u64,
220 #[serde(default)]
222 pub total_sync_mismatches: u64,
223 #[serde(default)]
225 pub sync_mismatch_breakdown: HashMap<String, u64>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230pub struct ModelMetrics {
231 pub model: String,
233 pub sessions: u64,
235 pub rounds: u64,
237 pub tokens: TokenUsage,
239 pub tool_calls: u64,
241 #[serde(default)]
243 pub prompt_cached_tool_outputs: u64,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
248pub struct MetricsDateFilter {
249 pub start_date: Option<NaiveDate>,
251 pub end_date: Option<NaiveDate>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
257pub struct SessionMetricsFilter {
258 pub start_date: Option<NaiveDate>,
260 pub end_date: Option<NaiveDate>,
262 pub model: Option<String>,
264 pub limit: Option<u32>,
266}
267
268#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
270#[serde(rename_all = "snake_case")]
271pub enum ForwardStatus {
272 Success,
274 Error,
276}
277
278impl ForwardStatus {
279 pub fn as_str(self) -> &'static str {
280 match self {
281 Self::Success => "success",
282 Self::Error => "error",
283 }
284 }
285
286 pub fn from_db(value: &str) -> Option<Self> {
287 match value {
288 "success" => Some(Self::Success),
289 "error" => Some(Self::Error),
290 _ => None,
291 }
292 }
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
296pub struct ForwardRequestMetrics {
297 pub forward_id: String,
298 pub endpoint: String,
299 pub model: String,
300 pub is_stream: bool,
301 pub started_at: DateTime<Utc>,
302 pub completed_at: Option<DateTime<Utc>>,
303 pub status_code: Option<u16>,
304 pub status: Option<ForwardStatus>,
305 pub token_usage: Option<TokenUsage>,
306 pub error: Option<String>,
307 pub duration_ms: Option<u64>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
311pub struct ForwardMetricsSummary {
312 pub total_requests: u64,
313 pub successful_requests: u64,
314 pub failed_requests: u64,
315 pub total_tokens: TokenUsage,
316 pub avg_duration_ms: Option<u64>,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
320pub struct ForwardEndpointMetrics {
321 pub endpoint: String,
322 pub requests: u64,
323 pub successful: u64,
324 pub failed: u64,
325 pub tokens: TokenUsage,
326 pub avg_duration_ms: Option<u64>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
330pub struct ForwardMetricsFilter {
331 pub start_date: Option<NaiveDate>,
332 pub end_date: Option<NaiveDate>,
333 pub endpoint: Option<String>,
334 pub model: Option<String>,
335 pub limit: Option<u32>,
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_token_usage_default() {
344 let usage = TokenUsage::default();
345 assert_eq!(usage.prompt_tokens, 0);
346 assert_eq!(usage.completion_tokens, 0);
347 assert_eq!(usage.total_tokens, 0);
348 }
349
350 #[test]
351 fn test_token_usage_add_assign() {
352 let mut usage1 = TokenUsage {
353 prompt_tokens: 100,
354 completion_tokens: 50,
355 total_tokens: 150,
356 };
357 let usage2 = TokenUsage {
358 prompt_tokens: 200,
359 completion_tokens: 100,
360 total_tokens: 300,
361 };
362
363 usage1.add_assign(usage2);
364
365 assert_eq!(usage1.prompt_tokens, 300);
366 assert_eq!(usage1.completion_tokens, 150);
367 assert_eq!(usage1.total_tokens, 450);
368 }
369
370 #[test]
371 fn test_token_usage_serialization() {
372 let usage = TokenUsage {
373 prompt_tokens: 100,
374 completion_tokens: 50,
375 total_tokens: 150,
376 };
377
378 let json = serde_json::to_string(&usage).unwrap();
379 assert!(json.contains("\"prompt_tokens\":100"));
380 assert!(json.contains("\"completion_tokens\":50"));
381 }
382
383 #[test]
384 fn test_round_status_as_str() {
385 assert_eq!(RoundStatus::Running.as_str(), "running");
386 assert_eq!(RoundStatus::Success.as_str(), "success");
387 assert_eq!(RoundStatus::Error.as_str(), "error");
388 assert_eq!(RoundStatus::Cancelled.as_str(), "cancelled");
389 }
390
391 #[test]
392 fn test_round_status_from_db() {
393 assert_eq!(RoundStatus::from_db("running"), Some(RoundStatus::Running));
394 assert_eq!(RoundStatus::from_db("success"), Some(RoundStatus::Success));
395 assert_eq!(RoundStatus::from_db("invalid"), None);
396 }
397
398 #[test]
399 fn test_round_status_serialization() {
400 let status = RoundStatus::Success;
401 let json = serde_json::to_string(&status).unwrap();
402 assert!(json.contains("\"success\""));
403 }
404
405 #[test]
406 fn test_session_status_as_str() {
407 assert_eq!(SessionStatus::Running.as_str(), "running");
408 assert_eq!(SessionStatus::Completed.as_str(), "completed");
409 assert_eq!(SessionStatus::Error.as_str(), "error");
410 assert_eq!(SessionStatus::Cancelled.as_str(), "cancelled");
411 }
412
413 #[test]
414 fn test_session_status_from_db() {
415 assert_eq!(
416 SessionStatus::from_db("running"),
417 Some(SessionStatus::Running)
418 );
419 assert_eq!(
420 SessionStatus::from_db("completed"),
421 Some(SessionStatus::Completed)
422 );
423 assert_eq!(SessionStatus::from_db("invalid"), None);
424 }
425
426 #[test]
427 fn test_forward_status_as_str() {
428 assert_eq!(ForwardStatus::Success.as_str(), "success");
429 assert_eq!(ForwardStatus::Error.as_str(), "error");
430 }
431
432 #[test]
433 fn test_forward_status_from_db() {
434 assert_eq!(
435 ForwardStatus::from_db("success"),
436 Some(ForwardStatus::Success)
437 );
438 assert_eq!(ForwardStatus::from_db("error"), Some(ForwardStatus::Error));
439 assert_eq!(ForwardStatus::from_db("invalid"), None);
440 }
441
442 #[test]
443 fn test_metrics_date_filter_default() {
444 let filter = MetricsDateFilter::default();
445 assert!(filter.start_date.is_none());
446 assert!(filter.end_date.is_none());
447 }
448
449 #[test]
450 fn test_session_metrics_filter_default() {
451 let filter = SessionMetricsFilter::default();
452 assert!(filter.start_date.is_none());
453 assert!(filter.model.is_none());
454 assert!(filter.limit.is_none());
455 }
456
457 #[test]
458 fn test_forward_metrics_filter_default() {
459 let filter = ForwardMetricsFilter::default();
460 assert!(filter.start_date.is_none());
461 assert!(filter.endpoint.is_none());
462 assert!(filter.limit.is_none());
463 }
464
465 #[test]
466 fn test_tool_call_metrics_serialization() {
467 let metrics = ToolCallMetrics {
468 tool_call_id: "call-123".to_string(),
469 tool_name: "bash".to_string(),
470 started_at: Utc::now(),
471 completed_at: Some(Utc::now()),
472 success: Some(true),
473 error: None,
474 duration_ms: Some(150),
475 };
476
477 let json = serde_json::to_string(&metrics).unwrap();
478 assert!(json.contains("\"tool_call_id\":\"call-123\""));
479 assert!(json.contains("\"tool_name\":\"bash\""));
480 }
481
482 #[test]
483 fn test_round_metrics_serialization() {
484 let metrics = RoundMetrics {
485 round_id: "round-1".to_string(),
486 session_id: "session-1".to_string(),
487 model: "gpt-4".to_string(),
488 started_at: Utc::now(),
489 completed_at: None,
490 token_usage: TokenUsage::default(),
491 tool_calls: vec![],
492 status: RoundStatus::Running,
493 error: None,
494 duration_ms: None,
495 prompt_cached_tool_outputs: 0,
496 compression_count: 0,
497 tokens_saved: 0,
498 };
499
500 let json = serde_json::to_string(&metrics).unwrap();
501 assert!(json.contains("\"model\":\"gpt-4\""));
502 }
503
504 #[test]
505 fn test_session_metrics_serialization() {
506 let metrics = SessionMetrics {
507 session_id: "session-1".to_string(),
508 model: "gpt-4".to_string(),
509 started_at: Utc::now(),
510 completed_at: None,
511 total_rounds: 5,
512 total_token_usage: TokenUsage::default(),
513 tool_call_count: 10,
514 tool_breakdown: HashMap::new(),
515 status: SessionStatus::Running,
516 message_count: 15,
517 duration_ms: None,
518 prompt_cached_tool_outputs: 0,
519 total_compression_events: 0,
520 total_tokens_saved: 0,
521 };
522
523 let json = serde_json::to_string(&metrics).unwrap();
524 assert!(json.contains("\"session_id\":\"session-1\""));
525 assert!(json.contains("\"total_rounds\":5"));
526 }
527
528 #[test]
529 fn test_daily_metrics_serialization() {
530 let metrics = DailyMetrics {
531 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
532 total_sessions: 10,
533 total_rounds: 50,
534 total_token_usage: TokenUsage::default(),
535 total_tool_calls: 100,
536 prompt_cached_tool_outputs: 0,
537 model_breakdown: HashMap::new(),
538 tool_breakdown: HashMap::new(),
539 };
540
541 let json = serde_json::to_string(&metrics).unwrap();
542 assert!(json.contains("\"total_sessions\":10"));
543 assert!(json.contains("\"total_rounds\":50"));
544 }
545
546 #[test]
547 fn test_metrics_summary_serialization() {
548 let summary = MetricsSummary {
549 total_sessions: 100,
550 total_tokens: TokenUsage::default(),
551 total_tool_calls: 500,
552 active_sessions: 5,
553 prompt_cached_tool_outputs: 0,
554 total_sync_mismatches: 0,
555 sync_mismatch_breakdown: HashMap::new(),
556 };
557
558 let json = serde_json::to_string(&summary).unwrap();
559 assert!(json.contains("\"total_sessions\":100"));
560 assert!(json.contains("\"active_sessions\":5"));
561 }
562
563 #[test]
564 fn test_model_metrics_serialization() {
565 let metrics = ModelMetrics {
566 model: "gpt-4".to_string(),
567 sessions: 50,
568 rounds: 200,
569 tokens: TokenUsage::default(),
570 tool_calls: 100,
571 prompt_cached_tool_outputs: 0,
572 };
573
574 let json = serde_json::to_string(&metrics).unwrap();
575 assert!(json.contains("\"model\":\"gpt-4\""));
576 assert!(json.contains("\"sessions\":50"));
577 }
578
579 #[test]
580 fn test_forward_request_metrics_serialization() {
581 let metrics = ForwardRequestMetrics {
582 forward_id: "fwd-123".to_string(),
583 endpoint: "/api/chat".to_string(),
584 model: "gpt-4".to_string(),
585 is_stream: true,
586 started_at: Utc::now(),
587 completed_at: Some(Utc::now()),
588 status_code: Some(200),
589 status: Some(ForwardStatus::Success),
590 token_usage: None,
591 error: None,
592 duration_ms: Some(250),
593 };
594
595 let json = serde_json::to_string(&metrics).unwrap();
596 assert!(json.contains("\"forward_id\":\"fwd-123\""));
597 assert!(json.contains("\"endpoint\":\"/api/chat\""));
598 }
599
600 #[test]
601 fn test_forward_metrics_summary_serialization() {
602 let summary = ForwardMetricsSummary {
603 total_requests: 1000,
604 successful_requests: 950,
605 failed_requests: 50,
606 total_tokens: TokenUsage::default(),
607 avg_duration_ms: Some(200),
608 };
609
610 let json = serde_json::to_string(&summary).unwrap();
611 assert!(json.contains("\"total_requests\":1000"));
612 assert!(json.contains("\"successful_requests\":950"));
613 }
614
615 #[test]
616 fn test_token_usage_clone() {
617 let usage = TokenUsage {
618 prompt_tokens: 100,
619 completion_tokens: 50,
620 total_tokens: 150,
621 };
622 let cloned = usage.clone();
623 assert_eq!(usage.prompt_tokens, cloned.prompt_tokens);
624 }
625
626 #[test]
627 fn test_round_status_clone() {
628 let status = RoundStatus::Success;
629 let cloned = status.clone();
630 assert_eq!(status, cloned);
631 }
632
633 #[test]
634 fn test_session_status_clone() {
635 let status = SessionStatus::Completed;
636 let cloned = status.clone();
637 assert_eq!(status, cloned);
638 }
639
640 #[test]
641 fn test_forward_status_clone() {
642 let status = ForwardStatus::Success;
643 let cloned = status.clone();
644 assert_eq!(status, cloned);
645 }
646
647 #[test]
648 fn test_token_usage_eq() {
649 let usage1 = TokenUsage {
650 prompt_tokens: 100,
651 completion_tokens: 50,
652 total_tokens: 150,
653 };
654 let usage2 = TokenUsage {
655 prompt_tokens: 100,
656 completion_tokens: 50,
657 total_tokens: 150,
658 };
659 assert_eq!(usage1, usage2);
660 }
661
662 #[test]
663 fn test_round_status_eq() {
664 assert_eq!(RoundStatus::Running, RoundStatus::Running);
665 assert_ne!(RoundStatus::Running, RoundStatus::Success);
666 }
667
668 #[test]
669 fn test_session_status_eq() {
670 assert_eq!(SessionStatus::Running, SessionStatus::Running);
671 assert_ne!(SessionStatus::Running, SessionStatus::Completed);
672 }
673
674 #[test]
675 fn test_round_metrics_compression_fields_deserialize_with_defaults() {
676 let json = r#"{"round_id":"r1","session_id":"s1","model":"m","started_at":"2026-01-01T00:00:00Z","completed_at":null,"token_usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0},"tool_calls":[],"status":"running","error":null,"duration_ms":null,"prompt_cached_tool_outputs":0}"#;
677 let metrics: RoundMetrics = serde_json::from_str(json).unwrap();
678 assert_eq!(metrics.compression_count, 0);
679 assert_eq!(metrics.tokens_saved, 0);
680 }
681
682 #[test]
683 fn test_session_metrics_compression_fields_deserialize_with_defaults() {
684 let json = r#"{"session_id":"s1","model":"m","started_at":"2026-01-01T00:00:00Z","completed_at":null,"total_rounds":0,"total_token_usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0},"tool_call_count":0,"tool_breakdown":{},"status":"running","message_count":0,"duration_ms":null,"prompt_cached_tool_outputs":0}"#;
685 let metrics: SessionMetrics = serde_json::from_str(json).unwrap();
686 assert_eq!(metrics.total_compression_events, 0);
687 assert_eq!(metrics.total_tokens_saved, 0);
688 }
689}