Skip to main content

cc_token_usage/analysis/
session.rs

1use 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    // Merge turns and agent_turns, keeping track of which are agent turns
13    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    // Sort by timestamp
22    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        // Extract 5m/1h TTL breakdown
40        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        // Track max context
57        if context_size > max_context {
58            max_context = context_size;
59        }
60
61        // Compaction detection and context delta
62        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        // Track stop reasons
84        if let Some(ref reason) = turn.stop_reason {
85            *stop_reason_counts.entry(reason.clone()).or_insert(0) += 1;
86        }
87
88        // Aggregate tokens
89        total_tokens.add_usage(&turn.usage);
90        total_cost += pricing_cost.total;
91
92        // Model frequency
93        *model_counts.entry(&turn.model).or_insert(0) += 1;
94
95        // Agent summary
96        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    // Duration
127    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    // Compaction count
133    let compaction_count = turn_details.iter().filter(|t| t.is_compaction).count();
134
135    // Cache write percentages from total tokens
136    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    // Primary model
151    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}