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