@@ -8,6 +8,12 @@ export interface TokenUsage {
88 total_tokens : number ;
99}
1010
11+ // Helper function to calculate blended total like the CLI does
12+ function getBlendedTotal ( usage : TokenUsage ) : number {
13+ const nonCachedInput = usage . input_tokens - ( usage . cached_input_tokens || 0 ) ;
14+ return nonCachedInput + usage . output_tokens ;
15+ }
16+
1117export interface SessionData {
1218 id : string ;
1319 timestamp : string ;
@@ -47,21 +53,27 @@ export interface UsageSummary {
4753
4854// Cost per million tokens (estimated based on common model pricing)
4955const MODEL_COSTS : Record < string , { input : number ; output : number ; cache_write : number ; cache_read : number } > = {
50- 'gpt-5-high' : { input : 15 , output : 75 , cache_write : 18.75 , cache_read : 1.5 } ,
51- 'gpt-5' : { input : 3 , output : 15 , cache_write : 3.75 , cache_read : 0.3 } ,
52- 'gpt-5-mini' : { input : 0.25 , output : 1.25 , cache_write : 0.3 , cache_read : 0.03 } ,
56+ 'claude-3-5-sonnet-20241022' : { input : 3 , output : 15 , cache_write : 3.75 , cache_read : 0.3 } ,
57+ 'claude-3-5-haiku-20241022' : { input : 0.8 , output : 4 , cache_write : 1 , cache_read : 0.08 } ,
58+ 'claude-3-opus-20240229' : { input : 15 , output : 75 , cache_write : 18.75 , cache_read : 1.5 } ,
59+ 'gemini-2.5-flash-lite' : { input : 0.075 , output : 0.3 , cache_write : 0.01875 , cache_read : 0.00188 } ,
60+ 'gemini-2.5-flash' : { input : 0.075 , output : 0.3 , cache_write : 0.01875 , cache_read : 0.00188 } ,
61+ 'gpt-5' : { input : 2.5 , output : 10 , cache_write : 1.25 , cache_read : 0.125 } ,
62+ 'gpt-4o' : { input : 2.5 , output : 10 , cache_write : 1.25 , cache_read : 0.125 } ,
5363 'gpt-4' : { input : 30 , output : 60 , cache_write : 30 , cache_read : 3 } ,
5464 'gpt-3.5-turbo' : { input : 0.5 , output : 1.5 , cache_write : 0.5 , cache_read : 0.05 } ,
5565 'llama3.2' : { input : 0 , output : 0 , cache_write : 0 , cache_read : 0 } , // OSS model
66+ 'mistral' : { input : 0 , output : 0 , cache_write : 0 , cache_read : 0 } , // OSS model
5667} ;
5768
5869function calculateTokenCost ( usage : TokenUsage , model : string ) : number {
5970 const costs = MODEL_COSTS [ model ] || MODEL_COSTS [ 'claude-3-5-sonnet-20241022' ] ; // Default to Sonnet
6071
61- const inputCost = ( usage . input_tokens / 1_000_000 ) * costs . input ;
72+ const nonCachedInput = usage . input_tokens - ( usage . cached_input_tokens || 0 ) ;
73+ const inputCost = ( nonCachedInput / 1_000_000 ) * costs . input ;
6274 const outputCost = ( usage . output_tokens / 1_000_000 ) * costs . output ;
6375 const cacheWriteCost = ( ( usage . cached_input_tokens || 0 ) / 1_000_000 ) * costs . cache_write ;
64- const cacheReadCost = 0 ; // Assuming cache read is included in input tokens
76+ const cacheReadCost = 0 ; // Cache reads are typically much cheaper
6577
6678 return inputCost + outputCost + cacheWriteCost + cacheReadCost ;
6779}
@@ -100,23 +112,49 @@ export async function parseSessionFile(filePath: string): Promise<SessionMetrics
100112 let totalInputTokens = 0 ;
101113 let totalOutputTokens = 0 ;
102114 let totalCacheTokens = 0 ;
103- let model = 'claude-3-5-sonnet-20241022 ' ; // Default
115+ let model = 'gpt-5 ' ; // Default based on config
104116
105117 // Count messages as a proxy for usage when actual token data is not available
106118 let messageCount = 0 ;
107119 let hasUserMessages = false ;
108120 let hasAssistantMessages = false ;
121+ let projectFromCwd = '' ;
109122
110123 for ( const line of lines ) {
111124 try {
112125 const event = JSON . parse ( line ) ;
113126
114- // Look for token count events (actual usage data)
115- if ( event . msg ?. type === 'token_count' && event . msg . usage ) {
116- const usage : TokenUsage = event . msg . usage ;
117- totalInputTokens += usage . input_tokens ;
118- totalOutputTokens += usage . output_tokens ;
119- totalCacheTokens += usage . cached_input_tokens || 0 ;
127+ // Look for token count events - handle various event structures from codex CLI
128+ // Based on the CLI source, TokenCount events come as EventMsg::TokenCount(TokenUsage)
129+ if ( event . msg && typeof event . msg === 'object' ) {
130+ // Direct TokenUsage structure (EventMsg::TokenCount)
131+ if ( 'input_tokens' in event . msg || 'output_tokens' in event . msg || 'total_tokens' in event . msg ) {
132+ const usage = event . msg as TokenUsage ;
133+ // Use cumulative values as they represent session totals
134+ if ( usage . input_tokens !== undefined ) {
135+ totalInputTokens = Math . max ( totalInputTokens , usage . input_tokens ) ;
136+ }
137+ if ( usage . output_tokens !== undefined ) {
138+ totalOutputTokens = Math . max ( totalOutputTokens , usage . output_tokens ) ;
139+ }
140+ if ( usage . cached_input_tokens !== undefined ) {
141+ totalCacheTokens = Math . max ( totalCacheTokens , usage . cached_input_tokens ) ;
142+ }
143+ }
144+
145+ // Nested structure with type field
146+ if ( event . msg . type === 'token_count' && event . msg . usage ) {
147+ const usage : TokenUsage = event . msg . usage ;
148+ if ( usage . input_tokens !== undefined ) {
149+ totalInputTokens = Math . max ( totalInputTokens , usage . input_tokens ) ;
150+ }
151+ if ( usage . output_tokens !== undefined ) {
152+ totalOutputTokens = Math . max ( totalOutputTokens , usage . output_tokens ) ;
153+ }
154+ if ( usage . cached_input_tokens !== undefined ) {
155+ totalCacheTokens = Math . max ( totalCacheTokens , usage . cached_input_tokens ) ;
156+ }
157+ }
120158 }
121159
122160 // Look for messages to estimate usage when no token data available
@@ -129,57 +167,69 @@ export async function parseSessionFile(filePath: string): Promise<SessionMetrics
129167 }
130168 }
131169
132- // Extract model info from various sources
170+ // Extract project info from environment context (cwd)
171+ if ( event . content && Array . isArray ( event . content ) ) {
172+ for ( const content of event . content ) {
173+ if ( content . type === 'input_text' && content . text . includes ( '<cwd>' ) ) {
174+ const cwdMatch = content . text . match ( / < c w d > ( [ ^ < ] + ) < \/ c w d > / ) ;
175+ if ( cwdMatch && cwdMatch [ 1 ] ) {
176+ projectFromCwd = cwdMatch [ 1 ] . split ( '/' ) . pop ( ) || cwdMatch [ 1 ] ;
177+ }
178+ }
179+ }
180+ }
181+
182+ // Extract model info from various sources - default to what's actually configured
133183 if ( event . msg ?. model ) {
134184 model = event . msg . model ;
135185 }
136186 if ( event . model ) {
137187 model = event . model ;
138188 }
139-
140- // Check for git repo info to determine if it's Claude Code usage
141- if ( event . git ?. repository_url && event . git . repository_url . includes ( 'codexia' ) ) {
142- model = 'gpt-5-high' ; // Likely using Claude 4 for development
143- }
144189 } catch {
145190 // Skip invalid JSON lines
146191 }
147192 }
148193
149- // If no actual token data, estimate based on messages
150- if ( totalInputTokens === 0 && totalOutputTokens === 0 && hasUserMessages && hasAssistantMessages ) {
151- // Rough estimation: user input ~100 tokens, assistant response ~300 tokens per exchange
194+ // If no actual token data, estimate based on messages for meaningful sessions
195+ if ( totalInputTokens === 0 && totalOutputTokens === 0 && hasUserMessages && hasAssistantMessages && messageCount >= 2 ) {
196+ // Conservative estimation based on typical coding conversations
152197 const estimatedExchanges = Math . ceil ( messageCount / 2 ) ;
153- totalInputTokens = estimatedExchanges * 150 ; // Average input
154- totalOutputTokens = estimatedExchanges * 400 ; // Average output
155- totalCacheTokens = estimatedExchanges * 50 ; // Some caching
198+ totalInputTokens = estimatedExchanges * 200 ; // Average user input with context
199+ totalOutputTokens = estimatedExchanges * 600 ; // Average assistant response with code
200+ totalCacheTokens = estimatedExchanges * 100 ; // Context caching
156201 }
157202
158- const totalTokens = totalInputTokens + totalOutputTokens ;
203+ // Calculate total tokens using the same logic as CLI (blended total)
204+ const mockUsage : TokenUsage = {
205+ input_tokens : totalInputTokens ,
206+ output_tokens : totalOutputTokens ,
207+ cached_input_tokens : totalCacheTokens ,
208+ total_tokens : totalInputTokens + totalOutputTokens ,
209+ } ;
210+ const totalTokens = getBlendedTotal ( mockUsage ) ;
159211
160212 // Skip sessions with no meaningful activity
161213 if ( totalTokens === 0 && messageCount < 2 ) {
162214 return null ;
163215 }
164216
165- // Extract project path from git repo or session path
166- let projectPath = sessionData . git ?. repository_url || filePath ;
167- if ( projectPath . includes ( '/' ) ) {
168- projectPath = projectPath . split ( '/' ) . pop ( ) || projectPath ;
169- }
170- if ( projectPath . includes ( '.git' ) ) {
171- projectPath = projectPath . replace ( '.git' , '' ) ;
172- }
173- if ( projectPath . startsWith ( 'git@github.com:' ) ) {
174- projectPath = projectPath . replace ( 'git@github.com:' , '' ) . replace ( '.git' , '' ) ;
217+ // Extract project path - prefer cwd from environment context, fallback to filename
218+ let projectPath = projectFromCwd ;
219+ if ( ! projectPath ) {
220+ // Fallback to extracting from file path
221+ projectPath = filePath . split ( '/' ) . pop ( ) || filePath ;
222+ if ( projectPath . includes ( 'rollout-' ) && projectPath . endsWith ( '.jsonl' ) ) {
223+ // For rollout files, use the directory name instead
224+ const pathParts = filePath . split ( '/' ) ;
225+ if ( pathParts . length > 1 ) {
226+ // Try to get a meaningful project name from the path or use generic name
227+ projectPath = 'Unknown Session' ;
228+ }
229+ }
175230 }
176231
177- const estimatedCost = calculateTokenCost ( {
178- input_tokens : totalInputTokens ,
179- output_tokens : totalOutputTokens ,
180- cached_input_tokens : totalCacheTokens ,
181- total_tokens : totalTokens ,
182- } , model ) ;
232+ const estimatedCost = calculateTokenCost ( mockUsage , model ) ;
183233
184234 return {
185235 sessionId : sessionData . id ,
@@ -292,4 +342,4 @@ export async function calculateUsageSummary(): Promise<UsageSummary> {
292342 projectBreakdown,
293343 timelineData,
294344 } ;
295- }
345+ }
0 commit comments