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 AwaitingResponse,
59 Completed,
61 Error,
63 Cancelled,
65}
66
67impl SessionStatus {
68 pub fn as_str(self) -> &'static str {
69 match self {
70 Self::Running => "running",
71 Self::AwaitingResponse => "awaiting_response",
72 Self::Completed => "completed",
73 Self::Error => "error",
74 Self::Cancelled => "cancelled",
75 }
76 }
77
78 pub fn from_db(value: &str) -> Option<Self> {
79 match value {
80 "running" => Some(Self::Running),
81 "awaiting_response" => Some(Self::AwaitingResponse),
82 "completed" => Some(Self::Completed),
83 "error" => Some(Self::Error),
84 "cancelled" => Some(Self::Cancelled),
85 _ => None,
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub struct ToolCallMetrics {
93 pub tool_call_id: String,
95 pub tool_name: String,
97 pub started_at: DateTime<Utc>,
99 pub completed_at: Option<DateTime<Utc>>,
101 pub success: Option<bool>,
103 pub error: Option<String>,
105 pub duration_ms: Option<u64>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
111pub struct RoundMetrics {
112 pub round_id: String,
114 pub session_id: String,
116 pub model: String,
118 pub started_at: DateTime<Utc>,
120 pub completed_at: Option<DateTime<Utc>>,
122 pub token_usage: TokenUsage,
124 pub tool_calls: Vec<ToolCallMetrics>,
126 pub status: RoundStatus,
128 pub error: Option<String>,
130 pub duration_ms: Option<u64>,
132 #[serde(default)]
134 pub prompt_cached_tool_outputs: u32,
135 #[serde(default)]
137 pub prompt_cached_tool_tokens_saved: u32,
138 #[serde(default)]
140 pub compression_count: u32,
141 #[serde(default)]
143 pub tokens_saved: u32,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
148pub struct SessionMetrics {
149 pub session_id: String,
151 pub model: String,
153 pub started_at: DateTime<Utc>,
155 pub completed_at: Option<DateTime<Utc>>,
157 pub total_rounds: u32,
159 pub total_token_usage: TokenUsage,
161 pub tool_call_count: u32,
163 pub tool_breakdown: HashMap<String, u32>,
165 pub status: SessionStatus,
167 pub message_count: u32,
169 pub duration_ms: Option<u64>,
171 #[serde(default)]
173 pub prompt_cached_tool_outputs: u64,
174 #[serde(default)]
176 pub prompt_cached_tool_tokens_saved: u64,
177 #[serde(default)]
179 pub total_compression_events: u64,
180 #[serde(default)]
182 pub total_tokens_saved: u64,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
187pub struct SessionDetail {
188 pub session: SessionMetrics,
190 pub rounds: Vec<RoundMetrics>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
196pub struct DailyMetrics {
197 pub date: NaiveDate,
199 pub total_sessions: u32,
201 pub total_rounds: u32,
203 pub total_token_usage: TokenUsage,
205 pub total_tool_calls: u32,
207 pub model_breakdown: HashMap<String, TokenUsage>,
209 pub tool_breakdown: HashMap<String, u32>,
211 #[serde(default)]
213 pub prompt_cached_tool_outputs: u64,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
218pub struct MetricsSummary {
219 pub total_sessions: u64,
221 pub total_tokens: TokenUsage,
223 pub total_tool_calls: u64,
225 pub active_sessions: u64,
227 #[serde(default)]
229 pub prompt_cached_tool_outputs: u64,
230 #[serde(default)]
232 pub tool_context_tokens_saved: u64,
233 #[serde(default)]
235 pub total_compression_events: u64,
236 #[serde(default)]
238 pub total_tokens_saved: u64,
239 #[serde(default)]
241 pub non_tool_compression_tokens_saved: u64,
242 #[serde(default)]
244 pub completed_sessions: u64,
245 #[serde(default)]
247 pub awaiting_response_sessions: u64,
248 #[serde(default)]
250 pub error_sessions: u64,
251 #[serde(default)]
253 pub cancelled_sessions: u64,
254 #[serde(default)]
256 pub total_sync_mismatches: u64,
257 #[serde(default)]
259 pub sync_mismatch_breakdown: HashMap<String, u64>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
264pub struct ModelMetrics {
265 pub model: String,
267 pub sessions: u64,
269 pub rounds: u64,
271 pub tokens: TokenUsage,
273 pub tool_calls: u64,
275 #[serde(default)]
277 pub prompt_cached_tool_outputs: u64,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
282pub struct MetricsDateFilter {
283 pub start_date: Option<NaiveDate>,
285 pub end_date: Option<NaiveDate>,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
291pub struct SessionMetricsFilter {
292 pub start_date: Option<NaiveDate>,
294 pub end_date: Option<NaiveDate>,
296 pub model: Option<String>,
298 pub limit: Option<u32>,
300}
301
302#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
304#[serde(rename_all = "snake_case")]
305pub enum ForwardStatus {
306 Pending,
308 Success,
310 Error,
312}
313
314impl ForwardStatus {
315 pub fn as_str(self) -> &'static str {
316 match self {
317 Self::Pending => "pending",
318 Self::Success => "success",
319 Self::Error => "error",
320 }
321 }
322
323 pub fn from_db(value: &str) -> Option<Self> {
324 match value {
325 "pending" => Some(Self::Pending),
326 "success" => Some(Self::Success),
327 "error" => Some(Self::Error),
328 _ => None,
329 }
330 }
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
334pub struct ForwardRequestMetrics {
335 pub forward_id: String,
336 pub endpoint: String,
337 pub model: String,
338 pub is_stream: bool,
339 pub started_at: DateTime<Utc>,
340 pub completed_at: Option<DateTime<Utc>>,
341 pub status_code: Option<u16>,
342 pub status: Option<ForwardStatus>,
343 pub token_usage: Option<TokenUsage>,
344 pub error: Option<String>,
345 pub duration_ms: Option<u64>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
349pub struct ForwardMetricsSummary {
350 pub total_requests: u64,
351 pub successful_requests: u64,
352 pub failed_requests: u64,
353 pub total_tokens: TokenUsage,
354 pub avg_duration_ms: Option<u64>,
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
358pub struct ForwardEndpointMetrics {
359 pub endpoint: String,
360 pub requests: u64,
361 pub successful: u64,
362 pub failed: u64,
363 pub tokens: TokenUsage,
364 pub avg_duration_ms: Option<u64>,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
368pub struct ForwardMetricsFilter {
369 pub start_date: Option<NaiveDate>,
370 pub end_date: Option<NaiveDate>,
371 pub endpoint: Option<String>,
372 pub model: Option<String>,
373 pub limit: Option<u32>,
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_token_usage_default() {
382 let usage = TokenUsage::default();
383 assert_eq!(usage.prompt_tokens, 0);
384 assert_eq!(usage.completion_tokens, 0);
385 assert_eq!(usage.total_tokens, 0);
386 }
387
388 #[test]
389 fn test_token_usage_add_assign() {
390 let mut usage1 = TokenUsage {
391 prompt_tokens: 100,
392 completion_tokens: 50,
393 total_tokens: 150,
394 };
395 let usage2 = TokenUsage {
396 prompt_tokens: 200,
397 completion_tokens: 100,
398 total_tokens: 300,
399 };
400
401 usage1.add_assign(usage2);
402
403 assert_eq!(usage1.prompt_tokens, 300);
404 assert_eq!(usage1.completion_tokens, 150);
405 assert_eq!(usage1.total_tokens, 450);
406 }
407
408 #[test]
409 fn test_token_usage_serialization() {
410 let usage = TokenUsage {
411 prompt_tokens: 100,
412 completion_tokens: 50,
413 total_tokens: 150,
414 };
415
416 let json = serde_json::to_string(&usage).unwrap();
417 assert!(json.contains("\"prompt_tokens\":100"));
418 assert!(json.contains("\"completion_tokens\":50"));
419 }
420
421 #[test]
422 fn test_round_status_as_str() {
423 assert_eq!(RoundStatus::Running.as_str(), "running");
424 assert_eq!(RoundStatus::Success.as_str(), "success");
425 assert_eq!(RoundStatus::Error.as_str(), "error");
426 assert_eq!(RoundStatus::Cancelled.as_str(), "cancelled");
427 }
428
429 #[test]
430 fn test_round_status_from_db() {
431 assert_eq!(RoundStatus::from_db("running"), Some(RoundStatus::Running));
432 assert_eq!(RoundStatus::from_db("success"), Some(RoundStatus::Success));
433 assert_eq!(RoundStatus::from_db("invalid"), None);
434 }
435
436 #[test]
437 fn test_round_status_serialization() {
438 let status = RoundStatus::Success;
439 let json = serde_json::to_string(&status).unwrap();
440 assert!(json.contains("\"success\""));
441 }
442
443 #[test]
444 fn test_session_status_as_str() {
445 assert_eq!(SessionStatus::Running.as_str(), "running");
446 assert_eq!(
447 SessionStatus::AwaitingResponse.as_str(),
448 "awaiting_response"
449 );
450 assert_eq!(SessionStatus::Completed.as_str(), "completed");
451 assert_eq!(SessionStatus::Error.as_str(), "error");
452 assert_eq!(SessionStatus::Cancelled.as_str(), "cancelled");
453 }
454
455 #[test]
456 fn test_session_status_from_db() {
457 assert_eq!(
458 SessionStatus::from_db("running"),
459 Some(SessionStatus::Running)
460 );
461 assert_eq!(
462 SessionStatus::from_db("awaiting_response"),
463 Some(SessionStatus::AwaitingResponse)
464 );
465 assert_eq!(
466 SessionStatus::from_db("completed"),
467 Some(SessionStatus::Completed)
468 );
469 assert_eq!(SessionStatus::from_db("invalid"), None);
470 }
471
472 #[test]
473 fn test_forward_status_as_str() {
474 assert_eq!(ForwardStatus::Pending.as_str(), "pending");
475 assert_eq!(ForwardStatus::Success.as_str(), "success");
476 assert_eq!(ForwardStatus::Error.as_str(), "error");
477 }
478
479 #[test]
480 fn test_forward_status_from_db() {
481 assert_eq!(
482 ForwardStatus::from_db("pending"),
483 Some(ForwardStatus::Pending)
484 );
485 assert_eq!(
486 ForwardStatus::from_db("success"),
487 Some(ForwardStatus::Success)
488 );
489 assert_eq!(ForwardStatus::from_db("error"), Some(ForwardStatus::Error));
490 assert_eq!(ForwardStatus::from_db("invalid"), None);
491 }
492
493 #[test]
494 fn test_metrics_date_filter_default() {
495 let filter = MetricsDateFilter::default();
496 assert!(filter.start_date.is_none());
497 assert!(filter.end_date.is_none());
498 }
499
500 #[test]
501 fn test_session_metrics_filter_default() {
502 let filter = SessionMetricsFilter::default();
503 assert!(filter.start_date.is_none());
504 assert!(filter.model.is_none());
505 assert!(filter.limit.is_none());
506 }
507
508 #[test]
509 fn test_forward_metrics_filter_default() {
510 let filter = ForwardMetricsFilter::default();
511 assert!(filter.start_date.is_none());
512 assert!(filter.endpoint.is_none());
513 assert!(filter.limit.is_none());
514 }
515
516 #[test]
517 fn test_tool_call_metrics_serialization() {
518 let metrics = ToolCallMetrics {
519 tool_call_id: "call-123".to_string(),
520 tool_name: "bash".to_string(),
521 started_at: Utc::now(),
522 completed_at: Some(Utc::now()),
523 success: Some(true),
524 error: None,
525 duration_ms: Some(150),
526 };
527
528 let json = serde_json::to_string(&metrics).unwrap();
529 assert!(json.contains("\"tool_call_id\":\"call-123\""));
530 assert!(json.contains("\"tool_name\":\"bash\""));
531 }
532
533 #[test]
534 fn test_round_metrics_serialization() {
535 let metrics = RoundMetrics {
536 round_id: "round-1".to_string(),
537 session_id: "session-1".to_string(),
538 model: "gpt-4".to_string(),
539 started_at: Utc::now(),
540 completed_at: None,
541 token_usage: TokenUsage::default(),
542 tool_calls: vec![],
543 status: RoundStatus::Running,
544 error: None,
545 duration_ms: None,
546 prompt_cached_tool_outputs: 0,
547 prompt_cached_tool_tokens_saved: 0,
548 compression_count: 0,
549 tokens_saved: 0,
550 };
551
552 let json = serde_json::to_string(&metrics).unwrap();
553 assert!(json.contains("\"model\":\"gpt-4\""));
554 }
555
556 #[test]
557 fn test_session_metrics_serialization() {
558 let metrics = SessionMetrics {
559 session_id: "session-1".to_string(),
560 model: "gpt-4".to_string(),
561 started_at: Utc::now(),
562 completed_at: None,
563 total_rounds: 5,
564 total_token_usage: TokenUsage::default(),
565 tool_call_count: 10,
566 tool_breakdown: HashMap::new(),
567 status: SessionStatus::Running,
568 message_count: 15,
569 duration_ms: None,
570 prompt_cached_tool_outputs: 0,
571 prompt_cached_tool_tokens_saved: 0,
572 total_compression_events: 0,
573 total_tokens_saved: 0,
574 };
575
576 let json = serde_json::to_string(&metrics).unwrap();
577 assert!(json.contains("\"session_id\":\"session-1\""));
578 assert!(json.contains("\"total_rounds\":5"));
579 }
580
581 #[test]
582 fn test_daily_metrics_serialization() {
583 let metrics = DailyMetrics {
584 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
585 total_sessions: 10,
586 total_rounds: 50,
587 total_token_usage: TokenUsage::default(),
588 total_tool_calls: 100,
589 prompt_cached_tool_outputs: 0,
590 model_breakdown: HashMap::new(),
591 tool_breakdown: HashMap::new(),
592 };
593
594 let json = serde_json::to_string(&metrics).unwrap();
595 assert!(json.contains("\"total_sessions\":10"));
596 assert!(json.contains("\"total_rounds\":50"));
597 }
598
599 #[test]
600 fn test_metrics_summary_serialization() {
601 let summary = MetricsSummary {
602 total_sessions: 100,
603 total_tokens: TokenUsage::default(),
604 total_tool_calls: 500,
605 active_sessions: 5,
606 prompt_cached_tool_outputs: 0,
607 tool_context_tokens_saved: 0,
608 total_compression_events: 0,
609 total_tokens_saved: 0,
610 non_tool_compression_tokens_saved: 0,
611 completed_sessions: 80,
612 awaiting_response_sessions: 10,
613 error_sessions: 7,
614 cancelled_sessions: 3,
615 total_sync_mismatches: 0,
616 sync_mismatch_breakdown: HashMap::new(),
617 };
618
619 let json = serde_json::to_string(&summary).unwrap();
620 assert!(json.contains("\"total_sessions\":100"));
621 assert!(json.contains("\"active_sessions\":5"));
622 }
623
624 #[test]
625 fn test_model_metrics_serialization() {
626 let metrics = ModelMetrics {
627 model: "gpt-4".to_string(),
628 sessions: 50,
629 rounds: 200,
630 tokens: TokenUsage::default(),
631 tool_calls: 100,
632 prompt_cached_tool_outputs: 0,
633 };
634
635 let json = serde_json::to_string(&metrics).unwrap();
636 assert!(json.contains("\"model\":\"gpt-4\""));
637 assert!(json.contains("\"sessions\":50"));
638 }
639
640 #[test]
641 fn test_forward_request_metrics_serialization() {
642 let metrics = ForwardRequestMetrics {
643 forward_id: "fwd-123".to_string(),
644 endpoint: "/api/chat".to_string(),
645 model: "gpt-4".to_string(),
646 is_stream: true,
647 started_at: Utc::now(),
648 completed_at: Some(Utc::now()),
649 status_code: Some(200),
650 status: Some(ForwardStatus::Success),
651 token_usage: None,
652 error: None,
653 duration_ms: Some(250),
654 };
655
656 let json = serde_json::to_string(&metrics).unwrap();
657 assert!(json.contains("\"forward_id\":\"fwd-123\""));
658 assert!(json.contains("\"endpoint\":\"/api/chat\""));
659 }
660
661 #[test]
662 fn test_forward_metrics_summary_serialization() {
663 let summary = ForwardMetricsSummary {
664 total_requests: 1000,
665 successful_requests: 950,
666 failed_requests: 50,
667 total_tokens: TokenUsage::default(),
668 avg_duration_ms: Some(200),
669 };
670
671 let json = serde_json::to_string(&summary).unwrap();
672 assert!(json.contains("\"total_requests\":1000"));
673 assert!(json.contains("\"successful_requests\":950"));
674 }
675
676 #[test]
677 fn test_token_usage_clone() {
678 let usage = TokenUsage {
679 prompt_tokens: 100,
680 completion_tokens: 50,
681 total_tokens: 150,
682 };
683 let cloned = usage.clone();
684 assert_eq!(usage.prompt_tokens, cloned.prompt_tokens);
685 }
686
687 #[test]
688 fn test_round_status_clone() {
689 let status = RoundStatus::Success;
690 let cloned = status.clone();
691 assert_eq!(status, cloned);
692 }
693
694 #[test]
695 fn test_session_status_clone() {
696 let status = SessionStatus::Completed;
697 let cloned = status.clone();
698 assert_eq!(status, cloned);
699 }
700
701 #[test]
702 fn test_forward_status_clone() {
703 let status = ForwardStatus::Pending;
704 let cloned = status.clone();
705 assert_eq!(status, cloned);
706 }
707
708 #[test]
709 fn test_token_usage_eq() {
710 let usage1 = TokenUsage {
711 prompt_tokens: 100,
712 completion_tokens: 50,
713 total_tokens: 150,
714 };
715 let usage2 = TokenUsage {
716 prompt_tokens: 100,
717 completion_tokens: 50,
718 total_tokens: 150,
719 };
720 assert_eq!(usage1, usage2);
721 }
722
723 #[test]
724 fn test_round_status_eq() {
725 assert_eq!(RoundStatus::Running, RoundStatus::Running);
726 assert_ne!(RoundStatus::Running, RoundStatus::Success);
727 }
728
729 #[test]
730 fn test_session_status_eq() {
731 assert_eq!(SessionStatus::Running, SessionStatus::Running);
732 assert_ne!(SessionStatus::Running, SessionStatus::Completed);
733 assert_ne!(SessionStatus::AwaitingResponse, SessionStatus::Completed);
734 }
735
736 #[test]
737 fn test_round_metrics_compression_fields_deserialize_with_defaults() {
738 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}"#;
739 let metrics: RoundMetrics = serde_json::from_str(json).unwrap();
740 assert_eq!(metrics.compression_count, 0);
741 assert_eq!(metrics.tokens_saved, 0);
742 }
743
744 #[test]
745 fn test_session_metrics_compression_fields_deserialize_with_defaults() {
746 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}"#;
747 let metrics: SessionMetrics = serde_json::from_str(json).unwrap();
748 assert_eq!(metrics.total_compression_events, 0);
749 assert_eq!(metrics.total_tokens_saved, 0);
750 }
751
752 #[test]
753 fn test_metrics_summary_additive_fields_deserialize_with_defaults() {
754 let json = r#"{"total_sessions":1,"total_tokens":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0},"total_tool_calls":0,"active_sessions":0}"#;
755 let summary: MetricsSummary = serde_json::from_str(json).unwrap();
756 assert_eq!(summary.prompt_cached_tool_outputs, 0);
757 assert_eq!(summary.total_compression_events, 0);
758 assert_eq!(summary.total_tokens_saved, 0);
759 assert_eq!(summary.completed_sessions, 0);
760 assert_eq!(summary.awaiting_response_sessions, 0);
761 assert_eq!(summary.error_sessions, 0);
762 assert_eq!(summary.cancelled_sessions, 0);
763 }
764}