Skip to content

Commit 6675b74

Browse files
committed
Better breakup options
1 parent e288f60 commit 6675b74

1 file changed

Lines changed: 151 additions & 59 deletions

File tree

llms_wrapper/cost_logger.py

Lines changed: 151 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import sqlite3
66
import os
7+
import calendar
78
from typing import Any, Optional
89
from pathlib import Path
910
import 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+
612699
def 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

621714
def 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("\nCost 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("\nCost 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

Comments
 (0)