cc_token_usage/analysis/
session.rs1use std::collections::HashMap;
2
3use crate::data::models::{SessionData, ValidatedTurn};
4use crate::pricing::calculator::PricingCalculator;
5
6use super::{AgentSummary, AggregatedTokens, SessionResult, TurnCostBreakdown, TurnDetail};
7
8pub fn analyze_session(
9 session: &SessionData,
10 calc: &PricingCalculator,
11) -> SessionResult {
12 let mut all_turns: Vec<(&ValidatedTurn, bool)> = Vec::new();
14 for turn in &session.turns {
15 all_turns.push((turn, false));
16 }
17 for turn in &session.agent_turns {
18 all_turns.push((turn, true));
19 }
20
21 all_turns.sort_by_key(|(turn, _)| turn.timestamp);
23
24 let mut turn_details = Vec::new();
25 let mut total_tokens = AggregatedTokens::default();
26 let mut total_cost = 0.0;
27 let mut stop_reason_counts: HashMap<String, usize> = HashMap::new();
28 let mut agent_summary = AgentSummary::default();
29 let mut model_counts: HashMap<&str, usize> = HashMap::new();
30 let mut max_context: u64 = 0;
31 let mut prev_context_size: Option<u64> = None;
32
33 for (i, (turn, is_from_agent_file)) in all_turns.iter().enumerate() {
34 let input = turn.usage.input_tokens.unwrap_or(0);
35 let output = turn.usage.output_tokens.unwrap_or(0);
36 let cache_create = turn.usage.cache_creation_input_tokens.unwrap_or(0);
37 let cache_read = turn.usage.cache_read_input_tokens.unwrap_or(0);
38
39 let (cache_write_5m, cache_write_1h) = if let Some(ref detail) = turn.usage.cache_creation {
41 (
42 detail.ephemeral_5m_input_tokens.unwrap_or(0),
43 detail.ephemeral_1h_input_tokens.unwrap_or(0),
44 )
45 } else {
46 (0, 0)
47 };
48
49 let context_size = input + cache_create + cache_read;
50 let cache_hit_rate = if context_size > 0 {
51 (cache_read as f64 / context_size as f64) * 100.0
52 } else {
53 0.0
54 };
55
56 if context_size > max_context {
58 max_context = context_size;
59 }
60
61 let is_compaction = match prev_context_size {
63 Some(prev) => prev > 0 && (context_size as f64) < (prev as f64 * 0.9),
64 None => false,
65 };
66 let context_delta = match prev_context_size {
67 Some(prev) => context_size as i64 - prev as i64,
68 None => 0,
69 };
70 prev_context_size = Some(context_size);
71
72 let pricing_cost = calc.calculate_turn_cost(&turn.model, &turn.usage);
73
74 let cost_breakdown = TurnCostBreakdown {
75 input_cost: pricing_cost.input_cost,
76 output_cost: pricing_cost.output_cost,
77 cache_write_5m_cost: pricing_cost.cache_write_5m_cost,
78 cache_write_1h_cost: pricing_cost.cache_write_1h_cost,
79 cache_read_cost: pricing_cost.cache_read_cost,
80 total: pricing_cost.total,
81 };
82
83 if let Some(ref reason) = turn.stop_reason {
85 *stop_reason_counts.entry(reason.clone()).or_insert(0) += 1;
86 }
87
88 total_tokens.add_usage(&turn.usage);
90 total_cost += pricing_cost.total;
91
92 *model_counts.entry(&turn.model).or_insert(0) += 1;
94
95 let is_agent = turn.is_agent || *is_from_agent_file;
97 if is_agent {
98 agent_summary.total_agent_turns += 1;
99 agent_summary.agent_output_tokens += output;
100 agent_summary.agent_cost += pricing_cost.total;
101 }
102
103 turn_details.push(TurnDetail {
104 turn_number: i + 1,
105 timestamp: turn.timestamp,
106 model: turn.model.clone(),
107 input_tokens: input,
108 output_tokens: output,
109 cache_write_5m_tokens: cache_write_5m,
110 cache_write_1h_tokens: cache_write_1h,
111 cache_read_tokens: cache_read,
112 context_size,
113 cache_hit_rate,
114 cost: pricing_cost.total,
115 cost_breakdown,
116 stop_reason: turn.stop_reason.clone(),
117 is_agent,
118 is_compaction,
119 context_delta,
120 user_text: turn.user_text.clone(),
121 assistant_text: turn.assistant_text.clone(),
122 tool_names: turn.tool_names.clone(),
123 });
124 }
125
126 let duration_minutes = match (session.first_timestamp, session.last_timestamp) {
128 (Some(first), Some(last)) => (last - first).num_seconds() as f64 / 60.0,
129 _ => 0.0,
130 };
131
132 let compaction_count = turn_details.iter().filter(|t| t.is_compaction).count();
134
135 let total_5m = total_tokens.cache_write_5m_tokens;
137 let total_1h = total_tokens.cache_write_1h_tokens;
138 let total_cache_write = total_5m + total_1h;
139 let cache_write_5m_pct = if total_cache_write > 0 {
140 (total_5m as f64 / total_cache_write as f64) * 100.0
141 } else {
142 0.0
143 };
144 let cache_write_1h_pct = if total_cache_write > 0 {
145 (total_1h as f64 / total_cache_write as f64) * 100.0
146 } else {
147 0.0
148 };
149
150 let model = model_counts
152 .into_iter()
153 .max_by_key(|(_, count)| *count)
154 .map(|(m, _)| m.to_string())
155 .unwrap_or_default();
156
157 SessionResult {
158 session_id: session.session_id.clone(),
159 project: session
160 .project
161 .clone()
162 .unwrap_or_else(|| "(unknown)".to_string()),
163 turn_details,
164 agent_summary,
165 total_tokens,
166 total_cost,
167 stop_reason_counts,
168 duration_minutes,
169 max_context,
170 compaction_count,
171 cache_write_5m_pct,
172 cache_write_1h_pct,
173 model,
174 }
175}