Skip to content

Commit 270957b

Browse files
committed
feat: add session usage tracking with token counts and cost estimation
- Add SessionUsage struct with input/output/cache tokens and cost - Add get_sessions_usage Tauri command to fetch usage data - Calculate cost based on Claude Opus 4.5 pricing - Add TypeScript types for SessionUsage and SessionUsageEntry
1 parent 0206e3c commit 270957b

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

src-tauri/src/lib.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ pub struct Project {
168168
pub last_active: u64,
169169
}
170170

171+
#[derive(Debug, Serialize, Deserialize, Default)]
172+
pub struct SessionUsage {
173+
pub input_tokens: u64,
174+
pub output_tokens: u64,
175+
pub cache_creation_tokens: u64,
176+
pub cache_read_tokens: u64,
177+
pub cost_usd: f64, // estimated cost in USD
178+
}
179+
171180
#[derive(Debug, Serialize, Deserialize)]
172181
pub struct Session {
173182
pub id: String,
@@ -176,6 +185,7 @@ pub struct Session {
176185
pub summary: Option<String>,
177186
pub message_count: usize,
178187
pub last_modified: u64,
188+
pub usage: Option<SessionUsage>,
179189
}
180190

181191
#[derive(Debug, Serialize, Deserialize)]
@@ -219,10 +229,19 @@ struct RawLine {
219229
is_meta: Option<bool>,
220230
}
221231

232+
#[derive(Debug, Deserialize, Default)]
233+
struct RawUsage {
234+
input_tokens: Option<u64>,
235+
output_tokens: Option<u64>,
236+
cache_creation_input_tokens: Option<u64>,
237+
cache_read_input_tokens: Option<u64>,
238+
}
239+
222240
#[derive(Debug, Deserialize)]
223241
struct RawMessage {
224242
role: Option<String>,
225243
content: Option<serde_json::Value>,
244+
usage: Option<RawUsage>,
226245
}
227246

228247
/// Entry from history.jsonl - used as fast session index
@@ -479,6 +498,7 @@ async fn list_sessions(project_id: String) -> Result<Vec<Session>, String> {
479498
summary,
480499
message_count,
481500
last_modified,
501+
usage: None,
482502
});
483503
}
484504
}
@@ -490,6 +510,92 @@ async fn list_sessions(project_id: String) -> Result<Vec<Session>, String> {
490510
.map_err(|e| e.to_string())?
491511
}
492512

513+
/// Pricing constants (USD per 1M tokens) - Claude Opus 4.5 pricing as baseline
514+
const PRICE_INPUT_PER_M: f64 = 15.0;
515+
const PRICE_OUTPUT_PER_M: f64 = 75.0;
516+
const PRICE_CACHE_WRITE_PER_M: f64 = 3.75; // cache creation
517+
const PRICE_CACHE_READ_PER_M: f64 = 0.30; // cache read
518+
519+
/// Calculate cost from token counts
520+
fn calculate_cost(usage: &SessionUsage) -> f64 {
521+
let input_cost = (usage.input_tokens as f64 / 1_000_000.0) * PRICE_INPUT_PER_M;
522+
let output_cost = (usage.output_tokens as f64 / 1_000_000.0) * PRICE_OUTPUT_PER_M;
523+
let cache_write_cost = (usage.cache_creation_tokens as f64 / 1_000_000.0) * PRICE_CACHE_WRITE_PER_M;
524+
let cache_read_cost = (usage.cache_read_tokens as f64 / 1_000_000.0) * PRICE_CACHE_READ_PER_M;
525+
input_cost + output_cost + cache_write_cost + cache_read_cost
526+
}
527+
528+
/// Read usage data from a session file
529+
fn read_session_usage(path: &Path) -> SessionUsage {
530+
use std::io::{BufRead, BufReader};
531+
532+
let file = match fs::File::open(path) {
533+
Ok(f) => f,
534+
Err(_) => return SessionUsage::default(),
535+
};
536+
537+
let reader = BufReader::new(file);
538+
let mut usage = SessionUsage::default();
539+
540+
for line in reader.lines() {
541+
let line = match line {
542+
Ok(l) => l,
543+
Err(_) => continue,
544+
};
545+
if let Ok(parsed) = serde_json::from_str::<RawLine>(&line) {
546+
// Only assistant messages have usage data
547+
if parsed.line_type.as_deref() == Some("assistant") {
548+
if let Some(msg) = &parsed.message {
549+
if let Some(u) = &msg.usage {
550+
usage.input_tokens += u.input_tokens.unwrap_or(0);
551+
usage.output_tokens += u.output_tokens.unwrap_or(0);
552+
usage.cache_creation_tokens += u.cache_creation_input_tokens.unwrap_or(0);
553+
usage.cache_read_tokens += u.cache_read_input_tokens.unwrap_or(0);
554+
}
555+
}
556+
}
557+
}
558+
}
559+
560+
usage.cost_usd = calculate_cost(&usage);
561+
usage
562+
}
563+
564+
#[derive(Debug, Serialize, Deserialize)]
565+
pub struct SessionUsageEntry {
566+
pub session_id: String,
567+
pub usage: SessionUsage,
568+
}
569+
570+
#[tauri::command]
571+
async fn get_sessions_usage(project_id: String) -> Result<Vec<SessionUsageEntry>, String> {
572+
tauri::async_runtime::spawn_blocking(move || {
573+
let project_dir = get_claude_dir().join("projects").join(&project_id);
574+
575+
if !project_dir.exists() {
576+
return Err("Project not found".to_string());
577+
}
578+
579+
let mut results = Vec::new();
580+
581+
for entry in fs::read_dir(&project_dir).map_err(|e| e.to_string())? {
582+
let entry = entry.map_err(|e| e.to_string())?;
583+
let path = entry.path();
584+
let name = path.file_name().unwrap().to_string_lossy().to_string();
585+
586+
if name.ends_with(".jsonl") && !name.starts_with("agent-") {
587+
let session_id = name.trim_end_matches(".jsonl").to_string();
588+
let usage = read_session_usage(&path);
589+
results.push(SessionUsageEntry { session_id, usage });
590+
}
591+
}
592+
593+
Ok(results)
594+
})
595+
.await
596+
.map_err(|e| e.to_string())?
597+
}
598+
493599
/// Read only the first N lines of a session file to get summary (much faster than reading entire file)
494600
fn read_session_head(path: &Path, max_lines: usize) -> (Option<String>, usize) {
495601
use std::io::{BufRead, BufReader};
@@ -690,6 +796,7 @@ async fn list_all_sessions() -> Result<Vec<Session>, String> {
690796
summary: final_summary,
691797
message_count: head_msg_count, // approximate from head
692798
last_modified,
799+
usage: None,
693800
});
694801
}
695802

@@ -736,6 +843,7 @@ async fn list_all_sessions() -> Result<Vec<Session>, String> {
736843
summary,
737844
message_count: head_msg_count,
738845
last_modified,
846+
usage: None,
739847
});
740848
}
741849
}
@@ -2493,6 +2601,7 @@ fn find_session_project(session_id: String) -> Result<Option<Session>, String> {
24932601
summary,
24942602
message_count: 0,
24952603
last_modified: 0,
2604+
usage: None,
24962605
}));
24972606
}
24982607
}
@@ -7260,6 +7369,7 @@ pub fn run() {
72607369
.invoke_handler(tauri::generate_handler![
72617370
list_projects,
72627371
list_sessions,
7372+
get_sessions_usage,
72637373
list_all_sessions,
72647374
list_all_chats,
72657375
get_session_messages,

src/types/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,27 @@ export interface Project {
4242
last_active: number;
4343
}
4444

45+
export interface SessionUsage {
46+
input_tokens: number;
47+
output_tokens: number;
48+
cache_creation_tokens: number;
49+
cache_read_tokens: number;
50+
cost_usd: number;
51+
}
52+
4553
export interface Session {
4654
id: string;
4755
project_id: string;
4856
project_path: string | null;
4957
summary: string | null;
5058
message_count: number;
5159
last_modified: number;
60+
usage?: SessionUsage;
61+
}
62+
63+
export interface SessionUsageEntry {
64+
session_id: string;
65+
usage: SessionUsage;
5266
}
5367

5468
export interface Message {

0 commit comments

Comments
 (0)