Skip to main content

cc_token_usage/analysis/
session.rs

1use std::collections::HashMap;
2
3use crate::data::models::SessionData;
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 all_turns = session.all_responses();
13
14    let mut turn_details = Vec::new();
15    let mut total_tokens = AggregatedTokens::default();
16    let mut total_cost = 0.0;
17    let mut stop_reason_counts: HashMap<String, usize> = HashMap::new();
18    let mut agent_summary = AgentSummary::default();
19    let mut model_counts: HashMap<&str, usize> = HashMap::new();
20    let mut max_context: u64 = 0;
21    let mut prev_context_size: Option<u64> = None;
22
23    for (i, turn) in all_turns.iter().enumerate() {
24        let input = turn.usage.input_tokens.unwrap_or(0);
25        let output = turn.usage.output_tokens.unwrap_or(0);
26        let cache_create = turn.usage.cache_creation_input_tokens.unwrap_or(0);
27        let cache_read = turn.usage.cache_read_input_tokens.unwrap_or(0);
28
29        // Extract 5m/1h TTL breakdown
30        let (cache_write_5m, cache_write_1h) = if let Some(ref detail) = turn.usage.cache_creation {
31            (
32                detail.ephemeral_5m_input_tokens.unwrap_or(0),
33                detail.ephemeral_1h_input_tokens.unwrap_or(0),
34            )
35        } else {
36            (0, 0)
37        };
38
39        let context_size = input + cache_create + cache_read;
40        let cache_hit_rate = if context_size > 0 {
41            (cache_read as f64 / context_size as f64) * 100.0
42        } else {
43            0.0
44        };
45
46        // Track max context
47        if context_size > max_context {
48            max_context = context_size;
49        }
50
51        // Compaction detection and context delta
52        let is_compaction = match prev_context_size {
53            Some(prev) => prev > 0 && (context_size as f64) < (prev as f64 * 0.9),
54            None => false,
55        };
56        let context_delta = match prev_context_size {
57            Some(prev) => context_size as i64 - prev as i64,
58            None => 0,
59        };
60        prev_context_size = Some(context_size);
61
62        let pricing_cost = calc.calculate_turn_cost(&turn.model, &turn.usage);
63
64        let cost_breakdown = TurnCostBreakdown {
65            input_cost: pricing_cost.input_cost,
66            output_cost: pricing_cost.output_cost,
67            cache_write_5m_cost: pricing_cost.cache_write_5m_cost,
68            cache_write_1h_cost: pricing_cost.cache_write_1h_cost,
69            cache_read_cost: pricing_cost.cache_read_cost,
70            total: pricing_cost.total,
71        };
72
73        // Track stop reasons
74        if let Some(ref reason) = turn.stop_reason {
75            *stop_reason_counts.entry(reason.clone()).or_insert(0) += 1;
76        }
77
78        // Aggregate tokens
79        total_tokens.add_usage(&turn.usage);
80        total_cost += pricing_cost.total;
81
82        // Model frequency
83        *model_counts.entry(&turn.model).or_insert(0) += 1;
84
85        // Agent summary
86        let is_agent = turn.is_agent;
87        if is_agent {
88            agent_summary.total_agent_turns += 1;
89            agent_summary.agent_output_tokens += output;
90            agent_summary.agent_cost += pricing_cost.total;
91        }
92
93        turn_details.push(TurnDetail {
94            turn_number: i + 1,
95            timestamp: turn.timestamp,
96            model: turn.model.clone(),
97            input_tokens: input,
98            output_tokens: output,
99            cache_write_5m_tokens: cache_write_5m,
100            cache_write_1h_tokens: cache_write_1h,
101            cache_read_tokens: cache_read,
102            context_size,
103            cache_hit_rate,
104            cost: pricing_cost.total,
105            cost_breakdown,
106            stop_reason: turn.stop_reason.clone(),
107            is_agent,
108            is_compaction,
109            context_delta,
110            user_text: turn.user_text.clone(),
111            assistant_text: turn.assistant_text.clone(),
112            tool_names: turn.tool_names.clone(),
113        });
114    }
115
116    // Duration
117    let duration_minutes = match (session.first_timestamp, session.last_timestamp) {
118        (Some(first), Some(last)) => (last - first).num_seconds() as f64 / 60.0,
119        _ => 0.0,
120    };
121
122    // Compaction count
123    let compaction_count = turn_details.iter().filter(|t| t.is_compaction).count();
124
125    // Cache write percentages from total tokens
126    let total_5m = total_tokens.cache_write_5m_tokens;
127    let total_1h = total_tokens.cache_write_1h_tokens;
128    let total_cache_write = total_5m + total_1h;
129    let cache_write_5m_pct = if total_cache_write > 0 {
130        (total_5m as f64 / total_cache_write as f64) * 100.0
131    } else {
132        0.0
133    };
134    let cache_write_1h_pct = if total_cache_write > 0 {
135        (total_1h as f64 / total_cache_write as f64) * 100.0
136    } else {
137        0.0
138    };
139
140    // Primary model
141    let model = model_counts
142        .into_iter()
143        .max_by_key(|(_, count)| *count)
144        .map(|(m, _)| m.to_string())
145        .unwrap_or_default();
146
147    SessionResult {
148        session_id: session.session_id.clone(),
149        project: session
150            .project
151            .clone()
152            .unwrap_or_else(|| "(unknown)".to_string()),
153        turn_details,
154        agent_summary,
155        total_tokens,
156        total_cost,
157        stop_reason_counts,
158        duration_minutes,
159        max_context,
160        compaction_count,
161        cache_write_5m_pct,
162        cache_write_1h_pct,
163        model,
164    }
165}