44
55import sqlite3
66import os
7+ import calendar
78from typing import Any , Optional
89from pathlib import Path
910import json
@@ -527,13 +528,18 @@ def get_output_tokens(self) -> int:
527528 except Exception as e :
528529 raise Exception (f"Failed to get output_tokens sum: { e } " ) from e
529530
530- def stats (self ) -> dict :
531+ def stats (self , date_from = None , date_to = None ) -> dict :
531532 """
532- Generate summary statistics over the entire table .
533+ Generate summary statistics, optionally filtered to a date range .
533534 Includes rows with NULL values using 'NULL' as the key.
534535
536+ Args:
537+ date_from: Optional lower bound for datetime (inclusive, 'YYYY-MM-DD HH:MM:SS')
538+ date_to: Optional upper bound for datetime (inclusive, 'YYYY-MM-DD HH:MM:SS')
539+
535540 Returns:
536- Dictionary with three top-level keys:
541+ Dictionary with four top-level keys:
542+ - 'by_all': {modelalias: cost, ...}
537543 - 'by_user': {user: {modelalias: cost, ...}, ...}
538544 - 'by_project': {project: {modelalias: cost, ...}, ...}
539545 - 'by_user_project': {"user / project": {modelalias: cost, ...}, ...}
@@ -543,57 +549,65 @@ def stats(self) -> dict:
543549 Raises:
544550 Exception: If query fails
545551 """
552+ conditions = []
553+ values = []
554+ if date_from is not None :
555+ conditions .append ('datetime >= ?' )
556+ values .append (date_from )
557+ if date_to is not None :
558+ conditions .append ('datetime <= ?' )
559+ values .append (date_to )
560+ where = ('WHERE ' + ' AND ' .join (conditions )) if conditions else ''
561+
546562 try :
547563 conn = sqlite3 .connect (self .db_path , timeout = 5.0 )
548564 try :
549565 result = {
566+ 'by_all' : {},
550567 'by_user' : {},
551568 'by_project' : {},
552569 'by_user_project' : {}
553570 }
554571
555- # Get cost by user and modelalias
556- cursor = conn .execute ('''
557- SELECT user, modelalias, SUM(cost) as total_cost
558- FROM logs
559- GROUP BY user, modelalias
560- ORDER BY user, modelalias
561- ''' )
572+ # Grand total by modelalias
573+ cursor = conn .execute (
574+ f'SELECT modelalias, SUM(cost) FROM logs { where } GROUP BY modelalias ORDER BY modelalias' ,
575+ values
576+ )
577+ for modelalias , cost in cursor :
578+ alias_key = modelalias if modelalias is not None else 'NULL'
579+ result ['by_all' ][alias_key ] = float (cost ) if cost else 0.0
562580
563- for row in cursor :
564- user , modelalias , cost = row
581+ # Cost by user and modelalias
582+ cursor = conn .execute (
583+ f'SELECT user, modelalias, SUM(cost) FROM logs { where } GROUP BY user, modelalias ORDER BY user, modelalias' ,
584+ values
585+ )
586+ for user , modelalias , cost in cursor :
565587 user_key = user if user is not None else 'NULL'
566588 if user_key not in result ['by_user' ]:
567589 result ['by_user' ][user_key ] = {}
568590 alias_key = modelalias if modelalias is not None else 'NULL'
569591 result ['by_user' ][user_key ][alias_key ] = float (cost ) if cost else 0.0
570592
571- # Get cost by project and modelalias
572- cursor = conn .execute ('''
573- SELECT project, modelalias, SUM(cost) as total_cost
574- FROM logs
575- GROUP BY project, modelalias
576- ORDER BY project, modelalias
577- ''' )
578-
579- for row in cursor :
580- project , modelalias , cost = row
593+ # Cost by project and modelalias
594+ cursor = conn .execute (
595+ f'SELECT project, modelalias, SUM(cost) FROM logs { where } GROUP BY project, modelalias ORDER BY project, modelalias' ,
596+ values
597+ )
598+ for project , modelalias , cost in cursor :
581599 project_key = project if project is not None else 'NULL'
582600 if project_key not in result ['by_project' ]:
583601 result ['by_project' ][project_key ] = {}
584602 alias_key = modelalias if modelalias is not None else 'NULL'
585603 result ['by_project' ][project_key ][alias_key ] = float (cost ) if cost else 0.0
586604
587- # Get cost by user/project combination and modelalias
588- cursor = conn .execute ('''
589- SELECT user, project, modelalias, SUM(cost) as total_cost
590- FROM logs
591- GROUP BY user, project, modelalias
592- ORDER BY user, project, modelalias
593- ''' )
594-
595- for row in cursor :
596- user , project , modelalias , cost = row
605+ # Cost by user/project combination and modelalias
606+ cursor = conn .execute (
607+ f'SELECT user, project, modelalias, SUM(cost) FROM logs { where } GROUP BY user, project, modelalias ORDER BY user, project, modelalias' ,
608+ values
609+ )
610+ for user , project , modelalias , cost in cursor :
597611 user_key = user if user is not None else 'NULL'
598612 project_key = project if project is not None else 'NULL'
599613 key = f"{ user_key } / { project_key } "
@@ -609,42 +623,120 @@ def stats(self) -> dict:
609623 except Exception as e :
610624 raise Exception (f"Failed to generate stats: { e } " ) from e
611625
626+ def get_months (self ) -> list [str ]:
627+ """
628+ Return sorted list of distinct 'YYYY-MM' strings present in the logs table.
629+
630+ Raises:
631+ Exception: If query fails
632+ """
633+ try :
634+ conn = sqlite3 .connect (self .db_path , timeout = 5.0 )
635+ try :
636+ cursor = conn .execute (
637+ "SELECT DISTINCT strftime('%Y-%m', datetime) FROM logs ORDER BY 1"
638+ )
639+ return [row [0 ] for row in cursor if row [0 ] is not None ]
640+ finally :
641+ conn .close ()
642+ except Exception as e :
643+ raise Exception (f"Failed to get months: { e } " ) from e
644+
645+ def fflt (value : float ) -> str :
646+ return f"{ value :.8f} " .rstrip ('0' ).rstrip ('.' )
647+
648+
649+ def print_stats (all_stats : dict , by : str , byllm : bool ):
650+ """Print cost statistics according to the selected grouping and LLM flag."""
651+
652+ def section_total (groups : dict ) -> str :
653+ return fflt (sum (sum (aliases .values ()) for aliases in groups .values ()))
654+
655+ if by == 'all' :
656+ aliases = all_stats ['by_all' ]
657+ grand = fflt (sum (aliases .values ()))
658+ if byllm :
659+ print (f"Total Cost: ${ grand } " )
660+ for alias , cost in aliases .items ():
661+ print (f" { alias } : ${ fflt (cost )} " )
662+ else :
663+ print (f"Total Cost: ${ grand } " )
664+
665+ elif by == 'user' :
666+ print ("Cost by User:" )
667+ groups = all_stats ['by_user' ]
668+ for user , aliases in groups .items ():
669+ print (f" { user } : ${ fflt (sum (aliases .values ()))} " )
670+ if byllm :
671+ for alias , cost in aliases .items ():
672+ print (f" { alias } : ${ fflt (cost )} " )
673+ if len (groups ) > 1 :
674+ print (f" Total: ${ section_total (groups )} " )
675+
676+ elif by == 'project' :
677+ print ("Cost by Project:" )
678+ groups = all_stats ['by_project' ]
679+ for project , aliases in groups .items ():
680+ print (f" { project } : ${ fflt (sum (aliases .values ()))} " )
681+ if byllm :
682+ for alias , cost in aliases .items ():
683+ print (f" { alias } : ${ fflt (cost )} " )
684+ if len (groups ) > 1 :
685+ print (f" Total: ${ section_total (groups )} " )
686+
687+ elif by == 'user+project' :
688+ print ("Cost by User/Project:" )
689+ groups = all_stats ['by_user_project' ]
690+ for combo , aliases in groups .items ():
691+ print (f" { combo } : ${ fflt (sum (aliases .values ()))} " )
692+ if byllm :
693+ for alias , cost in aliases .items ():
694+ print (f" { alias } : ${ fflt (cost )} " )
695+ if len (groups ) > 1 :
696+ print (f" Total: ${ section_total (groups )} " )
697+
698+
612699def get_args ():
613700 parser = argparse .ArgumentParser (description = 'Show costs' )
614701 parser .add_argument ('file' , type = str , help = 'Cost database file' )
702+ parser .add_argument ('--sum' , action = 'store_true' ,
703+ help = 'Output only the total cost as a float (ignores all other options)' )
704+ parser .add_argument ('--by' , choices = ['user' , 'project' , 'user+project' , 'all' ], default = 'all' ,
705+ help = 'Grouping: user, project, user+project, or all (default: all)' )
706+ parser .add_argument ('--byllm' , action = 'store_true' ,
707+ help = 'Add per-LLM breakdown within the selected grouping' )
708+ parser .add_argument ('--bymonth' , action = 'store_true' ,
709+ help = 'Group output by year-month' )
615710 args = parser .parse_args ()
616- argsconfig = {}
617- argsconfig .update (vars (args ))
618- return argsconfig
711+ return vars (args )
619712
620713
621714def main ():
622715 config = get_args ()
623716 costs = Log2Sqlite (config ['file' ])
624- all_stats = costs .stats ()
625- print ("Cost by User:" )
626-
627- def fflt (value : float ):
628- return f"{ value :.8f} " .rstrip ('0' ).rstrip ('.' )
629-
630- for user , aliases in all_stats ['by_user' ].items ():
631- total = fflt (sum (aliases .values ()))
632- print (f" { user } : ${ total } " )
633- for alias , cost in aliases .items ():
634- cost = fflt (cost )
635- print (f" { alias } : ${ cost } " )
636-
637- # Print project costs
638- print ("\n Cost by Project:" )
639- for project , aliases in all_stats ['by_project' ].items ():
640- total = fflt (sum (aliases .values ()))
641- print (f" { project } : ${ total } " )
642-
643- # Print user/project combinations
644- print ("\n Cost by User/Project:" )
645- for combo , aliases in all_stats ['by_user_project' ].items ():
646- total = fflt (sum (aliases .values ()))
647- print (f" { combo } : ${ total } " )
717+
718+ if config ['sum' ]:
719+ total_cost , _ , _ , _ = costs .get ()
720+ print (total_cost )
721+ return
722+
723+ by = config ['by' ]
724+ byllm = config ['byllm' ]
725+
726+ if config ['bymonth' ]:
727+ months = costs .get_months ()
728+ for month in months :
729+ year , mon = map (int , month .split ('-' ))
730+ last_day = calendar .monthrange (year , mon )[1 ]
731+ date_from = f"{ month } -01 00:00:00"
732+ date_to = f"{ month } -{ last_day :02d} 23:59:59"
733+ all_stats = costs .stats (date_from = date_from , date_to = date_to )
734+ print (f"=== { month } ===" )
735+ print_stats (all_stats , by , byllm )
736+ print ()
737+ else :
738+ all_stats = costs .stats ()
739+ print_stats (all_stats , by , byllm )
648740
649741
650742# Example usage
0 commit comments