@@ -6,6 +6,16 @@ import { unreachable } from '@core/errorutil';
66import { sort } from 'fast-sort' ;
77import { glob } from 'tinyglobby' ;
88import * as v from 'valibot' ;
9+ import {
10+ aggregateByModel ,
11+ aggregateModelBreakdowns ,
12+ calculateTotals ,
13+ createModelBreakdowns ,
14+ extractUniqueModels ,
15+ filterByDateRange ,
16+ isDuplicateEntry ,
17+ markAsProcessed ,
18+ } from './data-aggregation.ts' ;
919import { logger } from './logger.ts' ;
1020import {
1121 PricingFetcher ,
@@ -305,15 +315,13 @@ export async function loadDailyUsageData(
305315
306316 // Check for duplicate message + request ID combination
307317 const uniqueHash = createUniqueHash ( data ) ;
308- if ( uniqueHash != null && processedHashes . has ( uniqueHash ) ) {
318+ if ( isDuplicateEntry ( uniqueHash , processedHashes ) ) {
309319 // Skip duplicate message
310320 continue ;
311321 }
312322
313323 // Mark this combination as processed
314- if ( uniqueHash != null ) {
315- processedHashes . add ( uniqueHash ) ;
316- }
324+ markAsProcessed ( uniqueHash , processedHashes ) ;
317325
318326 const date = formatDate ( data . timestamp ) ;
319327 // If fetcher is available, calculate cost based on mode and tokens
@@ -341,70 +349,24 @@ export async function loadDailyUsageData(
341349 }
342350
343351 // Aggregate by model first
344- const modelAggregates = new Map < string , {
345- inputTokens : number ;
346- outputTokens : number ;
347- cacheCreationTokens : number ;
348- cacheReadTokens : number ;
349- cost : number ;
350- } > ( ) ;
351-
352- for ( const entry of entries ) {
353- const modelName = entry . model ?? 'unknown' ;
354- // Skip synthetic model
355- if ( modelName === '<synthetic>' ) {
356- continue ;
357- }
358- const existing = modelAggregates . get ( modelName ) ?? {
359- inputTokens : 0 ,
360- outputTokens : 0 ,
361- cacheCreationTokens : 0 ,
362- cacheReadTokens : 0 ,
363- cost : 0 ,
364- } ;
365-
366- modelAggregates . set ( modelName , {
367- inputTokens : existing . inputTokens + ( entry . data . message . usage . input_tokens ?? 0 ) ,
368- outputTokens : existing . outputTokens + ( entry . data . message . usage . output_tokens ?? 0 ) ,
369- cacheCreationTokens : existing . cacheCreationTokens + ( entry . data . message . usage . cache_creation_input_tokens ?? 0 ) ,
370- cacheReadTokens : existing . cacheReadTokens + ( entry . data . message . usage . cache_read_input_tokens ?? 0 ) ,
371- cost : existing . cost + entry . cost ,
372- } ) ;
373- }
352+ const modelAggregates = aggregateByModel (
353+ entries ,
354+ entry => entry . model ,
355+ entry => entry . data . message . usage ,
356+ entry => entry . cost ,
357+ ) ;
374358
375359 // Create model breakdowns
376- const modelBreakdowns : ModelBreakdown [ ] = Array . from ( modelAggregates . entries ( ) )
377- . map ( ( [ modelName , stats ] ) => ( {
378- modelName,
379- ...stats ,
380- } ) )
381- . sort ( ( a , b ) => b . cost - a . cost ) ; // Sort by cost descending
360+ const modelBreakdowns = createModelBreakdowns ( modelAggregates ) ;
382361
383362 // Calculate totals
384- const totals = entries . reduce (
385- ( acc , entry ) => ( {
386- inputTokens :
387- acc . inputTokens + ( entry . data . message . usage . input_tokens ?? 0 ) ,
388- outputTokens :
389- acc . outputTokens + ( entry . data . message . usage . output_tokens ?? 0 ) ,
390- cacheCreationTokens :
391- acc . cacheCreationTokens
392- + ( entry . data . message . usage . cache_creation_input_tokens ?? 0 ) ,
393- cacheReadTokens :
394- acc . cacheReadTokens
395- + ( entry . data . message . usage . cache_read_input_tokens ?? 0 ) ,
396- totalCost : acc . totalCost + entry . cost ,
397- } ) ,
398- {
399- inputTokens : 0 ,
400- outputTokens : 0 ,
401- cacheCreationTokens : 0 ,
402- cacheReadTokens : 0 ,
403- totalCost : 0 ,
404- } ,
363+ const totals = calculateTotals (
364+ entries ,
365+ entry => entry . data . message . usage ,
366+ entry => entry . cost ,
405367 ) ;
406368
407- const modelsUsed = [ ... new Set ( entries . map ( e => e . model ) . filter ( ( m ) : m is string => m != null && m !== '<synthetic>' ) ) ] ;
369+ const modelsUsed = extractUniqueModels ( entries , e => e . model ) ;
408370
409371 return {
410372 date,
@@ -413,23 +375,13 @@ export async function loadDailyUsageData(
413375 modelBreakdowns,
414376 } ;
415377 } )
416- . filter ( item => item != null )
417- . filter ( ( item ) => {
418- // Filter by date range if specified
419- if ( options ?. since != null || options ?. until != null ) {
420- const dateStr = item . date . replace ( / - / g, '' ) ; // Convert to YYYYMMDD
421- if ( options . since != null && dateStr < options . since ) {
422- return false ;
423- }
424- if ( options . until != null && dateStr > options . until ) {
425- return false ;
426- }
427- }
428- return true ;
429- } ) ;
378+ . filter ( item => item != null ) ;
379+
380+ // Filter by date range if specified
381+ const filtered = filterByDateRange ( results , item => item . date , options ?. since , options ?. until ) ;
430382
431383 // Sort by date based on order option (default to descending)
432- return sortByDate ( results , item => item . date , options ?. order ) ;
384+ return sortByDate ( filtered , item => item . date , options ?. order ) ;
433385}
434386
435387export async function loadSessionData (
@@ -555,70 +507,24 @@ export async function loadSessionData(
555507 }
556508
557509 // Aggregate by model
558- const modelAggregates = new Map < string , {
559- inputTokens : number ;
560- outputTokens : number ;
561- cacheCreationTokens : number ;
562- cacheReadTokens : number ;
563- cost : number ;
564- } > ( ) ;
565-
566- for ( const entry of entries ) {
567- const modelName = entry . model ?? 'unknown' ;
568- // Skip synthetic model
569- if ( modelName === '<synthetic>' ) {
570- continue ;
571- }
572- const existing = modelAggregates . get ( modelName ) ?? {
573- inputTokens : 0 ,
574- outputTokens : 0 ,
575- cacheCreationTokens : 0 ,
576- cacheReadTokens : 0 ,
577- cost : 0 ,
578- } ;
579-
580- modelAggregates . set ( modelName , {
581- inputTokens : existing . inputTokens + ( entry . data . message . usage . input_tokens ?? 0 ) ,
582- outputTokens : existing . outputTokens + ( entry . data . message . usage . output_tokens ?? 0 ) ,
583- cacheCreationTokens : existing . cacheCreationTokens + ( entry . data . message . usage . cache_creation_input_tokens ?? 0 ) ,
584- cacheReadTokens : existing . cacheReadTokens + ( entry . data . message . usage . cache_read_input_tokens ?? 0 ) ,
585- cost : existing . cost + entry . cost ,
586- } ) ;
587- }
510+ const modelAggregates = aggregateByModel (
511+ entries ,
512+ entry => entry . model ,
513+ entry => entry . data . message . usage ,
514+ entry => entry . cost ,
515+ ) ;
588516
589517 // Create model breakdowns
590- const modelBreakdowns : ModelBreakdown [ ] = Array . from ( modelAggregates . entries ( ) )
591- . map ( ( [ modelName , stats ] ) => ( {
592- modelName,
593- ...stats ,
594- } ) )
595- . sort ( ( a , b ) => b . cost - a . cost ) ;
518+ const modelBreakdowns = createModelBreakdowns ( modelAggregates ) ;
596519
597520 // Calculate totals
598- const totals = entries . reduce (
599- ( acc , entry ) => ( {
600- inputTokens :
601- acc . inputTokens + ( entry . data . message . usage . input_tokens ?? 0 ) ,
602- outputTokens :
603- acc . outputTokens + ( entry . data . message . usage . output_tokens ?? 0 ) ,
604- cacheCreationTokens :
605- acc . cacheCreationTokens
606- + ( entry . data . message . usage . cache_creation_input_tokens ?? 0 ) ,
607- cacheReadTokens :
608- acc . cacheReadTokens
609- + ( entry . data . message . usage . cache_read_input_tokens ?? 0 ) ,
610- totalCost : acc . totalCost + entry . cost ,
611- } ) ,
612- {
613- inputTokens : 0 ,
614- outputTokens : 0 ,
615- cacheCreationTokens : 0 ,
616- cacheReadTokens : 0 ,
617- totalCost : 0 ,
618- } ,
521+ const totals = calculateTotals (
522+ entries ,
523+ entry => entry . data . message . usage ,
524+ entry => entry . cost ,
619525 ) ;
620526
621- const modelsUsed = [ ... new Set ( entries . map ( e => e . model ) . filter ( ( m ) : m is string => m != null && m !== '<synthetic>' ) ) ] ;
527+ const modelsUsed = extractUniqueModels ( entries , e => e . model ) ;
622528
623529 return {
624530 sessionId : latestEntry . sessionId ,
@@ -630,22 +536,12 @@ export async function loadSessionData(
630536 modelBreakdowns,
631537 } ;
632538 } )
633- . filter ( item => item != null )
634- . filter ( ( item ) => {
635- // Filter by date range if specified
636- if ( options ?. since != null || options ?. until != null ) {
637- const dateStr = item . lastActivity . replace ( / - / g, '' ) ; // Convert to YYYYMMDD
638- if ( options . since != null && dateStr < options . since ) {
639- return false ;
640- }
641- if ( options . until != null && dateStr > options . until ) {
642- return false ;
643- }
644- }
645- return true ;
646- } ) ;
539+ . filter ( item => item != null ) ;
647540
648- return sortByDate ( results , item => item . lastActivity , options ?. order ) ;
541+ // Filter by date range if specified
542+ const filtered = filterByDateRange ( results , item => item . lastActivity , options ?. since , options ?. until ) ;
543+
544+ return sortByDate ( filtered , item => item . lastActivity , options ?. order ) ;
649545}
650546
651547export async function loadMonthlyUsageData (
@@ -665,45 +561,11 @@ export async function loadMonthlyUsageData(
665561 }
666562
667563 // Aggregate model breakdowns across all days
668- const modelAggregates = new Map < string , {
669- inputTokens : number ;
670- outputTokens : number ;
671- cacheCreationTokens : number ;
672- cacheReadTokens : number ;
673- cost : number ;
674- } > ( ) ;
675-
676- for ( const daily of dailyEntries ) {
677- for ( const breakdown of daily . modelBreakdowns ) {
678- // Skip synthetic model
679- if ( breakdown . modelName === '<synthetic>' ) {
680- continue ;
681- }
682- const existing = modelAggregates . get ( breakdown . modelName ) ?? {
683- inputTokens : 0 ,
684- outputTokens : 0 ,
685- cacheCreationTokens : 0 ,
686- cacheReadTokens : 0 ,
687- cost : 0 ,
688- } ;
689-
690- modelAggregates . set ( breakdown . modelName , {
691- inputTokens : existing . inputTokens + breakdown . inputTokens ,
692- outputTokens : existing . outputTokens + breakdown . outputTokens ,
693- cacheCreationTokens : existing . cacheCreationTokens + breakdown . cacheCreationTokens ,
694- cacheReadTokens : existing . cacheReadTokens + breakdown . cacheReadTokens ,
695- cost : existing . cost + breakdown . cost ,
696- } ) ;
697- }
698- }
564+ const allBreakdowns = dailyEntries . flatMap ( daily => daily . modelBreakdowns ) ;
565+ const modelAggregates = aggregateModelBreakdowns ( allBreakdowns ) ;
699566
700567 // Create model breakdowns
701- const modelBreakdowns : ModelBreakdown [ ] = Array . from ( modelAggregates . entries ( ) )
702- . map ( ( [ modelName , stats ] ) => ( {
703- modelName,
704- ...stats ,
705- } ) )
706- . sort ( ( a , b ) => b . cost - a . cost ) ;
568+ const modelBreakdowns = createModelBreakdowns ( modelAggregates ) ;
707569
708570 // Collect unique models
709571 const modelsSet = new Set < string > ( ) ;
0 commit comments