use std::collections::HashMap;
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
pub use bamboo_domain::TokenUsage;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RoundStatus {
Running,
Success,
Error,
Cancelled,
}
impl RoundStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Running => "running",
Self::Success => "success",
Self::Error => "error",
Self::Cancelled => "cancelled",
}
}
pub fn from_db(value: &str) -> Option<Self> {
match value {
"running" => Some(Self::Running),
"success" => Some(Self::Success),
"error" => Some(Self::Error),
"cancelled" => Some(Self::Cancelled),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SessionStatus {
Running,
Completed,
Error,
Cancelled,
}
impl SessionStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Running => "running",
Self::Completed => "completed",
Self::Error => "error",
Self::Cancelled => "cancelled",
}
}
pub fn from_db(value: &str) -> Option<Self> {
match value {
"running" => Some(Self::Running),
"completed" => Some(Self::Completed),
"error" => Some(Self::Error),
"cancelled" => Some(Self::Cancelled),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolCallMetrics {
pub tool_call_id: String,
pub tool_name: String,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub success: Option<bool>,
pub error: Option<String>,
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RoundMetrics {
pub round_id: String,
pub session_id: String,
pub model: String,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub token_usage: TokenUsage,
pub tool_calls: Vec<ToolCallMetrics>,
pub status: RoundStatus,
pub error: Option<String>,
pub duration_ms: Option<u64>,
#[serde(default)]
pub prompt_cached_tool_outputs: u32,
#[serde(default)]
pub compression_count: u32,
#[serde(default)]
pub tokens_saved: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionMetrics {
pub session_id: String,
pub model: String,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub total_rounds: u32,
pub total_token_usage: TokenUsage,
pub tool_call_count: u32,
pub tool_breakdown: HashMap<String, u32>,
pub status: SessionStatus,
pub message_count: u32,
pub duration_ms: Option<u64>,
#[serde(default)]
pub prompt_cached_tool_outputs: u64,
#[serde(default)]
pub total_compression_events: u64,
#[serde(default)]
pub total_tokens_saved: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionDetail {
pub session: SessionMetrics,
pub rounds: Vec<RoundMetrics>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DailyMetrics {
pub date: NaiveDate,
pub total_sessions: u32,
pub total_rounds: u32,
pub total_token_usage: TokenUsage,
pub total_tool_calls: u32,
pub model_breakdown: HashMap<String, TokenUsage>,
pub tool_breakdown: HashMap<String, u32>,
#[serde(default)]
pub prompt_cached_tool_outputs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MetricsSummary {
pub total_sessions: u64,
pub total_tokens: TokenUsage,
pub total_tool_calls: u64,
pub active_sessions: u64,
#[serde(default)]
pub prompt_cached_tool_outputs: u64,
#[serde(default)]
pub total_sync_mismatches: u64,
#[serde(default)]
pub sync_mismatch_breakdown: HashMap<String, u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModelMetrics {
pub model: String,
pub sessions: u64,
pub rounds: u64,
pub tokens: TokenUsage,
pub tool_calls: u64,
#[serde(default)]
pub prompt_cached_tool_outputs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct MetricsDateFilter {
pub start_date: Option<NaiveDate>,
pub end_date: Option<NaiveDate>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct SessionMetricsFilter {
pub start_date: Option<NaiveDate>,
pub end_date: Option<NaiveDate>,
pub model: Option<String>,
pub limit: Option<u32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ForwardStatus {
Success,
Error,
}
impl ForwardStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Success => "success",
Self::Error => "error",
}
}
pub fn from_db(value: &str) -> Option<Self> {
match value {
"success" => Some(Self::Success),
"error" => Some(Self::Error),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ForwardRequestMetrics {
pub forward_id: String,
pub endpoint: String,
pub model: String,
pub is_stream: bool,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub status_code: Option<u16>,
pub status: Option<ForwardStatus>,
pub token_usage: Option<TokenUsage>,
pub error: Option<String>,
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ForwardMetricsSummary {
pub total_requests: u64,
pub successful_requests: u64,
pub failed_requests: u64,
pub total_tokens: TokenUsage,
pub avg_duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ForwardEndpointMetrics {
pub endpoint: String,
pub requests: u64,
pub successful: u64,
pub failed: u64,
pub tokens: TokenUsage,
pub avg_duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct ForwardMetricsFilter {
pub start_date: Option<NaiveDate>,
pub end_date: Option<NaiveDate>,
pub endpoint: Option<String>,
pub model: Option<String>,
pub limit: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_usage_default() {
let usage = TokenUsage::default();
assert_eq!(usage.prompt_tokens, 0);
assert_eq!(usage.completion_tokens, 0);
assert_eq!(usage.total_tokens, 0);
}
#[test]
fn test_token_usage_add_assign() {
let mut usage1 = TokenUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let usage2 = TokenUsage {
prompt_tokens: 200,
completion_tokens: 100,
total_tokens: 300,
};
usage1.add_assign(usage2);
assert_eq!(usage1.prompt_tokens, 300);
assert_eq!(usage1.completion_tokens, 150);
assert_eq!(usage1.total_tokens, 450);
}
#[test]
fn test_token_usage_serialization() {
let usage = TokenUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let json = serde_json::to_string(&usage).unwrap();
assert!(json.contains("\"prompt_tokens\":100"));
assert!(json.contains("\"completion_tokens\":50"));
}
#[test]
fn test_round_status_as_str() {
assert_eq!(RoundStatus::Running.as_str(), "running");
assert_eq!(RoundStatus::Success.as_str(), "success");
assert_eq!(RoundStatus::Error.as_str(), "error");
assert_eq!(RoundStatus::Cancelled.as_str(), "cancelled");
}
#[test]
fn test_round_status_from_db() {
assert_eq!(RoundStatus::from_db("running"), Some(RoundStatus::Running));
assert_eq!(RoundStatus::from_db("success"), Some(RoundStatus::Success));
assert_eq!(RoundStatus::from_db("invalid"), None);
}
#[test]
fn test_round_status_serialization() {
let status = RoundStatus::Success;
let json = serde_json::to_string(&status).unwrap();
assert!(json.contains("\"success\""));
}
#[test]
fn test_session_status_as_str() {
assert_eq!(SessionStatus::Running.as_str(), "running");
assert_eq!(SessionStatus::Completed.as_str(), "completed");
assert_eq!(SessionStatus::Error.as_str(), "error");
assert_eq!(SessionStatus::Cancelled.as_str(), "cancelled");
}
#[test]
fn test_session_status_from_db() {
assert_eq!(
SessionStatus::from_db("running"),
Some(SessionStatus::Running)
);
assert_eq!(
SessionStatus::from_db("completed"),
Some(SessionStatus::Completed)
);
assert_eq!(SessionStatus::from_db("invalid"), None);
}
#[test]
fn test_forward_status_as_str() {
assert_eq!(ForwardStatus::Success.as_str(), "success");
assert_eq!(ForwardStatus::Error.as_str(), "error");
}
#[test]
fn test_forward_status_from_db() {
assert_eq!(
ForwardStatus::from_db("success"),
Some(ForwardStatus::Success)
);
assert_eq!(ForwardStatus::from_db("error"), Some(ForwardStatus::Error));
assert_eq!(ForwardStatus::from_db("invalid"), None);
}
#[test]
fn test_metrics_date_filter_default() {
let filter = MetricsDateFilter::default();
assert!(filter.start_date.is_none());
assert!(filter.end_date.is_none());
}
#[test]
fn test_session_metrics_filter_default() {
let filter = SessionMetricsFilter::default();
assert!(filter.start_date.is_none());
assert!(filter.model.is_none());
assert!(filter.limit.is_none());
}
#[test]
fn test_forward_metrics_filter_default() {
let filter = ForwardMetricsFilter::default();
assert!(filter.start_date.is_none());
assert!(filter.endpoint.is_none());
assert!(filter.limit.is_none());
}
#[test]
fn test_tool_call_metrics_serialization() {
let metrics = ToolCallMetrics {
tool_call_id: "call-123".to_string(),
tool_name: "bash".to_string(),
started_at: Utc::now(),
completed_at: Some(Utc::now()),
success: Some(true),
error: None,
duration_ms: Some(150),
};
let json = serde_json::to_string(&metrics).unwrap();
assert!(json.contains("\"tool_call_id\":\"call-123\""));
assert!(json.contains("\"tool_name\":\"bash\""));
}
#[test]
fn test_round_metrics_serialization() {
let metrics = RoundMetrics {
round_id: "round-1".to_string(),
session_id: "session-1".to_string(),
model: "gpt-4".to_string(),
started_at: Utc::now(),
completed_at: None,
token_usage: TokenUsage::default(),
tool_calls: vec![],
status: RoundStatus::Running,
error: None,
duration_ms: None,
prompt_cached_tool_outputs: 0,
compression_count: 0,
tokens_saved: 0,
};
let json = serde_json::to_string(&metrics).unwrap();
assert!(json.contains("\"model\":\"gpt-4\""));
}
#[test]
fn test_session_metrics_serialization() {
let metrics = SessionMetrics {
session_id: "session-1".to_string(),
model: "gpt-4".to_string(),
started_at: Utc::now(),
completed_at: None,
total_rounds: 5,
total_token_usage: TokenUsage::default(),
tool_call_count: 10,
tool_breakdown: HashMap::new(),
status: SessionStatus::Running,
message_count: 15,
duration_ms: None,
prompt_cached_tool_outputs: 0,
total_compression_events: 0,
total_tokens_saved: 0,
};
let json = serde_json::to_string(&metrics).unwrap();
assert!(json.contains("\"session_id\":\"session-1\""));
assert!(json.contains("\"total_rounds\":5"));
}
#[test]
fn test_daily_metrics_serialization() {
let metrics = DailyMetrics {
date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
total_sessions: 10,
total_rounds: 50,
total_token_usage: TokenUsage::default(),
total_tool_calls: 100,
prompt_cached_tool_outputs: 0,
model_breakdown: HashMap::new(),
tool_breakdown: HashMap::new(),
};
let json = serde_json::to_string(&metrics).unwrap();
assert!(json.contains("\"total_sessions\":10"));
assert!(json.contains("\"total_rounds\":50"));
}
#[test]
fn test_metrics_summary_serialization() {
let summary = MetricsSummary {
total_sessions: 100,
total_tokens: TokenUsage::default(),
total_tool_calls: 500,
active_sessions: 5,
prompt_cached_tool_outputs: 0,
total_sync_mismatches: 0,
sync_mismatch_breakdown: HashMap::new(),
};
let json = serde_json::to_string(&summary).unwrap();
assert!(json.contains("\"total_sessions\":100"));
assert!(json.contains("\"active_sessions\":5"));
}
#[test]
fn test_model_metrics_serialization() {
let metrics = ModelMetrics {
model: "gpt-4".to_string(),
sessions: 50,
rounds: 200,
tokens: TokenUsage::default(),
tool_calls: 100,
prompt_cached_tool_outputs: 0,
};
let json = serde_json::to_string(&metrics).unwrap();
assert!(json.contains("\"model\":\"gpt-4\""));
assert!(json.contains("\"sessions\":50"));
}
#[test]
fn test_forward_request_metrics_serialization() {
let metrics = ForwardRequestMetrics {
forward_id: "fwd-123".to_string(),
endpoint: "/api/chat".to_string(),
model: "gpt-4".to_string(),
is_stream: true,
started_at: Utc::now(),
completed_at: Some(Utc::now()),
status_code: Some(200),
status: Some(ForwardStatus::Success),
token_usage: None,
error: None,
duration_ms: Some(250),
};
let json = serde_json::to_string(&metrics).unwrap();
assert!(json.contains("\"forward_id\":\"fwd-123\""));
assert!(json.contains("\"endpoint\":\"/api/chat\""));
}
#[test]
fn test_forward_metrics_summary_serialization() {
let summary = ForwardMetricsSummary {
total_requests: 1000,
successful_requests: 950,
failed_requests: 50,
total_tokens: TokenUsage::default(),
avg_duration_ms: Some(200),
};
let json = serde_json::to_string(&summary).unwrap();
assert!(json.contains("\"total_requests\":1000"));
assert!(json.contains("\"successful_requests\":950"));
}
#[test]
fn test_token_usage_clone() {
let usage = TokenUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let cloned = usage.clone();
assert_eq!(usage.prompt_tokens, cloned.prompt_tokens);
}
#[test]
fn test_round_status_clone() {
let status = RoundStatus::Success;
let cloned = status.clone();
assert_eq!(status, cloned);
}
#[test]
fn test_session_status_clone() {
let status = SessionStatus::Completed;
let cloned = status.clone();
assert_eq!(status, cloned);
}
#[test]
fn test_forward_status_clone() {
let status = ForwardStatus::Success;
let cloned = status.clone();
assert_eq!(status, cloned);
}
#[test]
fn test_token_usage_eq() {
let usage1 = TokenUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let usage2 = TokenUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
assert_eq!(usage1, usage2);
}
#[test]
fn test_round_status_eq() {
assert_eq!(RoundStatus::Running, RoundStatus::Running);
assert_ne!(RoundStatus::Running, RoundStatus::Success);
}
#[test]
fn test_session_status_eq() {
assert_eq!(SessionStatus::Running, SessionStatus::Running);
assert_ne!(SessionStatus::Running, SessionStatus::Completed);
}
#[test]
fn test_round_metrics_compression_fields_deserialize_with_defaults() {
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}"#;
let metrics: RoundMetrics = serde_json::from_str(json).unwrap();
assert_eq!(metrics.compression_count, 0);
assert_eq!(metrics.tokens_saved, 0);
}
#[test]
fn test_session_metrics_compression_fields_deserialize_with_defaults() {
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}"#;
let metrics: SessionMetrics = serde_json::from_str(json).unwrap();
assert_eq!(metrics.total_compression_events, 0);
assert_eq!(metrics.total_tokens_saved, 0);
}
}